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
182 lines
7.1 KiB
Markdown
182 lines
7.1 KiB
Markdown
# Déploiement
|
|
|
|
Procédures d'exploitation pour `horaires.ita-ito.com`. Le [README](README.md)
|
|
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`.
|
|
|
|
```bash
|
|
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 :
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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 :
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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 :
|
|
|
|
```bash
|
|
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 YR` date 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 en `https` sans
|
|
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 `2300` a é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
|
|
|
|
```bash
|
|
# 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.
|