The transport log settles it: the device reaches the server over https and validates the Let's Encrypt chain without trouble. The dedicated port, the Traefik entrypoint and the plain-HTTP router were all built on a hypothesis the evidence has since refused. DEVICE_ALLOW_HTTP goes back to false, so the device routes refuse an unencrypted request again. DEPLOY.md is rewritten around the real cause — the firmware does not follow redirects, and a trailing slash was answered with a 308 — and records the three hypotheses that were wrong, so nobody spends another evening on them. It also names the trap that made this slow: Traefik, a production Next server and tcpdump were each read as saying "no traffic" when all three were simply silent by default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
7.1 KiB
Déploiement
Procédures d'exploitation pour horaires.ita-ito.com. Le README
couvre l'installation locale et la configuration d'Authentik.
Première installation sur le VPS
Prérequis : Docker, Docker Compose, et un Traefik déjà en route possédant le
réseau externe nommé par TRAEFIK_NETWORK.
git clone <dépôt> /opt/horaires && cd /opt/horaires
cp .env.example .env
sed -i "s|^AUTH_SECRET=.*|AUTH_SECRET=$(openssl rand -base64 33)|" .env
sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -base64 24)|" .env
# Renseignez APP_DOMAIN, AUTH_URL, AUTH_AUTHENTIK_*, TRANSLATION_API_KEY.
mkdir -p backups
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait
Les migrations s'appliquent au démarrage. Si elles échouent, le conteneur
s'arrête avant de servir quoi que ce soit, plutôt que d'exposer une base
incohérente : docker compose logs app dira pourquoi.
Ensuite :
docker compose exec app node .migrator/node_modules/prisma/build/index.js migrate status
curl -fsS https://horaires.ita-ito.com/api/health
Puis, une fois connecté : Paramètres pour le canton et les intervalles de réveil, Fériés → Synchroniser maintenant, Horaires pour la vraie semaine, et enfin l'appairage de l'écran (README).
Mise à jour
cd /opt/horaires
./scripts/backup.sh # ou attendre le dump nocturne
git pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait
docker compose logs -f app # ^C une fois « démarrage de l'application »
--wait rend la main seulement quand le healthcheck passe. S'il ne passe pas,
la mise à jour a échoué et l'ancien conteneur a déjà été remplacé : voir le
retour arrière ci-dessous.
Avant une mise à jour qui touche au rendu (polices, gabarit, logo), sachez que le nom de fichier de l'image change et que tous les écrans redessineront une fois. C'est sans gravité, seulement un cycle de batterie.
Sauvegarde
Le service backup dépose un dump compressé par jour dans ./backups et purge
au-delà de BACKUP_KEEP_DAYS (14 par défaut).
Dump immédiat :
docker compose exec -T db pg_dump -U horaires horaires | gzip > backups/horaires-$(date +%Y%m%d-%H%M%S).sql.gz
Ces fichiers contiennent les horaires, les messages et le journal d'audit. Ils
ne contiennent aucun secret : les jetons d'appareil n'y figurent que sous
forme d'empreintes, et les identifiants Authentik vivent dans .env, qui n'est
pas dans les sauvegardes. Sauvegardez .env séparément, ailleurs.
Restauration
docker compose -f docker-compose.yml -f docker-compose.prod.yml stop app
gunzip -c backups/horaires-20260920-030000.sql.gz \
| docker compose exec -T db psql -U horaires -d horaires
docker compose -f docker-compose.yml -f docker-compose.prod.yml start app
Après une restauration, les écrans appairés depuis le dump ne le sont plus du point de vue de la base : ils recevront un 401 et devront être réappairés par le portail captif. Rien d'autre ne se perd.
Retour arrière
L'image porte la version du dépôt, donc revenir en arrière veut dire revenir au commit :
git log --oneline -10
git checkout <commit-précédent>
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait
Si la mise à jour comportait une migration, revenir au code ne suffit pas : le schéma est déjà migré. Prisma ne défait pas une migration. Restaurez alors le dump pris avant la mise à jour, puis reprenez le code. C'est la raison d'être de la sauvegarde préalable.
Ce qu'il faut surveiller
| Signe | Où | Que faire |
|---|---|---|
| L'écran n'a pas donné signe de vie | alerte du tableau de bord | batterie, puis Wi-Fi de la boutique |
| Synchro des fériés en échec | alerte, page Fériés | le cache local tient ; relancer à la main |
| Messages sans traduction | alerte, page Messages | npm run translate:test pour savoir pourquoi |
| Conteneur unhealthy | docker compose ps |
docker compose logs app |
Le panneau ne se connecte pas
C'est arrivé sur cette installation. Le diagnostic a pris une soirée et trois fausses pistes ; voici ce qu'il faut savoir avant de recommencer.
La cause, une fois pour toutes
Le firmware ne suit pas les redirections. Il demande /api/setup/ avec une
barre oblique finale ; Next répondait 308 pour normaliser, l'écran traitait ce
code comme un échec, n'obtenait jamais de jeton, puis se faisait refuser
/api/display en 401 et affichait « API connection cannot be established ».
L'application sert désormais les deux écritures (skipTrailingSlashRedirect et
des réécritures dans next.config.ts). Si vous ajoutez une route destinée à
l'appareil, ajoutez-y sa variante avec barre oblique.
Ce qui n'était PAS en cause
Trois hypothèses ont été poursuivies avant la bonne. Elles sont consignées ici pour que personne ne les reprenne :
- La chaîne TLS. La racine
ISRG Root YRdate de mai 2026 et n'est pas dans le magasin d'Ubuntu ; j'en ai conclu qu'un firmware antérieur ne pouvait pas la valider. Faux : le journal montre l'écran arriver enhttpssans difficulté. - Le réseau de la boutique. Un test depuis un téléphone sur le même Wi-Fi a montré que rien n'était filtré.
- Le port. Un entrypoint Traefik en clair sur
2300a été mis en place puis retiré : il n'a jamais servi à rien.
Pourquoi c'était si long : les outils silencieux
Trois fois de suite, un outil muet a été lu comme un diagnostic négatif.
| Outil | Piège |
|---|---|
| Traefik | ne journalise pas les accès par défaut |
| Next.js en production | ne journalise pas les requêtes |
tcpdump |
met sa sortie en tampon sans -l : fichier vide malgré du trafic |
Et l'écran disait la réponse depuis le début. Il envoyait son rapport
d'erreur sur /api/log — sans jeton, puisqu'il n'avait jamais réussi à
s'appairer — et la route répondait 401. Un appareil incapable de
s'authentifier est précisément celui qu'il faut écouter : ces journaux sont
maintenant acceptés et écrits dans les logs du serveur.
Le diagnostic, dans l'ordre
# 1. Par quel transport l'écran arrive-t-il, et arrive-t-il ?
docker compose logs app --since 3h | grep "\[device\]"
# 2. Que dit l'écran lui-même ?
docker compose logs app --since 3h | grep "appareil non authentifié"
# 3. Les deux écritures répondent-elles sans redirection ?
for p in /api/setup /api/setup/ /api/display /api/display/; do
curl -s -o /dev/null -w "$p -> %{http_code} %{redirect_url}\n" \
"https://horaires.ita-ito.com$p"
done
Un 308 à l'étape 3 est la panne d'origine qui revient.
Si le TLS est réellement en cause un jour
Le firmware valide aujourd'hui la chaîne Let's Encrypt. Si une racine future
lui échappait vraiment, deux voies : preferredChain: "ISRG Root X1" sur le
resolver dans traefik.yml, ou un entrypoint en clair réservé aux quatre
routes de l'appareil. Dans ce second cas, DEVICE_ALLOW_HTTP=true est
nécessaire : l'application refuse une requête d'appareil non chiffrée sans
cette décision écrite.