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
| Router | OpenWrt-capable (SSH + opkg) |
|---|---|
| Server | Pi 5 or mini-PC with Docker |
| Storage | SSD with MBTiles + ZIM (optional) |
| Power | 12/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
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
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)
- Join the SSID → captive portal appears → Continue/Accept.
- Open http://srv/ (homepage), then http://srv:8080/ (TileServer-GL).
- 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
- OpenWrt: openNDS on OpenWrt • openNDS docs
- OpenWrt DHCP/DNS: dnsmasq on OpenWrt • UCI address mapping example (serverfault)
- TileServer-GL: config (MBTiles) • Docker examples
- OpenMapTiles: docs & quickstart • custom extracts
- Kiwix (optional wiki/manuals): kiwix-serve
- Docker Engine (Debian): install guide
- Pi power/PoE+: PoE+ HAT brief
- OSM attribution: copyright • OSMF guidelines