Files
ita-ito-horaires/docker-compose.prod.yml
T
vliaudatandClaude Opus 5 0bd06e03e4 fix: route the panel through a dedicated Traefik entrypoint
A router priority cannot escape an entrypoint's HTTP→HTTPS redirection:
Traefik applies it before routing. The device paths therefore listen on
their own port, which never redirects.

That turns out to be the better arrangement anyway. Nothing but the four
device paths is reachable on 2300, and Traefik guarantees it rather than
application code — a stronger property than refusing the other routes
after the fact.

Requires the matching `device` entrypoint in the shared traefik.yml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-21 22:20:36 +02:00

114 lines
4.8 KiB
YAML

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