The Collection Problem
If you’ve been following the self-hosted maps series, your docker-compose.yml is starting to look like a conference schedule. Nominatim for geocoding. OSRM for routing. A tile server for basemaps. PostGIS underneath all of it. Photon if you want fast autocomplete. Maybe Pelias if you need multi-source geocoding. Every piece is excellent at its job. Every piece is also its own container to manage, its own data pipeline to feed, its own port to remember.
Here’s the thing: most workloads don’t need the sharpest tool at every layer. They need good enough across all layers, deployed by one person, running on one server, answering before their next coffee break.
That’s the problem OpenRouteService solves. One stack. Directions, isochrones, matrix, geocoding, elevation. All of it behind one API, served out of one container, fed by one OSM extract. The Swiss Army knife approach to self-hosted mapping — not the sharpest blade in every category, but you only have to carry one knife.
Full example: Compose + extract pipeline at github.com/KingPin/sumguy-examples/tree/main/self-hosting/openrouteservice-all-in-one
What ORS Actually Is
OpenRouteService comes out of HeiGIT — the Heidelberg Institute for Geoinformation Technology, affiliated with Heidelberg University. These are people who study geographic information systems for a living, and they’ve been running a public instance of ORS at openrouteservice.org for years. The self-hosted version is the same software. You’re not running a stripped-down community fork.
The routing core is built on GraphHopper, a solid Java-based routing engine. ORS wraps it with an opinionated configuration system and layers on the additional APIs — isochrones, matrix, geocoding via Pelias, and elevation data from SRTM. The whole thing ships as a single Java application, which is refreshing compared to the usual microservices-all-the-way-down philosophy.
The public API also has a free tier with rate limits, so you can test against it before committing to the self-hosted setup. Just swap the base URL and your API key for your local instance URL. That transition is genuinely that clean.
What you get in one deployment:
- Directions API — turn-by-turn routing for car, bike, foot, wheelchair, HGV, and more
- Isochrones API — reachability polygons (“what’s within 15 minutes of this point”)
- Matrix API — many-to-many travel time/distance table for fleet and logistics work
- Geocoding — via Pelias, bundled and configured for OSM data
- Elevation — SRTM-based elevation profiles along routes
- Optimization — vehicle routing problem solving (VRP) via the Vroom integration
That last one — Vroom-based route optimization — is the kind of feature that usually requires a separate paid service or a lot of bespoke code. ORS includes it.
Hardware Reality Check
Let’s be honest about what “one stack” costs. ORS is a Java application built around GraphHopper graphs. Those graphs are stored in memory during operation, and they are not small.
For a single US state extract — say, California — expect:
- Import RAM: 4–8 GB during graph building (build time, not runtime)
- Runtime RAM: 3–6 GB depending on how many profiles you enable
- Disk: 10–20 GB for the built graphs, plus the PBF source
For a full US extract or a European country, double those numbers. For full-planet, you’re looking at 32 GB+ RAM just for runtime. The planet is not a home lab use case.
The practical sweet spot for home use is a regional PBF covering exactly what you need. If your application serves a metro area, use a state or sub-region extract. A Ryzen 5 mini PC with 16 GB RAM handles a single-state US extract comfortably at modest QPS.
How does this compare to running the pieces separately? Running Nominatim + OSRM + Pelias individually is actually heavier in aggregate — each has its own data import, its own RAM footprint, its own disk overhead. ORS consolidates the graph and the Pelias geocoding into a system that’s leaner than the sum of its parts. You trade per-service optimization for operational simplicity, and for most use cases, that’s the right trade.
The Compose Deployment
The official image is openrouteservice/openrouteservice. You’ll need your OSM extract as a .pbf file on the host — ORS reads it on first start and builds the routing graphs from it. Graph building happens inside the container, not during image pull. Plan for that time.
services: ors: image: openrouteservice/openrouteservice:latest container_name: ors ports: - "8080:8082" volumes: - ./ors-data:/home/ors environment: ORS_URL: http://localhost:8080/ors JAVA_OPTS: "-Xms2g -Xmx6g" ors.engine.source_file: /home/ors/files/region.osm.pbf ors.engine.profiles.car.enabled: true ors.engine.profiles.bike-regular.enabled: true ors.engine.profiles.foot-walking.enabled: true restart: unless-stoppedBefore starting, drop your PBF into ./ors-data/files/:
mkdir -p ors-data/files# Download a regional extract from Geofabrikcurl -L https://download.geofabrik.de/north-america/us/california-latest.osm.pbf \ -o ors-data/files/region.osm.pbf
docker compose up -ddocker compose logs -f orsGraph building takes 5–30 minutes depending on extract size and your hardware. Watch for a log line like Graphs were built successfully. Once that appears, ORS starts accepting requests.
The JAVA_OPTS env var controls heap. -Xmx6g caps the JVM at 6 GB. Set this based on your actual RAM — if you have 16 GB and you’re running a medium extract, -Xmx8g gives the JVM room to work without starving the rest of the system. Don’t set it higher than ~70% of available RAM.
Profile selection matters for both build time and runtime footprint. Only enable the profiles you actually use. Building car + bike + foot is the common case. Adding HGV (heavy goods vehicles), wheelchair, or e-bike adds significant graph weight.
Isochrones: The Killer Feature
Routing is useful. Isochrones are the thing that makes people’s eyes go wide in demos.
An isochrone answers: “Given this point, what area is reachable within N minutes by this mode of transport?” The result is a polygon — or a set of concentric polygons for multiple time thresholds — you can render on a map, use for geospatial joins, or feed into spatial queries.
Use cases:
- Real estate: show apartments within a 20-minute drive of an office
- Delivery zones: calculate true service areas based on drive time, not radius
- Accessibility planning: what’s reachable by wheelchair in 10 minutes from a given transit stop
- Emergency services: coverage analysis for response time modeling
- Retail site selection: how many customers live within 15 minutes of a potential location
The API call is simple:
curl -X POST http://localhost:8080/ors/v2/isochrones/driving-car \ -H "Content-Type: application/json" \ -d '{ "locations": [[-122.4194, 37.7749]], "range": [900, 1800], "range_type": "time" }'That asks for 15-minute and 30-minute drive-time isochrones around San Francisco’s city center. range values are in seconds. Swap driving-car for foot-walking or cycling-regular to get the same polygon for pedestrian or bike travel.
The response is GeoJSON — polygons ready to drop into any mapping library, PostGIS table, or QGIS layer:
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [[...]] }, "properties": { "value": 900, "center": [-122.4194, 37.7749], "area": 47.3 } } ]}The area property gives you the reachable area in square kilometers. Handy for quick sanity checks — if a 15-minute walk comes back as 200 km², something is wrong.
Matrix API: Fleet Logistics Without a SaaS Contract
The directions API gives you A→B routing. The matrix API gives you all the A→B distances at once, for many origins and many destinations simultaneously.
Concrete example: you have 5 delivery vehicles and 50 drop points. You need to know the travel time from each vehicle’s current location to each drop point to build an optimal assignment. That’s a 5×50 matrix — 250 route calculations. You could loop and call /directions 250 times. Or you call /matrix once and get all 250 values in a single response.
curl -X POST http://localhost:8080/ors/v2/matrix/driving-car \ -H "Content-Type: application/json" \ -d '{ "locations": [ [-122.4194, 37.7749], [-122.4089, 37.7853], [-122.3988, 37.7651] ], "metrics": ["duration", "distance"], "resolve_locations": false }'Response gives you durations and distances as 2D arrays — row = source, column = destination. Everything in seconds and meters respectively. Feed this directly into your scheduling logic, your VRP solver, or the built-in Vroom optimization endpoint.
For logistics use cases, this is where ORS earns its keep. A standalone OSRM install can do matrix calculations too, but you’d be running OSRM separately, without the isochrones, without Pelias geocoding, and without Vroom optimization. ORS gives you the full logistics toolkit in one place.
API Ergonomics
ORS has a proper OpenAPI spec at http://localhost:8080/ors/openapi.json once your instance is running. Swagger UI is available at http://localhost:8080/ors/swagger-ui — useful for exploration and testing without writing curl commands.
Official client libraries exist for Python (openrouteservice) and JavaScript (openrouteservice-js). Both support the self-hosted use case by letting you configure a custom base URL. The swap from public API to local instance is literally one parameter:
import openrouteservice
# Public APIclient = openrouteservice.Client(key="your-api-key")
# Self-hostedclient = openrouteservice.Client( base_url="http://localhost:8080/ors")
# Everything else works the sameroute = client.directions( coordinates=[[-122.4194, 37.7749], [-118.2437, 34.0522]], profile="driving-car")The directions response includes turn-by-turn instructions, elevation profile if enabled, distance, duration, and the route geometry as encoded polyline or GeoJSON. Useful query parameters for the /directions endpoint:
profile—driving-car,driving-hgv,cycling-regular,cycling-road,foot-walking,foot-hiking,wheelchairformat—json(default) orgeojsoninstructions—true/false, whether to include turn-by-turnelevation—true/false, whether to include elevation dataalternative_routes— request up to 3 alternative routes
The documentation is genuinely good. Every endpoint has examples, parameter descriptions, and response schemas. It’s a refreshing contrast to “here’s a README with three lines and a broken example.”
When ORS Is the Right Call
ORS makes sense when you want to answer yes to most of these:
- Small to medium team — one person or a small dev team operating the stack
- Multiple API types needed — you need routing and isochrones and matrix, not just one of them
- Modest QPS — hundreds to low thousands of requests per day, not millions
- Logistics or mobility use cases — the matrix and Vroom optimization pay dividends here
- Regional scope — a country, a large metro, a state — not global
If you’re building a consumer app that needs to handle thousands of geocoding requests per minute, run Nominatim separately with proper hardware and tuning. If you need hyper-accurate bicycle routing with elevation-aware profiles and custom surface weighting, BRouter or Valhalla go deeper on that. If you’re running a global service and raw routing throughput is the only metric that matters, OSRM is faster at pure point-to-point routing — it does less, which means it does that one thing with less overhead.
But if you’re a developer, a data analyst, or a small shop who needs the full toolbox — who doesn’t want to spend a week standing up and integrating five separate services — ORS is the answer. One docker compose up, one data pipeline, one API to learn.
The Verdict
The self-hosted maps ecosystem is genuinely excellent and also genuinely fragmented. Every specialized tool is a separate project, a separate data import, a separate operational concern. ORS takes a clear position: trade per-component optimization for operational simplicity, and do it without compromising on features.
Isochrones alone are worth the deployment for a lot of use cases. The matrix API is table stakes for any logistics workload. The Pelias geocoding removes the need for a separate Nominatim install for most cases. The Vroom optimization is the kind of bonus feature that usually costs money.
If you’re looking at a pile of individual OSM service containers and thinking “there has to be a better way” — there is. It’s one container, one PBF, one API. Your 2 AM self will appreciate not having to remember which port Nominatim is on versus which port OSRM is on.
Related posts
- Nominatim: Self-Hosted Geocoding — standalone geocoder if you need dedicated throughput
- OSRM: Blazing-Fast Self-Hosted Routing — raw routing speed when ORS is overkill
- Nominatim vs Photon vs Pelias — geocoding engine comparison
- The Full Self-Hosted Maps Stack — combining everything with a tile server