Skip to content
Go back

Self-Hosted Maps Stack Picker

By SumGuy 13 min read
Self-Hosted Maps Stack Picker

Before You Install Everything

The maps cluster has covered a lot of ground. Nominatim, Photon, Pelias, OSRM, Valhalla, GraphHopper, BRouter, Tileserver-GL, planetiler, PMTiles, MapLibre, OpenRouteService, OsmAnd — over a dozen tools, each with its own import pipeline, data requirements, and operational quirks. If you’ve been reading along, there’s a real chance you now feel like you need to run all of it. You don’t.

Here’s the thing about self-hosted maps: the tooling exists at every level of complexity because the use cases exist at every level of complexity. A small home automation setup that just wants to reverse-geocode phone location has no business running a full routing engine. A cycling app serving 10,000 users has no business capping itself to static tiles with no navigation.

This post is a decision tool. Tell me what you’re actually trying to do and what hardware you have. We’ll figure out the smallest stack that solves your problem, with no extra moving parts.

The Four Shapes of a Maps Need

Before picking components, you need to identify which shape your use case has. Almost everything maps-related falls into one of four buckets:

Geocoding only — You have addresses and you want coordinates (or the reverse). No navigation, no tile rendering, just lookup. This is the most common home lab use case and also the one where people most dramatically over-build.

Routing only — You need turn-by-turn directions, travel time estimates, or isochrone generation (reachability maps). You might already be handling tiles and geocoding via third-party APIs or you’re embedding into an app that handles its own display layer.

Tiles only — You want to serve your own map imagery: vector tiles, raster tiles, or both. Your users see a map. You don’t need directions built-in. Maybe you’re replacing a Mapbox embed, or building a dashboard, or serving OsmAnd clients.

Full stack — You genuinely need all three: addresses resolve to coordinates, those coordinates show up on your own tiles, and users can navigate between them. This is the legitimate “get off all third-party APIs” setup. It’s real work, but it’s achievable on a single beefy machine.

Figure out your shape first. The rest of this post is organized around it.


Geocoding-Only Path

If your shape is geocoding-only, the decision tree is short.

Start with Nominatim. It covers 90% of geocoding needs: forward geocoding, reverse geocoding, structured search. One Docker image, one regional PBF, a few hours of import time. If you’re reverse-geocoding home assistant device locations, backfilling addresses on old records, or resolving user-submitted addresses in a small app, Nominatim is the answer. Full setup: Nominatim: Self-Hosted Geocoding.

Add Photon if you have a user-facing search box. Nominatim’s search is decent for structured input and tolerable for fuzzy queries, but it’s not optimized for the “user typing pizz and expecting autocomplete” pattern. Photon solves that — same OSM data, built for typeahead latency. The stack is Nominatim (for reverse geocoding and structured lookup) plus Photon (for the search widget). See Nominatim vs Photon vs Pelias for when each one wins.

Add libpostal only if address parsing is a bottleneck. Free-form international address input where structured parsing accuracy matters — libpostal’s normalization helps. This is a narrow use case. If you’re unsure, you don’t need it.

Reverse geocoding for a photo library? Nominatim pairs naturally with Immich and PhotoPrism. Both support custom geocoding endpoints. Point them at your local Nominatim instance and your GPS-tagged photos stop phoning home every time you open an album. Same principle as the Home Assistant reverse geocoding post.

Hardware for geocoding-only:

ExtractRAMDiskImport time
Single US state4–8 GB20–80 GB15–60 min
USA only8–16 GB200–350 GB NVMe4–10 hr
Europe16–32 GB400–600 GB NVMe8–18 hr
Full planet64+ GB1+ TB NVMe2–4 days

The regional extracts are where the value is. If you’re US-based and serving US addresses, you don’t need the planet. The north-america-latest.osm.pbf fits on a 1 TB NVMe with room for the OS and other services.


Routing-Only Path

Your use case has directions. Either you’re already handling tiles via MapLibre against a third-party style, or you’re building something headless (ETAs, logistics, time-matrix queries) where you don’t need to show a map at all.

Pick one of three engines:

OSRM is the fastest. If you need raw routing speed and your profile needs are simple — driving, walking, cycling, custom but unchanging — OSRM delivers. Sub-10ms response times for most queries. The trade-off: changing the profile means re-building the graph, which takes time. OSRM is a great choice when you know your use case is stable and you need throughput. It’s a bad choice when you want to experiment with routing profiles or need multi-modal routing (e.g., combine walking + transit).

Valhalla is the flexible option. It supports more transportation modes, allows run-time costing adjustments without re-ingesting, and has a richer API surface including isochrone generation and map matching. It’s slightly slower than OSRM on raw point-to-point queries, but for most applications the difference is invisible. If you’re building a real app with non-trivial routing requirements, Valhalla is usually the better long-term choice. Full comparison: OSRM vs Valhalla vs GraphHopper.

GraphHopper splits the difference. Good routing speed, JVM operational model, solid documentation. Comfortable choice if you already run JVM services. If you don’t, OSRM or Valhalla will have fewer surprises.

BRouter for cycling. BRouter is built specifically for cyclists. It understands elevation profiles, surface types, cycleway classifications, and configurable preference files. If you’re building a cycling route planner, BRouter is the right tool. For everything else, it’s the wrong tool.

Hardware for routing-only:

CoverageRAMDiskBuild time
Single country (small)4–8 GB10–50 GB20–60 min
USA8–16 GB50–150 GB2–6 hr
Europe16–32 GB100–250 GB4–12 hr
Full planet64+ GB300+ GB NVMe12–36 hr

Routing graph builds are CPU-bound. More cores helps directly. A 16-core machine will finish a US routing graph in roughly half the time of an 8-core box.


Tiles-Only Path

You want to serve map tiles. Maybe you’re replacing a Mapbox embed, or you want a self-hosted base layer for a dashboard, or you’re serving OsmAnd clients on your LAN.

The modern tiles path looks like this: planetiler → PMTiles or MBTiles → Tileserver-GL (or a serverless PMTiles server) → MapLibre on the frontend.

planetiler generates vector tiles from an OSM PBF in one shot. It’s dramatically faster than older tile-generation tools and produces spec-compliant tiles. One command in, one .pmtiles or .mbtiles archive out.

PMTiles is the format you want for new setups. A single-file archive that supports HTTP range requests — meaning you can serve it from Cloudflare R2, any static file server, or even a plain Nginx with no tile server software at all. No running process, no port to maintain. See Tileserver-GL vs OpenMapTiles vs Renderd for when each serving approach makes sense.

Tileserver-GL is the right choice when you need raster tile rendering (PNG/JPEG output for older clients), WMTS endpoints for GIS software, or a serving layer with multiple style support. It’s a real server process with real operational overhead, but it’s solid and well-maintained.

MapLibre GL JS on the frontend. It’s the open-source fork of Mapbox GL JS, takes the same style spec, supports PMTiles natively as of recent versions. For a self-hosted stack with no Mapbox involvement, MapLibre is the obvious choice. More in the MapLibre vs Mapbox deep-dive.

OsmAnd clients on your LAN? Serve your tiles and your BRouter graphs from the same box. OsmAnd supports custom tile sources and custom BRouter endpoints. The setup is covered in OsmAnd with Self-Hosted Tiles and BRouter — it’s one of the more satisfying setups to get working.

Hardware for tiles-only:

CoverageRAMDiskplanetiler time
Single US state4 GB5–30 GB10–40 min
USA8 GB30–80 GB1–3 hr
Europe16 GB80–150 GB2–5 hr
Full planet32 GB150–400 GB4–10 hr

planetiler is more RAM-efficient than older tools. A full planet with 16 GB RAM is feasible if you accept slower processing. 32 GB is comfortable.


Full-Stack Path

You need the whole thing: addresses resolve to your coordinates, tiles are served from your hardware, users can navigate between points. This is the real “cut the cord” setup.

You have two approaches:

OpenRouteService as a bundle. OpenRouteService ships a Docker image that includes geocoding, routing (Valhalla under the hood), and isochrones in one package. The operational simplicity is real — it’s one service, one import, one API surface to manage. If you want to ship something quickly and don’t need fine-grained control over each component, OpenRouteService is a reasonable choice. Details in OpenRouteService: The All-in-One Stack.

The trade-off: you’re locked into ORS’s choices for each component. When Valhalla updates and ORS hasn’t caught up, you wait. Bundles trade flexibility for convenience.

Nominatim + OSRM/Valhalla + Tileserver-GL separately. More containers, more configuration — but more control. Each component updates on its own schedule. You can run Nominatim on the big-NVMe box and Tileserver-GL on the fast-CPU box. When something breaks, you know exactly which piece broke.

A minimal full-stack Compose structure looks like this:

docker-compose.yml
services:
nominatim:
image: mediagis/nominatim:4.5
ports: ["8080:8080"]
environment:
PBF_URL: https://download.geofabrik.de/north-america-latest.osm.pbf
REPLICATION_URL: https://download.geofabrik.de/north-america-updates/
volumes:
- nominatim-data:/var/lib/postgresql/16/main
shm_size: 1gb
restart: unless-stopped
valhalla:
image: ghcr.io/gis-ops/docker-valhalla/valhalla:latest
ports: ["8002:8002"]
volumes:
- ./valhalla_data:/custom_files
environment:
tile_urls: https://download.geofabrik.de/north-america-latest.osm.pbf
restart: unless-stopped
tileserver:
image: maptiler/tileserver-gl:latest
ports: ["8081:8080"]
volumes:
- ./tiles:/data
restart: unless-stopped
volumes:
nominatim-data:

Starting point, not a production config. Each service needs its own tuning, but this shape gives you the three planes — geocoding, routing, tiles — each replaceable independently.

Full-stack hardware budget:

CoverageRAMDiskSetup time
USA on one box32–64 GB600 GB–1 TB NVMe12–24 hr first run
Planet on one box128+ GB3+ TB NVMe3–7 days
USA split across two boxes16 GB each400 GB each8–16 hr

Most hobbyists should start with USA or a regional extract. Expand only when the actual need exists, not because the planet extract exists.


Mobile Use Case

If your goal is offline mobile navigation, the stack looks different than the server-side cases above.

OsmAnd is the client. It’s the de-facto open-source mobile navigation app, runs on Android and iOS, and supports custom tile sources and routing backends. Your job is to give it:

The OsmAnd with self-hosted tiles post covers the integration. The short version: you configure a custom tile source URL in OsmAnd pointing at your Tileserver-GL instance, and configure BRouter as a custom routing provider. Once it’s set up, the phone navigates against your hardware and not against anyone else’s servers.


Maintenance Reality

Running a maps stack is not fire-and-forget. Here’s the honest picture of what each component requires ongoing:

Nominatim — Daily diffs from the Geofabrik replication endpoint apply in seconds. Weekly is fine for most setups. Major version upgrades require a re-index (hours). Plan for it.

Routing engines — OSM extracts go stale. A routing graph on a 12-month-old extract will route you down closed roads and miss new ones. Re-building quarterly is reasonable — hours for OSRM/Valhalla on US data, overnight for planet. Calendar it.

Tiles — Same story. Regenerating a US tile archive monthly with planetiler is realistic. Full planet, quarterly. Static use cases (historical data, custom layers) can skip this entirely.

Tileserver-GL — Mostly stateless. Upgrade the image, restart, done.

The combined maintenance burden for a US-only full stack is roughly 2–4 hours a quarter for data refreshes. Geocoding-only with daily diffs: almost nothing. Factor this in when deciding how much stack to run.


The Decision Matrix

If you want to skip everything above and go straight to the answer:

I need…Hardware I haveRun this
Address lookup / reverse geocodingAny box with 16 GB RAM + NVMeNominatim (regional extract)
Fuzzy autocomplete searchSame as aboveNominatim + Photon
Routing (speed priority)8+ GB RAMOSRM
Routing (flexible profiles)8+ GB RAMValhalla
Cycling-specific routing4+ GB RAMBRouter
Serve vector tiles8+ GB RAMplanetiler + Tileserver-GL (or PMTiles)
Map display in browserAnyMapLibre GL JS
Offline mobile navigationServer: 8+ GB, phone: OsmAndTileserver-GL + BRouter + OsmAnd
Full geocoding + routing + tiles (USA)32–64 GB RAM, 600 GB+ NVMeNominatim + Valhalla + Tileserver-GL
Everything, all at once, one image16+ GB RAMOpenRouteService (bundle)
Full planet, full stack128+ GB RAM, 3+ TB NVMeSeparate components, see full-stack path

The Principle Behind the Matrix

The single most useful thing I can tell you after spending this cluster on maps tooling: run the fewest moving parts that solve your actual problem.

Maps infrastructure accumulates. You start with Nominatim. Then you add Valhalla “just in case.” Then Tileserver-GL because it seems cool. Three months later you have a stack nobody touches except when something breaks, and you’re spending a Saturday re-importing OSM data for a service one script pings twice a month.

Pick your shape. Start with the minimum viable component set. Expand when you hit a real limitation, not when you anticipate a hypothetical one.

The geocoding-only path is probably where 60% of people reading this should stop. Nominatim alone solves most address use cases.

Your 2 AM self will appreciate having fewer things to restart.

Maps Cluster Posts


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Boundary vs Teleport

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts