Files
serienbrief_django/docker-compose.yml
T
2026-05-21 10:36:16 +02:00

310 lines
9.3 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 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