VPS Tutorials
What to Do When Let's Encrypt Certificate Renewal Fails on a VPS (2026): certbot, Cloudflare, DNS-01, and Port 80 Troubleshooting Checklist
Let's Encrypt certificates are valid for only 90 days, and certbot.timer can look healthy while renewal still fails. This article lays out a troubleshooting checklist for VPS HTTPS certificate renewal failures in this order: certbot renew --dry-run, port 80, Nginx challenge, Cloudflare proxy, DNS-01, deploy hook, and expiry monitoring.

Let's Encrypt certificate renewal failures usually don't start on the day the certificate expires.
They often fail several times long before that, and you just don't notice. By the time browsers warn that the certificate has expired, users can't open HTTPS, payment callbacks fail, and API clients throw TLS errors, the problem has already become a production incident.
What makes it worse is that many VPS users assume that because they installed certbot and the system has certbot.timer, automatic renewal must be fine. In reality, a timer only means it runs on schedule; it doesn't mean every renewal succeeds.
This article follows a troubleshooting order: first confirm which certificates exist, then run a dry-run, then check port 80, Nginx, Cloudflare, DNS-01, deploy hooks, and monitoring.
First command:
sudo certbot certificates
Pay attention to:
- Certificate Name
- Domains
- Expiry Date
- Certificate Path
- Key Type
If the certificate here shows only a dozen or even a few days left, don't wait for automatic renewal to "fix itself." Run a dry-run directly.
sudo certbot renew --dry-run
The official Certbot documentation also recommends using a dry-run to test the automatic renewal path. It walks through a simulated renewal process without replacing the real certificate, making it the safest first diagnostic command.
If the dry-run succeeds, at least it shows that the current validation method, web server cooperation, and DNS or challenge are basically fine. If it fails, keep troubleshooting based on the error message.
First check the timer:
systemctl list-timers | grep certbot
systemctl status certbot.timer
Then check the logs:
journalctl -u certbot --since "7 days ago" --no-pager
journalctl -u certbot.timer --since "7 days ago" --no-pager
Depending on how it was installed, it may use cron or a systemd timer. You can also check:
ls /etc/cron.d/
ls /etc/cron.daily/
A common misconception is that the timer is active but every run fails; or that the system uses the snap version of certbot, whose log location differs somewhat from the package-manager version. Don't just look at "is there a timer"—look at the dry-run and the logs.
Let's Encrypt's HTTP-01 challenge accesses:
http://你的域名/.well-known/acme-challenge/<TOKEN>
The official Let's Encrypt documentation states clearly that HTTP-01 can only be completed on port 80. You cannot specify 8080, 8888, or any other port.
So you need to confirm:
- The domain's A / AAAA records point to this VPS
- The VPS security group / firewall allows port 80
- Nginx/Apache listens on port 80
.well-known/acme-challenge/isn't incorrectly redirected or blocked- The Cloudflare proxy hasn't changed the validation path
First check from the outside whether port 80 is reachable:
curl -I http://example.com
Then test the challenge path. You can temporarily create a test file:
sudo mkdir -p /var/www/html/.well-known/acme-challenge
printf 'ok\n' | sudo tee /var/www/html/.well-known/acme-challenge/test
curl http://example.com/.well-known/acme-challenge/test
If it returns ok, that means this path is at least reachable from the public internet. Delete it after testing:
sudo rm /var/www/html/.well-known/acme-challenge/test
If it's unreachable, keep checking the Nginx configuration and firewall.
Many sites force HTTP to redirect to HTTPS:
return 301 https://$host$request_uri;
This is usually fine; Let's Encrypt can follow a certain number of HTTP/HTTPS redirects. But if you redirect .well-known to the wrong directory, to a login page, to another domain, or if the CDN/WAF returns 403, it will fail.
It's recommended to reserve a separate path for the challenge:
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/html;
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
After changing it, test first:
sudo nginx -t
sudo systemctl reload nginx
Don't restart directly. Let nginx -t pass first, then reload.
Many VPS sites sit behind Cloudflare. Cloudflare itself isn't the problem, but it adds another layer to the troubleshooting path.
Common pitfalls:
- After a DNS record is orange-cloud proxied, validation requests reach Cloudflare first, then the origin
- The origin's port 80 isn't open, but Cloudflare caching or rules make surface access look normal
- WAF / Redirect Rule / Page Rule affects
/.well-known/acme-challenge/ - Cloudflare SSL mode and origin certificate status get tangled up in troubleshooting
If you use HTTP-01, first confirm that the origin's port 80 is genuinely reachable. If necessary, you can temporarily switch the relevant records to DNS only (grey cloud) for testing, or switch to DNS-01.
If the site sits behind Cloudflare long-term, needs a wildcard certificate, or can't conveniently open port 80, DNS-01 is often more suitable.
Let's Encrypt's DNS-01 challenge creates a TXT record in DNS:
_acme-challenge.example.com TXT "..."
It suits these situations:
- You need a
*.example.comwildcard certificate - The origin can't conveniently open port 80
- The site is behind Cloudflare / a reverse proxy
- Multiple servers share the same certificate process
But the core issue with DNS-01 is propagation time. Seeing the TXT record locally doesn't mean Let's Encrypt can see it from its validation nodes.
You can check like this:
dig TXT _acme-challenge.example.com @1.1.1.1
dig TXT _acme-challenge.example.com @8.8.8.8
You can also query the authoritative NS:
dig example.com NS +short
dig TXT _acme-challenge.example.com @ns1.example-dns.com
If multiple nameservers return inconsistent results, renewal may fail randomly. This problem is common with some DNS providers, primary/secondary DNS, and manual TXT record updates.
When using a DNS API token, pay attention to permissions:
- Grant DNS edit permission only for the target zone
- Don't put a global API key in the web root
- Set credential file permissions to 600
For example:
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
The official Let's Encrypt documentation states that DNS-01 can issue wildcard certificates, while HTTP-01 cannot.
So if you want:
*.example.com
Don't keep struggling with port 80; you should use DNS-01.
When using the Cloudflare plugin, the general process is:
sudo apt install python3-certbot-dns-cloudflare
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d example.com \
-d '*.example.com'
Different systems, the snap version of certbot, and different DNS plugin package names may vary. Before running it for real, first check how your current certbot is installed:
which certbot
certbot --version
There's another scenario: certbot renewed successfully, but Nginx wasn't reloaded and is still serving the old certificate.
First check the expiry time of the certificate file:
sudo openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates
Then check the certificate actually returned online:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
If the local file is new but the live one is old, reload the web service.
sudo nginx -t
sudo systemctl reload nginx
A better approach is to set up a deploy hook so it reloads automatically after a successful renewal:
sudo certbot renew --deploy-hook "systemctl reload nginx"
You can also put the hook in:
/etc/letsencrypt/renewal-hooks/deploy/
Note: deploy hooks should only do lightweight, deterministic actions, such as reloading nginx. Don't put complex deployment scripts in them.
After seeing a failure, some people keep running:
sudo certbot renew --force-renewal
This isn't a good habit. Let's Encrypt has rate limits, and repeated failures may prevent you from issuing more certificates for a short period. When troubleshooting, prefer:
sudo certbot renew --dry-run
If you need to test the process frequently, you can use the staging environment to avoid consuming production quota:
sudo certbot certonly --staging -d example.com
Once the configuration is truly fixed, switch back to the production environment.
Don't rely only on certbot itself. You need at least an external or local reminder.
A simple local check:
DOMAIN=example.com
END_DATE=$(echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN:443" 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
END_TS=$(date -d "$END_DATE" +%s)
NOW_TS=$(date +%s)
DAYS_LEFT=$(( (END_TS - NOW_TS) / 86400 ))
echo "$DOMAIN certificate expires in $DAYS_LEFT days"
If it's under 14 days, you should alert. For production, external monitoring is more recommended, because if the VPS's own DNS, network, or task scheduling has problems, the local script may fail along with it.
When Let's Encrypt renewal fails, follow this order:
-
sudo certbot certificatescheck certificates and expiry times -
sudo certbot renew --dry-runreproduce the renewal error -
systemctl list-timers | grep certbotcheck scheduled tasks -
journalctl -u certbot --since "7 days ago"check logs - HTTP-01: confirm port 80 is publicly reachable
- HTTP-01: confirm
/.well-known/acme-challenge/is reachable - Nginx: reload after
nginx -t - Cloudflare: check orange cloud, WAF, Redirect Rule, SSL mode
- DNS-01: confirm the TXT is visible on multiple resolvers and authoritative NS
- Wildcard certificates: use DNS-01, not HTTP-01
- Renewal succeeded but live certificate is old: check deploy hook / reload
- Don't repeatedly
--force-renewal; prefer dry-run and staging - Set a 14-day expiry reminder
Certificate renewal failures aren't mysticism. Most problems fall into four categories: validation requests can't get through, DNS TXT hasn't propagated, renewal succeeded but the service wasn't reloaded, and nobody monitors the expiry time. Get dry-run, the challenge path, DNS-01, and deploy hooks in order, and your HTTPS certificate won't scare you every 90 days.