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 Type | Self-Hosted Tool | Google Maps Equivalent | Protocol |
|---|
| Map Tiles (raster) | TileServer GL / OpenTileServer | Maps JavaScript API (tiles) | WMTS / XYZ |
| Map Tiles (vector) | TileServer GL (MapLibre) | Maps JavaScript API (vector) | Mapbox Style Spec |
| Geocoding (search) | Nominatim | Geocoding API / Places API | REST (JSON) |
| Reverse Geocoding | Nominatim | Reverse Geocoding API | REST (JSON) |
| Routing (driving) | OSRM / Valhalla | Directions API | REST (JSON) |
| Routing (multi-modal) | Valhalla | Directions API (transit) | REST (JSON) |
| Routing (cycling/walking) | Valhalla / GraphHopper | Directions API (bike/walk) | REST (JSON) |
| Isochrones | Valhalla / ORS | Not directly available | REST (JSON) |
| Static Map Images | TileServer GL (rendered) | Static Maps API | HTTP (PNG/SVG) |
| Geospatial Database | PostGIS | Not directly available | SQL |
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:
| Region | RAM Required | Disk Space | Import Time |
|---|
| Small country (< 1 GB PBF) | 4 GB | 20 GB | 30 min |
| Medium country (1–5 GB PBF) | 8 GB | 50 GB | 2 hours |
| Large country (5–10 GB PBF) | 16 GB | 100 GB | 4 hours |
| Full planet | 64 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.
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"
|
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:
| Profile | Use Case |
|---|
car.lua | Driving directions, fastest route by car |
bicycle.lua | Cycling routes, bike-friendly paths |
foot.lua | Walking routes, pedestrian paths |
truck.lua | Commercial 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
| Feature | Google Maps | Self-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 limiting | 50 req/s standard | Unlimited (hardware-bound) |
| Data privacy | Data sent to Google | Fully on-premise |
| Offline support | No | Yes |
| Custom styling | Limited | Full control |
| Transit routing | Yes | Valhalla (with GTFS data) |
| Global coverage | Complete | Complete (OSM planet) |
| Initial setup effort | Minutes | 1–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.
Most tools support data import/export. Always:
- Backup your current data
- Test the migration on a staging environment
- 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?
- Review the comparison table to identify your requirements
- Visit the official documentation (links provided above)
- Start with a Docker Compose setup for easy testing
- Join the community forums for troubleshooting