OSRM Routing Self-Hosted: Car and Truck Profiles on a Budget VPS
Once you’ve got self-hosted map tiles running, the next thing most location-based apps need is routing — turn-by-turn directions, ETAs, distance calculations. The managed options (Google Directions API, Mapbox Directions) bill per request, and if your app does delivery, logistics, or ride-hailing, routing calls happen constantly. That’s a bill that scales with your busiest days, which is exactly when you want costs to be predictable, not exactly when they explode.
OSRM (Open Source Routing Machine) is the self-hosted answer. It’s fast, it’s been production-hardened for over a decade, and it runs comfortably on a modest VPS once you know the setup.
What OSRM actually needs
OSRM works in two phases: a one-time (or periodic) preprocessing step that builds a routing graph from OpenStreetMap data, and a runtime step that serves route queries against that pre-built graph. The preprocessing is the heavy part; the runtime is lightweight and fast.
# Download an OSM extract for your region (Geofabrik is standard)
wget https://download.geofabrik.de/your-region-latest.osm.pbf
# Extract, using a routing profile (car.lua, foot.lua, bicycle.lua, or custom)
docker run -v "${PWD}:/data" osrm/osrm-backend osrm-extract -p /opt/car.lua /data/your-region-latest.osm.pbf
# Partition and customize (required for the multi-level Dijkstra algorithm OSRM uses)
docker run -v "${PWD}:/data" osrm/osrm-backend osrm-partition /data/your-region-latest.osrm
docker run -v "${PWD}:/data" osrm/osrm-backend osrm-customize /data/your-region-latest.osrm
This preprocessing step is where you’re most likely to hit memory limits on a small VPS — more on that below.
Running the routing server
services:
osrm-car:
image: osrm/osrm-backend
restart: unless-stopped
volumes:
- ./data:/data
command: osrm-routed --algorithm mld /data/your-region-latest.osrm
ports:
- "127.0.0.1:5000:5000"
Same rule as always: bind to 127.0.0.1, never expose it directly, proxy it through Nginx like every other service on the box.
Multiple vehicle profiles on one server
If your app needs different routing for cars and trucks — trucks can’t use certain roads, have different turn restrictions, need wider clearances — run a separate OSRM instance per profile, each with its own preprocessed graph and its own port:
services:
osrm-car:
image: osrm/osrm-backend
volumes: ["./data/car:/data"]
command: osrm-routed --algorithm mld /data/car.osrm
ports: ["127.0.0.1:5000:5000"]
osrm-truck:
image: osrm/osrm-backend
volumes: ["./data/truck:/data"]
command: osrm-routed --algorithm mld /data/truck.osrm
ports: ["127.0.0.1:5001:5001"]
The truck profile is a modified Lua file — start from the default car.lua and adjust weight restrictions, excluded road classes, and turn penalties for larger vehicles. This is where OSRM’s flexibility pays off: you’re not stuck with a generic “driving” mode, you can model your actual fleet’s constraints.
Then route each through Nginx under its own path or subdomain, so your app can call /route/car/... or /route/truck/... and land on the right instance.
The memory problem, and how to survive it
osrm-extract and osrm-partition are memory-hungry — processing a region with a dense road network can spike well past what a 1-2GB VPS has available, and the process gets killed by the OOM killer with no useful error message in your application logs, just a vanished process.
The fix that actually works on a budget VPS: add swap space before you run preprocessing, not after you’ve already hit the wall.
fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
Swap won’t make preprocessing fast, but it’ll make it finish instead of dying silently. Once the graph is built, the runtime server itself is much lighter and doesn’t need this headroom.
Querying the API
Once running, OSRM’s HTTP API is straightforward:
GET /route/v1/driving/{lon1},{lat1};{lon2},{lat2}?overview=full&geometries=geojson
Returns distance, duration, and a route geometry you can render directly on your MapLibre map. No API key, no per-request billing, no rate limit beyond what your own VPS can handle.
When self-hosted routing is the wrong call
Be honest about the trade-off: if your app needs global coverage on day one, self-hosting means managing OSM extracts and rebuilds for every region you operate in — that’s real ongoing maintenance, not a one-time setup cost. It’s the right call when your operating area is regional and relatively fixed (a country, a metro area), which covers most delivery, logistics, and local-service apps. It’s the wrong call if you’re trying to compete with Google Maps’ worldwide coverage from a single VPS.
For a regional product, though, the economics are hard to beat: one preprocessing run, periodic OSM data refreshes (monthly or quarterly is usually enough), and unlimited routing queries afterward — no bill that grows with your busiest day.
