Networking
SSL/TLS certificate renewal for self-hosted sites: how to stop expiry outages
How Let's Encrypt renewal works, why it silently fails, how to check expiry, and how to set up Caddy, Certbot or a DNS challenge so your certificates renew without you.
By Raktim Ranjit · Published · 3 min read
Short answer: use software that renews certificates automatically (Caddy, Traefik or Certbot with a timer), make sure the challenge it uses can actually succeed from the internet, and add an external check that alerts you when expiry is under 14 days. Most expiry outages are renewals that failed quietly weeks earlier.
How do certificates get issued?
A certificate authority such as Let's Encrypt must verify that you control the domain. This is done through the ACME protocol with a challenge.
- HTTP-01: the CA fetches a file from
http://yourdomain/.well-known/acme-challenge/.... Needs port 80 reachable from the internet. Cannot issue wildcards. - TLS-ALPN-01: proves control over port 443. Used by some servers.
- DNS-01: you publish a TXT record at
_acme-challenge.yourdomain. Works without any open port, and is the only way to get wildcard certificates such as*.example.com. Needs API access to your DNS provider.
Let's Encrypt certificates last 90 days, and clients are expected to renew when around 30 days remain. The short life is deliberate: it forces automation.
What is the easiest setup?
Caddy. Give it a site address and it handles issuance and renewal itself, including redirecting HTTP to HTTPS.
app.example.com {
reverse_proxy 127.0.0.1:3000
}It stores certificates in its data directory. Persist that directory when running in Docker (/data), or Caddy requests new certificates on every restart and you can hit rate limits.
What about Certbot with Nginx?
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com
sudo systemctl status certbot.timer # confirms automatic renewal is scheduled
sudo certbot renew --dry-run # tests the renewal process safelyRun --dry-run after setting it up and after any change to your web server or firewall.
Why do renewals fail silently?
- Port 80 got blocked by a firewall change, or your proxy now redirects the challenge path to HTTPS and the redirect loops.
- DNS changed: the domain now points elsewhere, so the CA validates the wrong server.
- The DNS API token expired or was revoked, so DNS-01 cannot write the TXT record.
- A reload hook is missing, so the new certificate is on disk but the server still serves the old one in memory.
- The ACME client's directory was not persisted in a container, so state was lost.
- Rate limits after repeated failed attempts or repeated re-issuance.
- The server's clock is wrong.
How do you check expiry yourself?
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null \
| openssl x509 -noout -enddate
# days remaining, usable in a cron job
end=$(echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
echo $(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 )) days leftDo this from a machine outside your network, because that is how your visitors see it.
What monitoring should you add?
- An uptime tool that checks HTTPS and alerts when the certificate has under 14 days left. Uptime Kuma has this built in.
- An alert on renewal failures in Certbot or Caddy logs.
- A calendar reminder 60 days after any manual issuance, in case you have a certificate that does not auto-renew.
What about internal services?
For services with no public access, use DNS-01 to obtain real certificates for a name like grafana.internal.example.com, or run a private certificate authority and install its root on your devices. Self-signed certificates that everyone clicks past teach people to ignore browser warnings.
Part of keeping a server healthy is making expiry a non-event. It belongs on the self-hosting checklist.
References
Author
Raktim Ranjit is a software engineer and the founder of NodeDR Infotech. He builds and maintains the software described here.