
Auto-renewal rarely fails with a big, obvious error. You usually notice later, when browsers show warnings or API clients refuse connections. This TLS certificate renewal troubleshooting tutorial walks through diagnosing and fixing failed Let’s Encrypt renewals on a Linux VPS. It uses commands and reversible changes.
The same workflow applies to Ubuntu 24.04/26.04, Debian 12/13, AlmaLinux 9/10, Rocky Linux 9/10, and most cPanel-backed servers. Expect small differences in paths and service names.
If you want a clean baseline, start with a HostMyCode VPS. You control DNS, the firewall, the web server, and cron.
Renewals depend on all four working together.
What you’ll troubleshoot (and what success looks like)
Let’s Encrypt renewals fail for a handful of repeatable reasons. You’re done when:
certbot renew --dry-runsucceeds- A scheduled timer/cron runs daily (or twice daily)
- Web server reloads after renewal without dropping connections
- You can prove the live site serves the new cert chain
If you run a reverse proxy, confirm where TLS terminates. If Nginx terminates TLS in front of Apache/cPanel, renewal must update the cert where TLS actually lives.
If you’re not sure which pattern you set up, see our guide on putting Nginx in front of WordPress safely.
Prerequisites (don’t skip these checks)
- Root or sudo access
- Domain(s) point to your server IP (A/AAAA records correct)
- Port 80 reachable from the public internet (for HTTP-01), or DNS access for DNS-01
- Time synced (NTP active) to avoid weird ACME validation edge cases
Quick time sanity check:
timedatectl status
If NTP service shows inactive, fix that first. Ubuntu/Debian usually use systemd-timesyncd. RHEL-family systems often use chronyd.
Step 1: Identify how certificates are managed on your server
Before you change anything, confirm what tool “owns” the certificates. The right fix depends on that owner.
1) Certbot (common on Ubuntu/Debian VPS)
Check whether Certbot is installed. Then confirm whether it already manages any certificate lineages:
certbot --version
sudo certbot certificates
ls -la /etc/letsencrypt/
If you see /etc/letsencrypt/live/yourdomain/, Certbot is managing those certs.
2) cPanel AutoSSL (WHM-managed servers)
On cPanel servers, renewal is usually handled by AutoSSL. In most cases, you should not run Certbot directly.
In WHM, check: SSL/TLS → Manage AutoSSL and SSL/TLS Status.
If AutoSSL looks unhealthy, our dedicated guide on cPanel AutoSSL troubleshooting will get you to the root cause faster.
3) Custom/manual cert deployment
If you don’t see Certbot lineages and you’re not on cPanel, your web server config may reference certificates directly.
- Nginx:
/etc/nginx/sites-enabled/or/etc/nginx/conf.d/ - Apache:
/etc/apache2/sites-enabled/or/etc/httpd/conf.d/
Search for certificate file references:
sudo grep -R "ssl_certificate" -n /etc/nginx 2>/dev/null | head
sudo grep -R "SSLCertificateFile" -n /etc/apache2 /etc/httpd 2>/dev/null | head
Step 2: Reproduce the failure safely with a dry run
Don’t troubleshoot from assumptions. Run a simulated renewal to get full error output without burning rate limits.
sudo certbot renew --dry-run -v
Common outcomes:
- Success: Certbot works; the problem is automation (timer/cron) or the reload hook.
- Validation failures: HTTP-01 blocked, wrong vhost, redirect loop, CDN/WAF interference, or DNS pointing somewhere else.
- Plugin/config errors: Nginx/Apache config is broken, so Certbot can’t place or serve challenge files.
Step 3: Fix the most common HTTP-01 validation failures
With HTTP-01, Let’s Encrypt requests:
http://yourdomain/.well-known/acme-challenge/<token>
Your server must return the exact token content over port 80.
3.1 Confirm DNS points where you think it does
dig +short A yourdomain.com
dig +short AAAA yourdomain.com
Compare that to what your server reports as its public IP:
curl -4 ifconfig.co
curl -6 ifconfig.co 2>/dev/null || true
If you have an AAAA record but IPv6 isn’t working end-to-end, validation can fail over IPv6. Either fix IPv6 routing/firewall, or remove the AAAA record until you’re ready to support it.
3.2 Make sure port 80 is reachable
From the server, confirm something is listening on port 80:
sudo ss -lntp | awk 'NR==1 || $4 ~ /:80$/'
If nothing listens on 80, start a minimal HTTP listener. For Nginx:
sudo systemctl enable --now nginx
If the port is listening but validation still fails, check your firewall rules. With UFW:
sudo ufw status verbose
You want at least:
80/tcp ALLOW443/tcp ALLOW
If you need a known-good baseline, follow our UFW firewall setup. Then run the dry run again.
3.3 Verify the challenge path reaches your server (not a different vhost)
Make a test file in the webroot and fetch it over HTTP. On a typical Nginx setup:
sudo mkdir -p /var/www/html/.well-known/acme-challenge
echo test-$(date +%s) | sudo tee /var/www/html/.well-known/acme-challenge/ping.txt
Request it externally (replace the domain):
curl -i http://yourdomain.com/.well-known/acme-challenge/ping.txt
If the response doesn’t match what you wrote, you’re usually dealing with one of these:
- A redirect to another hostname that doesn’t resolve
- A default vhost answering instead of your site
- A CDN/WAF caching, rewriting, or blocking that path
3.4 Fix “too many redirects” without weakening HTTPS
The clean pattern is simple. Allow /.well-known/acme-challenge/ over HTTP, and redirect everything else to HTTPS.
For Nginx, add this inside the server block that listens on 80:
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
Test and reload:
sudo nginx -t
sudo systemctl reload nginx
Step 4: Fix web server config errors that block renewals
If Certbot reports Nginx/Apache config failures, fix those first. Certbot won’t proceed until your web server config validates cleanly.
Nginx: read the failing line, then run a full config test
sudo nginx -t
sudo journalctl -u nginx --no-pager -n 50
Common culprits:
- Duplicate
listen 443 sslblocks for the same hostname - Certificate paths pointing to files that no longer exist
- Syntax errors after a copy/paste edit
Apache: validate vhosts and modules
sudo apachectl -t
sudo systemctl status apache2 2>/dev/null || sudo systemctl status httpd
sudo journalctl -u apache2 --no-pager -n 50 2>/dev/null || sudo journalctl -u httpd --no-pager -n 50
If you run Nginx in front of Apache (common in cPanel-style stacks), keep real client IPs. Also keep TLS routing unambiguous.
Our Nginx-in-front-of-Apache setup covers safe headers and logging details.
Step 5: Fix renewals that fail only in automation (timer/cron issues)
If certbot renew --dry-run works manually but certs still expire, the scheduler isn’t running. Or it’s running and failing quietly.
5.1 Systemd timer (Ubuntu/Debian packages)
systemctl list-timers | grep -E "certbot|letsencrypt" || true
systemctl status certbot.timer 2>/dev/null || true
If the timer exists but hasn’t been executing, enable it and check recent logs:
sudo systemctl enable --now certbot.timer
sudo systemctl start certbot.timer
journalctl -u certbot.timer --no-pager -n 50
5.2 Cron (older installs or custom scripts)
sudo crontab -l
sudo ls -la /etc/cron.daily | grep -i certbot || true
If you find a custom cron job, make it write to a log. No-output cron is the classic “everything was fine until it wasn’t” failure mode.
Example safe cron line:
15 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "/bin/systemctl reload nginx" >> /var/log/certbot-renew.log 2>&1
Adjust the deploy hook for Apache:
--deploy-hook "/bin/systemctl reload apache2"
Step 6: Reload the right service after renewal (deploy hooks)
Renewal updates files on disk. Your server won’t present the new certificate until the terminating process reloads its TLS context.
Nginx and Apache need a reload. Some app servers do too, but that’s less common on typical hosting stacks.
First, see whether you already have hooks:
sudo ls -la /etc/letsencrypt/renewal-hooks/deploy/ 2>/dev/null || true
sudo ls -la /etc/letsencrypt/renewal-hooks/post/ 2>/dev/null || true
Create a deploy hook for Nginx (runs only after a successful renewal):
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
set -eu
/usr/sbin/nginx -t
/bin/systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
The config test is the safety belt here. If the config is broken, the hook won’t reload.
Step 7: Diagnose “certificate renewed but browser still shows the old one”
This almost always comes down to one of two issues. You either renewed the wrong lineage, or the live service is serving different files than you expect.
7.1 Confirm which certificate the site serves
echo | openssl s_client -servername yourdomain.com -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -subject -issuer -dates
Check notAfter. If it’s still near expiry, the process didn’t reload. Another common cause is a vhost pointing at a different certificate path.
7.2 Confirm the vhost points to Let’s Encrypt paths
Nginx typical paths:
/etc/letsencrypt/live/yourdomain.com/fullchain.pem/etc/letsencrypt/live/yourdomain.com/privkey.pem
Verify the symlinks exist:
sudo ls -la /etc/letsencrypt/live/yourdomain.com/
Then confirm your Nginx config references those exact files:
sudo grep -R "yourdomain.com" -n /etc/nginx/sites-enabled /etc/nginx/conf.d 2>/dev/null | head -n 30
Step 8: Use DNS-01 for tricky environments (closed port 80, CDN-only, multi-region)
If you can’t reliably open port 80, DNS-01 is usually the clean exit. It also helps when a CDN/WAF keeps interfering.
DNS-01 validates via a TXT record, so inbound HTTP isn’t part of the equation.
Certbot supports DNS plugins for popular DNS providers, but the exact steps depend on where your DNS lives. For client work, document who has DNS access and where credentials are stored.
“Lost DNS login” breaks more renewals than people admit.
If you want everything under one roof, HostMyCode domains help keep DNS ownership clear: HostMyCode domains.
Minimal manual DNS-01 example (interactive) for a single cert:
sudo certbot certonly --manual --preferred-challenges dns -d yourdomain.com -d www.yourdomain.com
This isn’t ideal for automation, but it’s a strong diagnostic. If DNS-01 succeeds while HTTP-01 fails, your issue is almost certainly routing, firewall, vhost behavior, or the CDN/WAF.
Step 9: Troubleshoot ACME challenge failures caused by security rules
Security tooling can block /.well-known/ outright. The usual suspects are aggressive bot blocking, WAF rules, or a deny-all location block that catches the ACME path.
- If you use Nginx bot rules, explicitly exempt the ACME path.
- If you use ModSecurity in cPanel, whitelist the ACME validation endpoint or the rule IDs that trigger on it.
For WHM servers, our ModSecurity tuning guide walks through exceptions without disabling the WAF.
Step 10: Add monitoring so you don’t get surprised again
Fixing renewals is good. Catching failures before users do is better.
10.1 Alert if the certificate is within 14 days of expiry
Create a simple check script:
sudo tee /usr/local/bin/check-cert-expiry.sh >/dev/null <<'EOF'
#!/bin/sh
set -eu
DOMAIN="${1:?domain required}"
DAYS_WARN="${2:-14}"
END_DATE=$(echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN:443" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
END_EPOCH=$(date -d "$END_DATE" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (END_EPOCH - NOW_EPOCH) / 86400 ))
if [ "$DAYS_LEFT" -le "$DAYS_WARN" ]; then
echo "WARNING: $DOMAIN cert expires in $DAYS_LEFT days ($END_DATE)"
exit 1
fi
echo "OK: $DOMAIN cert expires in $DAYS_LEFT days ($END_DATE)"
EOF
sudo chmod +x /usr/local/bin/check-cert-expiry.sh
Run it:
/usr/local/bin/check-cert-expiry.sh yourdomain.com 14
Then wire it into your monitoring or cron email. If you’re building a broader baseline, follow our server monitoring tutorial.
Include certificate expiry as a standard check.
Step 11: Quick fix matrix (symptom → likely cause → fastest test)
- “Timeout during connect (likely firewall problem)” → port 80 blocked →
ss -lntp | grep :80and confirm firewall allows 80 - “Invalid response” → wrong vhost / CDN cache →
curl -i http://domain/.well-known/acme-challenge/ping.txt - “Too many redirects” → HTTP→HTTPS redirect mishandled → add ACME exception in port 80 vhost
- Manual renew works, auto doesn’t → timer/cron broken →
systemctl list-timers | grep certbot - Renewed but site serves old cert → no reload / wrong cert path →
openssl s_clientoutput + check vhost cert file paths
Summary: the safe renewal workflow you can reuse
Keep the workflow boring and repeatable. Validate DNS, prove port 80 reachability, and confirm the ACME challenge path.
Next, run certbot renew --dry-run. Then verify the live certificate with openssl s_client. After that, add a deploy hook that reloads your web server only if the config test passes.
If you’re standardizing this across multiple customer sites, you’ll spend less time firefighting on infrastructure where you control the moving parts. A managed VPS hosting plan from HostMyCode is a practical choice when you need stable SSL renewals, consistent firewall rules, and predictable updates.
For moves from shared hosting or an older VPS, use HostMyCode migrations so the TLS cutover stays clean and downtime-free.
If SSL renewals keep breaking, the underlying issue is usually unclear ownership of DNS, firewall rules, or web server config. Run this tutorial on a HostMyCode VPS, or hand it off to our managed VPS hosting team so renewals and safe reload hooks stay consistent across all your sites.
FAQ
Do I need port 80 open for Let’s Encrypt renewals?
For HTTP-01 challenges, yes. If you can’t keep port 80 reachable, switch to DNS-01 and validate via TXT records instead.
Can I force Certbot to renew a specific certificate?
Yes. Use certbot renew --cert-name yourdomain.com. This is useful on multi-site servers where only one lineage is failing.
Why did renewal succeed but Nginx still serves the old certificate?
Nginx probably didn’t reload, or the vhost points at a different certificate file. Verify with openssl s_client, then confirm ssl_certificate paths in your site config.
Is DNS-01 better than HTTP-01 on hosting VPS servers?
DNS-01 is more resilient to redirects and CDNs, but it adds DNS credential management. For a single-site VPS, HTTP-01 is usually simpler if port 80 is reachable.