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.0

Image 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.com

Production docker-compose

compose.yaml
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_pass for the internal port; proxy_pass for 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 1
done
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.

Related