Docker Compose til produktion — SaaS deployment med health checks og netværk
Docker Compose produktionsopsætning til SaaS: health checks, interne netværk, secrets management, ressourcegrænser, log-rotation og zero-downtime deploy via GitHub Actions.
Af M-Bus Gateway
M-Bus Gateway platformen kører på Docker Compose på Hetzner. Her er produktionskonfigurationen med health checks, netværksisolation og zero-downtime deploy.
docker-compose.yml: Komplet produktionskonfiguration
# docker-compose.yml
services:
# ─── Database ──────────────────────────────────────────────────────
db:
image: timescale/timescaledb:latest-pg16
restart: unless-stopped
environment:
POSTGRES_DB: mbus
POSTGRES_USER: mbus
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mbus -d mbus"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
deploy:
resources:
limits:
memory: 2G
cpus: "2.0"
# ─── Redis ─────────────────────────────────────────────────────────
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass-file /run/secrets/redis_password --maxmemory 256mb --maxmemory-policy allkeys-lru
secrets:
- redis_password
volumes:
- redis-data:/data
networks:
- internal
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# ─── MQTT Broker ────────────────────────────────────────────────────
mosquitto:
image: eclipse-mosquitto:2.0
restart: unless-stopped
ports:
- "8883:8883"
volumes:
- ./mosquitto/config:/mosquitto/config:ro
- ./mosquitto/certs:/mosquitto/certs:ro
- mosquitto-data:/mosquitto/data
networks:
- internal
- external # Port 8883 eksponeret til gateways
# ─── API Server ─────────────────────────────────────────────────────
server:
image: ghcr.io/kirken20/mbus-gateway/server:${IMAGE_TAG:-latest}
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
environment:
DATABASE_URL: postgresql+asyncpg://mbus:${DB_PASSWORD}@db/mbus
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
env_file: .env.prod
networks:
- internal
- external
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
memory: 1G
cpus: "1.5"
# ─── Celery Workers ─────────────────────────────────────────────────
celery-worker:
image: ghcr.io/kirken20/mbus-gateway/server:${IMAGE_TAG:-latest}
restart: unless-stopped
command: celery -A server.src.workers.celery_app worker --loglevel=info --concurrency=4 -Q critical,default,low
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
env_file: .env.prod
networks:
- internal
celery-beat:
image: ghcr.io/kirken20/mbus-gateway/server:${IMAGE_TAG:-latest}
restart: unless-stopped
command: celery -A server.src.workers.celery_app beat --loglevel=info --scheduler redisbeat.RedisScheduler
depends_on:
- redis
env_file: .env.prod
networks:
- internal
# ─── Nginx reverse proxy ─────────────────────────────────────────────
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- certbot-www:/var/www/certbot:ro
- certbot-conf:/etc/letsencrypt:ro
depends_on:
server:
condition: service_healthy
networks:
- external
volumes:
postgres-data:
redis-data:
mosquitto-data:
certbot-www:
certbot-conf:
networks:
internal:
internal: true # Ingen adgang fra host/internet
external:
secrets:
db_password:
file: ./secrets/db_password.txt
redis_password:
file: ./secrets/redis_password.txt
Zero-downtime deploy: GitHub Actions
# .github/workflows/deploy.yml (udsnit)
- name: Deploy til Hetzner
run: |
ssh -i ${{ secrets.HETZNER_SSH_KEY }} deploy@178.105.90.8 << 'ENDSSH'
cd /opt/mbus-gateway
# Pull seneste image (bygget i CI):
docker pull ghcr.io/kirken20/mbus-gateway/server:${{ github.sha }}
# Update IMAGE_TAG og deploy med rolling restart:
export IMAGE_TAG=${{ github.sha }}
docker compose up -d --no-deps --wait server celery-worker celery-beat
# Vent på health check inden success:
docker compose ps server | grep "healthy" || exit 1
echo "Deploy succesfuld: ${{ github.sha }}"
ENDSSH
Log-rotation: Undgå disk-overflow
# Docker log-rotation via daemon.json:
# /etc/docker/daemon.json på Hetzner:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
# Per-service override i docker-compose:
services:
server:
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
Ressourcemonitorering
# Tjek ressourceforbrug live:
docker stats --no-stream
# CONTAINER CPU% MEM USAGE/LIMIT MEM% NET I/O
# server 2.1% 312MiB / 1GiB 30.5% 1.2GB / 890MB
# db 8.3% 1.1GiB / 2GiB 55.0% 4.5GB / 2.1GB
# celery-worker 0.5% 185MiB / 1GiB 18.1% 120MB / 45MB
# mosquitto 0.1% 12MiB / 256MiB 4.7% 8.2GB / 7.1GB
# Health status:
docker compose ps
# Logs fra service:
docker compose logs --tail=100 -f server
Konklusion
Docker Compose med depends_on + health checks sikrer korrekt opstartsrækkefølge. Interne netværk isolerer services fra internet. Docker secrets håndterer adgangskoder sikkert (aldrig i environment variables). Zero-downtime deploy via --no-deps --wait starter kun de opdaterede services og venter på health check. Log-rotation forhindrer disk-overflow.
Se Hetzner Object Storage guide eller GitHub Actions CI guide.