Erste lauffähige Version
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
# =============================================================================
|
||||
# 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
|
||||
Reference in New Issue
Block a user