Files
ita-ito-horaires/docker-compose.prod.yml
T
vliaudatandClaude Opus 5 eae3f89aca fix: let the panel reach the API over plain HTTP, deliberately
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. Let's Encrypt's ISRG Root YR was issued in May 2026 and is
not even in an up-to-date Ubuntu CA bundle yet; the kit's firmware
predates it. The handshake fails before a request is ever sent, which is
why neither Traefik nor the application saw anything at all while the
device reported "API connection cannot be established".

Ruled out first, with evidence: TLS 1.2 and the ECDHE-RSA-AES-GCM suites
an ESP32 needs are both offered, and the intermediate is not
cross-signed by an older root, so no alternate path exists in what is
served.

A Traefik router now serves four device paths over :80, ahead of the
entrypoint-wide redirect. The administration stays on TLS. The device
token travels in clear; it is used for nothing else and is revocable
from the settings page, and the image URL is an unguessable content hash.

DEVICE_ALLOW_HTTP existed but was never read — a setting that does
nothing misrepresents what it protects. The device routes now refuse an
unencrypted request unless it is set, so opening this door is a written
decision rather than the silent consequence of a proxy change.

DEPLOY.md records the whole diagnosis, including the commands that
distinguish a TLS failure from an application one, and what to do the
day the firmware learns the new roots.

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

108 lines
4.5 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.
#
# The priority beats the entrypoint-wide HTTP→HTTPS redirection, which is
# otherwise applied to everything on :80. 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: web
traefik.http.routers.horaires-device.priority: "2147483647"
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}