La nuit où Traefik a avalé les WebSocket

Trois heures à chercher pourquoi le streaming ne marchait pas en production. Le coupable : un reverse proxy trop zélé et des timeouts par défaut.

Le symptôme

23h14. L’infra Cloud tourne. Traefik route le trafic, Let’s Encrypt fournit les certificats, les conteneurs répondent. Je déploie une app avec du WebSocket temps réel. La page s’affiche. Le CSS charge. L’API REST répond.

Je lance une session longue via WebSocket. Les premiers messages arrivent. Puis, au bout d’une minute pile, la connexion tombe. Pas d’erreur côté client, le WebSocket se ferme proprement, comme si le serveur avait décidé de raccrocher.

Je reteste en local, sans Traefik. La connexion tient indéfiniment. Le problème est entre Traefik et le backend.

La chasse

23h30. Premier réflexe : vérifier les logs Traefik. Rien. Pas d’erreur, pas de warning. La connexion disparaît en silence.

Deuxième réflexe : tester avec wscat directement depuis le VPS, en bypassant Traefik. La connexion tient. Le problème est confirmé : Traefik coupe la connexion.

Le coupable, trouvé après une heure de documentation : les respondingTimeouts de Traefik. Par défaut, si aucune donnée ne transite pendant un certain temps, Traefik considère que la connexion est morte et la ferme. Pour une API REST classique, c’est raisonnable. Pour un WebSocket où le serveur peut réfléchir pendant plusieurs minutes sans rien envoyer, c’est un piège.

Le fix

Dans la configuration Traefik, un transport dédié pour les services WebSocket :

# Traefik dynamic config
http:
  services:
    app-ws:
      loadBalancer:
        servers:
          - url: 'http://backend:3000'
        responseForwarding:
          flushInterval: '100ms'

  middlewares:
    ws-headers:
      headers:
        customRequestHeaders:
          X-Forwarded-Proto: 'https'

Plus l’augmentation des timeouts dans la config statique de Traefik pour les entrypoints concernés. Une connexion WebSocket légitime peut rester ouverte des heures.

Les autres pièges de la même nuit

Le WebSocket n’était que le premier acte. La soirée a continué.

Acte 2 : le body trop gros. Upload de fichier PDF. Traefik retourne 413 Request Entity Too Large. Par défaut, la limite est conservatrice. Il faut configurer le middleware buffering ou ajuster les limites au niveau du service. Le genre de défaut sain en théorie, invisible jusqu’au moment où un vrai utilisateur uploade un vrai fichier.

Acte 3 : le healthcheck trop strict. Un conteneur met 15 secondes à démarrer. Traefik le considère mort et ne lui route plus de trafic. Le conteneur finit par être prêt, mais Traefik a déjà basculé sur le circuit « no healthy backend ». Il faut ajuster les intervalles et les seuils de healthcheck dans les labels Docker.

Acte 4 : le header forwarding. L’app backend vérifie X-Forwarded-Proto pour savoir si la requête vient en HTTPS. Sans le middleware de headers approprié, le backend pense être en HTTP et génère des URLs de redirection sans TLS. Le navigateur refuse la redirection mixed-content. L’utilisateur voit une page blanche.

Le rôle Ansible qui encode tout ça

À 2h du matin, chaque fix était encodé dans le rôle Ansible traefik de Cloud. La config Traefik, les middlewares, les labels Docker par défaut, tout est versionné, testable, reproductible. Quand un nouveau service est déployé, il hérite automatiquement des bons timeouts, des bons headers, des bons healthchecks.

C’est ça l’intérêt de l’IaC : chaque nuit de debug produit de la configuration permanente. Le rôle Ansible de Traefik est un musée des erreurs passées. Chaque directive a une histoire.

La defense in depth en pratique

Le setup final dans Cloud est pensé en couches :

  • Traefik route le trafic avec TLS auto via Let’s Encrypt, discovery Docker native
  • Docker socket proxy en lecture seule, Traefik n’a jamais accès direct au socket Docker
  • 3 réseaux Docker isolés : proxy, monitoring, database, un conteneur compromis ne voit pas les autres
  • Fail2ban qui exporte ses métriques dans Prometheus, les tentatives d’intrusion sont monitorées, pas juste bloquées

21 rôles Ansible, 15 services, un seul make deploy. Chaque couche a été testée par une nuit de debug. Le monitoring Prometheus + Grafana vérifie en continu que tout ce qui a cassé une fois ne recasse pas.

Ce que j’en retiens

L’infra, c’est la couche où les surprises sont les plus silencieuses. Un bug applicatif plante avec une stack trace. Un bug d’infra disparaît dans le silence entre deux composants. La connexion WebSocket qui timeout sans erreur, le fichier rejeté sans message, le header manquant qui produit une page blanche, ce sont des fantômes.

La seule défense, c’est de transformer chaque bug en configuration versionnée. Et de ne jamais déployer un vendredi soir. Sauf qu’on le fait quand même.

← Retour au carnet