Home · About · Services · Industries · Projects · Products · Process · Blog · FAQ · Contact
Let's Encrypt auto-renewal: automating TLS certificates in 2026 Certificate lifetimes are getting shorter and Let's Encrypt no longer emails you before expiry. How we set up issuance, renewal, reloads and monitoring so…
An expired certificate is one of the most avoidable outages there is. The site is up, the code is fine, and every visitor gets a full-page browser warning because a renewal job stopped working weeks ago. Until mid-2025 Let's Encrypt would at least email you first. It no longer does, and certificate lifetimes are shrinking, so renewal has to be automatic and it has to be watched.
The short answer: Let's Encrypt auto-renewal needs four things. An ACME client (Certbot, acme.sh, or the one built into Traefik or Caddy) that renews on a timer, a hook that reloads the web server after each renewal, a dry run against the staging environment, and an external check that alerts you when any public certificate is close to expiry.
What changed, and why manual renewal is finished Several changes since 2025 affect anyone running Let's Encrypt in production. None of them are a problem if renewal is fully automated. All of them are a problem if a person is part of the process.
Expiry reminder emails ended in June 2025. If your safety net was an email to an old admin address, it is gone. OCSP is gone. Let's Encrypt stopped putting OCSP URLs in new certificates in May 2025 and switched its OCSP responders off in August 2025, so ssl_stapling in NGINX does nothing for these certificates and only adds log warnings. Remove it. Lifetimes are dropping. Let's Encrypt plans to cut its default from 90 days to 64 days in early 2027 and to 45 days in early 2028, and already offers opt-in shorter profiles, including six-day certificates. ACME Renewal Information (ARI, RFC 9773) is now a published standard. The CA can tell a client when to renew, for example ahead of a mass revocation, and current Certbot, acme.sh, Traefik and cert-manager releases can act on it. The practical lesson is to stop thinking in fixed numbers. Do not hard-code "renew at 30 days left" in your own scripts, and do not assume 90 days anywhere in monitoring. Let the client decide when to renew, and alert on a proportion of the lifetime instead.
Pick the client that matches how you deploy There is no single right tool. The right one is whichever sits closest to the thing serving TLS, so that issuance, renewal and reload happen in one place.
Plain NGINX or Apache on a VPS: Certbot, installed from snap or the distribution package. It ships with a systemd timer that runs twice a day. Containerised stacks behind Traefik, including Dokploy: let Traefik's built-in ACME resolver handle it. No separate client, no reload step. Caddy: automatic HTTPS is the default behaviour. Point a domain at it and it manages the certificate. Minimal systems or many DNS providers: acme.sh is a single shell script with a long list of DNS API integrations. Kubernetes: cert-manager, which stores certificates as secrets and renews them for ingress resources. Avoid mixing two clients for the same name. A common mess on inherited servers is Certbot and a hosting control panel both renewing the same domain, each overwriting the other's files, with the web server pointed at whichever copy happens to be older.
Certbot on NGINX: issue, reload, prove it For a single server with port 80 open, the HTTP-01 challenge in webroot mode is the least intrusive option. Certbot writes a token file, Let's Encrypt fetches it over plain HTTP, and NGINX keeps running throughout. One detail matters: a server-level return 301 runs before any location is matched, so put the HTTPS redirect inside location / and let the challenge path through.
# /etc/nginx/sites-available/example.com (port 80 block)
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
return 301 https://$host$request_uri;
}
}
# Issue the certificate
sudo mkdir -p /var/www/acme
sudo nginx -t && sudo systemctl reload nginx
sudo certbot certonly --webroot -w /var/www/acme \
-d example.com -d www.example.com \
--deploy-hook 'systemctl reload nginx'
# Confirm the timer exists and renewal works end to end
systemctl list-timers | grep -i certbot
sudo certbot renew --dry-runPassing --deploy-hook on the first run stores it in the renewal configuration under /etc/letsencrypt/renewal/, so every future renewal reloads NGINX. A deploy hook only runs when a certificate actually changes. For hooks that should apply to every certificate on the box, drop an executable script into /etc/letsencrypt/renewal-hooks/deploy/ instead. A reload, not a restart, keeps existing connections alive. Note that renew --dry-run skips deploy hooks unless you add --run-deploy-hooks, so test the reload command separately.
The most common failure we see is not Certbot at all. A renewal succeeds, the new files land on disk, and the web server keeps serving the old certificate from memory because nothing reloaded it. The certificate on disk says 80 days left; the one browsers see expires tomorrow. Always monitor what the server presents, not what is on disk.
For wildcard certificates, or servers where port 80 is closed, use the DNS-01 challenge with your DNS provider's plugin and an API token limited to that zone. We covered that setup in our post on running many subdomains from one NGINX server.
Traefik and Dokploy: let the proxy own it On container platforms the proxy should own certificates. Traefik requests a certificate the first time it sees a router with a resolver attached, stores it in a JSON file, renews it in the background and serves the new one without a reload. Dokploy configures this for you; on a hand-built stack the static configuration looks like this.
# traefik.yml (static configuration)
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
certificatesResolvers:
le:
acme:
email: ops@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
# Then on each service (Docker labels)
# traefik.http.routers.app.rule=Host(`app.example.com`)
# traefik.http.routers.app.entrypoints=websecure
# traefik.http.routers.app.tls.certresolver=leThree details catch people out. The acme.json file must have permissions of 600 or Traefik refuses to use it. It must live on a persistent volume, otherwise every container recreation requests fresh certificates and you will hit rate limits. And it contains private keys, so it belongs in your backups and nowhere else, certainly not in a Git repository.
Rate limits, staging and CAA Let's Encrypt limits how fast you can issue. The two that matter in practice are 50 new certificates per registered domain every seven days, and five certificates for the exact same set of names every seven days. A broken deployment script that requests a new certificate on every start can use those up in an afternoon, and then you wait.
Test new automation against the staging environment first: certbot --test-cert, or the staging caServer URL in Traefik. Staging certificates are not trusted by browsers, but the limits are far higher. Persist the client's state (/etc/letsencrypt, acme.json, the cert-manager secrets) so a rebuild does not mean reissuing. Add a CAA record so only the CAs you use may issue for your domain, for example: example.com. CAA 0 issue "letsencrypt.org". If you use a second CA as a fallback, list it in CAA as well, or the fallback will fail when you need it. Monitor from outside, on a percentage Renewal jobs fail quietly: a DNS token expires, a firewall change closes port 80, a CDN starts answering the challenge path, a disk fills up. The only reliable check is one that connects to each public name, reads the certificate actually being served and compares its expiry with today.
#!/usr/bin/env bash
# Warn when less than a sixth of a served certificate's lifetime remains.
# Uses GNU date, so run it from a Linux host.
set -uo pipefail
now=$(date +%s)
for host in example.com www.example.com app.example.com; do
dates=$(echo | timeout 10 openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null \
| openssl x509 -noout -startdate -enddate 2>/dev/null)
if [ -z "$dates" ]; then
echo "WARN: could not read a certificate from $host"; continue
fi
start=$(date -d "$(sed -n 's/^notBefore=//p' <<<"$dates")" +%s)
end=$(date -d "$(sed -n 's/^notAfter=//p' <<<"$dates")" +%s)
if (( (end - now) * 6 < end - start )); then
echo "WARN: $host expires $(date -d "@$end" +%F), $(( (end - now) / 86400 )) days left"
fi
doneRun it from a different machine from the one being checked, and send the output to whatever already pages someone: an uptime monitor, a chat webhook or your alerting system. Most uptime services can also check certificate expiry, which is worth switching on. Clients normally renew when about a third of the lifetime is left, so alerting at a sixth gives the client time to retry and still leaves you days to fix it by hand. That works out at roughly 15 days for a 90-day certificate, 10 for 64 days and 7 for 45 days, without changing the script.
Keep an inventory too. A list of every hostname with a public certificate, which client renews it and where the files live is what turns a 2am warning into a five-minute fix.
Checklist One ACME client per hostname, as close as possible to the process that serves TLS. Renewal runs on a timer or inside the proxy, with no manual step anywhere. A deploy hook or proxy reload so the new certificate is actually served. certbot renew --dry-run, or a staging run, passes after every infrastructure change. Client state on persistent, backed-up storage with tight file permissions. CAA records list every CA you rely on. External expiry checks against what each host serves, with alerts that reach a person. No monitoring rule or script that assumes a 90-day lifetime. If you have inherited servers and cannot answer where each certificate comes from or what renews it, start with the inventory. That usually reveals the one forgotten subdomain that would have expired first.
Infigini IT Services · 11 Kanwali Road, Dehradun 248001, India · info@infigini.com · +91 95684 87789