Skip to main content

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

ServiceImageNotes
apidocker/api.DockerfileHealthcheck 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).
workerdocker/worker.DockerfileConsumes campaign-fanout, push-ios, push-android, push-web, webhooks, maintenance. stop_grace_period: 45s so in-flight pushes drain on SIGTERM.
dashboarddocker/dashboard.DockerfileNext.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.
docsdocker/docs.DockerfileDocumentation site, static files behind nginx. 127.0.0.1:3002 locally; DOCS_DOMAIN under Traefik.
backupmongo:7 (profile backup)Opt-in mongodump sidecar: docker compose --profile backup up -d. See Operations › Backups.
mongomongo:7--wiredTigerCacheSizeGB ${MONGO_CACHE_GB:-1}. Data in the mongo_data volume.
redisredis:7-alpine--maxmemory-policy noeviction --appendonly yes. Data in redis_data.
danger
Redis must run with noeviction

Under 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

PortServiceExposure
3000API127.0.0.1 only, via docker-compose.override.yml — put TLS in front (Dokploy / Traefik)
3001Dashboard127.0.0.1 only, same file — an admin surface, put TLS in front
3002Docs127.0.0.1 only, same file
27017MongoNot published
6379RedisNot 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.