Files

310 lines
9.3 KiB
YAML
Raw Permalink Normal View History

2026-05-21 10:36:16 +02:00
# =============================================================================
# Serienbrief Compose (Production)
# Ubuntu Server 22.04/24.04 LTS · Docker Engine 27.x · Compose v2
# =============================================================================
# Die App spricht intern nur HTTP. TLS terminiert ein vorgelagerter Nginx-
# Reverse-Proxy (außerhalb dieses Compose-Stacks, z.B. zentraler LAN-Proxy).
# Der hier enthaltene "nginx"-Service ist NUR ein App-interner Proxy für:
# - Ausspielen von static/media über X-Accel-Redirect
# - Connection-Pooling und einfache Rate-Limits
# =============================================================================
name: serienbrief
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
tag: "{{.Name}}"
x-restart: &default-restart
restart: unless-stopped
x-security-opts: &default-security-opts
security_opt:
- no-new-privileges:true
services:
# ---------------------------------------------------------------------------
# App-interner Nginx nimmt Traffic vom äußeren Proxy entgegen (HTTP).
# Hört nur auf Loopback bzw. dem konfigurierten LAN-Bind.
# ---------------------------------------------------------------------------
nginx:
image: nginx:1.27-alpine
<<: [*default-restart, *default-security-opts]
depends_on:
web:
condition: service_healthy
ports:
# Standardmäßig nur lokal der externe Proxy spricht über das Docker-
# Host-Interface. Für direkten LAN-Zugriff LAN_BIND_IP in .env setzen.
- "${APP_BIND_IP:-127.0.0.1}:${APP_BIND_PORT:-8080}:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- static_files:/var/www/static:ro
- media_files:/var/www/media:ro
- nginx_cache:/var/cache/nginx
- nginx_run:/var/run
networks:
- frontend
logging: *default-logging
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
memory: 128M
cpus: "0.5"
# ---------------------------------------------------------------------------
# Django Gunicorn WSGI
# ---------------------------------------------------------------------------
web:
build:
context: ./app
dockerfile: Dockerfile
target: runtime
args:
APP_UID: ${APP_UID:-10001}
APP_GID: ${APP_GID:-10001}
image: serienbrief/web:${APP_VERSION:-latest}
<<: [*default-restart, *default-security-opts]
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
env_file: .env
environment:
DJANGO_SETTINGS_MODULE: config.settings.production
ROLE: web
volumes:
- static_files:/app/staticfiles
- media_files:/app/media
networks:
- frontend
- backend
logging: *default-logging
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=3).status==200 else 1)"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
read_only: true
tmpfs:
- /tmp:size=256M,mode=1777
deploy:
resources:
limits:
memory: 1G
cpus: "1.5"
# ---------------------------------------------------------------------------
# Celery Worker DOCX→PDF-Generierung
# ---------------------------------------------------------------------------
worker:
image: serienbrief/web:${APP_VERSION:-latest}
<<: [*default-restart, *default-security-opts]
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
web:
condition: service_started
env_file: .env
environment:
DJANGO_SETTINGS_MODULE: config.settings.production
ROLE: worker
command: ["celery", "-A", "config", "worker", "--loglevel=info", "--concurrency=2", "--max-tasks-per-child=50"]
volumes:
- media_files:/app/media
networks:
- backend
logging: *default-logging
healthcheck:
test: ["CMD-SHELL", "celery -A config inspect ping -d celery@$$HOSTNAME || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 30s
read_only: true
tmpfs:
- /tmp:size=512M,mode=1777
deploy:
resources:
limits:
memory: 2G
cpus: "2.0"
# ---------------------------------------------------------------------------
# Celery Beat Scheduler (Retention, Cleanup)
# ---------------------------------------------------------------------------
beat:
image: serienbrief/web:${APP_VERSION:-latest}
<<: [*default-restart, *default-security-opts]
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
env_file: .env
environment:
DJANGO_SETTINGS_MODULE: config.settings.production
ROLE: beat
command: ["celery", "-A", "config", "beat", "--loglevel=info", "--scheduler", "django_celery_beat.schedulers:DatabaseScheduler"]
networks:
- backend
logging: *default-logging
read_only: true
tmpfs:
- /tmp:size=64M,mode=1777
deploy:
resources:
limits:
memory: 256M
cpus: "0.3"
# ---------------------------------------------------------------------------
# PostgreSQL
# ---------------------------------------------------------------------------
db:
image: postgres:16-alpine
<<: [*default-restart, *default-security-opts]
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
PGDATA: /var/lib/postgresql/data/pgdata
secrets:
- postgres_password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./postgres/init:/docker-entrypoint-initdb.d:ro
networks:
- backend
logging: *default-logging
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
shm_size: 256mb
deploy:
resources:
limits:
memory: 1G
cpus: "1.0"
# ---------------------------------------------------------------------------
# Redis Celery Broker
# ---------------------------------------------------------------------------
redis:
image: redis:7-alpine
<<: [*default-restart, *default-security-opts]
command:
- "redis-server"
- "--requirepass"
- "${REDIS_PASSWORD}"
- "--maxmemory"
- "256mb"
- "--maxmemory-policy"
- "allkeys-lru"
- "--save"
- "900 1"
- "--appendonly"
- "yes"
volumes:
- redis_data:/data
networks:
- backend
logging: *default-logging
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "--no-auth-warning", "ping"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
memory: 384M
cpus: "0.5"
# ---------------------------------------------------------------------------
# Backup pg_dump + Media-Tar, nightly, 14 Tage Retention
# ---------------------------------------------------------------------------
backup:
image: postgres:16-alpine
<<: [*default-restart, *default-security-opts]
depends_on:
db:
condition: service_healthy
environment:
PGHOST: db
PGUSER: ${POSTGRES_USER}
PGDATABASE: ${POSTGRES_DB}
PGPASSWORD_FILE: /run/secrets/postgres_password
BACKUP_RETENTION_DAYS: "14"
secrets:
- postgres_password
volumes:
- ./backups:/backups
- media_files:/media:ro
networks:
- backend
entrypoint: ["/bin/sh", "-c"]
command:
- |
apk add --no-cache tar gzip findutils >/dev/null && \
while true; do
TS=$$(date +%Y%m%d_%H%M%S)
export PGPASSWORD=$$(cat $$PGPASSWORD_FILE)
echo "[$$(date -Iseconds)] starting backup $$TS"
pg_dump -Fc -f /backups/db_$$TS.dump && \
tar czf /backups/media_$$TS.tar.gz -C /media . && \
find /backups -name "db_*.dump" -mtime +$$BACKUP_RETENTION_DAYS -delete && \
find /backups -name "media_*.tar.gz" -mtime +$$BACKUP_RETENTION_DAYS -delete && \
echo "[$$(date -Iseconds)] backup done"
sleep 86400
done
logging: *default-logging
deploy:
resources:
limits:
memory: 256M
cpus: "0.5"
# =============================================================================
networks:
frontend:
driver: bridge
driver_opts:
com.docker.network.bridge.name: br-sb-front
backend:
driver: bridge
driver_opts:
com.docker.network.bridge.name: br-sb-back
# =============================================================================
volumes:
postgres_data:
redis_data:
static_files:
media_files:
nginx_cache:
nginx_run:
# =============================================================================
secrets:
postgres_password:
file: ./secrets/postgres_password.txt