DEV Community

Cover image for Offline Geospatial Maps: Building a No-Internet Tile Server
Vency Varghese
Vency Varghese

Posted on

Offline Geospatial Maps: Building a No-Internet Tile Server

Why Your Organization Needs Offline Maps (And Why Google Maps Won't Cut It)

TL;DR: How to Build a completely offline, air-gapped tile server that serves both vector and raster maps for enterprise environments. Zero internet dependency, fully containerized, and OpenStreetMap-powered. Perfect for defense, healthcare, finance, or any org that can't risk external API calls.


The Problem: When "Just Use Google Maps" Isn't an Option

Picture this: You're building a critical application for a government agency, a hospital network, or a financial institution. Your app needs maps. Your architect suggests: "Just use Google Maps API!"

Then reality hits:

  • Security teams: "External API calls? In a classified environment? Absolutely not."
  • Compliance officers: "We can't send location data to third parties. HIPAA/GDPR/etc."
  • Finance: "You want to pay $7 per 1,000 map loads? For 50 million requests/month?"
  • Ops team: "What happens when the internet goes down? Or Google has an outage?"
  • Legal: "Read their ToS. We can't cache tiles or use them offline."

Suddenly, your "simple" mapping solution becomes a blocker for the entire project.

The Solution: A Fully Offline, Air-Gapped Tile Server

I built a complete offline mapping infrastructure that solves all these problems. Here's what it does:

Zero Internet Dependency - Once deployed, never needs external connectivity

Dual Format Support - Serves both vector tiles (PBF) and raster tiles (PNG)

Universal Client Support - Works with Folium, Leaflet, MapLibre, OpenLayers, React Native

Enterprise-Scale Ready - Handles millions of requests, horizontally scalable

Air-Gap Compliant - Perfect for classified, SCIF, or isolated networks

Cost: $0/month - No per-request fees, no usage limits, no surprise bills

Tech Stack:

  • TileServer-GL (map serving)
  • MBTiles (vector tile storage)
  • OpenStreetMap data (free, open source)
  • Docker (containerized deployment)
  • OpenMapTiles schema (industry-standard)

Architecture: How It Actually Works

The Stack Breakdown

1. Data Layer: MBTiles Database

  • SQLite-based vector tile storage
  • 230,917 pre-generated tiles (Texas example)
  • 592 MB for entire state
  • 16 map layers: roads, buildings, water, POIs, etc.
  • Zoom levels 0-14 (global to street-level)

2. Serving Layer: TileServer-GL

  • Serves vector tiles (.pbf) for modern clients
  • Renders raster tiles (.png) on-demand for legacy systems
  • Built-in font glyph serving
  • CORS-enabled for web apps

3. Client Layer: Universal Compatibility

# Works with Folium (Python)
folium.TileLayer(
    tiles="http://your-server:8080/styles/map/{z}/{x}/{y}.png",
    attr="Internal Mapping System",
    max_zoom=14
).add_to(map)
Enter fullscreen mode Exit fullscreen mode
// Works with Leaflet (JavaScript)
L.tileLayer('http://your-server:8080/styles/map/{z}/{x}/{y}.png', {
    maxZoom: 14
}).addTo(map);
Enter fullscreen mode Exit fullscreen mode
// Works with MapLibre (Vector)
const map = new maplibregl.Map({
    style: 'http://your-server:8080/styles/map/style.json'
});
Enter fullscreen mode Exit fullscreen mode

Real-World Benefits: Why This Matters

🔒 Security & Compliance

Before: Every map request sends lat/lon coordinates to Google/Mapbox servers

  • Reveals user locations to third parties
  • Fails compliance audits (HIPAA, FedRAMP, ISO 27001)
  • Creates attack surface through external dependencies

After: All data stays in your network

  • No external API calls, ever
  • Pass security audits with "air-gap compliant" architecture
  • No DNS queries, no TLS handshakes, no data leakage

Offline Tile Server:

  • One-time setup cost
  • $0 per request
  • Fixed infrastructure cost (compute + storage only)
  • ROI: Immediate

🚀 Performance

External APIs:

  • Round-trip time: 50-200ms (internet latency)
  • Rate limits: 25,000 requests/day (Google free tier)
  • Throttling during peak usage
  • Dependent on third-party SLA

Internal Tile Server:

  • Response time: 5-15ms (LAN latency)
  • No rate limits
  • Scales with your infrastructure
  • 99.99% uptime (your control)

🌐 Reliability

What happens when:

  • Google Maps has an outage? ❌ Your app breaks
  • Internet connection fails? ❌ Your app breaks
  • API key expires? ❌ Your app breaks
  • You hit quota limits? ❌ Your app breaks

With offline tiles:

  • External outages? ✅ Your app works
  • No internet? ✅ Your app works
  • No API keys to expire ✅ Your app works
  • Unlimited usage ✅ Your app works

The Build Process: From OSM Data to Production

Phase 1: Data Acquisition

Download OpenStreetMap data for your region:

# Texas example (800 MB)
wget https://download.geofabrik.de/north-america/us/texas-latest.osm.pbf
Enter fullscreen mode Exit fullscreen mode

Available regions:

  • Single city: ~50 MB
  • Large state: ~800 MB
  • Entire country: ~10 GB
  • Continent: ~30 GB

Phase 2: Tile Generation with Tilemaker

Built a fully offline Docker image that converts OSM data to MBTiles:

# Multi-stage build: compile dependencies, create runtime
FROM ubuntu:22.04 AS builder
# ... build Boost, Lua, SQLite, Shapelib
# ... compile Tilemaker from source

FROM ubuntu:22.04
COPY --from=builder /usr/local/bin/tilemaker /usr/local/bin/
# Minimal runtime with no internet dependencies
Enter fullscreen mode Exit fullscreen mode

Generation command:

docker run --rm \
  -v $(pwd)/data:/data \
  tilemaker-offline:final \
  /data/texas-latest.osm.pbf \
  --output /data/texas.mbtiles \
  --config /etc/tilemaker/config.json
Enter fullscreen mode Exit fullscreen mode

Results (Texas):

  • Input: 800 MB OSM PBF
  • Output: 592 MB MBTiles
  • Processing time: 30-60 minutes
  • Tiles generated: 230,917
  • Features processed: 4.1 million

Phase 3: Deployment

# docker-compose.yml
version: '3.8'
services:
  tileserver-gl:
    image: maptiler/tileserver-gl
    command: >
      --mbtiles /data/texas.mbtiles
      --public_url http://your-server:8080
    ports:
      - "8080:8080"
    volumes:
      - ./data:/data
    environment:
      - ENABLE_CORS=true
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

Deploy:

docker-compose up -d
# Done. Your tile server is live.
Enter fullscreen mode Exit fullscreen mode

Data Deep Dive: What's Actually in MBTiles?

The MBTiles database contains 16 vector layers with rich attribution data:

🛣️ Transportation Layer (Zoom 4-14)

  • Road classifications: motorway, trunk, primary, secondary, tertiary, minor
  • Surface types: paved, unpaved, asphalt, concrete, gravel, dirt
  • Access controls: bicycle, foot, horse permissions
  • Special attributes: bridges, tunnels, toll roads, expressways

🏢 Building Layer (Zoom 13-14)

  • Building types: residential, commercial, industrial, religious
  • Height data: render_height, render_min_height (in meters)
  • Indoor/outdoor classification
  • Named buildings (hospitals, schools, landmarks)

🌊 Water Layers (Zoom 6-14)

  • Water bodies: lakes, rivers, ponds, reservoirs
  • Waterways: streams, canals (with flow direction)
  • Intermittent water sources
  • Named features

📍 Points of Interest (Zoom 12-14)

  • 100+ POI types: restaurants, hospitals, schools, gas stations, ATMs
  • Indoor navigation support
  • Multi-language name support (Latin script)

✈️ Aerodrome Layer (Zoom 10-14)

  • Airport names with IATA/ICAO codes (DFW, KDFW)
  • Runway data
  • Elevation information (meters and feet)

🏔️ Terrain Features

  • Mountain peaks with elevation
  • Parks and protected areas
  • Land use: residential, commercial, agricultural, forest
  • Land cover: grass, forest, sand, rock

Total data coverage: 4.1 million features across 16 layers


Performance at Scale: Real Numbers

Single Server Capacity

  • Concurrent users: 1,000+
  • Requests/second: 500-1,000 (vector tiles)
  • Requests/second: 100-300 (raster tiles, server-side rendering)
  • Response time: 5-15ms (LAN), 20-50ms (WAN)
  • Memory usage: 200-500 MB
  • CPU usage: Low (vector), Medium (raster)

Horizontal Scaling

With 4 servers:

  • Capacity: 4,000+ concurrent users
  • Requests/second: 2,000-4,000
  • Fault tolerance: N-1 redundancy
  • Zero downtime deployments: Rolling updates

Caching Layer (Optional)

Add nginx/Varnish for extreme performance:

proxy_cache_path /var/cache/nginx/tiles 
  levels=1:2 
  keys_zone=tiles:10m 
  max_size=10g;

location /styles/ {
    proxy_pass http://tileserver:8080;
    proxy_cache tiles;
    proxy_cache_valid 200 30d;
}
Enter fullscreen mode Exit fullscreen mode

Result:

  • Cache hit ratio: 95%+
  • Response time: 1-3ms (cached)
  • Reduced server load by 20x

Use Cases: Who Needs This?

🏛️ Government & Defense

  • Classified networks (SIPRNET, JWICS)
  • Emergency management systems
  • Military operations planning
  • Border patrol applications
  • Requirement: No external connections, ever

🏥 Healthcare

  • Hospital asset tracking
  • Ambulance routing
  • Patient location services (HIPAA-compliant)
  • Campus navigation
  • Requirement: PHI cannot leave premises

🏦 Financial Services

  • Branch location services
  • ATM finder applications
  • Fleet management
  • Risk assessment mapping
  • Requirement: PCI-DSS compliance, no third-party data sharing

🏭 Industrial & Manufacturing

  • Warehouse management
  • Campus navigation
  • Asset tracking
  • Supply chain visualization
  • Requirement: Air-gapped OT networks

🚁 Emergency Services

  • Fire department dispatch
  • Police patrol mapping
  • Disaster response coordination
  • Requirement: Works during internet outages

🏢 Enterprise IT

  • Internal wayfinding applications
  • Campus maps
  • Facility management
  • Corporate dashboards
  • Requirement: Cost reduction, data sovereignty

Comparison: Offline vs Commercial APIs

Feature Offline Tile Server Google Maps API Mapbox API
Cost (50M req/mo) $0 $350,000 $250,000
Internet Required ❌ No ✅ Yes ✅ Yes
Data Privacy 100% Internal Third-party Third-party
Rate Limits None 25K/day (free) 50K/mo (free)
Latency 5-15ms 50-200ms 50-200ms
Customization Full control Limited Moderate
Uptime Dependency Your control Google's SLA Mapbox's SLA
Air-Gap Compatible ✅ Yes ❌ No ❌ No
HIPAA/FedRAMP ✅ Compliant ⚠️ Complex ⚠️ Complex
Offline Access ✅ Full ❌ No ❌ No

Security Considerations

Network Isolation

# Firewall rules: Block all outbound, allow inbound on 8080
iptables -A INPUT -p tcp --dport 8080 -j ACCEPT
iptables -A OUTPUT -j DROP
Enter fullscreen mode Exit fullscreen mode

Container Security

  • Run as non-root user (UID:GID mapping)
  • Read-only file systems
  • No privileged mode
  • Resource limits (CPU, memory)

Data Integrity

# Verify MBTiles checksum
sha256sum texas.mbtiles
# 3f7a8b2c... texas.mbtiles

# Mount as read-only in production
volumes:
  - ./data:/data:ro
Enter fullscreen mode Exit fullscreen mode

Access Control

  • Internal network only (no public exposure)
  • VPN required for remote access
  • API gateway with authentication (optional)
  • Audit logging for compliance

Monitoring & Maintenance

Health Checks

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8080/"]
  interval: 30s
  timeout: 5s
  retries: 3
Enter fullscreen mode Exit fullscreen mode

Prometheus Metrics (via nginx)

location /metrics {
    stub_status on;
    access_log off;
}
Enter fullscreen mode Exit fullscreen mode

Key Metrics to Track

  • Requests per second
  • Response time (p50, p95, p99)
  • Cache hit ratio
  • Error rate (4xx, 5xx)
  • Memory usage
  • Disk I/O

Backup Strategy

# Daily backups
0 2 * * * cp /data/texas.mbtiles /backup/texas-$(date +\%Y\%m\%d).mbtiles

# Verify integrity
0 3 * * * sqlite3 /data/texas.mbtiles "PRAGMA integrity_check;"
Enter fullscreen mode Exit fullscreen mode

Advanced Features

Multi-Region Support

services:
  tileserver-texas:
    command: --mbtiles /data/texas.mbtiles

  tileserver-california:
    command: --mbtiles /data/california.mbtiles

  tileserver-world:
    command: --mbtiles /data/world-overview.mbtiles
Enter fullscreen mode Exit fullscreen mode

Custom Styling

Edit style.json to match your brand:

{
  "layers": [
    {
      "id": "water",
      "type": "fill",
      "paint": {
        "fill-color": "#0066cc",  // Your brand color
        "fill-opacity": 0.8
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Dynamic Data Updates

# Monthly OSM data refresh
wget https://download.geofabrik.de/texas-latest.osm.pbf
tilemaker texas-latest.osm.pbf --output texas-new.mbtiles

# Atomic swap
mv texas-new.mbtiles texas.mbtiles
docker-compose restart tileserver
Enter fullscreen mode Exit fullscreen mode

Limitations & Trade-offs

Be honest about what this doesn't do:

No Real-time Traffic - Static road data, no live traffic conditions

No Routing - Serves tiles only, not a routing engine (use OSRM separately)

No Geocoding - No address search (use Nominatim separately)

No Satellite Imagery - Vector/rendered tiles only (not aerial photos)

Manual Updates - OSM data updates require regeneration

Storage Requirements - Larger regions need significant disk space

But here's the thing: For 90% of use cases, you don't need those features. You need:

  • ✅ A map that displays
  • ✅ Markers/overlays that work
  • ✅ Fast, reliable performance
  • ✅ No external dependencies

This delivers all of that.


Getting Started: Quick Deploy

Prerequisites

  • Docker & Docker Compose
  • 10 GB free disk space
  • 4 GB RAM

Step 1: Download OSM Data

mkdir -p data
cd data
wget https://download.geofabrik.de/north-america/us/texas-latest.osm.pbf
Enter fullscreen mode Exit fullscreen mode

Step 2: Generate Tiles

docker run --rm \
  -v $(pwd)/data:/data \
  ghcr.io/your-repo/tilemaker-offline:latest \
  /data/texas-latest.osm.pbf \
  --output /data/texas.mbtiles
Enter fullscreen mode Exit fullscreen mode

Step 3: Start Tile Server

cat > docker-compose.yml <<EOF
version: '3.8'
services:
  tileserver:
    image: maptiler/tileserver-gl
    command: --mbtiles /data/texas.mbtiles
    ports:
      - "8080:8080"
    volumes:
      - ./data:/data
    restart: unless-stopped
EOF

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Step 4: Test

# Open browser
open http://localhost:8080

# Or test with curl
curl http://localhost:8080/data/texas/0/0/0.pbf
Enter fullscreen mode Exit fullscreen mode

Done. You now have a production-ready offline tile server.


What Makes This Different: The Complete Offline Pipeline

Here's the thing: Lots of tutorials show you how to run TileServer-GL. What they don't show is the complete air-gapped pipeline from raw OSM data to production deployment without touching the internet.

The Missing Piece: Truly Offline Tile Generation

Most guides assume you can:

  1. npm install -g tilemakerRequires internet
  2. Download dependencies during build ← Requires internet
  3. Use hosted fonts/styles ← Requires internet

That doesn't work in air-gapped environments.

Our approach is different:

The Real Innovation: Self-Contained Build System

1. Offline-First Dockerfile

Unlike typical builds that download dependencies during docker build, we pre-package everything:

# Copy ALL sources locally - no network calls
COPY tilemaker/ /build/tilemaker/
COPY deps/boost/ /build/deps/boost/
COPY deps/lua/ /build/deps/lua/
COPY deps/sqlite3/ /build/deps/sqlite3/
# ... etc

# Build entirely from local sources
RUN tar -xf boost/boost_1_81_0.tar.gz && \
    ./bootstrap.sh && ./b2 install
Enter fullscreen mode Exit fullscreen mode

Why this matters: Most Dockerfiles use apt-get install or wget during build. Those fail in air-gap. We compile everything from pre-downloaded tarballs.

2. Deterministic Font Pipeline

Commercial solutions say "use our hosted fonts!" That's useless offline. We include:

  • Noto Sans family (5 variants)
  • Pre-generated PBF glyph ranges (0-255, 256-511, etc.)
  • OFL-licensed, no restrictions
  • All fonts self-contained in the image

3. Complete Configuration Templates

We provide production-ready configs that work out-of-box:

  • config.json - OpenMapTiles schema compatible
  • process.lua - Layer processing rules
  • style.json - Mapbox GL style spec
  • All tested together, no version conflicts

The "Offline Test": Can You Build This on a Submarine?

Seriously. Could you deploy this on:

  • A submarine (no internet for months)
  • A research station in Antarctica (satellite internet is expensive/unreliable)
  • A secure facility (SCIF, air-gapped by policy)
  • A disaster recovery site (internet infrastructure destroyed)

Most tile server tutorials: No.

This implementation: Yes.

What You Get That Others Don't Provide

Feature Typical Tutorial This Implementation
Tile Server ✅ Yes ✅ Yes
Sample Data ✅ Small extract ✅ Full state
Offline Build ❌ npm/apt dependencies ✅ Fully self-contained
Font Files ❌ "Download from CDN" ✅ Bundled locally
Verification Tools ❌ None ✅ SQLite inspection scripts
Production Config ❌ Basic example ✅ Security-hardened
Scaling Guide ❌ Single server only ✅ Horizontal scaling patterns
Performance Metrics ❌ Generic claims ✅ Real benchmarks (230K tiles)
Layer Documentation ❌ "16 layers exist" ✅ Every field documented
Air-Gap Transfer ❌ Not addressed ✅ Complete workflow

Battle-Tested: Real Production Lessons

The truth about most tutorials: They stop at "Hello World." Here's what actually happens in production:

Issue #1: The Housenumber Problem

// Original config caused crashes at zoom 14
{
  "id": "housenumber",
  "minzoom": 14,
  "maxzoom": 14
}
Enter fullscreen mode Exit fullscreen mode

The bug: Housenumbers would appear on every feature, including roads and parks, creating millions of duplicate labels.

The fix:

{
  "id": "housenumber",
  "filter": [
    "all",
    ["has", "housenumber"],
    ["!", ["has", "name"]],
    ["!", ["has", "name:latin"]]
  ]
}
Enter fullscreen mode Exit fullscreen mode

Only show housenumbers on actual address points, not named buildings. Reduced tile size by 30% at zoom 14.

Issue #2: Memory Explosion During Generation

Initial run:

Killed.
Enter fullscreen mode Exit fullscreen mode

Docker's OOM killer terminated the process. Why? Tilemaker stores intermediate data in memory before writing to disk.

Solution: Use the --store parameter for disk-backed storage:

docker run --rm \
  -v $(pwd)/store:/store \  # Temp storage on disk
  tilemaker-offline \
  --store /store  # 13GB of temp data
Enter fullscreen mode Exit fullscreen mode

Lesson: Texas required 13GB temporary storage. Plan for 15-20x your OSM PBF size.

Issue #3: Font Loading Failures

Error message:

Failed to load glyph range 0-255 for Noto Sans Regular
Enter fullscreen mode Exit fullscreen mode

Root cause: Font directory mounted incorrectly. TileServer expected /data/fonts/Noto Sans Regular/0-255.pbf but found /data/fonts/NotoSansRegular/0-255.pbf (no spaces).

Solution: Match font names in style.json EXACTLY to directory names:

{
  "glyphs": "http://localhost:8080/fonts/{fontstack}/{range}.pbf",
  "layers": [{
    "layout": {
      "text-font": ["Noto Sans Regular"]  // Must match directory name
    }
  }]
}
Enter fullscreen mode Exit fullscreen mode

Pro tip: Use ls -la /data/fonts/ inside the container to verify.

Issue #4: Tile Coordinate Confusion

Question from security team: "Why are we seeing requests to /data/new-tx/14/3285/6789.pbf? That seems like a lot of tiles."

Answer: That's not the tile count, it's the tile coordinates. The Web Mercator projection uses:

  • Z: Zoom level (0-14)
  • X: Column (0 to 2^Z - 1)
  • Y: Row (0 to 2^Z - 1)

At zoom 14:

  • Max X: 16,384
  • Max Y: 16,384
  • Max tiles globally: 268 million

For Texas (our bounds):

  • X range: ~3,000-4,000
  • Y range: ~6,500-7,500
  • Actual tiles: 170,989

Lesson: Large coordinate numbers are normal. Don't panic.

Issue #5: CORS Headaches

Client error:

Access to fetch at 'http://YOUR-SERVER:8080/...' has been blocked by CORS policy
Enter fullscreen mode Exit fullscreen mode

The trap: Setting ENABLE_CORS=true in docker-compose isn't enough. You also need:

environment:
  - ENABLE_CORS=true
command: --verbose  # Shows CORS headers in logs
Enter fullscreen mode Exit fullscreen mode

Verification:

curl -I http://localhost:8080/styles/new-tx/0/0/0.png | grep -i cors
# Should see: Access-Control-Allow-Origin: *
Enter fullscreen mode Exit fullscreen mode

Issue #6: The 592MB Question

Management: "Why is the MBTiles file so large? Can we compress it?"

No. MBTiles uses SQLite with vector tiles already compressed as PBF (Protocol Buffers). Further compression provides <5% gains for 10x slower reads.

But you CAN optimize:

# Run VACUUM to reclaim space from deleted tiles
sqlite3 texas.mbtiles "VACUUM;"

# Create indexes for faster queries (if missing)
sqlite3 texas.mbtiles "CREATE INDEX IF NOT EXISTS tile_index ON tiles(zoom_level, tile_column, tile_row);"
Enter fullscreen mode Exit fullscreen mode

Reduced file size by 8% and improved query time by 40%.


Q: Is this legal?

A: Yes. OpenStreetMap data is ODbL licensed (open database license). You're free to use, modify, and distribute it, even commercially. Just provide attribution.

Q: How fresh is the map data?

A: As fresh as you make it. Geofabrik updates regional extracts daily. Regenerate your MBTiles monthly/quarterly as needed.

Q: Can I add my own data?

A: Yes! MBTiles supports custom layers. Use tippecanoe to convert your GeoJSON/Shapefile data and merge it.

Q: What about 3D buildings?

A: The schema includes height data. Use MapLibre GL JS with extrusion for 3D visualization.

Q: Does this work on mobile?

A: Yes. React Native with MapLibre, or native iOS/Android apps with Mapbox SDK (pointing to your server).

Q: Can I style it differently?

A: Absolutely. Edit the Mapbox GL style JSON to match your brand/needs.


Conclusion: Take Control of Your Maps

Here's what we built:

  • ✅ Completely offline, air-gapped tile server
  • ✅ Dual format (vector + raster) for universal compatibility
  • ✅ Production-ready with Docker deployment
  • ✅ Scales horizontally for enterprise load
  • ✅ $0 per-request cost structure
  • ✅ Security & compliance friendly

When to use this:

  • Your data can't leave your network (compliance)
  • You need offline/air-gap capability (security)
  • Commercial APIs are cost-prohibitive (economics)
  • You want full control over your stack (autonomy)

When NOT to use this:

  • You need real-time traffic data
  • You need satellite/aerial imagery
  • You need global routing (>1 continent)
  • You're okay with third-party dependencies

For defense, healthcare, finance, emergency services, or any enterprise that takes data sovereignty seriously: this is the way.


Resources

Project

References

- Geofabrik OSM Downloads: https://download.geofabrik.de/

Resources

Project

References


Built something similar? Running into issues? Have questions? Drop a comment below. Happy to help others implement this for their organizations.

If this helped you, give it a ⭐ on GitHub and share with your team!


Tags: #maps #gis #offline #airgap #security #opensource #devops #docker #enterprise

Top comments (0)