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?

This is almost always an OOM (Out of Memory) error during the initial .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?

This is the ‘Graph-Cache Trap’. Graphhopper does not hot-reload topology rules. You must manually delete the 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?

Use the 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 data is entirely extensible. You can use desktop tools like JOSM to draw your private roads, export them as a .osm XML file, and merge them with the Geofabrik PBF file before processing.

Why is the Matrix API returning errors about 'invalid coordinate format'?

Unlike Google Maps which expects [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?

Graphhopper can take several seconds to compute massive matrices. If your Golang http.Client lacks an explicit Timeout, the calling goroutine will block indefinitely, leading to memory leaks and a frozen API.

Does self-hosting Graphhopper mean I have unlimited Matrix API calls?

Yes, you bypass external API subscription limits. However, you are strictly bound by your server’s RAM. Requesting a 10,000x10,000 matrix will instantly cause a Java Out-Of-Memory crash if you haven’t allocated enough heap space.