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:
|
2026-05-22 07:59:03 +02:00
|
|
|
|
condition: service_started
|
2026-05-21 10:36:16 +02:00
|
|
|
|
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
|