Certbot renewed the certificate for an nginx host three days ago and the new file is on disk, but clients are still served the old, now-expired certificate. Why is nginx still using it, and what makes a renewal take effect automatically?
answer
- read once, at load time
- workers hold it in memory
- the live path is a symlink
- reload, do not restart
- --deploy-hook fires only on renewal
basics
~20 snginx reads certificate files once, when it starts or reloads, and keeps them in worker memory. A renewed file on disk changes nothing until nginx reloads, so renewal must trigger a reload — for example certbot's --deploy-hook.
solid answer
~40 snginx opens the files named by `ssl_certificate` and `ssl_certificate_key` at configuration load time and holds the parsed material in each worker process. Nothing rereads them afterwards, so certbot writing a fresh `fullchain.pem` into `/etc/letsencrypt/live/<domain>/` leaves the running workers serving the old certificate until it expires. The fix is a reload — `systemctl reload nginx` or `nginx -s reload` — which parses the config again, starts new workers with the new certificate and lets the old ones drain their connections, with no dropped requests. To make it automatic, hook the reload to renewal: `certbot renew --deploy-hook "systemctl reload nginx"`, which runs only when a certificate was actually renewed. If certbot was originally run with `--nginx`, its installer performs the reload itself. Verify what is actually served with `openssl s_client`, never with `ls`.
code
bash · 4 linesnginx -t && systemctl reload nginx
certbot renew --dry-run
certbot renew --deploy-hook "systemctl reload nginx"
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -datesgo deeper
Be ready to say that nginx loads certificates at start or reload and holds them in memory, and that systemctl reload nginx after renewal is what actually changes what clients get.
Explain the reload mechanics — new workers with the new context, old workers draining — and wire renewal to it with a deploy hook rather than a scheduled blanket reload.
Show how you verify and monitor: read the certificate off the wire with openssl s_client, alert on renewal-job failure as well as on expiry, and rehearse the path with certbot renew --dry-run.
Own certificate lifecycle as platform policy: one automated issuance and distribution path for every edge host, external expiry probes, and a rule that no certificate is installed by hand — expired certificates are a design failure, not an individual's oversight.
## Where the certificate lives once nginx is running When nginx loads its configuration it reads the PEM files named by `ssl_certificate` and `ssl_certificate_key`, hands them to OpenSSL, and keeps the resulting SSL context in memory. Worker processes inherit it. From then on nginx never touches those paths again — there is no file watcher, no expiry check, and no periodic reread. This is deliberate: parsing keys on every handshake would be slow, and keys are often read while nginx still holds the privileges to do so. The practical consequence is the one this question describes. Certbot renewed successfully, the file on disk is new, `openssl x509 -in fullchain.pem -noout -dates` shows a healthy validity window — and users still see an expired certificate, because they are talking to worker processes that loaded the old bytes weeks ago. ## What certbot actually wrote Certbot does not overwrite files in place. Each issuance lands in `/etc/letsencrypt/archive/<domain>/` as `cert2.pem`, `fullchain2.pem` and so on, and the stable paths under `/etc/letsencrypt/live/<domain>/` are **symlinks** repointed to the newest set. That is why you should always configure nginx against the `live` path: it stays constant across renewals. It is also why nothing changes for a running nginx — following a symlink is something that happened once, at load time. ## Reload, not restart ```bash nginx -t && systemctl reload nginx ``` A reload makes the master process re-read the configuration, spawn new workers with the new certificate, and tell the old workers to stop accepting connections and exit once their in-flight requests finish. No listening socket is closed, so no connection is refused. A restart, by contrast, drops the listeners briefly and interrupts in-flight requests, and it is unnecessary here. Always gate the reload behind `nginx -t`: a reload with a broken config leaves the old workers running, which hides the breakage until the next restart. ## Automating it Certbot installs a systemd timer (`certbot.timer`) or a cron entry that runs `certbot renew` roughly twice a day. `certbot renew` is a no-op for certificates not yet inside the renewal window — by default it acts when fewer than 30 days of validity remain — which is why the schedule is frequent and harmless. The missing link is telling nginx. Two shapes: - **`certbot --nginx`** — certbot's nginx installer edited your config in the first place and performs the reload itself after a successful renewal. - **`certbot certonly --webroot` / `--standalone` / DNS plugins** — certbot has no installer and will not touch nginx. You must supply the reload: ```bash certbot renew --deploy-hook "systemctl reload nginx" ``` `--deploy-hook` runs only when a certificate was actually renewed, so a twice-daily timer does not reload nginx twice a day for nothing. Persist it by putting a script in `/etc/letsencrypt/renewal-hooks/deploy/`, which runs for every renewed certificate without repeating the flag. ## Verifying, and monitoring so it never bites Check what is served on the wire, not what is on disk: ```bash openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \ | openssl x509 -noout -dates -subject ``` Because the on-disk file and the served certificate can disagree for exactly this reason, expiry monitoring must probe the endpoint from outside. A file-based check is the monitoring equivalent of the same bug: it goes green while customers see a browser interstitial. Test the whole loop at least once — `certbot renew --dry-run` exercises issuance and hooks against the staging environment without consuming rate limits. ## Related failure modes worth naming - Renewal fails silently for weeks because the HTTP-01 challenge path stopped being reachable after a config change, and nobody reads the timer's output. Alert on renewal failure, not only on expiry. - Several hosts share a certificate and only one of them reloads. - A container image baked with the certificate inside it: renewal requires a redeploy, not a reload.
- Why is a reload preferable to a restart here?A reload keeps the listening sockets open: the master re-reads the config, forks new workers with the new certificate, and lets old workers finish their in-flight requests before exiting. A restart closes the listeners for a moment, refusing connections and cutting active requests. Gate it with `nginx -t` so a bad config cannot take effect.
- Why point ssl_certificate at the live directory instead of the archive file certbot actually wrote?`/etc/letsencrypt/live/<domain>/` holds symlinks that certbot repoints at each renewal, so the configured path never changes. Naming `archive/<domain>/fullchain3.pem` directly pins you to one issuance, and the next renewal writes `fullchain4.pem` that nginx will never load.
- How should certificate expiry be monitored so this failure is caught?Probe the live endpoint from outside and read the certificate off the wire, alerting well before expiry — weeks, not days. Checking the file on disk reproduces the bug, since disk and served certificate disagree precisely when the reload is missing. Alert on renewal-job failure too, because that fails silently much earlier.
saying these in an interview costs you the question
- nginx picks up a new certificate file automatically
- You must restart nginx, a reload cannot swap certificates
- Checking the file's expiry on disk proves what clients receive
- certbot renew always reloads the web server for you
- Run certbot renew --force-renewal daily to be safe