310 lines
9.3 KiB
YAML
310 lines
9.3 KiB
YAML
# =============================================================================
|
||
# 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
|