Le trafic WebSocket à travers un proxy inverse est l’une de ces choses qui « marchent simplement » jusqu’à ce que vous mettiez à l’échelle : les timeouts entrent en jeu en pleine conversation, les tableaux de bord montrent des erreurs TLS qui se révèlent être des déconnexions d’oisiveté, l’upstream coupe des connexions que le client n’a jamais fermées. Cet article est la configuration et le réglage d’exploitation qu’on déploie chez les clients faisant tourner des backends de chat, des tableaux de bord en direct, des passerelles MQTT-sur-WebSocket, et toute application où une connexion bidirectionnelle de longue durée est sur le chemin critique.
Comment vérifier
Une connexion WebSocket fonctionnelle ressemble à ça vue de l’extérieur :
curl -i -N -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
-H 'Sec-WebSocket-Version: 13' \
https://app.example.com/ws
wscat -c wss://app.example.com/ws
sudo journalctl -u caddy --since '5m ago' | grep -i 'websocket\|upgrade'
ss -tnp | grep -E ':(80|443).*ESTAB' | wc -l
Le curl devrait retourner 101 Switching Protocols et ne pas se fermer. wscat devrait se connecter et écho. Le journal ne devrait pas montrer de motifs error reading upstream. Le compte ss vous dit combien de connexions actives de longue durée Caddy courtise — utile pour les vérifications de capacité.
Ce qui se passe
WebSocket démarre comme une requête HTTP. Le client envoie un GET avec les en-têtes Connection: Upgrade et Upgrade: websocket. Le serveur (l’upstream, pas Caddy) accepte avec une réponse 101 Switching Protocols. Après ça, la connexion n’est plus HTTP — les deux côtés échangent des messages WebSocket tramés jusqu’à ce qu’un côté ferme.
La directive reverse_proxy de Caddy gère ça nativement. Aucune directive spéciale, pas de préambule proxy_set_header Upgrade $http_upgrade comme Nginx l’exige. Caddy détecte l’en-tête Upgrade dans la requête, ouvre la connexion upstream, transmet la réponse d’upgrade, puis fait passer les octets dans les deux sens jusqu’à ce qu’une extrémité ferme. La plomberie keep-alive HTTP/1.1 dans transport http n’interfère pas — la connexion est « possédée » par le gestionnaire WebSocket.
Les problèmes d’exploitation viennent des timeouts. Par défaut Caddy gardera une connexion WebSocket ouverte tant que les deux extrémités sont actives. Mais l’upstream peut avoir son propre timeout d’oisiveté, le répartiteur de charge ou le NAT au milieu peut couper la connexion après une période silencieuse, et le client peut se mettre en veille sur un appareil mobile. La solution est les pings au niveau application (le protocole WebSocket a une trame ping/pong intégrée précisément pour ça) plus un réglage de timeout par saut.
La procédure
-
Le proxy WebSocket minimal. Aucune directive spéciale nécessaire :
app.example.com { reverse_proxy localhost:3000 }reverse_proxydétecte l’upgrade et transmet. Terminaison HTTPS, automatique. Fait. -
Régler la durée de vie de la connexion upstream. Pour les connexions WebSocket de longue durée, les timeouts lecture/écriture de Caddy sur la connexion upstream doivent permettre des périodes d’oisiveté entre les trames de niveau application :
app.example.com { reverse_proxy localhost:3000 { transport http { read_buffer 4KB write_buffer 4KB dial_timeout 5s read_timeout 0 write_timeout 0 } } }read_timeout 0etwrite_timeout 0désactivent les timeouts par I/O. Les connexions WebSocket qui sont oisives (pas de trames en vol) sont maintenues en vie par la politique de ping de l’upstream, pas par Caddy. -
Gérer l’en-tête Origin. Les clients WebSocket envoient
Origin(l’URL de la page d’origine). L’upstream peut le vérifier pour la protection cross-origin — mais l’upstream voit Caddy comme pair immédiat. Transmettez l’Origin d’origine :app.example.com { reverse_proxy localhost:3000 { header_up Origin {>Origin} header_up Host {host} } }Le
{>Origin}est l’en-tête Origin de la requête, passé tel quel. -
Plusieurs upstreams WebSocket avec affinité par IP. Un backend de chat où chaque client devrait rester sur le même upstream (pour l’état de canal en mémoire) :
chat.example.com { reverse_proxy ws-1.internal:3000 ws-2.internal:3000 ws-3.internal:3000 { lb_policy ip_hash health_uri /health health_interval 5s health_timeout 1s } }ip_hashchoisit l’upstream en hachant l’IP source du client — le même client frappe toujours le même upstream tant que l’upstream est sain. Pour une application de chat avec état local au canal, ça évite les recherches inter-upstream. -
WebSocket plus HTTP sur le même préfixe de route. Certaines apps mélangent WebSocket et HTTP sous
/api/:app.example.com { @websocket { header Connection *Upgrade* header Upgrade websocket } handle /api/* { reverse_proxy localhost:3000 { transport http { read_timeout 0 write_timeout 0 } } } }Le sélecteur
@websocketest informatif ici ;reverse_proxygère l’upgrade automatiquement. Le réglage de timeout par gestionnaire s’applique à la fois à HTTP et à WebSocket sur la même route. -
Pings du backend pour garder le NAT en vie. Ceci est au niveau application, pas Caddy — mais c’est le correctif d’exploitation pour les rapports « déconnexions après exactement cinq minutes ». Configurez votre serveur WebSocket pour envoyer un ping toutes les 30-60 secondes :
// Exemple Node.js avec ws const wss = new WebSocket.Server({ port: 3000 }); wss.on('connection', (ws) => { const interval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) ws.ping(); }, 30000); ws.on('close', () => clearInterval(interval)); });L’implémentation WebSocket du navigateur répond aux pings automatiquement. Les timeouts d’oisiveté NAT et répartiteur de charge au milieu sont remis à zéro sur chaque trame, y compris les pings.
Notes d’exploitation
- La métrique
caddy_http_requests_in_flightde Caddy inclut les connexions WebSocket. Une connexion WebSocket qui est ouverte depuis une heure se montre comme une requête en vol — utile pour la planification de capacité, mais n’alertez pas sur les comptes absolus sauf si vous attendez uniquement des requêtes de courte durée. - L’en-tête
Sec-WebSocket-Protocolnégocie les sous-protocoles (par exemple, MQTT-sur-WebSocket utilisemqttici). Caddy le passe tel quel. - Fermer Caddy avec
systemctl restart caddycoupe chaque connexion WebSocket. Utilisezsystemctl reload caddy(qui envoie SIGUSR1 — rechargement gracieux) pour les changements de config. Les redémarrages complets devraient être programmés. - Un
read_timeout 0côté upstream signifie qu’une connexion WebSocket oisive reste ouverte pour toujours du point de vue de Caddy. Si l’upstream disparaît silencieusement (panique noyau, partition réseau), Caddy peut tenir la connexion pendant de nombreuses minutes avant que TCP keepalive déclenche une fermeture. Poseztcp_keepaliveau niveau système (net.ipv4.tcp_keepalive_time = 60danssysctl.conf) pour borner ça. - WebSocket-sur-HTTP/2 est supporté dans Caddy via les extensions WebSocket dans RFC 8441. La plupart des navigateurs n’envoient pas encore d’upgrades WebSocket sur HTTP/2 — ils retombent sur HTTP/1.1 sur la même connexion. Planifiez en conséquence.
Stack Harbor opère des flottes WebSocket pour les clients construisant chat, tableaux de bord et API temps réel — vérifications de santé ajustées pour les connexions de longue durée, politiques d’affinité d’upstream sous gestion de configuration, et le pipeline de métriques alertant sur le taux de déconnexion, pas seulement le taux de requête. Si vous avez une charge temps réel sur Caddy qui a besoin des politiques de timeout et de répartition de charge réglées correctement pour la forme du trafic, c’est le genre de travail couvert par notre pratique Exploitation infogérée.