All resources

Practical guide · Certbot + Nginx

Certbot renewed the certificate, but Nginx still serves the old one

Certbot has written a new certificate. An external TLS connection still receives the old one. Check which certificate Nginx loads, which server answers, and whether the reload succeeded before requesting another renewal.

This guide covers Linux with Bash, OpenSSL, Certbot and Nginx managed by systemd, with static certificate file paths. Replace example.com and the certificate name with your own. Containers, a CDN or a load balancer require checking the component that actually terminates TLS.

1. Identify the certificate on disk

On the Nginx host, list the certificates managed by Certbot. Use the displayed Certificate Name: it can differ from the domain, for example example.com-0001. Read the leaf certificate in that lineage and note its SHA-256 fingerprint, serial number and validity dates. Never copy the private key into a diagnostic report.

sudo certbot certificates
sudo openssl x509 \
  -in /etc/letsencrypt/live/example.com/cert.pem \
  -noout -sha256 -fingerprint -serial -dates

A successful certbot renew exit status does not prove that a certificate was renewed: the command also succeeds when nothing is due. A dry run tests renewal with the staging service; it does not replace the production certificate.

2. Inspect a fresh external TLS connection

Run this command from another machine outside the hosting network. The hostname is also sent as SNI so the server can select the correct virtual host. This command inspects the presented certificate; it does not establish trust in the chain or verify the hostname.

set -o pipefail
openssl s_client -connect example.com:443 \
  -servername example.com </dev/null |
  openssl x509 -noout -sha256 -fingerprint -serial -dates

Compare SHA-256 fingerprints, not just dates. A match means that this connection received the certificate you inspected on disk. A mismatch identifies a different certificate, but does not by itself prove that Nginx failed to reload. If the connection fails or no certificate is printed, investigate that error first.

3. Check the TLS terminator and reload Nginx

Check the effective Nginx configuration locally with sudo nginx -T. Find the server_name, listen, ssl_certificate and ssl_certificate_key directives for this hostname. Configuration output can contain secrets: do not publish it. With Certbot, the usual certificate path ends in live/<certificate-name>/fullchain.pem; an old archive path or a copied file will not follow the lineage automatically.

sudo nginx -t && sudo systemctl reload nginx

For Nginx managed by systemd on this host, validate the configuration before reloading. If either command fails, inspect the service journal and the Nginx error log. Then repeat the external probe on a new connection and compare fingerprints again. Certificate renewal and successful deployment are separate checks.

4. Make the reload part of deployment

If you use certonly with webroot or DNS validation and manage Nginx deployment yourself, use a deploy hook after successful renewal. On a standard Linux installation, place the following executable script in /etc/letsencrypt/renewal-hooks/deploy/reload-nginx. Check binary paths on your host and test the script with sudo before relying on it. A Certbot Nginx installer may already handle deployment; inspect the existing setup before adding a second mechanism.

#!/bin/sh
set -eu
/usr/sbin/nginx -t
/usr/bin/systemctl reload nginx

Certbot renew --dry-run does not run deploy hooks by default. The --run-deploy-hooks option runs them during the test and can reload the live service using the active certificate. Schedule that test deliberately. Avoid repeated --force-renewal calls to troubleshoot a deployment problem.

5. Reproduce the mismatch locally

The downloadable Bash script starts an isolated Nginx container bound to 127.0.0.1 on a random port. It creates two self-signed certificates for renewal.test, replaces the files and compares the served fingerprint before and after reload. It simulates file replacement, not ACME issuance. Requirements: Linux, a local Docker daemon and OpenSSL; Docker may download the pinned Nginx image.

Download and inspect the demonstration script

After reading the downloaded file, run bash nginx-certificate-reload.sh without sudo if your Docker setup permits it. BEFORE and SERVED must match; DISK must differ. AFTER must match DISK. The script checks these conditions and removes its container and temporary keys on exit. It does not modify your Nginx service.

6. Check the other paths, then keep monitoring

A CDN can legitimately serve a different certificate from the origin. Test those two TLS connections separately. For multiple backends or IPv4/IPv6, probe each relevant address while preserving the same SNI hostname. One successful probe does not cover every backend, network path or visitor. With separate RSA and ECDSA certificates, compare the corresponding certificate for the handshake used. In the example below, replace 192.0.2.10 with the address you are authorized to check. For IPv6, use [address]:443 in -connect.

openssl s_client -connect 192.0.2.10:443 \
  -servername example.com </dev/null |
  openssl x509 -noout -sha256 -fingerprint -serial -dates

Nocert periodically observes certificates served by reachable TLS endpoints and evaluates expiration notification rules in a daily cycle. Check the observed endpoint and observation time, then configure the recipients for expiration alerts. This ongoing monitoring complements the immediate external check; it does not issue certificates, reload Nginx or certify that each Certbot renewal was deployed successfully.

Technical sources