Aller au contenu

Derrière un reverse proxy (Docker)

Si votre machine coach fait déjà tourner des services en conteneurs derrière un reverse proxy — Traefik, avec une authentification unique (Authentik, Authelia) —, le tableau de bord peut s'y ranger : une adresse comme https://coach.example.org, ouverte depuis le téléphone après la connexion habituelle, au lieu d'un tunnel SSH.

Le conteneur ne contient que le moteur (scripts/, web/, config/). Votre workspace y est monté en lecture seule : le conteneur ne peut rien y écrire. L'index vit en mémoire, se reconstruit au démarrage (quelques secondes pour quelques centaines de fichiers) et se met à jour comme en local — un fichier écrit par la synchronisation apparaît en moins de 30 secondes, sans redémarrer le conteneur.

Des données de santé, sans authentification propre

Le tableau de bord montre votre HRV, votre FC de repos, votre poids, vos verdicts médicaux. Il n'a pas de connexion à lui : c'est le reverse proxy qui protège l'accès. Le fichier compose fourni ne publie aucun port et refuse de démarrer sans middleware d'authentification. Ne contournez ni l'un ni l'autre.

flowchart LR
    N[📱 navigateur] -->|HTTPS| P[reverse proxy<br/>Traefik]
    P -->|authentification| A[Authentik]
    P -->|réseau Docker interne| C[conteneur<br/>tableau de bord]
    W[(workspace)] -->|lecture seule| C
    S[daily-sync.sh] -->|écrit| W

Mise en route

Préréglage --preset docker — prépare l'hôte, pas le conteneur

Cette page suppose que la machine coach existe déjà (c'est sur elle que se lance docker compose, pas sur une machine à part) : ./install.sh --preset docker prépare cet hôte en configurant Claude Code (--ide claude, dont le runner de synchronisation a besoin) et en activant --daily-sync pour que le workspace monté par le conteneur reste à jour. L'authentification Garmin reste active (le conteneur, lui, ne parle jamais à Garmin — workspace monté en lecture seule — mais la synchronisation qui tourne sur l'hôte, oui) ; sans elle, la sync échouerait deux fois par jour faute de tokens. Pas de Remote Control, l'interface de cette machine étant le tableau de bord web. Voir Préréglages. Une option explicite reste prioritaire : ajoutez --workspace DIR si vos données vivent hors du dépôt du moteur. Le conteneur lui-même se lance toujours séparément, comme décrit plus bas (docker compose up -d --build) : le préréglage ne s'y substitue pas.

Sur la machine coach, dans le dépôt du moteur :

cd ~/ai-running-coach/deploy/dashboard
cp .env.example .env

Renseignez .env (il est ignoré par git) :

Variable Rôle
ARC_DASHBOARD_HOST Nom public (coach.example.org). C'est aussi le seul en-tête Host que le serveur accepte : toute autre adresse reçoit un 403.
ARC_WORKSPACE_HOST Chemin du workspace sur l'hôte, monté sur /workspace en lecture seule.
ARC_UID, ARC_GID Propriétaire du workspace (id -u, id -g). config/workspace.user.toml est en mode 600 : un autre utilisateur ne pourrait pas le lire.
TZ Fuseau horaire : il fixe « aujourd'hui ».
TRAEFIK_NETWORK Réseau Docker sur lequel Traefik joint ses services.
TRAEFIK_ENTRYPOINT Point d'entrée Traefik du routeur.
TRAEFIK_AUTH_MIDDLEWARE Obligatoire. Middleware d'authentification déjà défini dans Traefik (authentik@file…).
COMPOSE_FILE, AUTHENTIK_OUTPOST_SERVICE Avec Authentik uniquement : ajoutent la route de l'avant-poste (voir plus bas).

Puis :

docker compose up -d --build
docker compose ps        # l'état passe à « healthy » en une trentaine de secondes

Mettre à jour

Vos fichiers sont lus en direct : une séance ou un plan écrit par le coach apparaît sans rien toucher. L'image, elle, embarque le code du moteur : après un git pull du moteur, reconstruisez-la.

cd ~/ai-running-coach && git pull --ff-only
cd deploy/dashboard
diff <(grep -o '^[A-Z_]*=' .env.example | sort) <(grep -o '^[A-Z_]*=' .env | sort)
docker compose build --pull
docker compose up -d
docker compose ps        # « healthy » en une trentaine de secondes
  • diff signale une variable apparue dans .env.example et absente de votre .env (rien ne s'affiche si les deux concordent) ; docker compose refuserait de toute façon de démarrer sans une variable obligatoire.
  • --pull récupère aussi la dernière image Python de base, et avec elle ses correctifs de sécurité.
  • Faites-le en dehors des heures de synchronisation (07:15 et 14:15 par défaut) : la synchronisation lit le même dépôt du moteur.

Revenir en arrière : git -C ~/ai-running-coach checkout <commit-précédent> puis docker compose up -d --build ; git switch main pour revenir ensuite à la version courante.

Exemple : Traefik, Authentik et un tunnel Cloudflare

Une configuration courante sur un serveur personnel : Cloudflare termine le HTTPS et envoie le trafic par un tunnel (cloudflared) à Traefik, en HTTP sur le port 80 ; Traefik vérifie la session auprès d'Authentik avant de transmettre.

.env

ARC_DASHBOARD_HOST=coach.example.org
ARC_WORKSPACE_HOST=/home/moi/mon-workspace
ARC_UID=1000
ARC_GID=1000
TZ=Europe/Paris

TRAEFIK_NETWORK=traefik-network
# Le tunnel parle à Traefik en HTTP : même point d'entrée que vos autres services.
TRAEFIK_ENTRYPOINT=insecure
TRAEFIK_AUTH_MIDDLEWARE=authentik@file

COMPOSE_FILE=compose.yaml:compose.authentik.yaml
# Service Traefik (fournisseur file) qui pointe vers
# http://authentik:9000/outpost.goauthentik.io
AUTHENTIK_OUTPOST_SERVICE=sso@file

Reprenez les valeurs de vos services existants : le réseau, le point d'entrée et les noms du middleware et du service de l'avant-poste sont ceux de votre configuration Traefik.

compose.authentik.yaml ajoute un second routeur : les chemins /outpost.goauthentik.io/ du même hôte vont directement à l'avant-poste d'Authentik. C'est par là qu'Authentik reçoit le retour de connexion.

Authentik

Tout dépend du fournisseur Proxy qui protège vos services :

  • Forward auth (domain level) : un seul fournisseur couvre tous les sous-domaines. Le nouveau nom est protégé d'office.
  • Forward auth (single application) : un fournisseur par service. Créez un fournisseur Proxy dans ce mode, avec pour External host https://coach.example.org, une Application qui l'utilise, puis ajoutez l'application à l'avant-poste intégré (Outposts → authentik Embedded Outpost).

Dans les deux cas, limitez l'application à votre compte : liez-lui votre utilisateur, ou un groupe qui ne contient que vous. Un compte invité de votre Authentik, créé pour un autre service, n'a pas à voir vos données de santé.

Cloudflare

Dans le tableau de bord Cloudflare, Zero Trust → Networks → Tunnels → votre tunnel → Public Hostname → Add a public hostname :

Champ Valeur
Subdomain coach
Domain example.org
Type HTTP
URL la même que vos autres noms publics, par exemple traefik

Cloudflare crée l'enregistrement DNS (un CNAME vers le tunnel). Rien d'autre à ouvrir : aucun port de votre box n'est concerné.

Pour une seconde barrière, une application Cloudflare Access sur ce nom exige une connexion chez Cloudflare avant même d'atteindre votre serveur.

Sans Authentik

Supprimez les lignes COMPOSE_FILE et AUTHENTIK_OUTPOST_SERVICE, et nommez votre propre middleware : authelia@docker, ou un basicAuth défini dans la configuration dynamique de Traefik. Avec un point d'entrée HTTPS géré par Traefik, TRAEFIK_ENTRYPOINT=websecure (le nom habituel).

Ce que garantit le conteneur

Garantie Comment
Rien n'est écrit dans vos fichiers Workspace monté :ro, index en mémoire (--memory)
Rien n'est écrit dans le conteneur read_only: true, seul /tmp est un tmpfs
Pas de privilèges Utilisateur non root (ARC_UID), cap_drop: ALL, no-new-privileges
Pas d'accès direct Aucun port publié : seul Traefik, sur son réseau, l'atteint
Pas d'accès sans connexion Middleware d'authentification obligatoire, compose refuse de démarrer sans
Pas de DNS rebinding Seul ARC_DASHBOARD_HOST est accepté ; le serveur refuse de démarrer s'il n'est pas déclaré
Lecture seule côté HTTP POST, PUT, DELETE, PATCH → 405
Contenu verrouillé En-tête Content-Security-Policy strict, émis par le serveur

En-têtes de sécurité ajoutés par le proxy

Si un middleware de votre point d'entrée définit lui-même Content-Security-Policy (customResponseHeaders), il remplace celle du tableau de bord, qui est plus stricte. Préférez un point d'entrée où ce n'est pas le cas.

La sonde /healthz sert au HEALTHCHECK de l'image ; elle ne réindexe rien.

Le serveur compresse ses réponses (gzip) et fait revalider le HTML, le CSS et le JS par ETag (304 quand rien n'a changé) : inutile de configurer une compression côté proxy. Les réponses /api/* restent en Cache-Control: no-store.

Dépannage

Symptôme Cause probable
{"error": "hôte non autorisé"} (403) Le nom tapé dans le navigateur n'est pas ARC_DASHBOARD_HOST.
404 page not found de Traefik Mauvais TRAEFIK_NETWORK ou TRAEFIK_ENTRYPOINT : comparez avec un service qui fonctionne.
Boucle de redirection, ou 404 après la connexion La route de l'avant-poste manque (COMPOSE_FILE), ou Authentik ne connaît pas ce nom (fournisseur single application).
Le conteneur redémarre en boucle docker compose logs : souvent Permission denied sur le workspace — ARC_UID/ARC_GID ne sont pas ceux du propriétaire.
« Aujourd'hui » bascule à 2 h du matin TZ absent : le conteneur est en UTC.

Et sans Docker ?

scripts/arc_serve.py accepte --listen et --allowed-host (ou ARC_DASHBOARD_LISTEN et ARC_DASHBOARD_ALLOWED_HOSTS) : c'est ce qu'utilise l'image. Hors conteneur, scripts/dashboard.sh reste sur 127.0.0.1. Pour une autre machine, le tunnel SSH est plus simple.