# Production overlay: the application is published by an existing Traefik # rather than on a host port. # # docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait # # Traefik must already be running and own the external network named by # TRAEFIK_NETWORK. Nothing here holds a secret: every value comes from .env, # which is never committed. # # Using Nginx Proxy Manager instead? Delete the labels and the `edge` network, # keep the published port from docker-compose.yml bound to 127.0.0.1, and point # a proxy host at it. The forwarded headers matter either way: the application # builds the image URL handed to the panel from them, so X-Forwarded-Proto and # X-Forwarded-Host must both reach it or the device will be sent to the wrong # scheme. services: db: # The database is reached only over the compose network. ports: !override [] app: ports: !override [] networks: - default - edge # The database is the only thing worth persisting; the application writes # nothing to its own filesystem. read_only: true tmpfs: - /tmp labels: traefik.enable: "true" traefik.docker.network: ${TRAEFIK_NETWORK:-web} traefik.http.routers.horaires.rule: Host(`${APP_DOMAIN:?Set APP_DOMAIN in .env}`) traefik.http.routers.horaires.entrypoints: ${TRAEFIK_ENTRYPOINT:-websecure} traefik.http.routers.horaires.tls: "true" traefik.http.routers.horaires.tls.certresolver: ${TRAEFIK_CERTRESOLVER:-myresolver} traefik.http.routers.horaires.middlewares: horaires-hsts traefik.http.services.horaires.loadbalancer.server.port: "3010" # Everything is served over TLS, the panel included: its firmware # validates the chain without trouble. A plain-HTTP route existed here # for a while, on the belief that it could not — see DEPLOY.md. traefik.http.middlewares.horaires-hsts.headers.stsSeconds: "31536000" traefik.http.middlewares.horaires-hsts.headers.stsIncludeSubdomains: "true" backup: # A nightly dump kept for two weeks. Small, boring, and the only thing # standing between a bad migration and retyping a year of opening hours. image: postgres:16-alpine restart: unless-stopped security_opt: - no-new-privileges:true depends_on: db: condition: service_healthy environment: PGPASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} POSTGRES_USER: ${POSTGRES_USER:-horaires} POSTGRES_DB: ${POSTGRES_DB:-horaires} BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-14} volumes: - ./backups:/backups entrypoint: - /bin/sh - -c - | while true; do stamp="$$(date +%Y%m%d-%H%M%S)" if pg_dump -h db -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" \ | gzip > "/backups/horaires-$$stamp.sql.gz"; then echo "[backup] /backups/horaires-$$stamp.sql.gz" else echo "[backup] échec du dump $$stamp" >&2 rm -f "/backups/horaires-$$stamp.sql.gz" fi find /backups -name 'horaires-*.sql.gz' -mtime "+$$BACKUP_KEEP_DAYS" -delete sleep 86400 done networks: edge: external: true name: ${TRAEFIK_NETWORK:-web}