# ============================================================================= # 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