A Let's Encrypt certificate is valid for 90 days, so every deployment depends on renewal running unattended and on time. When the timer is missing, the validation port is closed, or the web server never reloads the new file, TLS breaks for all clients at once. This tutorial shows how to set up, test, verify and monitor automatic renewal with certbot on a Linux host.
How does automatic renewal with certbot work?
Automatic renewal with certbot works through a scheduled certbot renew run, usually twice a day via a systemd timer, which checks every certificate in /etc/letsencrypt/renewal/ and reissues only those with fewer than 30 days of validity left.
Each certificate has a renewal configuration file at /etc/letsencrypt/renewal/<your-domain>.conf. It stores the authenticator plugin, the installer, the domain list and any hooks used at first issuance. certbot renew replays exactly those parameters, which is why the command takes no arguments in normal operation.
graph TD
A["systemd timer: certbot.timer"] --> B["certbot renew"]
B --> C{"Fewer than 30 days left?"}
C -->|No| D["Skip lineage, exit 0"]
C -->|Yes| E["ACME challenge: HTTP-01 or DNS-01"]
E --> F{"Challenge passed?"}
F -->|No| G["Error in /var/log/letsencrypt/letsencrypt.log"]
F -->|Yes| H["New files in /etc/letsencrypt/live/"]
H --> I["Deploy hook reloads nginx"]
Prerequisites
- A Linux host with certbot 1.21 or newer (
certbot --version) - A user account with
sudorights - At least one certificate already issued by certbot, visible in
sudo certbot certificates - A running
systemd(systemctl is-system-running) - Public reachability of the validation endpoint
HTTP-01 validation requires inbound TCP port 80 on the host, even when all traffic is redirected to HTTPS afterwards. If a stateful cloud firewall sits in front of your workloads, keep port 80/tcp open for the ACME servers, or switch the affected certificates to DNS-01, which needs no inbound port at all.
Matching infrastructure at centron
Security by default: cloud firewalls filter traffic before it reaches the instance, managed centrally. Explore cloud firewalls →
Check whether renewal is already scheduled
Before adding anything, find out which scheduler your certbot installation already ships. Two active schedulers cause duplicate renewal attempts and duplicate failure mails.
| Install method | Scheduler unit | Check with |
|---|---|---|
| apt (Debian/Ubuntu) | certbot.timer plus /etc/cron.d/certbot |
systemctl list-timers certbot.timer |
| snap | snap.certbot.renew.timer |
systemctl list-timers 'snap.certbot*' |
| pip / source | none | you create the units yourself |
The Debian package installs both a cron job and a timer, but the cron job aborts when systemd is running, so only one renewal path is active. Check the current state:
$ systemctl list-timers certbot.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-08-18 00:00:00 UTC 7h 12min Mon 2026-08-17 12:00:00 UTC 4h ago certbot.timer certbot.service
$ sudo certbot certificatesIf list-timers returns no rows, the timer is not enabled. Enable it with sudo systemctl enable --now certbot.timer.
Test the renewal with --dry-run
Run sudo certbot renew --dry-run to perform a complete renewal against the Let's Encrypt staging environment: the challenge, the plugin and the hooks all execute, but no live certificate is written and no rate limit is consumed.
$ sudo certbot renew --dry-run
Processing /etc/letsencrypt/renewal/example.com.conf
Simulating renewal of an existing certificate for example.com and www.example.com
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/example.com/fullchain.pem (success)A dry run is the only safe way to prove the whole chain works. Certbot 1.4 and newer also execute deploy hooks during a dry run, so a broken reload script surfaces here rather than three months later. Add --no-hooks if you want to exclude them from the test.
Do not use --force-renewal to test anything. It issues a real certificate and counts against the limit of five duplicate certificates per week for an identical domain set.
Configure the systemd timer
Inspect the unit that ships with your package before changing it:
$ systemctl cat certbot.timerThe Debian and Ubuntu unit uses OnCalendar=*-*-* 00,12:00:00 with RandomizedDelaySec=43200. Two runs per day with a randomized delay of up to 12 hours spreads load on the ACME servers and gives a certificate roughly 60 renewal attempts before it expires. Do not pin the timer to a fixed minute across many hosts.
To change the schedule of a packaged timer, use a drop-in instead of editing the unit file, which package updates would overwrite:
$ sudo systemctl edit certbot.timerFor a pip or source installation, create both units yourself. The service unit:
[Unit]
Description=Certbot renewal
Documentation=https://certbot.eff.org/docs
[Service]
Type=oneshot
ExecStart=/usr/local/bin/certbot renew --quiet --no-random-sleep-on-renew
PrivateTmp=trueAnd the matching timer at /etc/systemd/system/certbot-renew.timer:
[Unit]
Description=Run certbot renew twice a day
[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=43200
Persistent=true
[Install]
WantedBy=timers.target--no-random-sleep-on-renew disables certbot's own internal sleep, because the timer already randomizes the start. Persistent=true triggers a missed run after the host was powered off.
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now certbot-renew.timer
$ systemctl list-timers certbot-renew.timerReload the web server with a deploy hook
A renewed certificate on disk has no effect until the running service rereads it. nginx, Apache, HAProxy and Postfix all keep the old certificate in memory until they are reloaded, so the client still sees the expiring one.
Place an executable script in /etc/letsencrypt/renewal-hooks/deploy/. Certbot runs every executable in that directory after a successful renewal, and only then:
#!/bin/sh
set -eu
nginx -t
systemctl reload nginx
logger -t certbot-deploy "reloaded nginx for ${RENEWED_DOMAINS}"$ sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shTwo environment variables are available inside a deploy hook. RENEWED_LINEAGE holds the path of the affected certificate directory, for example /etc/letsencrypt/live/example.com. RENEWED_DOMAINS holds the space-separated domain list. Use RENEWED_LINEAGE when a host serves several certificates and only one service should be reloaded.
The nginx -t line matters: a reload with a broken configuration fails, and without set -e the failure would be logged but ignored.
When do Let's Encrypt certificates expire?
Let's Encrypt certificates are valid for 90 days from issuance, and certbot renews them once fewer than 30 days remain, which leaves a 30-day window in which a failing renewal can still be noticed and repaired.
That window is the only safety margin you get, and it is not monitored for you. Let's Encrypt stopped sending expiration notification emails in June 2025, so an unnoticed renewal failure now stays silent until the certificate is dead. Let's Encrypt is also rolling out a short-lived certificate profile with a six-day lifetime; on that profile the margin shrinks to hours, which makes working automation mandatory rather than convenient.
certbot only handles ACME certificates. Workloads that require OV or EV validation, a warranty or a longer lifetime use commercial SSL/TLS certificates, which are renewed through the vendor process and installed manually or by your configuration management.
Verify the renewal
Check three separate things: the certificate on disk, the certificate the server actually serves, and the timer that will trigger the next run.
$ sudo certbot certificates
Certificate Name: example.com
Domains: example.com www.example.com
Expiry Date: 2026-11-10 08:14:52+00:00 (VALID: 84 days)
Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pemThe served certificate can differ from the one on disk if the reload never happened. Query the endpoint directly:
$ echo | openssl s_client -connect <your-domain>:443 -servername <your-domain> 2>/dev/null | openssl x509 -noout -dates
notBefore=Aug 12 08:14:53 2026 GMT
notAfter=Nov 10 08:14:52 2026 GMTIf notAfter is older than the expiry date reported by certbot certificates, the deploy hook did not run or did not reload the right service.
For past runs, read the unit log and certbot's own log:
$ journalctl -u certbot.service --since "-14 days"
$ sudo grep -i "error\|failure" /var/log/letsencrypt/letsencrypt.logAdd an independent check so a broken renewal is detected before the certificate expires. This script exits with status 1 when fewer than 21 days remain, which makes it usable from any monitoring agent or a cron job:
#!/bin/sh
set -eu
CERT=/etc/letsencrypt/live/<your-domain>/cert.pem
END=$(openssl x509 -enddate -noout -in "$CERT" | cut -d= -f2)
DAYS=$(( ( $(date -d "$END" +%s) - $(date +%s) ) / 86400 ))
if [ "$DAYS" -lt 21 ]; then
echo "CRITICAL: certificate expires in $DAYS days"
exit 1
fi
echo "OK: certificate valid for $DAYS days"Troubleshooting
Timeout during connect (likely firewall problem). The ACME server could not reach port 80. Confirm that something listens locally with sudo ss -tlnp | grep ':80', that the DNS A record still points to this host, and that no firewall rule between the internet and the host drops the connection. Certificates for hosts without a public port 80 must use DNS-01.
too many certificates already issued. You hit a rate limit, most often by repeating --force-renewal. The limit is five duplicate certificates per identical domain set per week, and it resets on a rolling basis. Wait it out and test with --dry-run from then on.
The timer runs but the certificate stays old. Two causes are common. Either two certbot installations coexist, typically an apt package plus a snap, and the active timer belongs to the one that does not own /etc/letsencrypt. Or the renewal configuration references a plugin that is no longer present, which produces "The requested nginx plugin does not appear to be installed". Check the authenticator and installer lines in /etc/letsencrypt/renewal/<your-domain>.conf and correct them there.
Wrap-up
Automatic renewal is working when three conditions hold: systemctl list-timers shows a scheduled run, certbot renew --dry-run completes without error, and the deploy hook reloads the service that serves the certificate. Re-run the dry run after every change to the web server configuration or the certbot installation, and keep an expiry check in your monitoring so a failed renewal is visible while the 30-day margin is still available.
Testen Sie Ihr Setup auf ccloud³
Registrieren Sie sich in der ccloud³ und erhalten Sie 200 € Startguthaben für Ihr Projekt – z. B. für eine PostgreSQL-VM mit automatischen Backups.