Setting up a local routing engine is notoriously difficult. Most generic tutorials offer a basic Docker command that crashes silently, leaving developers confused.
In this guide, we bypass the basic “Hello World” setups. We will build a production-grade local environment integrating OpenStreetMap (OSM) data, a properly tuned Graphhopper (Java) Docker container, and a high-concurrency Golang API Gateway.
1. Downloading and Cropping Map Data
Answer-first: Download raw OpenStreetMap data in .osm.pbf format from the Geofabrik server. To save gigabytes of RAM during local development, use osmium extract to crop the massive country-level map down to a single city bounding box.
The industry standard source for raw map data is download.geofabrik.de. You must download the Protocolbuffer Binary Format (.osm.pbf), as it is highly compressed and optimized for routing engines.
However, loading an entire country (e.g., vietnam-latest.osm.pbf) into memory requires upwards of 16GB of RAM. For local development on a standard laptop, this is a silent killer.
Pro-tip: Osmium Cropping
Install the osmium-tool and crop the map to a specific bounding box (e.g., Ho Chi Minh City):
# Crop map to bounding box: min_lon, min_lat, max_lon, max_lat
osmium extract -b 106.5,10.7,106.8,10.9 vietnam-latest.osm.pbf -o hcmc.osm.pbf
This reduces your map file from Gigabytes to Megabytes, ensuring lightning-fast startup times.
2. Running Graphhopper via Docker Compose
Answer-first: Run Graphhopper using the official graphhopper/graphhopper:latest image. You must allocate sufficient heap space using JAVA_OPTS=-Xmx6g to prevent Out-Of-Memory (OOM) crashes during the initial .pbf import phase.
Create a docker-compose.yml file to manage your routing engine. Notice the critical volume mappings and environment variables:
version: '3'
services:
graphhopper:
image: graphhopper/graphhopper:latest
ports:
- "8989:8989"
volumes:
- ./data:/data # Maps your PBF file
- ./config:/config # Maps your config.yml
- ./srtm:/data/srtm # Critical: Cache for Elevation Data
environment:
- JAVA_OPTS=-Xmx6g # Prevent OOM crashes during import
command: >
--input /data/hcmc.osm.pbf
--graph-location /data/graph-cache
--config /config/config.yml
3. Configuring Custom Models (Toll Roads & Elevation)
Answer-first: Edit config.yml to define Custom Models (e.g., avoiding toll roads) under the priority section. To enable 3D uphill/downhill routing, activate the srtm elevation provider. Crucial: You must delete the graph-cache folder whenever you change these rules.
To instruct the engine to avoid toll roads, define a custom weighting profile:
profiles:
- name: my_car_no_tolls
vehicle: car
weighting: custom
custom_model:
priority:
- if: "toll != NO"
multiply_by: 0.0
To enable ETA calculations that account for steep hills, enable SRTM elevation data. Ensure your Docker compose maps the cache_dir so you don’t re-download gigabytes of terrain data on every restart:
graph:
elevation:
provider: srtm
cache_dir: /data/srtm
4. The Golang API Gateway (Preventing Socket Exhaustion)
Answer-first: When writing a Golang client to call the Graphhopper Matrix API, you must configure a custom http.Transport with a high MaxIdleConnsPerHost (e.g., 100) and set an explicit Timeout. The default Go client will cause catastrophic socket exhaustion under high load.
By default, Go’s http.Client only allows 2 idle connections per host. If your microservice fires 50 concurrent Matrix requests to Graphhopper, Go opens and closes 48 new TCP connections every second. This leads to massive TIME_WAIT spikes and port exhaustion.
Here is the production-grade Golang setup:
package main
import (
"net/http"
"time"
)
// Define a globally reused transport and client
var routingTransport = &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100, // CRITICAL: Overrides the default limit of 2
IdleConnTimeout: 90 * time.Second,
}
var routingClient = &http.Client{
Transport: routingTransport,
Timeout: 15 * time.Second, // CRITICAL: Prevent goroutine leaks
}
When hitting the POST /matrix endpoint, Graphhopper strictly expects GeoJSON coordinate formatting: [Longitude, Latitude].
Now that the environment is ready, a common pitfall is connecting your API Gateway directly to the Routing Engine without location filtering. See Part 3: Spatial Indexing (Uber H3, PostGIS & Redis GEO) to learn how to use Spatial Indexing as a high-speed pre-filter.
FAQ: Production Troubleshooting
Why does my Graphhopper Docker container crash immediately after starting?
.osm.pbf graph import. You must set the JAVA_OPTS=-Xmx4g (or higher) environment variable in your docker-compose file.I changed my config.yml to avoid toll roads, but it still routes through them. Why?
graph-cache directory to force the engine to re-import the OSM data and bake in your new custom model.My laptop doesn't have 16GB of RAM to process the entire country. What should I do?
osmium extract command-line tool. You can crop a massive 2GB national PBF file down to a tiny 50MB city bounding box before feeding it into Graphhopper, saving vast amounts of RAM.What if the OpenStreetMap data is missing my company's private warehouse roads?
.osm XML file, and merge them with the Geofabrik PBF file before processing.Why is the Matrix API returning errors about 'invalid coordinate format'?
[Latitude, Longitude], the Graphhopper Matrix POST API strictly requires GeoJSON array formatting: [Longitude, Latitude].Why does my Golang API Gateway hang forever when requesting a massive 1000x1000 Matrix?
http.Client lacks an explicit Timeout, the calling goroutine will block indefinitely, leading to memory leaks and a frozen API.