Server

Let's Encrypt with Certbot, Including Renewal That Doesn't Break

By Prabath Thalangama· October 24, 2026· 4 min read
#tls#nginx#apache#security

Introduction

Certbot issues a free 90-day certificate in seconds. The real task is making renewal — which runs unattended, forever — actually reload your server and survive config changes. This covers issuance and, more importantly, the renewal setup that lasts.

Concepts

Let's Encrypt proves you control a domain via a challenge:

  • HTTP-01 (webroot) — you serve a token file at http://domain/.well-known/acme-challenge/<token>. Simple, but needs port 80 reachable and can't do wildcards.
  • DNS-01 — you publish a TXT record at _acme-challenge.domain. Works behind a firewall, works for wildcards, but needs API access to your DNS provider.

Certbot stores everything under /etc/letsencrypt/: live/<domain>/ (symlinks to the current cert), archive/ (all versions), renewal/<domain>.conf (how to renew this cert — the challenge method, webroot path, plugin).

Hands-on

Issue with webroot (no downtime, no Certbot touching your config)

sudo certbot certonly --webroot -w /var/www/certbot \
  -d example.com -d www.example.com \
  --agree-tos -m [email protected] --no-eff-email

Your web server needs one location for the challenge (keep this in the HTTP server {} even after redirecting everything else to HTTPS):

location /.well-known/acme-challenge/ {
    root /var/www/certbot;
}

Then reference fullchain.pem and privkey.pem from /etc/letsencrypt/live/example.com/ in your TLS config.

Issue a wildcard with DNS-01

sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d 'example.com' -d '*.example.com'

cloudflare.ini holds a scoped API token; chmod 600 it. Plugins exist for Route 53, DigitalOcean, Google, and generic --manual + a hook script.

Renewal — the part that matters

The certbot package installs a systemd timer (or cron job) that runs certbot renew twice a day. renew only acts on certs within 30 days of expiry, and it re-uses the method recorded in renewal/<domain>.conf.

systemctl list-timers | grep certbot
sudo certbot renew --dry-run          # simulate — do this after ANY change

Make the server pick up the new cert. Renewal writes new files but your running Nginx/Apache still holds the old one in memory. Add a deploy hook:

# /etc/letsencrypt/renewal-hooks/deploy/reload-web.sh
#!/bin/sh
systemctl reload nginx
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-web.sh

Anything in renewal-hooks/deploy/ runs only when a cert was actually renewed, for each renewed cert. pre/ and post/ hooks run every attempt.

Verification and troubleshooting

sudo certbot certificates                       # every cert, its domains, expiry
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate -issuer
  • --dry-run fails with "Invalid response ... 404" — the challenge file isn't served. Test manually: curl http://example.com/.well-known/acme-challenge/test after echo test > /var/www/certbot/.well-known/acme-challenge/test. Usually a redirect-to-HTTPS catching the ACME path too, or the wrong root.
  • too many certificates already issued — the rate limit is 5 duplicate certs (same exact domain set) per week. Don't loop certbot certonly; use renew. During testing use --staging.
  • unauthorized ... DNS problem on DNS-01 — the API token lacks edit permission on the zone, or DNS propagation is slower than certbot's wait. Add --dns-<provider>-propagation-seconds 60.
  • Renewal timer runs but cert never updates — the timer is disabled (systemctl enable --now certbot.timer), or renew errors silently. Run it by hand and read the output.
  • New cert issued, site still serves the old one — no deploy hook, or the hook failed. journalctl -u certbot and check renewal-hooks/deploy/.
  • certbot renewed but for the wrong domains — the renewal/*.conf still lists an old domain set. Re-run certonly with --cert-name and the correct -d list to rewrite it, or edit the conf and --dry-run.
  • Wildcard cert but certbot renew wants HTTP-01 — the original issue used DNS-01 but the conf lost the plugin lines. Re-issue with the DNS flags.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.