Fix Let's Encrypt Certificate Renewal Failures
Fix Certbot renewal failures for Django sites on Nginx: dry runs, timer checks, HTTP-01 challenge paths, redirects, firewalls, DNS changes, and reload hooks.
Problem statement
Your Django site's HTTPS certificate from Let's Encrypt is about to expire, or already has, and browsers show NET::ERR_CERT_DATE_INVALID. Certbot was installed months ago and was supposed to renew automatically. Common findings:
- the renewal timer or cron job is not running
- renewal runs but fails the HTTP-01 challenge, often because of a changed Nginx configuration, a redirect, a firewall rule, or a CDN
- renewal succeeds, but Nginx still serves the old certificate because it was never reloaded
- DNS moved to another server or load balancer, so the challenge request no longer reaches this machine
Let's Encrypt certificates are valid for a short period (90 days at the time of writing, and moving to shorter lifetimes), and Certbot tries to renew them well before expiry. That margin only helps if someone notices failures.
Quick answer
- Check certificate expiry and names:
sudo certbot certificates. - Check the scheduler:
systemctl list-timers | grep certbot(or the cron entry in/etc/cron.d/certbot). - Run
sudo certbot renew --dry-runand read the exact error. - Fix the challenge path: port 80 reachable from the internet, DNS pointing at this server,
/.well-known/acme-challenge/not redirected elsewhere or blocked. - Add a deploy hook so Nginx reloads after a successful renewal.
- Renew for real with
sudo certbot renew, then verify the served certificate withopenssl s_client. - Add expiry monitoring.
Step-by-step solution
1. Inspect certificates
sudo certbot certificates
Output lists each certificate, its domains, expiry date, and paths:
Certificate Name: www.example.com
Domains: example.com www.example.com
Expiry Date: 2026-10-02 08:14:11+00:00 (VALID: 15 days)
Certificate Path: /etc/letsencrypt/live/www.example.com/fullchain.pem
Compare with what Nginx actually serves:
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null \
| openssl x509 -noout -dates -subject -ext subjectAltName
If Certbot shows a newer expiry than the served certificate, renewal worked but Nginx was not reloaded (step 6).
2. Check the renewal scheduler
Packaged Certbot on Debian and Ubuntu installs a systemd timer; snap installations use a snap timer:
systemctl list-timers --all | grep -i certbot
systemctl status certbot.timer --no-pager
sudo journalctl -u certbot.service --since "30 days ago" --no-pager | tail -40
For snap-based installs:
systemctl list-timers --all | grep -i snap.certbot
If no timer or cron job exists, enable the one your installation provides, for example sudo systemctl enable --now certbot.timer.
Having both a snap-based Certbot and an apt-based one installed causes confusion about which configuration and timer is active. Keep one.
3. Run a dry run
sudo certbot renew --dry-run
The dry run uses Let's Encrypt's staging environment, so it does not count against production rate limits. Typical failures:
Certbot failed to authenticate some domains (authenticator: nginx).
Detail: 203.0.113.10: Invalid response from http://www.example.com/.well-known/acme-challenge/abc123: 404
Detail: Fetching http://www.example.com/.well-known/acme-challenge/abc123: Timeout during connect (likely firewall problem)
Detail: DNS problem: NXDOMAIN looking up A for old.example.com
The detail line tells you which category you are in.
4. Find out how the certificate renews
Each certificate has a renewal configuration:
sudo cat /etc/letsencrypt/renewal/www.example.com.conf
Key lines:
[renewalparams]
authenticator = nginx
installer = nginx
or authenticator = webroot with a webroot_path, or a DNS plugin. Renewal reuses these settings, so a certificate created months ago with webroot depends on that directory still being served.
5. Fix HTTP-01 challenge failures
The challenge requires Let's Encrypt's servers to fetch http://<domain>/.well-known/acme-challenge/<token> over plain HTTP on port 80 from this server.
Port 80 blocked. Many hardened setups close port 80 because the site "only uses HTTPS". Let's Encrypt's HTTP-01 challenge needs it:
sudo ufw status | grep -E "80|Nginx"
sudo ss -ltnp | grep ':80 '
Open port 80 and let Nginx redirect normal traffic to HTTPS.
Redirect or Django catching the path. A port 80 server block that proxies everything to Django, or redirects to another host, can break webroot challenges. Serve the challenge path explicitly before the redirect:
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://www.example.com$request_uri;
}
}
With root /var/www/letsencrypt, a request for /.well-known/acme-challenge/token is served from /var/www/letsencrypt/.well-known/acme-challenge/token, which matches Certbot's --webroot -w /var/www/letsencrypt.
Let's Encrypt follows HTTP-to-HTTPS redirects for challenges, so a plain redirect to HTTPS on the same host often works too. Problems appear when redirects go to a different host that does not have the token, or when the HTTPS side has an invalid certificate and redirects elsewhere.
Nginx configuration changed. With the nginx authenticator, Certbot temporarily adds a server block. If nginx -t fails because of an unrelated broken configuration, renewal fails. Run sudo nginx -t first.
6. Reload Nginx after renewal
Nginx reads certificate files at startup and reload. Add a deploy hook, which runs only after a successful renewal:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
nginx -t && systemctl reload nginx
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Scripts in /etc/letsencrypt/renewal-hooks/deploy/ run for every certificate renewal. The nginx installer usually reloads Nginx itself, but an explicit hook makes the behavior independent of the authenticator used.
7. DNS changed or a CDN sits in front
- DNS moved (new server, load balancer, or a domain that no longer points here): renewal of that name from this server can no longer succeed. Remove stale names by reissuing the certificate with only current domains, or issue certificates where traffic now terminates.
- Cloudflare proxy: HTTP-01 challenges pass through Cloudflare to the origin, but settings such as "Always Use HTTPS", redirect rules, or firewall rules can interfere. A DNS-01 challenge with the Cloudflare DNS plugin avoids HTTP entirely, or use a Cloudflare Origin CA certificate with Full (strict) mode instead of Let's Encrypt on the origin.
- IPv6: if an
AAAArecord exists, Let's Encrypt may validate over IPv6. The server must listen on IPv6 port 80 (listen [::]:80;) and the firewall must allow it.
8. Renew and verify
sudo certbot renew
sudo systemctl reload nginx
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null \
| openssl x509 -noout -enddate
If you need to renew one certificate immediately:
sudo certbot renew --cert-name www.example.com --force-renewal
Use --force-renewal sparingly; production issuance is rate limited.
Verification check:
certbot renew --dry-runsucceeds for all certificates- the served certificate's expiry date matches
certbot certificates - the timer shows a next run within the next day
- the deploy hook exists and is executable
9. Monitor expiry
Relying on the renewal timer alone means failures are silent. Add an external check that alerts when the served certificate expires in fewer than, for example, 14 days. Many uptime monitoring services include certificate expiry checks, and a simple script works too:
end=$(echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
days=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))
[ "$days" -lt 14 ] && echo "Certificate for www.example.com expires in $days days"
Explanation
Automatic renewal is a chain: a scheduler starts Certbot, Certbot proves domain control through the challenge method recorded when the certificate was first issued, Let's Encrypt issues a new certificate, and Nginx must reload to use it. Infrastructure changes made after the first issuance (closing port 80, adding a CDN, moving DNS, changing redirects) often break the challenge without anyone noticing until expiry warnings appear. A dry run shows exactly which link is broken.
Edge cases / notes
Wildcard certificates
Wildcard certificates require DNS-01 challenges, which need a DNS provider plugin and API credentials stored on the server. Protect those credentials carefully; they can modify your DNS.
Multiple servers behind a load balancer
HTTP-01 challenges may hit a server that did not request the certificate. Terminate TLS at the load balancer, use DNS-01, or share the challenge directory.
Rate limits
Repeated failed or forced issuances can hit Let's Encrypt rate limits. Use --dry-run while debugging.
Switching a certificate to a more robust challenge method
If HTTP-01 keeps breaking because of CDNs, redirects, or firewall policies, move the certificate to the webroot authenticator with an explicit challenge location, or to DNS-01.
Webroot with a dedicated directory:
sudo install -d -m 755 /var/www/letsencrypt
sudo certbot certonly --webroot -w /var/www/letsencrypt \
-d example.com -d www.example.com \
--cert-name www.example.com
With the location /.well-known/acme-challenge/ block from step 5 in place, renewals no longer depend on Certbot editing Nginx configuration, and application routing changes cannot break them. Running certonly with the same --cert-name updates the renewal configuration for that certificate.
DNS-01 with a provider plugin (example for Cloudflare):
sudo apt install python3-certbot-dns-cloudflare
sudo install -m 600 /dev/null /etc/letsencrypt/cloudflare.ini
sudo tee /etc/letsencrypt/cloudflare.ini >/dev/null <<'EOF'
dns_cloudflare_api_token = restricted-api-token
EOF
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d example.com -d www.example.com --cert-name www.example.com
Create the API token with permission to edit DNS only for the specific zone. DNS-01 works without port 80 and supports wildcard certificates, at the cost of storing a DNS credential on the server.
Notifications from Let's Encrypt
Let's Encrypt has phased out expiration reminder emails, so relying on email notices is no longer an option. Monitoring the served certificate yourself (step 9) is the dependable replacement.
Renewal logs worth keeping
Certbot writes detailed logs to /var/log/letsencrypt/letsencrypt.log, rotated by Certbot itself. When a scheduled renewal fails, that file contains the full ACME exchange, which is more detailed than the journal output. Check it before re-running commands, because repeated attempts rotate older logs away.
Common mistakes
- Closing port 80 entirely on a server that uses HTTP-01 renewals.
- Proxying
/.well-known/acme-challenge/to Django. - Forgetting to reload Nginx after manual renewals.
- Keeping stale domains in a certificate after DNS moved, which makes every renewal fail.
Next steps
- Review the initial setup in Configure HTTPS for Django with Let's Encrypt.
- If a CDN is involved, see Deploy Django Behind Cloudflare.
- Add certificate expiry checks with Set Up Uptime Monitoring and Alerts for Django.
- If HTTPS loops after renewal, see Fix Too Many Redirects with Django HTTPS Behind a Proxy.
FAQ
How do I check why Certbot renewal fails?
Run sudo certbot renew --dry-run and read the detail line for each domain. It shows whether the challenge request timed out, returned the wrong status, or failed DNS lookup.
Why does Nginx still show the old certificate after renewal?
Nginx loads certificates when it starts or reloads. Add a deploy hook that runs systemctl reload nginx after successful renewals.
Does Let's Encrypt renewal need port 80 open?
For the HTTP-01 challenge, yes. If you cannot open port 80, use a DNS-01 challenge with a DNS provider plugin.
How often does Certbot try to renew certificates?
The packaged timer runs twice a day, and Certbot renews certificates that are within its renewal window before expiry. Most runs do nothing because certificates are not yet due.