This article explains how to set up a routing server (computing a route between two points) from OpenStreetMap data, using GraphHopper. The goal is an HTTP API (e.g. /route) that a web app, a mobile app or a back end can call.
We will cover:
-
the available tools (SaaS vs self-hosted),
-
installing GraphHopper,
-
downloading/merging the OSM data,
-
configuration (profiles, CH/LM speed-ups, elevation),
-
import, deployment and exposure through Nginx + systemd,
-
testing and performance notes.
Tools and alternatives
Before self-hosting, here are two popular solutions:
-
GraphHopper: https://www.graphhopper.com/
-
OpenRouteService: https://openrouteservice.org/
To try the GraphHopper API quickly (explorer):
Useful tutorials and docs
-
GraphHopper (host your own worldwide route calculator): https://www.graphhopper.com/blog/2022/06/27/host-your-own-worldwide-route-calculator-with-graphhopper/
-
Installing GraphHopper: https://github.com/graphhopper/graphhopper#installation
-
Building OpenRouteService from source: https://giscience.github.io/openrouteservice/run-instance/building-from-source
1) Installing GraphHopper
Prerequisite: Java (JRE + JDK)
GraphHopper is a Java application. It needs a full Java environment (Java 17 or later for GraphHopper 11).
Install the JDK (it includes what is needed to run and compile):
sudo apt install -y default-jdk
Check the Java and compiler versions:
java -version
javac -version
Downloading GraphHopper
We fetch:
-
the “web” jar (HTTP server),
-
an example configuration file.
wget https://github.com/graphhopper/graphhopper/releases/download/11.0/graphhopper-web-11.0.jar
wget https://raw.githubusercontent.com/graphhopper/graphhopper/11.x/config-example.yml
Plan your own route
MapPlanner helps you calculate distances, elevation, duration and plan your hikes, runs and bike rides on an interactive map.
- Automatic distance & elevation calculation
- Topographic, satellite maps
- GPX/KML export for your GPS
- Real-time GPS tracking
2) Downloading the OpenStreetMap (OSM) data
GraphHopper computes routes from OSM data in PBF format (.osm.pbf).
Download by area (e.g. Europe, France):
Several files: merging PBFs
GraphHopper does not take several PBF files as input. To cover several regions (e.g. Europe + North America), you have to merge the PBFs into one.
Important:
-
ideally, avoid overlapping areas, or you risk inconsistencies.
Install osmium:
sudo apt-get install osmium-tool
Merge:
osmium merge europe-latest.osm.pbf north-america-latest.osm.pbf -o map.osm.pbf
Then point the GraphHopper configuration to map.osm.pbf.
3) Configuring GraphHopper
GraphHopper is driven by a YAML file.
Copy the example file:
cp config-example.yml config.yml
Routing profiles (car, foot, hiking, …)
Profiles define the behavior: car, walking, hiking, cycling…
Profile docs:
Custom models
Custom models let you tune the rules (road types, preferences, penalties…).
-
available models:
https://github.com/graphhopper/graphhopper/tree/master/core/src/main/resources/com/graphhopper/custom_models -
detailed documentation:
https://github.com/graphhopper/graphhopper/tree/master/docs/core
4) Speed-ups: CH vs LM vs “default”
GraphHopper can precompute speed-up structures during the import.
Two modes:
-
profiles_ch: fastest at query time, but expensive in RAM and import time. -
profiles_lm: more flexible, allows partial customization at query time.
Key points (practical summary):
-
CH:
-
very fast queries,
-
longer import (often ×10),
-
more RAM,
-
less flexible (no profile customization per request).
-
-
LM:
-
a good trade-off,
-
allows requests with parameters (dynamic custom model),
-
slower than CH but more flexible.
-
If CH is enabled, disabling turn costs is recommended:
-
turn_costs: false
(greatly reduces the generation time of a profile, and speeds up queries)
Note: adding several similar profiles usually adds little RAM (profiles overlap), but each LM profile can take a lot of disk space.
5) Adding elevation
GraphHopper can enrich the graph with altitude data.
Docs:
Available modes (+ a hidden one):
-
srtm: 3.1 GB compressed on disk, very imprecise data
-
cgiar: 3.2 GB compressed on disk, approximate data but with fewer inconsistencies than srtm
-
gmted: not tested
-
multi: not tested
-
SRTMGL1: URL not reachable, but precise data
How it works:
-
you set a mode (e.g. srtm) in the config,
-
GraphHopper automatically downloads the elevation tiles needed for the covered area.
6) Importing the data into GraphHopper
The import step converts the OSM data into an optimized graph the server can use.
Import command (with dedicated memory):
java -Xms62g -Xmx62g -jar *.jar import config.yml
Why import?
-
better optimized for the conversion,
-
frees the memory once done,
-
does not start the server.
Generated data:
-
a graph-data directory holds the cache and the imported data.
Important:
-
if you change the profiles or update the PBF, you usually have to delete graph-data before importing again.
Observed import times
-
Europe: 45 min and 38 GB of dedicated RAM, 12 GB on disk (no speed-up profiles, Ryzen 3800X)
-
Europe and North America: 92 min and 58 GB of RAM without speed-up profiles, 27.2 GB on disk (5 profiles: car, foot, hike, bike and mtb)
-
Europe and North America: 12 h and 62 GB of RAM with one LM speed-up profile (with a single LM: 9 h 10 and 46 GB), 60 GB on disk (each LM profile takes 14 GB)
-
Europe and North America: 7 h 50 with elevation and no speed-up profile, 30.2 GB
-
Europe and North America: 7 h with cgiar elevation, no speed-up profile and 2 extra profiles, 30.2 GB (the extra profiles changed neither the final size nor the generation time)
7) Updating the database
Check the GraphHopper releases:
If needed, update the config and the profiles:
-
example config:
https://github.com/graphhopper/graphhopper/blob/master/config-example.yml -
watch out for custom model naming.
Run the update: download the new PBF, delete the old graph, then run the step 6 import again:
rm -rf graph-data
java -Xms62g -Xmx62g -jar *.jar import config.yml
Update, then restart on the server:
nano config.yml
nano custom_models/*
sudo nano /etc/systemd/system/routing.service
sudo systemctl daemon-reload
sudo systemctl restart routing.service
Adjust the Xmx memory if needed (e.g. -Xmx28g).
8) Deploying GraphHopper (HTTP server)
Deployment docs:
Starting the server (Europe)
Recommended minimum (without CH/LM):
java -Xmx18g -jar *.jar server config.yml
Europe + North America (example)
/usr/bin/java -Xmx32g -XX:+UseZGC -jar graphhopper-web-11.0.jar server config.yml
Note:
-
*.jarcomes in handy when updating (replacing the jar).
9) Requests with custom parameters (dynamic custom model)
If LM is enabled for a profile, you can send a POST request to /route to adjust parameters.
Key point:
-
you must set
ch.disable=trueto force LM.
Use cases:
-
choosing “fastest” vs “shortest”,
-
excluding some road types.
Caution:
-
the weights/values you send can only be higher than the initial configuration.
Simple rule for distance_influence:
-
“fastest”: the lowest possible value (1 = smallest)
-
“shortest”: the highest value (100 or more)
10) Memory optimization: MMAP (don’t load everything into RAM)
By default, GraphHopper may keep a lot in memory. To lower RAM pressure, you can use memory-mapped file access.
In the config:
graph.dataaccess.default_type: MMAP
Instead of RAM_STORE.
Effects:
-
uses ~50% of the RAM set with -Xmx,
-
faster startup,
-
slower queries.
Also:
-
once imported, the PBF file is no longer needed.
11) Exposing the API through Nginx (reverse proxy + CORS)
Example Nginx config (proxy to port 8989), with your own domain name:
server {
listen 80;
listen [::]:80;
server_name routing.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name routing.example.com;
include snippets/ssl.conf;
access_log /var/log/nginx/routing_access.log;
error_log /var/log/nginx/routing_error.log;
root /home/www/graphhopper;
location / {
proxy_pass http://127.0.0.1:8989;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
# CORS: an API open to every origin, without credentials
proxy_hide_header 'Access-Control-Allow-Origin';
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding' always;
}
# Disable access to front page
location = / {
deny all;
}
}
To restrict the API to your own site, replace '*' with its origin (e.g. 'https://www.example.com').
Then open the domain set in the configuration to test and validate.
12) Creating a systemd service (automatic startup)
Create the service:
sudo nano /etc/systemd/system/routing.service
Content:
[Unit]
Description=Graphhopper Routing
After=network.target
[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/home/www/graphhopper
ExecStart=/usr/bin/java -Xmx16g -jar graphhopper-web-11.0.jar server config.yml
# if we crash, restart
TimeoutSec=30
Restart=on-failure
RestartSec=5s
TimeoutStartSec=120
TimeoutStopSec=30
# output goes to the journal
StandardOutput=journal
StandardError=journal
SyslogIdentifier=routing
[Install]
WantedBy=multi-user.target
Enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable routing.service
sudo systemctl start routing.service
13) Quick tests
Local test:
curl "http://localhost:8989/route?profile=foot&point=52.5300591%2C13.3565022&point=52.5060440%2C13.4378107"
14) GraphHopper API reference
Official web API docs:
https://github.com/graphhopper/graphhopper/blob/master/docs/web/api-doc.md