nginx reloaded nothing. Certbot still exited 0.

Certbot wrote a new fullchain.pem and exited 0. openssl on :443 still shows last month's notAfter. Prove disk vs wire, reload nginx, add a deploy hook.

Cover Image for nginx reloaded nothing. Certbot still exited 0.

Certbot finished the renew. The log line says success. /etc/letsencrypt/live/yourdomain.com/fullchain.pem has a notAfter about 89 days out.

openssl s_client against :443 still prints last month's date.

nginx loads certificates at start (and on reload). A successful renew on disk does nothing for visitors until something runs reload. That gap is this whole post.

Prove it (disk vs wire)

Run both. Compare the two end dates.

# disk — what certbot wrote
openssl x509 -noout -enddate \
  -in /etc/letsencrypt/live/yourdomain.com/fullchain.pem

# wire — what visitors get on :443
echo | openssl s_client -connect yourdomain.com:443 \
  -servername yourdomain.com 2>/dev/null \
  | openssl x509 -noout -enddate

If disk is fresh and wire is old, stop debugging ACME. The issue is reload (or the wrong server block / wrong cert path).

Same answer without SSH: paste the hostname into the free SSL check. Read the served notAfter. That is the wire date.

For the full failure map (timer, HTTP-01, rate limit, root mail), use the disk vs wire hub.

Fix now

Reload the process that terminates TLS for that name.

# systemd nginx
sudo nginx -t && sudo systemctl reload nginx

# or
sudo nginx -t && sudo nginx -s reload

Re-run the wire command (or the free check). The served notAfter should match disk.

If wire is still old after a clean reload:

  • Confirm which binary owns :443 (ss -lptn 'sport = :443' or sudo lsof -i :443).
  • Confirm the ssl_certificate path in the server block points at the same fullchain.pem you inspected.
  • If you terminate TLS on a load balancer or another host, reload that edge, not only the app box.

Make it permanent (deploy hook)

Certbot runs scripts in /etc/letsencrypt/renewal-hooks/deploy/ after a successful renew. Put the reload there so the next renew does not depend on memory.

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'HOOK'
#!/bin/sh
# Reload nginx only when certbot renewed at least one cert.
systemctl reload nginx
HOOK
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Dry-run does not always exercise deploy hooks the way a real renew does. After the next real renew (or a controlled renew in a maintenance window), compare disk and wire again.

Optional: only reload when the renewed lineage matches hosts this box serves. Keep the first version simple: one reload after any successful renew on this machine is enough for most single-nginx boxes.

How you catch it off-box

From inside the server, every log can still say "renewed." From outside, the served cert is the only signal that matches the browser.

Watch the wire. Not only the renew log.