# 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" # The admin is only ever served over TLS; say so to the browsers. traefik.http.middlewares.horaires-hsts.headers.stsSeconds: "31536000" traefik.http.middlewares.horaires-hsts.headers.stsIncludeSubdomains: "true" # --- The panel, in clear, on four paths only --- # # The e-ink firmware carries a certificate-authority bundle fixed when it # was built, so it cannot validate a chain rooted in an authority created # afterwards — which is exactly the case with Let's Encrypt's ISRG Root YR # (May 2026). The handshake fails before a request is ever sent, which is # why neither Traefik nor the application sees anything at all. # # This router therefore serves the four device paths over plain HTTP. The # administration stays on TLS. The trade-off is real and bounded: the # device token travels in clear, it is used for nothing else, and it can # be revoked from Paramètres → Appareils. The image URL is an unguessable # content hash. # # It listens on its own entrypoint rather than on :80. Traefik applies an # entrypoint's HTTP→HTTPS redirection before routing, so no router # priority can escape it — the port has to be one that never redirects. # That also makes the restriction structural: nothing but these four # paths is reachable on 2300, and it is Traefik that guarantees it rather # than application code. # # Requires the matching `device` entrypoint in the shared traefik.yml. # Remove this block the day the firmware learns the new roots, and set # DEVICE_ALLOW_HTTP=false — the application refuses plain requests # without it. traefik.http.routers.horaires-device.rule: >- Host(`${APP_DOMAIN}`) && (PathPrefix(`/api/setup`) || PathPrefix(`/api/display`) || PathPrefix(`/api/log`) || PathPrefix(`/api/device/`)) traefik.http.routers.horaires-device.entrypoints: device traefik.http.routers.horaires-device.service: horaires 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}