File size: 11,720 Bytes
20f83d9 | 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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | # π 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-compose` or `uvx podman-compose`)
- **Node.js 22+** (for running seed scripts on the host)
## π Quick Start
```bash
# 1. Clone and enter the repo
git clone https://github.com/koala73/worldmonitor.git
cd worldmonitor
npm install
# 2. Generate the REQUIRED secrets. Without these the stack will not start
# (see the "Required Environment Variables" table below).
echo "RELAY_SHARED_SECRET=$(openssl rand -hex 32)" >> .env
echo "REDIS_PASSWORD=$(openssl rand -hex 32)" >> .env
echo "REDIS_TOKEN=$(openssl rand -hex 32)" >> .env
# 3. Start the stack
docker compose up -d # or: uvx podman-compose up -d
# 4. Seed data into Redis
./scripts/run-seeders.sh
# 5. 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.
## π Required Environment Variables
These must be set before `docker compose up -d`, or one of the containers will exit on boot.
| Variable | Purpose | How to generate |
| --- | --- | --- |
| `RELAY_SHARED_SECRET` | Authenticates every non-public request the dashboard makes to the AIS relay. The relay refuses to start without it. | `openssl rand -hex 32` |
| `REDIS_PASSWORD` | Redis AUTH password (`--requirepass`). The Redis container refuses to start without it; the REST proxy uses it in its upstream connection string. | `openssl rand -hex 32` |
| `REDIS_TOKEN` | Bearer token the REST proxy (`redis-rest`) requires on every request, and the value the app sends as `UPSTASH_REDIS_REST_TOKEN`. The proxy and app containers refuse to start without it. | `openssl rand -hex 32` |
> Earlier releases shipped `wm-local-token` as a default for the REST token. That default has been removed (#3804) β the proxy was only reachable from `127.0.0.1:8079` so external exposure required a hostile `docker-compose.override.yml`, but any user who flipped that binding to `0.0.0.0` was instantly authenticated by a publicly documented string. Fresh installs and existing clones both need to set `REDIS_TOKEN` and `REDIS_PASSWORD` in `.env` from this release onward.
> Need to bring the relay up without auth for local debugging? Set `I_UNDERSTAND_THIS_DISABLES_AUTH=true` (the deprecated `ALLOW_UNAUTHENTICATED_RELAY=true` is still accepted). The relay will log a loud `[SECURITY]` warning at boot and every 5 minutes, and every non-public route will be reachable by anyone who can hit the port β **never use this on an internet-reachable host.**
## π API Keys
Create a `docker-compose.override.yml` to inject your keys. This file is **gitignored** β your secrets stay local.
```yaml
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_EMAIL: "" # https://acleddata.com (free for researchers)
ACLED_PASSWORD: "" # OAuth flow β tokens auto-refresh (preferred over ACLED_ACCESS_TOKEN)
ACLED_ACCESS_TOKEN: "" # Alternative: static token (expires every 24h)
# π°οΈ Earth Observation
NASA_FIRMS_API_KEY: "" # REQUIRED for seed-fire-detections.mjs β https://firms.modaps.eosdis.nasa.gov (free)
# βοΈ Aviation
AVIATIONSTACK_API: "" # https://aviationstack.com (free tier)
TRAVELPAYOUTS_API_TOKEN: "" # https://travelpayouts.com (flight price search β optional)
# π’ 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.
```bash
# 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:
```bash
# Re-seed every 30 minutes
*/30 * * * * cd /path/to/worldmonitor && ./scripts/run-seeders.sh >> /tmp/wm-seeders.log 2>&1
```
**Per-seeder timeout (`SEED_TIMEOUT`):** standalone seeders are each wrapped in a
wall-clock cap so one hung upstream can't starve the rest of the run. It defaults
to `1800` (30 min); override with `SEED_TIMEOUT=<seconds>`, or `SEED_TIMEOUT=0` to
disable. Bundle seeders (`seed-bundle-*.mjs`) are exempt β they already bound each
section internally. Requires the `timeout` command (GNU coreutils); if it's absent
the cap is silently skipped.
### π§ Manual seeder invocation
If you prefer to run seeders individually:
```bash
# Source .env so REDIS_TOKEN (and any API keys it holds) become available.
# Quick-start puts REDIS_TOKEN in .env, not in your shell β without this,
# the next line fails-loud with "REDIS_TOKEN: parameter null or not set".
set -a; . ./.env; set +a
export UPSTASH_REDIS_REST_URL=http://localhost:8079
export UPSTASH_REDIS_REST_TOKEN="${REDIS_TOKEN:?set REDIS_TOKEN in .env first}"
node scripts/seed-earthquakes.mjs
node scripts/seed-military-flights.mjs
# ... etc
```
`./scripts/run-seeders.sh` auto-sources `REDIS_TOKEN` from `.env`, so the wrapper is the simpler path. Use the manual form only when iterating on a single seeder.
## ποΈ 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) |
> **`redis-rest` command allowlist**: the bundled proxy (`docker/redis-rest-proxy.mjs`) only
> forwards a fixed allowlist of Redis commands and rejects `EVAL`/`EVALSHA`/`SCRIPT` (no Lua
> scripting). Two consequences for a self-hosted stack:
>
> - `@upstash/ratelimit`'s Lua-based sliding-window limiter (`server/_shared/rate-limit.ts`,
> `api/_rate-limit.js`) can't run against it. Both automatically detect the rejection once and
> fall back to a non-Lua fixed-window limiter (`INCR` + `EXPIRE NX`) for the rest of the
> process β rate limiting still enforces, just with fixed- instead of sliding-window semantics.
> - `scripts/ais-relay.cjs`'s own in-container seed loops (`UPSTASH_ENABLED`) also require
> `UPSTASH_REDIS_REST_URL` to start with `https://` by default, which the plain-HTTP proxy
> never satisfies. Set `UPSTASH_ALLOW_INSECURE_HTTP=true` on the `ais-relay` service (already
> wired for `redis-rest` in `docker-compose.yml`) to opt into using the proxy from
> inside the relay container.
## π¨ Building from Source
```bash
# 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) for `envsubst` in the nginx config
- Docker nginx mirrors Vercel's `script-src` policy and does not allow `'unsafe-inline'`; hash-pin any custom inline scripts before adding them to a self-hosted build.
- If you hit `npm ci` sync errors in Docker, regenerate the lockfile with the container's npm version:
```bash
docker run --rm -v "$(pwd)":/app -w /app node:24-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:
```yaml
# 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.):
```yaml
# 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:24-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) |
|