* feat: self-hosted Docker stack with nginx, Redis REST proxy, and seeders
Multi-stage Docker build: esbuild TS handler compilation, vite frontend
build, nginx + Node.js API under supervisord. Upstash-compatible Redis
REST proxy with command allowlist for security. AIS relay WebSocket
sidecar. Seeder wrapper script with auto-sourced env vars from
docker-compose.override.yml. Self-hosting guide with architecture
diagram, API key setup, and troubleshooting.
Security: Redis proxy command allowlist (blocks FLUSHALL/CONFIG/EVAL),
nginx security headers (X-Content-Type-Options, X-Frame-Options,
Referrer-Policy), non-root container user.
* feat(docker): add Docker secrets support for API keys
Entrypoint reads /run/secrets/* files and exports as env vars at
startup. Secrets take priority over environment block values and
stay out of docker inspect / process metadata.
Both methods (env vars and secrets) work simultaneously.
* fix(docker): point supervisord at templated nginx config
The entrypoint runs envsubst on nginx.conf.template and writes
the result to /tmp/nginx.conf (with LOCAL_API_PORT substituted
and listening on port 8080 for non-root). But supervisord was
still launching nginx with /etc/nginx/nginx.conf — the default
Alpine config that listens on port 80, which fails with
"Permission denied" under the non-root appuser.
* fix(docker): remove KEYS from Redis allowlist, fix nginx header inheritance, add LLM vars to seeders
- Remove KEYS from redis-rest-proxy allowlist (O(N) blocking, Redis DoS risk)
- Move security headers into each nginx location block to prevent add_header
inheritance suppression
- Add LLM_API_URL / LLM_API_KEY / LLM_MODEL to run-seeders.sh grep filter
so LLM API keys set in docker-compose.override.yml are forwarded to seed scripts
* fix(docker): add path-based POST to Redis proxy, expand allowlist, add missing seeder secrets
- Add POST /{command}/{args...} handler to redis-rest-proxy so Upstash-style
path POSTs work (setCachedJson uses POST /set/<key>/<value>/EX/<ttl>)
- Expand allowlist: HLEN, LTRIM (seed-military-bases, seed-forecasts),
ZREVRANGE (premium-stock-store), ZRANDMEMBER (seed-military-bases)
- Add ACLED_EMAIL, ACLED_PASSWORD, OPENROUTER_API_KEY, OLLAMA_API_URL,
OLLAMA_MODEL to run-seeders.sh so override keys reach host-run seeders
---------
Co-authored-by: Elie Habib <elie.habib@gmail.com>
7.2 KiB
🌍 Self-Hosting World Monitor
Run the full World Monitor stack locally with Docker/Podman.
📋 Prerequisites
- Docker or Podman (rootless works fine)
- Docker Compose or podman-compose (
pip install podman-composeoruvx podman-compose) - Node.js 22+ (for running seed scripts on the host)
🚀 Quick Start
# 1. Clone and enter the repo
git clone https://github.com/koala73/worldmonitor.git
cd worldmonitor
npm install
# 2. Start the stack
docker compose up -d # or: uvx podman-compose up -d
# 3. Seed data into Redis
./scripts/run-seeders.sh
# 4. Open the dashboard
open http://localhost:3000
The dashboard works out of the box with public data sources (earthquakes, weather, conflicts, etc.). API keys unlock additional data feeds.
🔑 API Keys
Create a docker-compose.override.yml to inject your keys. This file is gitignored — your secrets stay local.
services:
worldmonitor:
environment:
# 🤖 LLM — pick one or both (used for intelligence assessments)
GROQ_API_KEY: "" # https://console.groq.com (free, 14.4K req/day)
OPENROUTER_API_KEY: "" # https://openrouter.ai (free, 50 req/day)
# 📊 Markets & Economics
FINNHUB_API_KEY: "" # https://finnhub.io (free tier)
FRED_API_KEY: "" # https://fred.stlouisfed.org/docs/api/api_key.html (free)
EIA_API_KEY: "" # https://www.eia.gov/opendata/ (free)
# ⚔️ Conflict & Unrest
ACLED_ACCESS_TOKEN: "" # https://acleddata.com (free for researchers)
# 🛰️ Earth Observation
NASA_FIRMS_API_KEY: "" # https://firms.modaps.eosdis.nasa.gov (free)
# ✈️ Aviation
AVIATIONSTACK_API: "" # https://aviationstack.com (free tier)
# 🚢 Maritime
AISSTREAM_API_KEY: "" # https://aisstream.io (free)
# 🌐 Internet Outages (paid)
CLOUDFLARE_API_TOKEN: "" # https://dash.cloudflare.com (requires Radar access)
# 🔌 Self-hosted LLM (optional — any OpenAI-compatible endpoint)
LLM_API_URL: "" # e.g. http://localhost:11434/v1/chat/completions
LLM_API_KEY: ""
LLM_MODEL: ""
ais-relay:
environment:
AISSTREAM_API_KEY: "" # same key as above — relay needs it too
💰 Free vs Paid
| Status | Keys |
|---|---|
| 🟢 No key needed | Earthquakes, weather, natural events, UNHCR displacement, prediction markets, stablecoins, crypto, spending, climate anomalies, submarine cables, BIS data, cyber threats |
| 🟢 Free signup | GROQ, FRED, EIA, NASA FIRMS, AISSTREAM, Finnhub, AviationStack, ACLED, OpenRouter |
| 🟡 Free (limited) | OpenSky (higher rate limits with account) |
| 🔴 Paid | Cloudflare Radar (internet outages) |
🌱 Seeding Data
The seed scripts fetch upstream data and write it to Redis. They run on the host (not inside the container) and need the Redis REST proxy to be running.
# Run all seeders (auto-sources API keys from docker-compose.override.yml)
./scripts/run-seeders.sh
⚠️ Important: Redis data persists across container restarts via the redis-data volume, but is lost on docker compose down -v. Re-run the seeders if you remove volumes or see stale data.
To automate, add a cron job:
# Re-seed every 30 minutes
*/30 * * * * cd /path/to/worldmonitor && ./scripts/run-seeders.sh >> /tmp/wm-seeders.log 2>&1
🔧 Manual seeder invocation
If you prefer to run seeders individually:
export UPSTASH_REDIS_REST_URL=http://localhost:8079
export UPSTASH_REDIS_REST_TOKEN=wm-local-token
node scripts/seed-earthquakes.mjs
node scripts/seed-military-flights.mjs
# ... etc
🏗️ Architecture
┌─────────────────────────────────────────────┐
│ localhost:3000 │
│ (nginx) │
├──────────────┬──────────────────────────────┤
│ Static Files │ /api/* proxy │
│ (Vite SPA) │ │ │
│ │ Node.js API (:46123) │
│ │ 50+ route handlers │
│ │ │ │
│ │ Redis REST proxy (:8079) │
│ │ │ │
│ │ Redis (:6379) │
└──────────────┴──────────────────────────────┘
AIS Relay (WebSocket → AISStream)
| Container | Purpose | Port |
|---|---|---|
worldmonitor |
nginx + Node.js API (supervisord) | 3000 → 8080 |
worldmonitor-redis |
Data store | 6379 (internal) |
worldmonitor-redis-rest |
Upstash-compatible REST proxy | 8079 |
worldmonitor-ais-relay |
Live vessel tracking WebSocket | 3004 (internal) |
🔨 Building from Source
# Frontend only (for development)
npx vite build
# Full Docker image
docker build -t worldmonitor:latest -f Dockerfile .
# Rebuild and restart
docker compose down && docker compose up -d
./scripts/run-seeders.sh
⚠️ Build Notes
- The Docker image uses Node.js 22 Alpine for both builder and runtime stages
- Blog site build is skipped in Docker (separate dependencies)
- The runtime stage needs
gettext(Alpine package) forenvsubstin the nginx config - If you hit
npm cisync errors in Docker, regenerate the lockfile with the container's npm version:docker run --rm -v "$(pwd)":/app -w /app node:22-alpine npm install --package-lock-only
🌐 Connecting to External Infrastructure
Shared Redis (optional)
If you run other stacks that share a Redis instance, connect via an external network:
# docker-compose.override.yml
services:
redis:
networks:
- infra_default
networks:
infra_default:
external: true
Self-Hosted LLM
Any OpenAI-compatible endpoint works (Ollama, vLLM, llama.cpp server, etc.):
# docker-compose.override.yml
services:
worldmonitor:
environment:
LLM_API_URL: "http://your-host:8000/v1/chat/completions"
LLM_API_KEY: "your-key"
LLM_MODEL: "your-model-name"
extra_hosts:
- "your-host:192.168.1.100" # if not DNS-resolvable
🐛 Troubleshooting
| Issue | Fix |
|---|---|
📡 0/55 OK on health check |
Seeders haven't run — ./scripts/run-seeders.sh |
| 🔴 nginx won't start | Check podman logs worldmonitor — likely missing gettext package |
| 🔑 Seeders say "Missing UPSTASH_REDIS_REST_URL" | Stack isn't running, or run via ./scripts/run-seeders.sh (auto-sets env vars) |
📦 npm ci fails in Docker build |
Lockfile mismatch — regenerate with docker run --rm -v $(pwd):/app -w /app node:22-alpine npm install --package-lock-only |
| 🚢 No vessel data | Set AISSTREAM_API_KEY in both worldmonitor and ais-relay services |
| 🔥 No wildfire data | Set NASA_FIRMS_API_KEY |
| 🌐 No outage data | Requires CLOUDFLARE_API_TOKEN (paid Radar access) |