Last updated 2026-05-28
Deployment — Docker
Production-shape Docker deployment of notify. For the "what is the
image" overview see Installation
→ Docker; this page covers networking, secrets, healthchecks,
multi-arch, and a worked docker-compose stack.
When you'd use this
- You operate Docker hosts directly (no Kubernetes).
- Your local dev / staging stack lives in
docker-compose. - You want to verify a release tag with a smoke run before promoting it.
Multi-arch image
The published image is built for linux/amd64 and
linux/arm64. Docker pulls the right variant
automatically. To verify:
docker buildx imagetools inspect ghcr.io/elloloop/notify:0.1.0Image verification
Every published tag is signed via cosign keyless OIDC. Verify before deploying:
cosign verify ghcr.io/elloloop/notify:0.1.0 \ --certificate-identity-regexp 'https://github\.com/elloloop/notify/' \ --certificate-oidc-issuer https://token.actions.githubusercontent.comProduction docker-compose
services: notify: image: ghcr.io/elloloop/notify:0.1.0 restart: unless-stopped networks: - public # client port exposed via the reverse proxy - internal # internal port only reachable from the cluster - data # store + provider sidecars ports: - "8080:8080" # remove if running behind a TLS-terminating proxy expose: - "8081" # internal-only, not published - "9090" # /metrics, scraped by the Prometheus sidecar environment: NOTIFY_STORE_DRIVER: postgres NOTIFY_POSTGRES_DSN: postgres://notify:${POSTGRES_PASSWORD}@db:5432/notify?sslmode=require NOTIFY_POSTGRES_AUTOMIGRATE: "true" NOTIFY_AUTH_JWT_SECRET: ${NOTIFY_AUTH_JWT_SECRET} NOTIFY_INTERNAL_TOKEN: ${NOTIFY_INTERNAL_TOKEN} NOTIFY_AUTH_JWT_ISSUER: https://identity.example.com NOTIFY_ALLOWED_ORIGINS: https://app.example.com NOTIFY_LOG_LEVEL: info # Email via the elloloop EmailService container. NOTIFY_EMAIL_PROVIDER: emailservice NOTIFY_EMAIL_SERVICE_ADDRESS: emailservice:50053 NOTIFY_EMAIL_FROM: noreply@example.com # SMS via Twilio. NOTIFY_SMS_PROVIDER: twilio NOTIFY_SMS_ACCOUNT_SID: ${TWILIO_ACCOUNT_SID} NOTIFY_SMS_AUTH_TOKEN: ${TWILIO_AUTH_TOKEN} NOTIFY_SMS_FROM: "+15555550000" healthcheck: # No shell in the FROM scratch image, so probe from a separate # tool (an alpine sidecar, or the reverse proxy's health checks). disable: true deploy: resources: limits: cpus: "1.0" memory: 256M
db: image: postgres:16.13-alpine3.23 restart: unless-stopped networks: [data] environment: POSTGRES_USER: notify POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: notify volumes: - notify_pgdata:/var/lib/postgresql/data
prometheus: image: prom/prometheus:v3.5.0 restart: unless-stopped networks: [internal] volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
networks: public: internal: data:
volumes: notify_pgdata:Secrets management
Compose's .env file is fine for dev. Production:
- Project secrets from your secret manager (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager) and inject as env vars at launch.
- Avoid baking secrets into images.
- Rotate JWT secrets per the rotation procedure.
Reverse-proxy / TLS termination
notify speaks plaintext HTTP/2 (h2c) inside the container — terminate TLS upstream. Typical shapes:
- Caddy — automatic Let's Encrypt, transparent h2c proxy. One-liner site block:
app.example.com { reverse_proxy notify:8080 }. - nginx — explicit
grpc_passfor the internal port;proxy_passfor the client port (Connect over HTTP/1 works fine). - Traefik — annotate the docker service for auto-discovery.
- Cloudflare — front the client port at the edge, lock the origin to Cloudflare's IP ranges.
Smoke-test a release tag
# Boot, hit /healthz, kill.docker run -d --name notify-smoke \ -e NOTIFY_AUTH_DEV_MODE=true \ -e NOTIFY_STORE_DRIVER=memory \ -p 18080:8080 -p 18081:8081 -p 19090:9090 \ ghcr.io/elloloop/notify:0.1.0
# Wait for the listener.for i in $(seq 1 30); do if curl -fsS http://127.0.0.1:19090/healthz > /tmp/h.json; then cat /tmp/h.json break fi sleep 1done
docker rm -f notify-smoke
The same smoke is what .github/workflows/release.yml
runs against every published image before tagging it for promotion.
Resource sizing
notify's hot path is small: one Notifier.Notify is one
store insert plus one provider call per channel. The realtime
engine is in-memory and a per-conn buffered channel. Reasonable
starting points:
- CPU: 0.5 vCPU is enough for moderate traffic; the upper bound is dominated by provider call latency.
- Memory: 64 MiB baseline; budget ~5 KiB per active stream connection.
- Network: long-lived streams open a TCP connection per recipient — size your ephemeral port range accordingly if you expect 10k+ concurrent clients.