Du betreibst nginx mit Let’s Encrypt-Zertifikaten. Alles läuft. Bis du bemerkst, dass certbot renew bei jeder Ausführung nginx stoppt und neu startet — für jedes einzelne Zertifikat.
Bei einem Zertifikat: egal. Bei 42: addiert sich zu einer Downtime-Fenster, die Nutzer bemerken.
Der Fix heißt webroot-Modus. Er lässt certbot die ACME-Challenge über nginx ausliefern, ohne nginx anfassen zu müssen. Migration dauert 15 Minuten. Dann gibt es einen Gotcha, der bei einem von 42 Zertifikaten still fehlschlägt.
TL;DR
- nginx-Snippet für ACME anlegen und in alle Server-Blöcke einbinden
- Renewal-Configs auf
authenticator = webrootumstellen - Systemd-Timer-Override anpassen: pre/post-hook → deploy-hook
- Bei Server-Blöcken mit
return 301auf Root-Level: Redirect inlocation /wrappen, sonst frisst der Rewrite-Phase die Challenge weg
Das Problem: standalone stoppt nginx
certbot im standalone-Modus bindet selbst Port 80, um die ACME-Challenge zu beantworten. Dafür muss nginx vorher runter.
Das sieht im Systemd-Timer-Override so aus:
[Service]
ExecStart=
ExecStart=/usr/bin/certbot renew \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx"
Bei einer Handvoll Zertifikate ist die Gesamtpause kurz. Bei 42 Zertifikaten — jedes mit eigenem pre/post-hook-Zyklus — summiert sich das. nginx ist mehrfach für mehrere Sekunden weg.
Die Lösung: webroot-Modus
Im webroot-Modus läuft nginx durch. certbot legt die Challenge-Datei in ein Verzeichnis, nginx liefert sie aus, Let’s Encrypt prüft sie, Zertifikat wird erneuert. Kein Stopp, kein Neustart.
Schritt 1: ACME-Verzeichnis und nginx-Snippet
mkdir -p /var/www/acme
Dann das Snippet /etc/nginx/snippets/letsencrypt.conf:
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type "text/plain";
}
^~ ist wichtig: es hat Vorrang vor regulären Ausdrücken. Die Challenge-Requests kommen durch, egal welche anderen Location-Regeln im Server-Block stehen.
Schritt 2: Snippet in alle nginx-Sites einbinden
In jeden Server-Block, der über Port 80 erreichbar ist:
server {
listen 80;
server_name <DEIN-HOST>.<DEINE-DOMAIN>;
include snippets/letsencrypt.conf;
# ... Rest des Server-Blocks
}
Alternativ: den Location-Block direkt einfügen statt include. Snippet ist aber wartbarer wenn du 42 Sites hast.
Schritt 3: Renewal-Configs umstellen
In /etc/letsencrypt/renewal/ liegt für jedes Zertifikat eine .conf-Datei. Darin steht bisher:
authenticator = standalone
Das muss auf webroot:
authenticator = webroot
webroot_path = /var/www/acme
Für eine Handvoll Zertifikate: manuell. Für 42:
cd /etc/letsencrypt/renewal
# Backup zuerst
cp -r . ../renewal.bak
# standalone → webroot ersetzen
sed -i 's/^authenticator = standalone/authenticator = webroot/' *.conf
# webroot_path hinzufügen wo noch nicht vorhanden
for f in *.conf; do
grep -q 'webroot_path' "$f" || \
sed -i '/^authenticator = webroot/a webroot_path = /var/www/acme' "$f"
done
Danach kurz prüfen ob alles sauber aussieht:
grep -A1 'authenticator' /etc/letsencrypt/renewal/*.conf | head -30
Schritt 4: Systemd-Timer-Override anpassen
Der Override liegt üblicherweise unter /etc/systemd/system/certbot.service.d/override.conf oder ähnlich. Der pre/post-hook fliegt raus:
[Service]
ExecStart=
ExecStart=/usr/bin/certbot renew \
--deploy-hook "systemctl reload nginx"
--deploy-hook läuft nur wenn ein Zertifikat tatsächlich erneuert wurde — nicht bei jedem Renewal-Lauf. reload statt restart reicht: nginx liest die neuen Zertifikatsdateien ohne Verbindungen zu kappen.
systemctl daemon-reload
Der Gotcha: return 301 auf Server-Ebene
41 von 42 Zertifikaten haben beim ersten certbot renew --dry-run problemlos funktioniert. Eines nicht.
Der fehlende Server-Block sah so aus:
server {
listen 192.168.66.1:80;
server_name mail.<DEINE-DOMAIN>;
return 301 https://webmail.<DEINE-DOMAIN>$request_uri;
}
Ein simpler HTTP-zu-HTTPS-Redirect. Dazu kam das Snippet per include — also war ein location /.well-known/acme-challenge/ Block vorhanden. Trotzdem: Challenge-Request → 301 → https://webmail.<DEINE-DOMAIN>/.well-known/acme-challenge/... → 404.
Warum das passiert
nginx’s return-Direktive auf Server-Block-Ebene gehört zur Rewrite-Phase. Die Rewrite-Phase läuft vor dem Location-Matching. Das bedeutet: nginx sieht den Request, führt return 301 aus, und schaut sich die Location-Blöcke gar nicht erst an.
Ein location /.well-known/acme-challenge/ Block in demselben Server-Block nützt nichts, solange return 301 außerhalb eines Location-Blocks steht. Der Redirect feuert zuerst — für jeden Request, ausnahmslos.
Der Fix: Redirect in location / wrappen
server {
listen 192.168.66.1:80;
server_name mail.<DEINE-DOMAIN>;
location /.well-known/acme-challenge/ {
root /var/www/acme;
default_type "text/plain";
}
location / {
return 301 https://webmail.<DEINE-DOMAIN>$request_uri;
}
}
Jetzt läuft das Location-Matching normal durch. /.well-known/acme-challenge/ trifft zuerst (durch ^~ im Snippet oder durch spezifischeren Prefix-Match), normale Requests landen in location / und werden redirected.
Das gilt für alle Server-Blöcke die return auf Top-Level verwenden — egal ob Redirect zu HTTPS oder zu einer anderen Domain.
Verifikation
Erst trocken laufen lassen:
certbot renew --dry-run
Wenn einer fehlschlägt, siehst du in der Ausgabe welches Zertifikat und welche Domain. Dann den entsprechenden Server-Block prüfen: return auf Root-Ebene?
Für einen schnellen Check aller Renewal-Configs:
# Alle Domains aus den Renewal-Configs
grep -h '^domains' /etc/letsencrypt/renewal/*.conf
Dann für jeden hostname prüfen ob der zugehörige nginx-Server-Block ein Top-Level return hat:
grep -r 'return 3' /etc/nginx/sites-enabled/
Alles was dort auftaucht: Server-Block öffnen, return in location / wrappen.
Nach erfolgreichem dry-run, echter Lauf (falls die Zertifikate bald ablaufen) oder einfach warten bis der Timer das nächste Mal anspringt.
Fazit
Webroot ist der richtige Modus sobald nginx dauerhaft läuft. standalone macht nur Sinn wenn kein Webserver läuft — oder wenn man es beim ersten Setup schnell hinter sich bringen will.
Die zwei Lernpunkte:
returnauf Server-Ebene ignoriert Location-Blöcke. Das ist kein Bug, das ist nginx-Architektur. Rewrite-Phase vor Location-Phase. Wer redirectet, muss es inlocation /tun wenn er Ausnahmen haben will.Dry-run immer vor dem ersten echten Lauf. 41/42 wäre nicht aufgefallen bis das 42. Zertifikat abläuft — und dann nachts um 3 Uhr.
certbot renew --dry-run
# Alles grün? Dann ist's gut.