Nginx sous Windows devant Jenkins : le proxy, les logs et les mises à jour
Poser un nginx sur Windows Server pour terminer le TLS devant un Jenkins, c’est cinq minutes pour la version qui marche en démo et une vraie demi-journée pour la version qu’on assume en production.
La partie reverse proxy n’est pas la plus dure, elle tient dans un location bien réglé. Ce qui fait la différence, c’est ce que Windows ne vous donne pas et qu’aucun tutoriel ne mentionne: la rotation des logs et une mise à jour qui prend vraiment.
Un mot avant de commencer: si la même machine sert aussi de façade à un Perforce, ne cherchez pas à le faire passer par la conf ci-dessous. Le protocole P4 n’est pas du HTTP, il relève d’une tout autre approche dont je parle à part dans nginx devant Perforce
.
Jenkins : l’UI et les agents dans un seul location
Jenkins est du HTTP, donc il vit dans le bloc http avec un vrai reverse proxy. Sa subtilité est ailleurs: l’interface web et les agents modernes, qui se connectent au controller en WebSocket, passent par la même entrée et la conf doit servir les deux sans qu’on ait à les séparer en deux location.
La clé est une map qui calcule l’en-tête Connection selon que la requête demande une promotion WebSocket ou non et qu’on applique à tout le trafic:
http {
upstream jenkins {
server jenkins.domain.internal:8080;
keepalive 16;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
On ne peut pas écrire Connection "upgrade" en dur, sous peine de casser les requêtes HTTP ordinaires: la map renvoie upgrade quand le client demande une promotion WebSocket et close sinon, de sorte que le même location sert l’UI et les agents.
server {
listen 443 ssl;
http2 on;
server_name jenkins.example.com;
ssl_certificate ssl/jenkins.example.com.crt;
ssl_certificate_key ssl/jenkins.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
# Jenkins renvoie des en-têtes que nginx juge invalides par défaut
ignore_invalid_headers off;
location / {
proxy_pass http://jenkins;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# promotion WebSocket pour les agents, pilotée par la map
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# pas de bufferisation: compat CLI Jenkins et upload d'artefacts
proxy_request_buffering off;
proxy_buffering off;
client_max_body_size 10m;
proxy_connect_timeout 90s;
proxy_send_timeout 90s;
proxy_read_timeout 90s;
}
}
}
Trois points méritent qu’on s’y arrête.
ignore_invalid_headers off est nécessaire parce que Jenkins émet des en-têtes que nginx rejette autrement et sans cette ligne des pages de l’UI partent en vrac.
Les deux proxy_*_buffering off servent la CLI Jenkins et les transferts d’artefacts, qui supportent mal la bufferisation de nginx.
Et le proxy_read_timeout doit rester plus grand que l’intervalle de ping des agents WebSocket, sans quoi une session d’agent se ferait couper en silence: 90 secondes suffisent parce que l’agent ping bien plus souvent, mais c’est le premier paramètre à regarder si des agents tombent par intermittence sous charge.
Un location de confort, optionnel, quand l’UI sert beaucoup de statique:
location ~ "^/static/[0-9a-fA-F]{8}/(.*)$" {
rewrite "^/static/[0-9a-fA-F]{8}/(.*)" /$1 last;
}
Et un mot sur http2 on;: la vieille écriture listen 443 ssl http2; est dépréciée dans les nginx récents au profit de la directive séparée, autant la prendre tout de suite.
Le certificat, sur Windows
certbot a arrêté le support Windows début 2024 et win-acme n’est plus maintenu depuis mi-2024, donc le choix raisonnable aujourd’hui est simple-acme, le successeur du même auteur, qui s’installe par Chocolatey et fait le challenge et le renouvellement.
nginx ne se reconfigure pas tout seul comme avec le plugin nginx de certbot, donc on pointe nginx sur les fichiers déposés par l’outil et on recharge nginx au renouvellement.
Les deux ennuis que personne ne raconte
Voilà ce qui manque à la plupart des guides et c’est précisément ce qui fait la différence entre une démo et un service.
La rotation des logs, d’abord.
Sous Linux on a logrotate, sous Windows on n’a rien d’équivalent et nginx garde ses fichiers de log ouverts, donc il ne suffit pas de les renommer ou de les supprimer, nginx continuerait d’écrire dans l’ancien descripteur ou, pire, cesserait d’écrire.
Le motif correct est une tâche planifiée qui déplace les fichiers de log puis envoie à nginx le signal de réouverture:
# tâche planifiée quotidienne
$log = "C:\nginx\logs"
$stamp = Get-Date -Format "yyyyMMdd"
Move-Item "$log\access.log" "$log\access-$stamp.log"
Move-Item "$log\error.log" "$log\error-$stamp.log"
& C:\nginx\nginx.exe -s reopen # nginx rouvre ses fichiers de log
Get-ChildItem "$log\*-*.log" | Where-Object LastWriteTime -lt (Get-Date).AddDays(-30) | Remove-Item
Le nginx -s reopen est le point clé, c’est lui qui fait rouvrir à nginx des descripteurs neufs sur les noms d’origine et sans lui la rotation ne tourne pas, elle casse la journalisation.
La mise à jour par Chocolatey, ensuite, est le piège le plus sournois.
Le package nginx de Chocolatey s’installe dans un dossier versionné, du genre C:\tools\nginx-1.29.0 et une mise à jour pose un nouveau dossier versionné à côté, ce qui veut dire que le service Windows, la conf et les chemins de certificat pointent toujours sur l’ancienne version tandis que la nouvelle dort inutilisée.
On croit avoir mis à jour, on tourne toujours sur l’ancien binaire.
Deux sorties propres.
Soit on épingle un chemin d’installation stable au moment de l’install, ce que le package accepte en paramètre, pour que la version ne soit jamais dans le chemin.
Soit on fait pointer le service sur une jonction stable et on la repointe en post-install après chaque upgrade:
# un chemin stable, repointé après chaque mise à jour choco
$ver = (Get-ChildItem C:\tools\nginx-* | Sort-Object Name -Descending | Select-Object -First 1).FullName
if (Test-Path C:\nginx) { (Get-Item C:\nginx).Delete() }
New-Item -ItemType Junction -Path C:\nginx -Target $ver
Restart-Service nginx
Le service vise toujours C:\nginx, la jonction suit la dernière version installée et la conf comme les certificats vivent hors du dossier versionné pour ne pas être emportés par la rotation de versions.
Le proxy tient, le reste aussi
Le reverse proxy Jenkins tient dans une map d’upgrade sur un seul location, ignore_invalid_headers off et la bufferisation coupée et une fois ces trois réflexes acquis il ne bouge plus.
Le vrai travail est ailleurs, dans les deux évidences d’exploitation qui ne sont des évidences que sous Linux: la rotation des logs à la main avec nginx -s reopen et la mise à jour par Chocolatey qui vous laisse tourner sur l’ancien binaire tant que vous ne figez pas un chemin stable.
La version démo ignore tout ça et elle a l’air de marcher, jusqu’au jour où un agent tombe, où le disque se remplit de logs ou où un CVE nginx reste en place parce que l’upgrade n’a jamais pris.
[edit: 2026] L’implémentation Ansible de tout ceci: le rôle nginx .