Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Getmapstack

Self-hosted mapping stack. One command per country.

Replace Google Maps API - no API keys, no rate limits, no vendor lock-in.

Routing via Valhalla 3.8.3, geocoding via Photon 1.2.1, vector basemap tiles via Martin 1.13.0 (built with Planetiler 0.10.2).

One docker run command starts a container that publishes a single port, 4326. Your application sends POST /valhalla/route, GET /photon/api and GET /martin/basemap/{z}/{x}/{y} to that one port, and each prefix reaches its own service inside the container: Valhalla for routing, Photon for geocoding, Martin for vector tiles, over country OSM data baked into the image.

Quick start

docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/cyprus

Images are multi-arch: linux/amd64 and linux/arm64 (Apple Silicon, AWS Graviton).

Once it starts, open http://localhost:4326 for the explorer - one page that drives all three engines (routes, isochrones, matrices, geocoding, server-rendered images) and shows the matching command next to every answer. The map catalog UI - the vector tileset, style, fonts and sprites - is at http://localhost:4326/martin/.

Give it a moment to start - Photon opens its search index in a few seconds for a country this size, several minutes for the largest ones. curl -sf localhost:4326/healthz answers {"status":"ok"} once all three engines are up, and the container's own healthcheck runs that same probe, so docker ps reports healthy at the same moment. Then check that it answers, a car route from Nicosia to Limassol:

curl localhost:4326/valhalla/route \
  -d '{"locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],"costing":"auto"}'
{"trip":{"summary":{"length":84.736,"time":3770.219,"has_highway":true}}}

Every other method is in What you get.

Hosted API

Try the stack without installing - the same services for all supported countries at https://api.getmapstack.com (/valhalla, /photon and /martin prefixes):

curl https://api.getmapstack.com/valhalla/route \
  -d '{"locations":[{"lat":35.18,"lon":33.38},{"lat":34.67,"lon":33.04}],"costing":"auto"}'
curl "https://api.getmapstack.com/photon/api?q=Nicosia&limit=1"
curl "https://api.getmapstack.com/martin/basemap"
curl "https://api.getmapstack.com/martin/style/bright/static/33.3823,35.1856,13/600x400.png" -o map.png

Please keep your usage fair. No SLA - this is a demo that may change or disappear; run your own container for unlimited use.

Countries

Country Size Run
🇧🇪 Belgium 2.3 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/belgium
🇧🇳 Brunei 0.8 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/brunei
🇨🇾 Cyprus 0.4 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/cyprus
🇬🇪 Georgia 0.7 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/georgia
🇮🇩 Indonesia 2.7 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/indonesia
🇰🇿 Kazakhstan 1.4 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/kazakhstan
🇲🇾 Malaysia 1.1 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/malaysia
🇷🇸 Serbia 1.8 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/serbia
🇸🇬 Singapore 0.9 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/singapore
🇿🇦 South Africa 1.4 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/south-africa
🇰🇷 South Korea 1.9 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/south-korea
🇻🇳 Vietnam 1.2 GB docker run -p 4326:4326 ghcr.io/roma8ok/getmapstack/vietnam

What you get

All three engines serve their full API, not a trimmed subset - see Not included for the handful of features these images don't have the data to answer. Everything below runs against a plain docker run of a country image, with no configuration.

Task Request Service
Route between two or more points POST /route /valhalla
Route for a given departure or arrival time POST /route /valhalla
Visit many stops in the best order POST /optimized_route /valhalla
Time and distance for many pairs at once POST /sources_to_targets /valhalla
Area reachable within N minutes POST /isochrone /valhalla
Meeting point for several starting points POST /centroid /valhalla
Snap a GPS track to the road network POST /trace_route /valhalla
Per-segment attributes of a snapped track POST /trace_attributes /valhalla
Inspect what the router explored POST /expansion /valhalla
Nearest road to a coordinate POST /locate /valhalla
Road network as vector tiles POST /tile /valhalla
Coordinates from a place name, search and autocomplete GET /api?q= /photon
Coordinates from address fields GET /structured /photon
Address from coordinates GET /reverse /photon
Vector basemap tiles for any map library GET /basemap/{z}/{x}/{y} /martin
Map style, fonts, icons GET /style/bright, /font/..., /sprite/... /martin
Static map images, no JS GET/POST /style/bright/static/... /martin
Rendered raster tiles GET /style/bright/{z}/{x}/{y}.png /martin
Live PostGIS overlay MARTIN_POSTGRES env /martin
Versions and how old the data is GET /status /valhalla, /photon
Liveness check for the map service GET /health /martin
Whether the container is ready to serve GET /healthz the container

The self-hosted and hosted paths are identical apart from the host: localhost:4326/valhalla/route self-hosted, https://api.getmapstack.com/valhalla/route hosted.

Parameter-level reference for all three: Valhalla API · Photon API · Martin docs

Routing

Route

Cost options shape the result. This one avoids highways and tolls, asks for two alternatives and kilometers:

curl localhost:4326/valhalla/route -d '{
  "locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],
  "costing":"auto",
  "costing_options":{"auto":{"use_highways":0.2,"use_tolls":0}},
  "units":"kilometers",
  "alternates":2
}'
{"trip":{"summary":{"length":90.545,"time":5700.343,"has_highway":false}},"alternates":[...]}

The default car route between the same points is 84.7 km in 63 minutes over the highway; avoiding it costs 5.8 km and 32 minutes.

Costing profiles: auto, bicycle, pedestrian, truck, motorcycle, bus, taxi, motor_scooter. Truck costing takes vehicle dimensions:

curl localhost:4326/valhalla/route -d '{
  "locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],
  "costing":"truck",
  "costing_options":{"truck":{"height":4.11,"weight":21.77,"axle_load":9.07}}
}'

To route around an area, pass exclude_polygons - all polygons in one request may total at most 10 km of perimeter, summed across every polygon rather than measured per polygon. Each vertex is a [lon, lat] pair, the reverse of the lat/lon keys locations uses in the same request:

curl localhost:4326/valhalla/route -d '{
  "locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],
  "costing":"auto",
  "exclude_polygons":[[[33.36,35.15],[33.38,35.15],[33.38,35.17],[33.36,35.17],[33.36,35.15]]]
}'
{"trip":{"summary":{"length":88.282,"time":4004.178}}}

Decoding the shape: every leg carries shape as an encoded polyline at precision 6, while Google's algorithm and most off-the-shelf decoders default to precision 5. Decoded at precision 5, the route lands roughly ten times away from where it belongs. Use a precision-6 decoder, or ask for GeoJSON instead:

curl localhost:4326/valhalla/route -d '{
  "locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],
  "costing":"auto",
  "shape_format":"geojson"
}'

Time-dependent route

date_time accepts type: 0 for "depart now", type: 1 for "depart at", type: 2 for "arrive by", and type: 3 for "invariant" - the clock does not advance along the route, so every road is evaluated at the same moment. The answer carries local times and time zones per location:

curl localhost:4326/valhalla/route -d '{
  "locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413}],
  "costing":"auto",
  "date_time":{"type":1,"value":"2026-08-03T08:00"}
}'
{"trip":{"locations":[
  {"lat":35.1856,"lon":33.3823,"date_time":"2026-08-03T08:00","time_zone_offset":"+03:00","time_zone_name":"Asia/Nicosia"},
  {"lat":34.6786,"lon":33.0413,"date_time":"2026-08-03T09:01","time_zone_offset":"+03:00","time_zone_name":"Asia/Nicosia"}]}}

Departure time picks up time-of-day access restrictions and daylight-saving arithmetic, including routes that cross a time zone. It does not change travel speeds: the images carry no traffic data, so travel time is the same at rush hour and at night.

Optimized route

Reorders the stops between the first and the last to make the trip shortest. Nicosia, Paphos, Larnaca, Limassol in that order is 350.4 km:

curl localhost:4326/valhalla/optimized_route -d '{
  "locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.7754,"lon":32.4245},
               {"lat":34.9229,"lon":33.6233},{"lat":34.6786,"lon":33.0413}],
  "costing":"auto"
}'
{"trip":{"summary":{"length":249.218},"locations":[{"original_index":0},{"original_index":2},{"original_index":1},{"original_index":3}]}}

original_index maps each stop back to its position in the request - here the optimizer saved 101 km.

Time and distance matrix

Travel time and distance for every source-target pair, in one request:

curl localhost:4326/valhalla/sources_to_targets -d '{
  "sources":[{"lat":35.1856,"lon":33.3823}],
  "targets":[{"lat":34.9229,"lon":33.6233},{"lat":35.0333,"lon":33.2000}],
  "costing":"auto"
}'
{"sources_to_targets":[[{"from_index":0,"to_index":0,"time":2259,"distance":47.447},
                        {"from_index":0,"to_index":1,"time":3975,"distance":32.935}]]}

Keep the pairs regional: distant pairs come back with null time and distance even though a direct route request between the same points succeeds.

Isochrone

How far you get in 10 and 20 minutes by car, as polygons:

curl localhost:4326/valhalla/isochrone -d '{
  "locations":[{"lat":35.1856,"lon":33.3823}],
  "costing":"auto",
  "contours":[{"time":10,"color":"ff0000"},{"time":20,"color":"0000ff"}],
  "polygons":true,
  "denoise":0.5,
  "generalize":50
}'
{"type":"FeatureCollection","features":[{"properties":{"contour":20.0,"metric":"time","color":"#0000ff"},"geometry":{"type":"Polygon","coordinates":[...]}}]}

Drop the coordinates straight into any map library. Contours can be distances instead of times ({"distance":1.5}). Limits: up to 4 contours, 120 minutes or 200 km per contour, one location per request.

Meeting point

Where several people should meet, by travel time rather than by geometry:

curl localhost:4326/valhalla/centroid -d '{
  "locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.6786,"lon":33.0413},{"lat":34.9229,"lon":33.6233}],
  "costing":"auto"
}'
{"trip":{"locations":[{"lat":35.1856,"lon":33.3823},{"lat":34.83793,"lon":33.40012}],"summary":{"length":42.461,"time":929.531}}}

With three input stops, the top-level trip only carries the route from the first stop (trip.locations[0]) to the converging point (trip.locations[1]). The routes from the other input stops to that same point come back as full route objects - each with its own trip.locations and summary - in the alternates array, one per remaining stop in input order.

Map matching

Snap a raw GPS trace onto the road network and get a normal route back:

curl localhost:4326/valhalla/trace_route -d '{
  "shape":[{"lat":35.1856,"lon":33.3823},{"lat":35.1860,"lon":33.3830},
           {"lat":35.1869,"lon":33.3841},{"lat":35.1880,"lon":33.3855}],
  "costing":"auto",
  "shape_match":"map_snap"
}'
{"trip":{"summary":{"length":0.584,"time":79.172},"legs":[{"maneuvers":[{"street_names":["Agiou Dimitriou"]}]}]}}

/trace_attributes returns the matched road segments instead of driving instructions, and filters keeps the response to the attributes you asked for:

curl localhost:4326/valhalla/trace_attributes -d '{
  "shape":[{"lat":35.1856,"lon":33.3823},{"lat":35.1860,"lon":33.3830},
           {"lat":35.1869,"lon":33.3841},{"lat":35.1880,"lon":33.3855}],
  "costing":"auto",
  "shape_match":"map_snap",
  "filters":{"attributes":["edge.names","edge.speed","edge.road_class","edge.length"],"action":"include"}
}'
{"edges":[{"names":["Agiou Dimitriou"],"speed":30,"road_class":"residential","length":0.039},
          {"names":["Plapouta"],"speed":30,"road_class":"residential","length":0.007}]}

Expansion

The search tree the router walked, as GeoJSON - useful for debugging a surprising route or visualizing reachability:

curl localhost:4326/valhalla/expansion -d '{
  "locations":[{"lat":35.1856,"lon":33.3823}],
  "costing":"auto",
  "action":"isochrone",
  "contours":[{"time":1}],
  "expansion_properties":["edge_id","cost","distance","edge_status"]
}'
{"type":"FeatureCollection","properties":{"algorithm":"dijkstras"},
 "features":[{"properties":{"distance":161,"cost":21,"edge_status":"s","edge_id":3473392795306}}]}

action can also be route; expect a large response, since it contains every edge the search touched.

Locate

What road a coordinate belongs to:

curl localhost:4326/valhalla/locate -d '{
  "locations":[{"lat":35.1856,"lon":33.3823}],
  "costing":"auto",
  "verbose":true
}'
[{"input_lat":35.1856,"input_lon":33.3823,"edges":[{"edge_info":{"names":["Zappeiou"],"way_id":21056849,"speed_limit":0}}]}]

speed_limit is 0 where OSM carries no maxspeed tag - routing still uses the profile's default speed for that road class.

Road network tiles

The routing graph itself, as Mapbox Vector Tiles - layers edges, nodes, shortcuts and access_restrictions, with per-edge attributes like road class and speed. Not a basemap: no buildings, land use or labels, just every road the router knows about. The tile address goes in a nested tile object; the common /z/x/y.mvt path form is not supported:

curl localhost:4326/valhalla/tile -d '{"tile":{"z":14,"x":9711,"y":6479}}' -o nicosia.mvt

The same request as a GET with URL-encoded JSON works as a tile template - point MapLibre at it and the road network renders like any vector source:

sources: {
  valhalla: {
    type: "vector",
    tiles: ["http://localhost:4326/valhalla/tile?json=%7B%22tile%22%3A%7B%22z%22%3A{z}%2C%22x%22%3A{x}%2C%22y%22%3A{y}%7D%7D"],
    minzoom: 7
  }
}

Style the edges source-layer to see the network. Low zoom levels carry only the bigger road classes; the endpoint is marked beta upstream. For a real basemap with buildings, land use, water and labels, see Map.

Geocoding

Search

Place name to coordinates. lang picks the language of the returned names:

curl "localhost:4326/photon/api?q=Nicosia&limit=1&lang=en"
{"features":[{"geometry":{"type":"Point","coordinates":[33.3638783,35.1746503]},
  "properties":{"name":"Nicosia","type":"district","country":"Cyprus","state":"Cyprus"}}]}

Without lang the same query returns local names - Λευκωσία - Lefkoşa, Κύπρος - Kıbrıs.

The index is built for prefix matching, so autocomplete is the same endpoint with a partial query:

curl "localhost:4326/photon/api?q=Limas&limit=5&lang=en"
{"features":[{"properties":{"name":"Limassol","type":"district"}},
             {"properties":{"name":"Limassol District","type":"county"}},
             {"properties":{"name":"Limassol","type":"city"}},
             {"properties":{"name":"Limassol Medieval Castle","type":"house"}},
             {"properties":{"name":"Limassol Salt Lake","type":"other"}}]}

Narrow the results by proximity, by bounding box, by result layer, by country, or by OSM tag - bbox takes minLon,minLat,maxLon,maxLat, the reverse order of the lat/lon query parameters on the same endpoint:

curl "localhost:4326/photon/api?q=Agios&limit=3&lat=34.6786&lon=33.0413&lang=en"
curl "localhost:4326/photon/api?q=Agios&limit=3&bbox=32.9,34.6,33.2,34.8&lang=en"
curl "localhost:4326/photon/api?q=Larnaca&limit=3&layer=city&lang=en"
curl "localhost:4326/photon/api?q=Nicosia&limit=3&countrycode=CY&lang=en"
curl "localhost:4326/photon/api?q=hospital&limit=3&osm_tag=amenity:hospital&lang=en"

The last one turns the geocoder into a POI search:

{"features":[{"properties":{"name":"Nicosia General Hospital","osm_value":"hospital","city":"Apostolos Varnavas & Agios Makarios"}},
             {"properties":{"name":"Paphos General Hospital","osm_value":"hospital","city":"Paphos"}},
             {"properties":{"name":"Lito Private Hospital","osm_value":"hospital","city":"Paralimni"}}]}

A few more dials: zoom and location_bias_scale tune how strongly the lat/lon bias pulls results toward the focus point, dedupe=0 keeps near-duplicate entries the geocoder would otherwise fold, and include/exclude filter by category (osm.<key>.<value>). A category with no q at all is pure discovery - everything of one kind near a point:

curl "localhost:4326/photon/api?include=osm.amenity.hospital&limit=3&lat=35.1856&lon=33.3823&lang=en"

Structured search

When the address already comes split into fields - a checkout form, a CRM record - skip free-text guessing and pass the fields directly. Any subset of street, housenumber, city, district, county, state, postcode and countrycode works:

curl "localhost:4326/photon/structured?street=Zappeiou&housenumber=21&city=Nicosia&lang=en"
{"features":[{"geometry":{"type":"Point","coordinates":[33.3824628,35.1852708]},
  "properties":{"housenumber":"21","street":"Zappeiou","city":"Nicosia","postcode":"1036","type":"house"}}]}

At least one field is required, and q is not accepted here - free text and structured fields cannot mix in one request.

Reverse geocoding

Coordinates to address:

curl "localhost:4326/photon/reverse?lat=35.1853&lon=33.3825&limit=1"
{"features":[{"properties":{"street":"Zappeiou","housenumber":"21","city":"Λευκωσία","postcode":"1036"}}]}

radius (km) widens the search, layer restricts what comes back - streets only, for example:

curl "localhost:4326/photon/reverse?lat=35.1853&lon=33.3825&radius=5&limit=3&layer=street&lang=en"
{"features":[{"properties":{"name":"Perikleous","type":"street"}},
             {"properties":{"name":"Zappeiou","type":"street"}},
             {"properties":{"name":"Gianni Tsiatala","type":"street"}}]}

Buildings come back without a name - they carry housenumber and street instead, so check the number of features rather than the presence of a name.

Map

Martin serves a full vector basemap built by Planetiler from the same OSM snapshot as routing and geocoding - tiles, fonts, icons, a ready-to-use style, static map images and rendered raster tiles, all under /martin. Any map rendered from these tiles must show visible credit: (c) OpenMapTiles (c) OpenStreetMap contributors. The tileset carries that string in its TileJSON, so a MapLibre map built on the shipped style displays it on its own. Static images and rendered raster tiles come back as bare pixels - whatever page or document they land in has to carry the credit.

The shipped style is OpenFreeMap Bright, a maintained fork of OSM Bright, vendored here with five edits and the icons that come with it; make update-bright-style diffs the vendored copy against upstream. The tiles follow the OpenMapTiles schema, so any style written for that schema works instead - point a client at your own and serve it from wherever you like.

Vector tiles

TileJSON describes the tileset; /basemap/{z}/{x}/{y} serves the tiles themselves:

curl localhost:4326/martin/basemap
{"tiles":["http://localhost:4326/martin/basemap/{z}/{x}/{y}"],"name":"OpenMapTiles","attribution":"<a href=\"https://www.openmaptiles.org/\" target=\"_blank\">&copy; OpenMapTiles</a> <a href=\"https://www.openstreetmap.org/copyright\" target=\"_blank\">&copy; OpenStreetMap contributors</a>","bounds":[31.95244,34.2337399,34.96147,36.00323],"minzoom":0,"maxzoom":14}

Point MapLibre GL JS at the ready-made style instead of wiring up sources by hand:

import maplibregl from "maplibre-gl";

new maplibregl.Map({
  container: "map",
  style: "http://localhost:4326/martin/style/bright",
  center: [33.3823, 35.1856],
  zoom: 12
});

Fonts and icons

/font/{fontstack}/{range} cuts glyph PBFs on the fly for whatever text a style's layers need - curl "localhost:4326/martin/font/Noto%20Sans%20Regular/0-255" (URL-encode the space in the fontstack name). /sprite/bright.png and /sprite/bright.json (plus @2x and an sdf_sprite/ variant for tintable icons) serve the icon images the style's point layers reference.

Static images

Render a PNG or JPEG server-side, no browser or JavaScript involved - point, zoom and size go in the path:

curl "localhost:4326/martin/style/bright/static/33.3823,35.1856,13/600x400.png" -o map.png

POST a GeoJSON FeatureCollection to draw markers, lines or polygons on top of the same view:

curl -X POST "localhost:4326/martin/style/bright/static/33.3823,35.1856,13/600x400.png" \
  -H "Content-Type: application/json" \
  -d '{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[33.3823,35.1856]},"properties":{"circle-radius":8,"circle-color":"#e11"}}]}' \
  -o map-overlay.png

Both come back as a 600x400 PNG - the second one with a red dot over Nicosia.

Rendered raster tiles

The same style pre-rendered into ordinary {z}/{x}/{y}.png raster tiles, for clients that don't speak vector tiles:

curl "localhost:4326/martin/style/bright/12/2427/1619.png" -o tile.png

Serving to another host

The style's tile/glyph/sprite URLs default to http://localhost:4326/martin - fine inside a single container, wrong once a browser on another machine needs to fetch them. Set PUBLIC_URL to the martin-prefixed address a browser can reach, and the entrypoint templates it into the style at startup:

docker run -p 4326:4326 \
  -e PUBLIC_URL=https://maps.example.com/martin \
  ghcr.io/roma8ok/getmapstack/cyprus

Static images and rendered raster tiles are drawn by Martin inside the container, which often cannot reach PUBLIC_URL itself - a public hostname, a certificate, a firewall in the way. It does not have to: a render request under /style/bright/... is served from an internal copy of the style whose URLs never leave the container, whatever PUBLIC_URL is set to. One style id, bright, for browsers and for rendering alike.

Live PostGIS overlay

Set MARTIN_POSTGRES to a Postgres connection string and Martin adds every table with a geometry column as an extra tile source at /{table}/{z}/{x}/{y}, auto-discovered, no config file to edit:

environment:
  MARTIN_POSTGRES: postgres://user:password@host:5432/dbname

Leave it unset and the map stays fully static - no database, no extra moving part.

Data freshness

The routing and geocoding engines report their version and when their data was built:

curl localhost:4326/valhalla/status
{"version":"3.8.3","tileset_last_modified":1785100041,"available_actions":[...]}
curl localhost:4326/photon/status
{"status":"Ok","import_date":"2026-07-25T18:00:19Z","version":"1.2.1","git_commit":"b9d6ab92"}

Both timestamps are build times, not the OpenStreetMap snapshot date. Every published image also carries a date tag matching the OSM extract it was built from, next to latest. The vector tileset carries the same snapshot: all three services build from one pinned OSM download.

The map service answers curl localhost:4326/martin/health with OK - a liveness check, no version or build date. The tileset's own metadata sits in its TileJSON instead.

One probe covers the whole container: curl localhost:4326/healthz returns {"status":"ok"} once routing, geocoding and tiles all answer, and HTTP 503 with {"error":"upstream unavailable","status":503} while any of them is still opening its data or has died. That is the probe to give a load balancer or an orchestrator, and the one the image's own healthcheck runs. The verdict is cached for about five seconds, so polling it costs the engines nothing.

Environment variables

Everything the container publishes arrives through one process, and these are its knobs. All have working defaults - a plain docker run needs none of them.

Variable Default What it does
GMS_LISTEN :4326 Address inside the container. -p 8080:4326 is the usual way to change the port you connect to; this changes the one the container itself binds.
GMS_MAX_BODY_BYTES 10485760 Largest request body accepted, in bytes; over it, HTTP 413. Raise it for very long GPS traces posted to /valhalla/trace_attributes. The time a client is given to upload a body scales with this value, so a bigger limit is also a longer upload window.
GMS_UPSTREAM_TIMEOUT 60s How long an engine has to answer before the request fails with HTTP 504. Any Go duration (90s, 2m).
GMS_EXPLORER_ROOT /data/explorer Directory served at /. Mount your own page over it, or point this elsewhere, to replace the explorer.

Two more configure the map service specifically: PUBLIC_URL (serving to another host) and MARTIN_POSTGRES (live PostGIS overlay).

Not included

  • Elevation. Tiles are built without elevation data: /height answers with null values and routes carry no grade. Adding it means changing how the tiles are built and supplying a separate elevation dataset - it is not a flag on these images.
  • Traffic. No live or historical traffic. Travel times use free-flow speeds, so a route takes the same time at 08:00 and at 23:00.
  • Public transport. No GTFS feed is imported. costing: multimodal fails with "Locations are in unconnected regions" rather than falling back to walking, and /transit_available always answers false.
  • Full geometries. The geocoding index stores points only: geometry=1 answers HTTP 400. Area features still carry an extent bounding box where OSM has one.
  • One country per image. A route that leaves the country in the image has no data to follow - run the image for the country you need, or run several.

Build it yourself

Requires Docker. Build a country image locally instead of pulling from GHCR:

git clone https://github.com/roma8ok/getmapstack.git
cd getmapstack
make build-valhalla-builder
make build-photon-builder
make build-planetiler-builder
make fetch-osm COUNTRY=cyprus
make create-valhalla-tiles COUNTRY=cyprus
make create-photon-data COUNTRY=cyprus
make create-vector-tiles COUNTRY=cyprus
make build-server COUNTRY=cyprus
docker run -p 4326:4326 getmapstack/cyprus

fetch-osm pins one OSM snapshot so all three builders work from the same download. Intermediate artifacts (routing tiles, geocoding index, vector tiles) land in artifacts/. Images build for linux/amd64 and linux/arm64 by default - pass PLATFORMS=linux/arm64 (or your platform) for a faster single-arch build. make help lists all targets and available countries.

License

Code: MIT. Map data: © OpenStreetMap contributors, ODbL 1.0, sourced from Geofabrik extracts.

The images embed OSM-derived databases (routing tiles, geocoding index, vector tiles) redistributed under ODbL 1.0 - see NOTICE for full attribution. If you publicly use routing or geocoding results from these images, credit OpenStreetMap: "© OpenStreetMap contributors" linked to openstreetmap.org/copyright.

The basemap adds third-party design work: the tiles follow the OpenMapTiles schema (CC-BY 4.0), the style is OpenFreeMap Bright - a fork of OpenMapTiles' OSM Bright, style code BSD-3-Clause, style design CC-BY 4.0, the fork's own changes MIT - with Maki icons under CC0 1.0 and Noto fonts under OFL 1.1. Full license texts ship inside every image at /usr/share/doc/getmapstack/THIRD_PARTY_LICENSES.

About

Self-hosted mapping stack. One command per country. Replace Google Maps API - no API keys, no rate limits, no vendor lock-in.

Resources

Stars

49 stars

Watchers

3 watching

Forks

Packages

Contributors

Languages