Skip to content

50,000 free calls a month, card-free. Get an API key →

Documentation menu
docs / self-host · raw .md
docker compose up: a whole maps stack · 2:24 · all videos

Self-hosting

The point of MapMap is that you can run all of it yourself: the gateway, our routing engine, an optional geocoder, and the MCP server, from one Docker Compose file. This page is the orientation and a working first boot; the full production guides (sizing, backup, upgrades, hardening, air-gapped deployment) ship inside the source checkout.

Status, honestly: the hosted gateway at https://api.mapmap.ai is live; see conventions. Self-hosting is for when you want the same stack on your own infrastructure, under your own keys and data boundary.

What you need

  • Docker Engine 24+ with Compose v2 (docker compose version)
  • ~10 GB disk and 8 GB RAM for a UK-scale first tile build (serving needs less), plus ~1.5 GB once for shared global base-map sources (ocean water polygons, Natural Earth)
  • Outbound internet on first boot only (OSM extract and global base-map source downloads); air-gapped installs are supported via pre-built territory packages; a territory package bundles pre-built routing tiles and map data for a region, so no OSM download is needed

Get the code

The repository is private: source access comes with a commercial relationship or an evaluation. Email hello@mapmap.ai; you get the full workspace, including the distro/ Compose stack and the production operations guides. Everything below assumes you have a checkout.

The short version

sh
cd distro
cp .env.example .env

# Admin token: protects /admin/*. Compose refuses to start without it.
sed -i '' "s/^SN_ADMIN_TOKEN=$/SN_ADMIN_TOKEN=$(openssl rand -hex 32)/" .env   # macOS
# sed -i "s/^SN_ADMIN_TOKEN=$/SN_ADMIN_TOKEN=$(openssl rand -hex 32)/" .env    # Linux

Pick a territory: for a bootstrap build straight from OSM (the simplest first run), also set in .env:

sh
VALHALLA_TILE_URLS=https://download.geofabrik.de/europe/united-kingdom-latest.osm.pbf

Any Geofabrik extract works. The production alternative is a pre-built territory package, which skips the build entirely. Then:

sh
docker compose up -d --build
docker compose logs -f valhalla   # first boot builds tiles: ~30–60 min for the UK

While tiles build, docker compose ps shows the gateway stuck in Created; that is expected, not broken: the gateway waits for the routing engine's healthcheck. You are up when this succeeds:

sh
curl -fsS http://localhost:8080/health

Create an API key

Key management lives under /admin/*, authenticated with your admin token. All three body fields are required; there are no defaults:

sh
source .env
curl -fsS -X POST http://localhost:8080/admin/keys \
  -H "Authorization: Bearer ${SN_ADMIN_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"name": "fleet-ops-dev", "monthly_quota": 100000, "rate_per_min": 60}'

Returns 201 Created:

json
{
  "id": "…",
  "name": "fleet-ops-dev",
  "key": "snk_…",
  "created_at": "2026-07-15T09:30:00Z",
  "monthly_quota": 100000,
  "rate_per_min": 60
}

The key field is shown once: store it in your secret manager; only its BLAKE3 hash is persisted. Keys, quotas and usage live in the gateway's sqlite volume (gateway_data); back that volume up. Self-hosted deployments can alternatively let users self-serve their own snk_ keys via POST /v1/keys (see the API reference).

StatusMeaning
400Invalid name, quota or rate
401Missing or wrong admin token; source .env again and check the running container's token

First route

Prove the tiles built: a UK truck route (Dover → Birmingham), 4.0 m high, 44 t, avoiding tunnels that forbid tunnel code D loads under ADR (the European agreement on carriage of dangerous goods by road; tunnel codes B–E restrict which tunnels a hazmat load may use; full table in conventions):

sh
curl -fsS -X POST http://localhost:8080/route \
  -H "Authorization: Bearer snk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "locations": [
      {"lat": 51.1279, "lon": 1.3134},
      {"lat": 52.4862, "lon": -1.8904}
    ],
    "costing": "truck",
    "adr": {
      "dimensions": {
        "height_m": 4.0, "width_m": 2.55, "length_m": 16.5,
        "gross_weight_t": 44.0, "axle_load_t": null, "axle_count": null
      },
      "tunnel_code": "D", "hazmat": true
    }
  }'

Truncated response: a standard Valhalla trip, with the applied ADR profile echoed back:

json
{
  "trip": {
    "locations": [ { "lat": 51.1279, "lon": 1.3134 }, { "lat": 52.4862, "lon": -1.8904 } ],
    "legs": [ { "maneuvers": [ "…" ], "summary": { "time": 5400.0, "length": 160.0 }, "shape": "…" } ],
    "summary": { "time": 5400.0, "length": 160.0 },
    "status": 0,
    "status_message": "Found route between points",
    "units": "kilometers"
  },
  "adr": {
    "dimensions": { "height_m": 4.0, "width_m": 2.55, "length_m": 16.5, "gross_weight_t": 44.0 },
    "tunnel_code": "D", "hazmat": true
  }
}

time is seconds; length follows trip.units. A 502 here means the routing engine is unreachable or still building; check docker compose logs -f valhalla.

What's in the distro

ComponentRole
GatewayThe API: routing, ADR, metered keys, OpenAPI, llms.txt (port 8080)
Routing engineRoute computation with the bounded ADR costing extension
Geocoder (optional)Forward geocoding via Photon (geocoding compose profile)
MCP server (optional)Agent access over HTTP (mcp compose profile, port 8200)

Enable the optional services with compose profiles:

sh
docker compose --profile geocoding up -d photon   # needs the photon_data volume seeded first
docker compose --profile mcp up -d mcp            # agents connect to http://<host>:8200/mcp

Two ways to enable geocoding. The first-party path needs no extra service: point SN_GEOCODE_DIR at an sn-geocode index (a single territory's geocode layer from a signed package, or a multi-territory index merged with snfactory geocode-merge; the hosted gateway serves a whole-world merge this way) and the gateway serves /geocode and /geocode/reverse in-process from the same sn-geocode engine that runs on-device: no JVM, no separate search cluster, and identical ranking online and off. First-party POI hits include the details object of OSM display tags (opening hours, phone, website, …) that the Photon path cannot provide. This is the recommended path, and the only one that works air-gapped. The Photon path above is the fallback: it runs the Photon release JAR on a stock JRE, and its data volume must be seeded once with the JAR and a search-index dump (commands in the distro quickstart inside the checkout). First-party takes precedence when both are set. MCP configuration examples are on the MCP page.

Engines and optional services

  • SN_ROUTING_ENGINE selects the routing engine: valhalla (default), graphhopper, or auto. Every endpoint behaves identically either way, but graphhopper and auto need a GraphHopper server reachable at SN_GRAPHHOPPER_URL; it is not part of the default compose stack (a commented-out service block is provided). graphhopper serves every endpoint from GraphHopper; auto uses Valhalla first and falls back to GraphHopper on a Valhalla backend error.

  • Route optimisation runs as a sidecar; set SN_VROOM_URL to enable POST /optimise. Without it, that endpoint returns 503. Driver breaks, work-time limits and EU drivers'-hours generation are handled in the gateway on top of the solver; no extra service. See optimisation.

  • First-party geocoding: set SN_GEOCODE_DIR to a territory geocode index to serve /geocode and /geocode/reverse from sn-geocode in-process (no Photon). See geocoding.

  • Hosted tiles, styles, fonts and sprites are served from SN_TILES_DIR (the <territory>.pmtiles archives) and SN_MAP_ASSETS_DIR (glyphs and sprite sheets).

  • Feedback and telemetry sinks, all off unless you set them, all wired in distro/docker-compose.yml:

    VariableServiceEnables
    SN_MAP_ISSUES_DIRbothThe report_map_issue NDJSON queue; the gateway serves it at GET /admin/mcp-map-issues. Both services must agree on the path.
    SN_RETROS_DIRmcpSafety net for submit_integration_retro: every retro the gateway cannot take (no key, unreachable, rate-limited) is written here rather than lost. Set it even when the gateway path is configured. Nothing reads it automatically.
    SN_MCP_USAGE_DIRmcpOne NDJSON line per MCP tool call ({ts, tool, ok, duration_ms, error_code?}); never arguments or payloads. Unset = tracing only.
    SN_DEMAND_LOG_DIRgatewayCoarsened demand tap: one line per metered geospatial request, timestamps floored to 5 minutes and coordinates truncated to ~5 km geohash cells. Never key ids or raw coordinates.
    SN_ACTIVATION_EMAILgatewayDefault off, and it sends real email. On a key's first successful metered call, sends one note through the configured mailer (SN_RESEND_API_KEY / SN_MAIL_WEBHOOK_URL). Leave off unless you are the operator sending it; the activation.first_success tracing event fires either way.

    Retros submitted against your deployment land in your own integration_retros table and are readable at GET /admin/retros; they are never forwarded to MapMap. That table is only reached by the gateway POST, so a deployment with STUDIO_URL set but no gateway API key for the mcp service queues its retros in SN_RETROS_DIR instead of the admin console: mint an ordinary snk_… key and set GATEWAY_API_KEY if you want them in the console. Fuller operational notes, including the shared-path gotcha, are in deploy/hetzner/README.md §7b in the checkout.

  • Per-MAU SDK metering: an MAU is a distinct X-MapMap-User value per identity per calendar month. SN_MAU_INCLUDED (default 1000) sets the included count; SN_PRICE_PER_MAU_PENCE (default 30, GBP pence) prices each new user beyond it. Billing is fail-open: an uncovered debit flags the identity mau_overdrawn for the month and requests keep being served, never blocked. Set SN_PRICE_PER_MAU_PENCE=0 to disable billing entirely (typical for self-host).

Pointing clients at your instance

Everything that talks to the hosted gateway takes a base URL, so swap in your own origin. The web SDK accepts baseUrl in its map options; the MCP server in the compose stack already talks to your local engine directly. Put a reverse proxy with TLS in front of port 8080 before exposing the gateway beyond localhost; API keys travel in the Authorization header.

Production notes

  • For production tiles, prefer a signed territory package installed into the tiles volume over the OSM bootstrap build, and clear VALHALLA_TILE_URLS from .env once you manage tiles that way, so a container recreate never triggers an accidental rebuild.
  • Back up the gateway_data volume: it holds API keys, quotas and usage.
  • The checkout ships a full production guide covering sizing, backup, upgrades, hardening and air-gapped deployment.

Procurement pack

For buyer's counsel and security teams, the checkout ships a due-diligence pack: an SLA template (patch SLAs, support tiers, escrow), a security overview, a pre-answered vendor-security questionnaire, the licence-compliance story (permissive-only gate, ODbL handling), a deployment checklist, and a committed CycloneDX SBOM with a licence inventory, regenerated and published with every tagged release.

Next steps

  • Conventions: auth, error envelope, units, ADR tunnel codes
  • Territories: pre-built territory packages and air-gapped installs
  • MCP server: wire agents to your self-hosted stack
  • API reference: every endpoint, including /admin/*