Containerizing the OSRM routing engine eliminates third-party API rate limits, keeps sensitive logistics data on-premise, and delivers sub-millisecond route responses for batch geospatial workloads. This page is part of the Python Routing Engines & Isochrone Mapping guide and covers the complete pipeline from raw OSM data to a production-ready HTTP daemon: downloading a PBF extract, preprocessing it into a directed graph, tuning the Docker Compose service, and validating endpoints programmatically. Whether you are building fleet dispatch systems, accessibility models, or custom cost functions for routing solvers, a locally hosted OSRM instance gives you full control over routing logic and data freshness.


OSRM Docker Deployment Pipeline Five-stage pipeline: OSM PBF extract, osrm-extract, osrm-partition, osrm-customize, and osrm-routed HTTP daemon, with Python client querying the endpoint. OSM PBF Geofabrik extract .osm.pbf osrm-extract Lua profile → .osrm.edges osrm-partition MLD hierarchy → .osrm.cells osrm-customize Edge weights → .osrm.mldgr osrm-routed HTTP daemon :5000/route/v1 Python client requests.get() OSRM Docker Deployment Pipeline Preprocessing runs once per PBF update; osrm-routed serves live traffic

Prerequisites

Confirm these resources before starting. The preprocessing steps are memory-intensive and will abort silently if swap is exhausted.

System requirements

Resource Minimum Recommended
Docker Engine 20.10 Latest stable
Docker Compose v2 v2.20+
RAM 8 GB 16 GB+ for regional extracts
Storage 2× PBF file size NVMe SSD, 3× PBF size for workspace
CPU 4 cores 8+ cores reduces partition time

Python environment

# Install client-side dependencies (Python 3.9+)
pip install requests geopandas pyproj shapely

OSM data source

Download a PBF extract from Geofabrik. Metropolitan-area extracts (100–400 MB) are ideal for development. Country-level extracts work in production but require proportionally more RAM for preprocessing.


Conceptual Architecture

OSRM achieves sub-millisecond routing through two algorithms built on a preprocessed directed graph derived from building directed graphs from OSM PBF files.

Contraction Hierarchies (CH) collapse the graph by adding shortcut edges between high-importance nodes, then query with a bidirectional Dijkstra that expands only the contracted hierarchy. This delivers the lowest possible single-route latency on a static graph but does not support dynamic weight updates.

Multi-Level Dijkstra (MLD) partitions the graph into nested cells, each with precomputed boundary weights. MLD is the preferred algorithm for production deployments because osrm-customize can re-apply new edge weights (for traffic overlays or time-of-day profiles) to an already-partitioned graph in minutes rather than hours. The integrating custom traffic weights into OSRM page covers the re-customization workflow in detail.

The three preprocessing binaries map to distinct build artifacts:

  • osrm-extract reads the PBF and applies a Lua profile to emit .osrm.edges, .osrm.nodes, .osrm.restrictions, and .osrm.turn_weight_penalties.
  • osrm-partition groups nodes into hierarchical cells, producing .osrm.partition and .osrm.cells.
  • osrm-customize walks the cell boundaries and stores final edge weights in .osrm.mldgr.

The Docker image bundles all three binaries alongside the standard Lua profiles (car.lua, bicycle.lua, foot.lua), so no local C++ toolchain is required.


Step-by-Step Implementation

1. Acquire the OSM PBF Extract

# Download a metropolitan-area extract for development
mkdir -p ~/osrm-data && cd ~/osrm-data
wget https://download.geofabrik.de/north-america/us/district-of-columbia-latest.osm.pbf

# Verify checksum (Geofabrik publishes .md5 alongside each file)
wget https://download.geofabrik.de/north-america/us/district-of-columbia-latest.osm.pbf.md5
md5sum -c district-of-columbia-latest.osm.pbf.md5

Routing accuracy depends on OSM tag completeness in the extract. Before preprocessing, use osmium command-line tools to inspect road network coverage and confirm that highway, maxspeed, and access tags are populated at the density your logistics profile requires.

2. Extract the Routing Graph

The car.lua profile maps OSM tags to directed edges, applies maxspeed values, and encodes oneway=yes restrictions. Run extraction inside the same image that will serve routes to guarantee binary compatibility:

docker run --rm -t \
  -v "$(pwd):/data" \
  osrm/osrm-backend \
  osrm-extract -p /opt/car.lua /data/district-of-columbia-latest.osm.pbf

Expected output artifacts: district-of-columbia-latest.osrm, .osrm.edges, .osrm.nodes, .osrm.restrictions, .osrm.turn_weight_penalties, .osrm.names, .osrm.geometry, .osrm.datasource_names.

Extraction time scales roughly linearly with network density. A 200 MB metro PBF takes 2–5 minutes; a 3 GB country PBF can take 45–90 minutes. Plan preprocessing windows accordingly when scheduling nightly OSM refreshes.

3. Partition the Graph

osrm-partition performs recursive bisection of the network into a multi-level cell structure. This step is CPU-bound and benefits from high core counts:

docker run --rm -t \
  -v "$(pwd):/data" \
  osrm/osrm-backend \
  osrm-partition /data/district-of-columbia-latest.osrm

New artifacts: .osrm.partition, .osrm.cells, .osrm.cell_metrics.

4. Customize Edge Weights

osrm-customize walks all cell boundary edges and computes the final weight matrix. This is the step you re-run when applying a traffic overlay without touching the partition:

docker run --rm -t \
  -v "$(pwd):/data" \
  osrm/osrm-backend \
  osrm-customize /data/district-of-columbia-latest.osrm

New artifact: .osrm.mldgr (the customized metric graph).

5. Launch the Routing Daemon

docker run -d \
  --name osrm-local \
  -p 5000:5000 \
  -v "$(pwd):/data" \
  osrm/osrm-backend \
  osrm-routed \
    --algorithm mld \
    --max-table-size 1000 \
    --threads 4 \
  /data/district-of-columbia-latest.osrm

Flag reference:

  • --algorithm mld — required to use the MLD artifacts; omit if you preprocessed with CH.
  • --max-table-size 1000 — caps the distance matrix endpoint at 1000×1000 to prevent OOM under load.
  • --threads 4 — match to physical CPU cores for optimal query concurrency; hyperthreading yields diminishing returns here.

Confirm the daemon is ready:

docker logs osrm-local | grep "running and waiting for requests"
curl -sf http://localhost:5000/health && echo "healthy"

6. Validate Endpoints with Python

# requires: requests (pip install requests)
import requests
import json

def validate_osrm(host: str = "http://localhost:5000") -> dict:
    """Query the OSRM route endpoint and return key metrics."""
    # Coordinates for a short DC segment: [lon, lat]
    coords = "-77.0365,38.8977;-77.0091,38.8895"
    url = f"{host}/route/v1/driving/{coords}?geometries=geojson&overview=full&steps=false"

    resp = requests.get(url, timeout=5)
    resp.raise_for_status()
    data = resp.json()

    assert data["code"] == "Ok", f"Routing error: {data.get('message')}"
    route = data["routes"][0]

    print(f"Distance : {route['distance']:,.1f} m")
    print(f"Duration : {route['duration']:,.1f} s")
    print(f"Geometry : {route['geometry']['type']}")
    return route

route = validate_osrm()

For teams building graph-aware optimization layers, this REST response can be parsed into adjacency matrices or fed into NetworkX shortest path algorithms for logistics for VRP decomposition or constraint-based heuristics.


Configuration Reference

Key osrm-routed Flags

Flag Default Effect
--algorithm ch mld enables dynamic weight updates; ch gives lower single-route latency
--max-table-size 100 Maximum one-side dimension for /table requests; capped to prevent OOM
--threads 1 Worker threads for concurrent HTTP requests; set to physical core count
--max-viaroute-size 500 Maximum waypoints per /route request
--max-matching-size 100 Maximum GPS trace points per /match request

Lua Profile Tag Mapping (car.lua defaults)

OSM Tag Effect
highway=motorway High speed, no pedestrian access
highway=residential Low default speed (25 km/h)
oneway=yes Edge direction enforced; reverse direction dropped
access=private Edge excluded unless service=delivery override
maxspeed=* Overrides profile default; parsed from numeric and tagged values
turn:lanes=* Feeds turn restriction penalty logic

Customizing the profile for freight requires editing the speed tables and access logic. The configuring edge weights for freight logistics page details hgv, weight, and maxweight tag handling for heavy vehicle routing.


Production Optimization and Scaling

Docker Compose for Persistent Deployments

Replace ad-hoc docker run commands with a declarative Compose file to enforce restart policies, resource limits, and volume isolation:

# docker-compose.yml
services:
  osrm:
    image: osrm/osrm-backend:latest
    container_name: osrm-prod
    restart: unless-stopped
    ports:
      - "5000:5000"
    volumes:
      - ./osrm-data:/data:ro
    mem_limit: 16g
    memswap_limit: 16g
    command: >
      osrm-routed
        --algorithm mld
        --threads 8
        --max-table-size 500
        /data/district-of-columbia-latest.osrm
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:5000/health"]
      interval: 30s
      timeout: 5s
      retries: 3

Notes:

  • mem_limit and memswap_limit apply to standalone Compose; deploy.resources only takes effect in Swarm mode.
  • The :ro volume flag prevents accidental graph file modification by a rogue container process.
  • Set mem_limit to 1.2× the size of the preprocessed .osrm.* file set. The daemon memory-maps all files at startup.

Memory Sizing by Extract Scale

Extract Approximate RAM
Single metro area (city) 2–6 GB
US state 8–20 GB
Full USA 32–64 GB
Western Europe 64–128 GB

For large extracts consider filtering the PBF to the highway types relevant to your fleet before preprocessing. osmium tags-filter can reduce a country PBF by 40–60% by retaining only highway=motorway, highway=trunk, highway=primary, and highway=secondary.

Zero-Downtime Graph Updates

OSRM does not support hot-swapping graph files at runtime. The update sequence is:

  1. Preprocess new .osm.pbf into a parallel directory (osrm-data-next/).
  2. Start a second container (osrm-next) pointing at osrm-data-next/.
  3. Wait for the container health check to pass.
  4. Update the reverse proxy (Nginx or Traefik) upstream to point at osrm-next.
  5. Stop osrm-prod and promote osrm-next.

For teams running on cloud infrastructure, setting up OSRM on AWS EC2 with automated graph refresh covers EBS volume provisioning and the Instance Metadata Service pattern for IAM-authenticated S3 PBF downloads.


Validation and Testing

After deployment, run this validation matrix before routing traffic from production systems:

# requires: requests, geopandas, shapely (pip install requests geopandas shapely)
import requests
import geopandas as gpd
from shapely.geometry import LineString, shape

HOST = "http://localhost:5000"

def check_route_geometry(host: str) -> None:
    """Assert that a known short route returns a plausible LineString."""
    coords = "-77.0365,38.8977;-77.0091,38.8895"
    data = requests.get(
        f"{host}/route/v1/driving/{coords}?geometries=geojson&overview=full",
        timeout=5
    ).json()
    assert data["code"] == "Ok"
    geom = shape(data["routes"][0]["geometry"])
    assert isinstance(geom, LineString), "Expected LineString geometry"
    assert geom.length > 0, "Geometry has zero length"
    print(f"[PASS] Route geometry: {geom.length:.6f} degrees")

def check_table_endpoint(host: str) -> None:
    """Assert that a 3×3 distance matrix returns numeric durations."""
    coords = "-77.0365,38.8977;-77.0200,38.9000;-77.0091,38.8895"
    data = requests.get(
        f"{host}/table/v1/driving/{coords}?annotations=duration",
        timeout=10
    ).json()
    assert data["code"] == "Ok"
    durations = data["durations"]
    assert len(durations) == 3 and len(durations[0]) == 3
    assert durations[0][0] == 0.0, "Self-route duration should be 0"
    print(f"[PASS] Table endpoint 3×3: durations[0][1]={durations[0][1]:.1f}s")

def check_latency(host: str, max_ms: float = 50.0) -> None:
    """Assert sub-50 ms single-route latency (metro area, MLD algorithm)."""
    import time
    coords = "-77.0365,38.8977;-77.0091,38.8895"
    start = time.perf_counter()
    requests.get(f"{host}/route/v1/driving/{coords}", timeout=5)
    elapsed_ms = (time.perf_counter() - start) * 1000
    assert elapsed_ms < max_ms, f"Latency {elapsed_ms:.1f} ms exceeds {max_ms} ms budget"
    print(f"[PASS] Latency: {elapsed_ms:.1f} ms")

check_route_geometry(HOST)
check_table_endpoint(HOST)
check_latency(HOST)

Sanity metrics to monitor in production:

  • P99 /route latency should stay below 20 ms for metro-area graphs with MLD.
  • /table requests with more than 200 origins or destinations risk OOM; monitor container memory against mem_limit.
  • Log osrm-routed startup time after each graph update; an increase of more than 30% signals the graph grew unexpectedly.

Troubleshooting

std::bad_alloc during osrm-extract

The host has insufficient RAM or swap for the PBF size. Options in order of preference: (1) increase host RAM, (2) add swap (fallocate -l 8G /swapfile && mkswap /swapfile && swapon /swapfile), (3) reduce extract scope by filtering the PBF with osmium extract to a smaller bounding box, (4) pass --memory-swap to the docker run call to allow container swap.

No route found between coordinates

The two points lie on disconnected graph components. Common causes: (1) the points snap to pedestrian paths that car.lua excludes, (2) a water body or missing bridge creates a barrier, (3) access=private tags exclude the road class in use. Debug by querying /nearest/v1/driving/{lon},{lat} for each point and inspecting the snapped node. If the snapping distance exceeds 50 m, the point is not on a routable road. Verify OSM tag coverage using graph fragmentation prevention techniques for OSM data.

High latency on /table requests

Three causes: (1) --max-table-size is set higher than memory supports, causing paging; (2) the container is running CH instead of MLD — matrix queries perform better under MLD; (3) thread contention from simultaneous /route and /table requests. Lower --max-table-size, confirm --algorithm mld, and consider a dedicated /table container if batch matrix jobs run alongside real-time routing.

Container exits immediately after starting

Most commonly, the .osrm file path inside the container does not exist. Confirm with docker run --rm -v "$(pwd):/data" osrm/osrm-backend ls /data that the expected .osrm.* files are present. A second cause is a version mismatch: if the PBF was preprocessed with one OSRM version and the daemon image is a different major version, the file format is incompatible. Re-preprocess using the same image tag as the daemon.

MLD and CH produce different routes for the same query

This is expected for some queries. MLD uses partition-constrained shortest paths, which can differ slightly from CH’s globally optimal shortcuts when the cell boundary cuts through the optimal path. For pure shortest-path correctness on a static graph, use CH. For dynamic-weight deployments where you need to call osrm-customize frequently, accept MLD’s minor path differences in exchange for update flexibility.