← Back to posts
comparison guide self-hosted · · 15 min read

Self-Hosted Maps & Geocoding: OpenStreetMap Alternatives to Google Maps API 2026

Replace Google Maps API with self-hosted OpenStreetMap services — Nominatim for geocoding, OSRM for routing, TileServer GL for beautiful map tiles. Complete Docker guide with zero per-request costs.

OS
Editorial Team

Google Maps is the default choice for anything involving maps, geocoding, or routing. But when you self-host, scale up, or care about privacy, that default gets expensive fast. Google’s pricing model charges per API call, and those costs compound quickly — $7 per 1,000 geocoding requests, $5 per 1,000 route calculations, and unlimited free tiers that disappear the moment your app goes live.

The open-source alternative is the OpenStreetMap (OSM) ecosystem: a complete stack of self-hosted services for geocoding, routing, tile rendering, and spatial analysis that costs nothing in API fees and keeps all your data under your control.

This guide covers the entire self-hosted geospatial stack — what you can replace, what tools to use, and exactly how to deploy them with docker.

Why Self-Host Maps and Geocoding Services

There are four compelling reasons to move away from managed geospatial APIs:

Eliminate Per-Request Costs

Google Maps charges $7 per 1,000 requests for geocoding, $5 per 1,000 for directions, and $7 per 1,000 for places lookups. An app with 100,000 monthly active users making just 10 geocoding calls each would cost $7,000 per month. Self-hosting the same services costs only your server bill — typically $20–50/month for the entire stack.

Full Data Privacy

Every geocoding request to Google or Mapbox includes the user’s query, approximate location, and IP address. For applications handling sensitive location data — healthcare, law enforcement, personal tracking — sending that data to third parties may violate compliance requirements. Self-hosting keeps every query on your own infrastructure.

No Rate Limits or Quotas

Managed APIs throttle your requests. Google’s standard tier limits geocoding to 50 requests per second. Mapbox free tiers cap at 100,000 requests per month. When your traffic spikes during an event or your batch processing job runs, you hit walls. Self-hosted services scale with your hardware.

Offline Capability and Reliability

When you self-host, your maps and routing work even when the internet goes down. This matters for field operations, maritime navigation, disaster response, and any application that needs to function in areas with poor connectivity. You control the update cycle and are never at the mercy of an upstream outage.

The Self-Hosted Geospatial Stack

The OpenStreetMap ecosystem is modular. Each service handles one piece of the puzzle, and you can mix and match based on your needs:

Service TypeSelf-Hosted ToolGoogle Maps EquivalentProtocol
Map Tiles (raster)TileServer GL / OpenTileServerMaps JavaScript API (tiles)WMTS / XYZ
Map Tiles (vector)TileServer GL (MapLibre)Maps JavaScript API (vector)Mapbox Style Spec
Geocoding (search)NominatimGeocoding API / Places APIREST (JSON)
Reverse GeocodingNominatimReverse Geocoding APIREST (JSON)
Routing (driving)OSRM / ValhallaDirections APIREST (JSON)
Routing (multi-modal)ValhallaDirections API (transit)REST (JSON)
Routing (cycling/walking)Valhalla / GraphHopperDirections API (bike/walk)REST (JSON)
IsochronesValhalla / ORSNot directly availableREST (JSON)
Static Map ImagesTileServer GL (rendered)Static Maps APIHTTP (PNG/SVG)
Geospatial DatabasePostGISNot directly availableSQL

You rarely need all of these. A typical deployment runs 2–3 services depending on what your application requires.

Deploy TileServer GL for Map Tiles

TileServer GL renders and serves map tiles from OpenStreetMap data. It supports both raster (PNG) and vector (PBF) tile formats, can apply custom map styles, and handles caching automatically.

Prerequisites

  • A server with at least 4 GB RAM and 50 GB SSD storage
  • Docker and Docker Compose installed
  • OpenStreetMap data extract (PBF file) for your region

Step 1: Download Regional OSM Data

Start with a regional extract rather than the full planet file (70+ GB). The Geofabrik download server provides free extracts for every country and region.

1
2
3
4
5
6
7
8
mkdir -p ~/self-hosted-maps/data
cd ~/self-hosted-maps/data

# Download a country extract (example: Germany)
wget https://download.geofabrik.de/europe/germany-latest.osm.pbf

# For the full planet file (requires 64GB+ RAM to process):
# wget https://planet.openstreetmap.org/pbf/planet-latest.osm.pbf

Step 2: Generate Tiles with OpenMapTiles

TileServer GL needs pre-generated tiles. The OpenMapTiles project provides the tooling to convert OSM data into tile sets.

1
2
3
4
5
6
7
8
9
cd ~/self-hosted-maps

# Clone the OpenMapTiles quickstart repo
git clone https://github.com/openmaptiles/openmaptiles.git
cd openmaptiles

# Generate tiles for your region
# This creates tiles in MBTiles format
./quickstart.sh europe/germany

The quickstart script handles the entire pipeline: extracting OSM data, running imposm3 for database import, generating tiles with generate-osmborder and generate-tiles, and outputting an .mbtiles file.

Step 3: Run TileServer GL with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
cd ~/self-hosted-maps

# Create docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: "3"
services:
  tileserver:
    image: maptiler/tileserver-gl:latest
    ports:
      - "8080:80"
    volumes:
      - ./data:/data
      - ./config.json:/usr/src/app/config.json
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 4G
EOF

# Create TileServer GL configuration
cat > config.json << 'EOF'
{
  "options": {
    "paths": {
      "root": "/data",
      "fonts": "/data/fonts",
      "styles": "/data/styles"
    },
    "formatQuality": {
      "jpeg": 80,
      "webp": 90
    },
    "maxScaleFactor": 3,
    "maxSize": 2048,
    "serveStaticMaps": true
  },
  "styles": {},
  "data": {
    "germany": {
      "mbtiles": "germany.mbtiles"
    }
  }
}
EOF

docker compose up -d

Your tile server is now running at http://localhost:8080. The viewer interface shows all available tile layers, and you can test individual tiles at URLs like:

1
2
http://localhost:8080/germany/{z}/{x}/{y}.png
http://localhost:8080/germany/{z}/{x}/{y}@2x.png

Custom Map Styles

TileServer GL supports custom styles using the Mapbox Style Specification. You can create dark themes, satellite-like views, or minimal maps by downloading free style JSON files and placing them in the /data/styles directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "version": 8,
  "name": "Dark Matter",
  "sources": {
    "germany": {
      "type": "vector",
      "url": "mbtiles://germany.mbtiles"
    }
  },
  "layers": [
    {
      "id": "background",
      "type": "background",
      "paint": { "background-color": "#1a1a2e" }
    }
  ]
}

Reference popular open styles like OpenMapTiles’ free styles or the MapTiler community styles repository.

Deploy Nominatim for Geocoding

Nominatim is the standard self-hosted geocoding engine used by OpenStreetMap.org itself. It converts addresses into coordinates (forward geocoding) and coordinates back into addresses (reverse geocoding).

System Requirements

Nominatim is resource-intensive. For a country-level dataset:

RegionRAM RequiredDisk SpaceImport Time
Small country (< 1 GB PBF)4 GB20 GB30 min
Medium country (1–5 GB PBF)8 GB50 GB2 hours
Large country (5–10 GB PBF)16 GB100 GB4 hours
Full planet64 GB+1 TB+24+ hours

Step 1: Docker Deployment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mkdir -p ~/self-hosted-maps/nominatim/data
cd ~/self-hosted-maps/nominatim

cat > docker-compose.yml << 'EOF'
version: "3"
services:
  nominatim:
    image: mediagis/nominatim:4.4
    ports:
      - "8081:8080"
    environment:
      # PBF file path inside the container
      - PBF_FILE=/data/germany-latest.osm.pbf
      # Replication update interval (daily, hourly, or minutes)
      - REPLICATION_URL=https://download.geofabrik.de/europe/germany-updates/
      # Performance tuning
      - THREADS=4
      - IMPORT_STYLE=full
    volumes:
      - ../data:/data
     [postgresql](https://www.postgresql.org/)im-data:/var/lib/postgresql/14/main
    shm_size: "2g"
    restart: unless-stopped

volumes:
  nominatim-data:
EOF

The mediagis/nominatim Docker image is the recommended community distribution. It includes PostgreSQL with PostGIS, the Nominatim import tools, and automatic update support.

1
docker compose up -d

The first startup triggers the import process automatically. Monitor progress with:

1
docker compose logs -f nominatim

Step 2: Test Geocoding Queries

Once the import completes (check logs for “Import complete”), test the API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Forward geocoding — find coordinates for an address
curl "http://localhost:8081/search?q=Brandenburg+Gate,+Berlin&format=json&limit=1"

# Response:
# [{"place_id":12345,"licence":"Data © OpenStreetMap contributors",
#   "osm_type":"way","osm_id":123456789,"boundingbox":["..."],
#   "lat":"52.5162746","lon":"13.3777041",
#   "display_name":"Brandenburg Gate, Pariser Platz, Mitte, ...",
#   "class":"tourism","type":"attraction",
#   "importance":0.9,"icon":"https://..."}]

# Reverse geocoding — find address from coordinates
curl "http://localhost:8081/reverse?lat=52.5162746&lon=13.3777041&format=json"

# Structured search with specific fields
curl "http://localhost:8081/search?street=Potsdamer+Platz&city=Berlin&country=Germany&format=json"

Step 3: Performance Tuning

For production workloads, tune PostgreSQL and Nominatim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Add to docker-compose.yml environment:
environment:
  - PBF_FILE=/data/germany-latest.osm.pbf
  - THREADS=4
  # Flatnode file for faster imports on large datasets
  - FLATNODE_FILE=/data/flatnode.file
  # Custom PostgreSQL config
  - POSTGRES_SHARED_BUFFERS=2GB
  - POSTGRES_MAINTENANCE_WORK_MEM=4GB
  - POSTGRES_AUTOVACUUM=on

Add a flatnode file volume for datasets larger than a small country:

1
2
3
4
volumes:
  - ../data:/data
  - nominatim-data:/var/lib/postgresql/14/main
  - ../data/flatnode.file:/data/flatnode.file

Rate Limiting and Usage Policy

Nominatim’s usage policy requires a maximum of 1 request per second for public instances. For your self-hosted private instance, you control the limits — but be aware that Nominatim is optimized for accuracy, not throughput. For high-throughput geocoding (100+ requests/second), consider Pelias as an alternative.

Deploy OSRM for Routing

OSRM (Open Source Routing Machine) provides fast driving directions, distance matrices, and trip optimization. It powers the routing on openstreetmap.org’s main website.

Step 1: Prepare OSM Data and Build Routing Graph

OSRM requires a preprocessing step to build a routing graph from OSM data. This can be done with the OSRM Docker image:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
mkdir -p ~/self-hosted-maps/osrm
cd ~/self-hosted-maps/osrm

# Copy the OSM PBF file
cp ../data/germany-latest.osm.pbf .

# Extract the routing graph (car profile)
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm:v1.3.0 \
  osrm-extract -p /opt/car.lua /data/germany-latest.osm.pbf

# Contract the graph (builds shortcuts for fast queries)
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm:v1.3.0 \
  osrm-contract /data/germany-latest.osm.pbf

OSRM supports multiple routing profiles out of the box:

ProfileUse Case
car.luaDriving directions, fastest route by car
bicycle.luaCycling routes, bike-friendly paths
foot.luaWalking routes, pedestrian paths
truck.luaCommercial vehicle routing

Step 2: Run the OSRM Server

1
2
3
4
5
# In the same directory with the .osrm files
docker run -t -i -p 5000:5000 \
  -v "${PWD}:/data" \
  ghcr.io/project-osrm/osrm:v1.3.0 \
  osrm-routed --algorithm mld /data/germany-latest.osrm

Or with Docker Compose for persistent deployment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Add to your docker-compose.yml
  osrm:
    image: ghcr.io/project-osrm/osrm:v1.3.0
    ports:
      - "5000:5000"
    volumes:
      - ./osrm:/data
    command: osrm-routed --algorithm mld --max-table-size 1000 --max-viaroute-size 1000 /data/germany-latest.osrm
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 8G

Step 3: Test Routing Queries

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Route between two points (Berlin Brandenburg Gate to Alexanderplatz)
curl "http://localhost:5000/route/v1/driving/13.3777041,52.5162746;13.4132[matrix](https://matrix.org/).5219184?overview=full&geometries=json"

# Distance matrix (3x3)
curl "http://localhost:5000/table/v1/driving/13.377,52.516;13.413,52.522;13.405,52.520?annotations=duration,distance"

# Nearest point on road
curl "http://localhost:5000/nearest/v1/driving/13.388860,52.517037"

# Isochrone (reachable area within X seconds)
curl "http://localhost:5000/isochrone/v1/driving/13.377,52.516?durations=600,1200,1800"

The --algorithm mld flag uses Multi-Level Dijkstra, which is faster for large graphs. The --max-table-size parameter controls the maximum size of distance matrix queries (useful for fleet optimization).

Deploy Valhalla for Multi-Modal Routing

While OSRM excels at driving directions, Valhalla supports driving, cycling, walking, public transit, and even truck routing with height/weight restrictions — all from a single engine.

Docker Deployment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
mkdir -p ~/self-hosted-maps/valhalla
cd ~/self-hosted-maps/valhalla

# Download OSM data
cp ../data/germany-latest.osm.pbf .

# Create valhalla configuration
cat > valhalla.json << 'EOF'
{
  "additional": {
    "elevators": 4,
    "service_threads": 4,
    "use_luks": false,
    "logging": {
      "color": true,
      "file_name": "valhalla.log",
      "long_request": 100.0
    }
  },
  "loki": {
    "actions": ["locate","route","isochrone","sources_to_targets","trace_route","trace_attributes","transit_available","expansion","centroid","status"],
    "logging": {"long_request": 100},
    "service": {
      "proxy": "",
      "listen": "0.0.0.0:8002"
    }
  },
  "meili": {
    "auto": {"search_radius": 50, "gps_accuracy": 5, "breakage_distance": 2000, "search_radius": 50},
    "bicycle": {"search_radius": 50, "gps_accuracy": 5, "breakage_distance": 2000},
    "pedestrian": {"search_radius": 50, "gps_accuracy": 5, "breakage_distance": 2000, "search_radius": 50},
    "customizable": ["grid_size","breakage_distance","search_radius","gps_accuracy"]
  },
  "mjolnir": {
    "tile_dir": "/valhalla/valhalla_tiles",
    "tile_extract": "/valhalla/valhalla_tiles.tar",
    "admin": "/valhalla/admin_data.sqlite",
    "timezone": "/valhalla/tar/timezone.sqlite",
    "use_luks": false,
    "data_processing": {
      "allow_alt_name": true,
      "infer_internal_intersections": true,
      "infer_turn_channels": true,
      "apply_country_overrides": true,
      "use_admin_db": true,
      "use_direction_on_ways": true,
      "use_rest_area": false,
      "internal_speed": 5.0,
      "exit_speed": 25.0,
      "roundabout_speed": 15.0
    },
    "global_synchronized_cache": false,
    "tile_index_cache_size": 100,
    "data_processing_cache_size": 1000,
    "max_cache_size": 10000,
    "concurrency": 4,
    "generate_grid": true
  },
  "odin": {
    "use_admin_db": true,
    "service": {"verbose": false},
    "logging": {"long_request": 100}
  },
  "thor": {
    "costmatrix_allow_second_pass": false,
    "clear_reserved_memory": false,
    "log_location": "",
    "max_reserved_labels_count": {
      "auto_default": 200000,
      "bicycle_default": 200000,
      "pedestrian_default": 200000
    },
    "max_reserved_time": 10.0,
    "log_threshold": 0.0,
    "extend_routes": true
  }
}
EOF

Run Valhalla with Docker Compose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  valhalla:
    image: ghcr.io/gis-ops/docker-valhalla/valhalla:latest
    ports:
      - "8002:8002"
    volumes:
      - ./valhalla_tiles:/custom_files
      - ./germany-latest.osm.pbf:/custom_files/germany-latest.osm.pbf
    environment:
      - tile_urls=https://download.geofabrik.de/europe/germany-latest.osm.pbf
      - use_tiles_ignore_pbf=False
      - force_rebuild=True
      - force_rebuild_elevation=False
      - build_elevation=False
      - build_admins=True
      - build_time_zones=True
    restart: unless-stopped

Valhalla API Examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Driving route
curl "http://localhost:8002/route" \
  -H "Content-Type: application/json" \
  -d '{
    "locations": [
      {"lat": 52.5162746, "lon": 13.3777041},
      {"lat": 52.5219184, "lon": 13.4132148}
    ],
    "costing": "auto",
    "directions_options": {"units": "kilometers"}
  }'

# Cycling route
curl "http://localhost:8002/route" \
  -H "Content-Type: application/json" \
  -d '{
    "locations": [
      {"lat": 52.516, "lon": 13.377},
      {"lat": 52.522, "lon": 13.413}
    ],
    "costing": "bicycle"
  }'

# Isochrone (reachable area in 15 minutes by car)
curl "http://localhost:8002/isochrone" \
  -H "Content-Type: application/json" \
  -d '{
    "locations": [{"lat": 52.516, "lon": 13.377}],
    "costing": "auto",
    "contours": [{"time": 15, "color": "ff0000"}]
  }'

# Public transit routing (requires transit data)
curl "http://localhost:8002/route" \
  -H "Content-Type: application/json" \
  -d '{
    "locations": [
      {"lat": 52.520, "lon": 13.405},
      {"lat": 52.508, "lon": 13.376}
    ],
    "costing": "transit"
  }'

Complete Stack: Unified Docker Compose

Here’s a production-ready Docker Compose that runs the full stack — tiles, geocoding, and routing — behind a unified reverse proxy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
version: "3.8"

services:
  # Map tiles
  tileserver:
    image: maptiler/tileserver-gl:latest
    ports:
      - "8080:80"
    volumes:
      - ./data:/data
    restart: unless-stopped

  # Geocoding
  nominatim:
    image: mediagis/nominatim:4.4
    ports:
      - "8081:8080"
    environment:
      - PBF_FILE=/data/germany-latest.osm.pbf
      - REPLICATION_URL=https://download.geofabrik.de/europe/germany-updates/
      - THREADS=4
    volumes:
      - ./data:/data
      - nominatim-data:/var/lib/postgresql/14/main
    shm_size: "2g"
    restart: unless-stopped

  # Routing (OSRM for driving)
  osrm:
    image: ghcr.io/project-osrm/osrm:v1.3.0
    ports:
      - "5000:5000"
    volumes:
      - ./osrm:/data
    command: osrm-routed --algorithm mld --max-table-size 1000 /data/germany-latest.osrm
    restart: unless-stopped

  # Multi-modal routing (Valhalla)
  valhalla:
    image: ghcr.io/gis-ops/docker-valhalla/valhalla:latest
    ports:
      - "8002:8002"
    volumes:
      - ./valhalla_tiles:/custom_files
      - ./data/germany-latest.osm.pbf:/custom_files/germany-latest.osm.pbf
    environment:
      - tile_urls=https://download.geofabrik.de/europe/germany-latest.osm.pbf
      - force_rebuild=True
      - build_admins=True
    restart: unless-stopped

volumes:
  nominatim-data:

Integrating with Leaflet and MapLibre

Once your services are running, connect them to your frontend. Here’s how to use your self-hosted tiles with Leaflet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
  <style>
    #map { height: 600px; width: 100%; }
  </style>
</head>
<body>
  <div id="map"></div>
  <script>
    const map = L.map('map').setView([52.516, 13.377], 12);

    // Use your self-hosted tile server
    L.tileLayer('http://your-server:8080/germany/{z}/{x}/{y}.png', {
      attribution: '© OpenStreetMap contributors',
      maxZoom: 18
    }).addTo(map);

    // Geocode with your self-hosted Nominatim
    async function geocode(query) {
      const res = await fetch(
        `http://your-server:8081/search?q=${encodeURIComponent(query)}&format=json&limit=5`
      );
      return res.json();
    }

    // Route with your self-hosted OSRM
    async function route(from, to) {
      const res = await fetch(
        `http://your-server:5000/route/v1/driving/${from.lon},${from.lat};${to.lon},${to.lat}?overview=full&geometries=geojson`
      );
      return res.json();
    }

    // Add geocoded marker
    geocode('Brandenburg Gate, Berlin').then(results => {
      if (results.length > 0) {
        const r = results[0];
        L.marker([r.lat, r.lon]).addTo(map)
          .bindPopup(r.display_name);
        map.setView([r.lat, r.lon], 15);
      }
    });
  </script>
</body>
</html>

For vector tiles with MapLibre GL JS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const map = new maplibregl.Map({
  container: 'map',
  style: {
    version: 8,
    sources: {
      'self-hosted': {
        type: 'vector',
        tiles: ['http://your-server:8080/germany/{z}/{x}/{y}.pbf'],
        minzoom: 0,
        maxzoom: 14
      }
    },
    layers: [
      {
        id: 'background',
        type: 'background',
        paint: { 'background-color': '#1a1a2e' }
      }
      // Add more layers from your tile data
    ]
  },
  center: [13.377, 52.516],
  zoom: 12
});

Keeping Data Fresh

OpenStreetMap data updates constantly. Here’s how to keep your services current:

Nominatim handles updates automatically when you set REPLICATION_URL. The mediagis image runs the update process in the background at the interval specified by your Geofabrik feed (daily for most countries).

OSRM requires manual re-import for data updates:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Download updated PBF
wget -O germany-latest.osm.pbf https://download.geofabrik.de/europe/germany-latest.osm.pbf

# Rebuild routing graph
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm:v1.3.0 \
  osrm-extract -p /opt/car.lua /data/germany-latest.osm.pbf
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm:v1.3.0 \
  osrm-contract /data/germany-latest.osrm

# Restart OSRM to load new graph
docker compose restart osrm

TileServer GL needs tile regeneration. Automate this with a cron job:

1
2
3
# Weekly tile update
0 3 * * 0 cd /opt/openmaptiles && ./quickstart.sh europe/germany && \
  docker compose -f /opt/self-hosted-maps/docker-compose.yml restart tileserver

Comparison: Self-Hosted vs Managed APIs

FeatureGoogle MapsSelf-Hosted OSM Stack
Geocoding cost$7 / 1,000 requests$0
Routing cost$5 / 1,000 requests$0
Static Maps$2 / 1,000 loads$0
Rate limiting50 req/s standardUnlimited (hardware-bound)
Data privacyData sent to GoogleFully on-premise
Offline supportNoYes
Custom stylingLimitedFull control
Transit routingYesValhalla (with GTFS data)
Global coverageCompleteComplete (OSM planet)
Initial setup effortMinutes1–4 hours
Monthly cost at 1M requests~$5,000–12,000~$30–80 (server)

When Self-Hosting Makes Sense

Self-hosting the OSM geospatial stack is the right choice when:

  • Your monthly API costs exceed your server costs (typically at ~10,000+ geocoding requests/month)
  • You need to process large batch geocoding jobs (address databases, log analysis)
  • Privacy or compliance requirements forbid sending location data to third parties
  • You need offline map and routing capability
  • You want full control over map styling and data freshness
  • You’re building a fleet management, logistics, or delivery platform that makes thousands of routing calls daily

For small projects with occasional map display needs, managed APIs may still be more convenient. But as soon as your application scales, the self-hosted OSM stack becomes the more economical and powerful option.

Frequently Asked Questions (FAQ)

Which one should I choose in 2026?

The best choice depends on your specific requirements:

  • For beginners: Start with the simplest option that covers your core use case
  • For production: Choose the solution with the most active community and documentation
  • For teams: Look for collaboration features and user management
  • For privacy: Prefer fully open-source, self-hosted options with no telemetry

Refer to the comparison table above for detailed feature breakdowns.

Can I migrate between these tools?

Most tools support data import/export. Always:

  1. Backup your current data
  2. Test the migration on a staging environment
  3. Check official migration guides in the documentation

Are there free versions available?

All tools in this guide offer free, open-source editions. Some also provide paid plans with additional features, priority support, or managed hosting.

How do I get started?

  1. Review the comparison table to identify your requirements
  2. Visit the official documentation (links provided above)
  3. Start with a Docker Compose setup for easy testing
  4. Join the community forums for troubleshooting
Advertise here