OfflineHub Add-On — Captive Portal (openNDS) + Offline Maps (TileServer-GL)
CAPTIVE-PORTALOFFLINE MAPS12/24V

OfflineHub Add-On — openNDS + TileServer-GL

Operational recipe: router-based captive portal (openNDS) that lands users on your local homepage, plus TileServer-GL serving OpenMapTiles MBTiles — fully offline, attribution-ready.

What you get

  • Captive portal on an OpenWrt/GL.iNet travel router using openNDS (click-to-continue flow).
  • DNS shortcuts: every client resolves http://srv/ to your box (Pi/mini-PC).
  • Offline maps: TileServer-GL + MBTiles + local style (no internet/CDNs).
  • Attribution snippet for OpenStreetMap & OpenMapTiles (license-friendly).

Prereqs

RouterOpenWrt-capable (SSH + opkg)
ServerPi 5 or mini-PC with Docker
StorageSSD with MBTiles + ZIM (optional)
Power12/24 V → 5 V buck (Pi) or PoE+ HAT (Pi 4)

PoE+ HAT powers Pi 3B+/4 via 802.3at; Pi 5 prefers 5V/5A USB-C for headroom.

1) Router captive portal (openNDS)

SSH to the router. Install openNDS and bind to the LAN bridge br-lan:

# on OpenWrt
opkg update
opkg install opennds

uci set opennds.@opennds[0].enabled='1'
uci set opennds.@opennds[0].gatewayinterface='br-lan'
uci set opennds.@opennds[0].gatewayname='OfflineHub'
uci set opennds.@opennds[0].login_option_enabled='1'   # click-to-continue
uci commit opennds
/etc/init.d/opennds restart
Docs: OpenWrt openNDS page & openNDS ReadTheDocs. ThemeSpec lets you customize splash sequences later.

2) Map srv → your server (static lease + DNS)

Give your Pi/mini-PC a fixed lease and make http://srv/ resolve correctly for all clients:

# replace MAC with your server's MAC
SRV_MAC="AA:BB:CC:DD:EE:FF"

uci add dhcp host
uci set dhcp.@host[-1].name='srv'
uci set dhcp.@host[-1].mac="$SRV_MAC"
uci set dhcp.@host[-1].ip='192.168.88.10'

# short DNS alias for everyone:
uci add_list dhcp.@dnsmasq[0].address='/srv/192.168.88.10'

uci commit dhcp
/etc/init.d/dnsmasq restart

Static leases & dnsmasq address mappings are first-class in OpenWrt’s UCI.

3) Offline Maps — TileServer-GL + MBTiles

Layout on the server

sudo mkdir -p /srv/maps/{mbtiles,styles,fonts}
sudo chown -R $USER:$USER /srv/maps

Copy your region dataset to /srv/maps/mbtiles/region.mbtiles (OpenMapTiles quickstart or a prebuilt MBTiles extract).

TileServer-GL config

/srv/maps/tileserver-config.json

{
  "options": { "paths": { "root": "/data", "fonts": "fonts", "styles": "styles" } },
  "data":    { "region": { "mbtiles": "mbtiles/region.mbtiles" } },
  "styles":  { "basic":  { "style": "styles/style.json" } }
}

TileServer-GL supports mbtiles:// and direct "mbtiles": sources for local tiles.

Minimal offline style (no external CDNs)

/srv/maps/styles/style.json

{
  "version": 8,
  "name": "Basic Vector",
  "glyphs": "/fonts/{fontstack}/{range}.pbf",
  "sources": {
    "openmaptiles": { "type": "vector", "url": "mbtiles://mbtiles/region.mbtiles" }
  },
  "layers": [
    { "id":"water","type":"fill","source":"openmaptiles","source-layer":"water",
      "paint":{"fill-opacity":0.5} },
    { "id":"roads","type":"line","source":"openmaptiles","source-layer":"transportation",
      "paint":{"line-width":1} },
    { "id":"places","type":"symbol","source":"openmaptiles","source-layer":"place",
      "layout":{"text-field":"{name:latin}","text-size":12} }
  ]
}

Docker Compose service

# add to /srv/docker-compose.yml (or a separate compose file)
services:
  tileserver:
    image: maptiler/tileserver-gl
    restart: unless-stopped
    ports: ["8080:8080"]
    volumes:
      - /srv/maps:/data
    command: ["--config", "/data/tileserver-config.json"]
docker compose up -d tileserver
Open http://srv:8080/ — choose the basic style and pan/zoom fully offline.

Attribution snippet (add near your map)

<div style="font:12px/1.2 system-ui">
  © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors •
  Tiles © OpenMapTiles • ODbL & attribution required.
</div>

4) Optional: super-simple landing page for the portal

Host this at http://srv/portal.html and link it from your splash:

<!doctype html><meta charset="utf-8">
<title>OfflineHub Connected</title>
<style>body{margin:0;font:16px/1.5 system-ui} .wrap{max-width:720px;margin:8vh auto;padding:24px}
a.btn{display:inline-block;padding:10px 16px;border:1px solid #222;text-decoration:none}</style>
<div class="wrap">
  <h1>You're in ✅</h1>
  <p>Maps, docs & tools are local.</p>
  <p><a class="btn" href="http://srv/">Home</a> <a class="btn" href="http://srv:8080/">Maps</a></p>
</div>

The captive flow itself is provided by openNDS; ThemeSpec lets you customize advanced multi-step pages later.

5) Smoke test (5 minutes)

  1. Join the SSID → captive portal appears → Continue/Accept.
  2. Open http://srv/ (homepage), then http://srv:8080/ (TileServer-GL).
  3. If labels don’t render, add SDF fonts to /srv/maps/fonts/ (OpenMapTiles font pack).

Safety & gotchas

  • Power: If using Pi 5, budget for a solid 5V/5A; avoid undervoltage. Pi 3B+/4 can be powered via PoE+ HAT.
  • Legal: Show OSM attribution; keep it visible near the map. Don’t proxy user traffic; just portal-gate local access.
  • Offline assets: Keep styles, glyphs, and MBTiles local — no external CDNs in your style JSON.

Sources

GamerzCrave — field-ready, fully offline, zero fluff.
Scroll to Top