Docker Compose
docker-compose.yml at the repository root is the reference deployment. It runs the API, the worker, the dashboard, MongoDB 7 and Redis 7 on a private bridge network and, through the auto-loaded docker-compose.override.yml, publishes the API and the dashboard on loopback only (127.0.0.1:3000 and 127.0.0.1:3001), so a public VPS never exposes them past the reverse proxy you put in front. The base file itself publishes no ports; a deployment that passes explicit -f files (Dokploy) gets none. The dashboard's own settings are covered in Dashboard setup.
Install
git clone https://github.com/Aproder/opennotification.git
cd opennotification
cp .env.example .env
bun run generate:keys >> .env # ENCRYPTION_KEY, HMAC_SECRET, admin token, session secret, VAPID pair
bun run generate:admin-password >> .env
docker compose up -d
docker compose ps
Compose refuses to start without ENCRYPTION_KEY, HMAC_SECRET, ADMIN_API_TOKEN, SESSION_SECRET and ADMIN_PASSWORD_HASH (${VAR:?…} guards), so a half-filled .env fails loudly instead of running with empty secrets.
Service URLs (MONGO_URL, REDIS_URL) are set inside the compose file to the service names, so the same .env works whether the services run in Docker or directly on the host.
What each service does
| Service | Image | Notes |
|---|---|---|
api | docker/api.Dockerfile | Healthcheck on /health. Returns 503 while Mongo is down but keeps running; exits immediately if Redis is unreachable at boot (a queued push must never be accepted and lost). |
worker | docker/worker.Dockerfile | Consumes campaign-fanout, push-ios, push-android, push-web, webhooks, maintenance. stop_grace_period: 45s so in-flight pushes drain on SIGTERM. |
dashboard | docker/dashboard.Dockerfile | Next.js standalone build. Talks to the API as http://api:3000; needs ADMIN_API_TOKEN, SESSION_SECRET, ADMIN_PASSWORD_HASH (Compose refuses to start without them). Healthcheck on /login. |
docs | docker/docs.Dockerfile | Documentation site, static files behind nginx. 127.0.0.1:3002 locally; DOCS_DOMAIN under Traefik. |
backup | mongo:7 (profile backup) | Opt-in mongodump sidecar: docker compose --profile backup up -d. See Operations › Backups. |
mongo | mongo:7 | --wiredTigerCacheSizeGB ${MONGO_CACHE_GB:-1}. Data in the mongo_data volume. |
redis | redis:7-alpine | --maxmemory-policy noeviction --appendonly yes. Data in redis_data. |
noevictionUnder allkeys-lru Redis silently drops BullMQ job hashes and the queue rots with no error anywhere. The compose file sets the policy; if you bring your own Redis, set it yourself.
All three images run Bun on Alpine as a non-root user with tini as PID 1, so SIGTERM reaches the process — that signal is what starts the worker's drain.
Building images vs. pulling them
The compose file both builds and names the images (API_IMAGE, WORKER_IMAGE, DASHBOARD_IMAGE). CI publishes all three to GHCR from main (latest) and from v* tags. On a developer machine docker compose up builds from source; on a server set the two variables to a registry tag and run docker compose pull && docker compose up -d.
docker build -f docker/api.Dockerfile -t ghcr.io/you/opennotification-api:1.0 .
docker build -f docker/worker.Dockerfile -t ghcr.io/you/opennotification-worker:1.0 .
docker build -f docker/dashboard.Dockerfile -t ghcr.io/you/opennotification-dashboard:1.0 .
The build context is always the repository root, not docker/.
Running without Docker
If MongoDB and Redis already run on the machine (brew services start mongodb-community redis), the dev scripts read the repo-root .env and three terminals are the whole loop:
bun install
bun run dev:api # :3000
bun run dev:worker
bun run dev:dashboard # :3001
bun run dev starts all three at once. For a production process manager (systemd, pm2), the entry points are bun packages/api/src/index.ts and bun packages/worker/src/index.ts with the same environment.
Ports and networking
| Port | Service | Exposure |
|---|---|---|
| 3000 | API | 127.0.0.1 only, via docker-compose.override.yml — put TLS in front (Dokploy / Traefik) |
| 3001 | Dashboard | 127.0.0.1 only, same file — an admin surface, put TLS in front |
| 3002 | Docs | 127.0.0.1 only, same file |
| 27017 | Mongo | Not published |
| 6379 | Redis | Not published |
SDKs and browsers need to reach the API over HTTPS. Set API_PUBLIC_URL to that public URL (it is what the dashboard shows in setup hints) and add every browser origin that will call /v1/subscriptions to CORS_ORIGINS.
Upgrading
git pull
docker compose build # or: docker compose pull
docker compose up -d
Index creation is idempotent and runs on every API boot. There are no migrations to run by hand.