Automating Let’s Encrypt renewal on a single‑node web server without downtime
Let’s Encrypt certificates expire every 90 days. On a single‑node web server the usual approach is to run certbot renew from cron and reload the web service. The trick is to avoid any traffic loss while the new cert is in place. Below is a minimal, production‑ready recipe that works with Apache, Nginx, or any systemd‑managed web server.
1. Install Certbot and the web‑root plugin
# Debian/Ubuntu
sudo apt-get install certbot python3-certbot-nginx # or python3-certbot-apache
# RHEL/CentOS
sudo dnf install certbot python3-certbot-nginx
The nginx or apache plugin automatically configures the web‑root challenge and reloads the server after a successful renewal.
2. Obtain the first certificate
sudo certbot --nginx -d example.com -d www.example.com
Answer the prompts, choose “Yes” to redirect HTTP to HTTPS, and let Certbot finish. Verify that /etc/letsencrypt/live/example.com/fullchain.pem exists and that your site is reachable over HTTPS.
3. Configure a “no‑op” renewal hook
Certbot runs the renewal hooks before reloading the web server. If the reload fails, the old cert stays in use. To guarantee zero downtime, add a small script that checks the new cert’s validity and reloads only if the file changed.
Create /etc/letsencrypt/renewal-hooks/post/reload.sh:
#!/bin/bash
# reload.sh – reload web server only if cert changed
# Path to the current cert
CERT="/etc/letsencrypt/live/example.com/fullchain.pem"
# Timestamp of the cert file
TIMESTAMP=$(stat -c %Y "$CERT")
# Store the timestamp in a state file
STATE="/var/lib/letsencrypt/reload.state"
# Read the previous timestamp
PREV=$(cat "$STATE" 2>/dev/null || echo 0)
# If the cert is newer, reload
if [ "$TIMESTAMP" -ne "$PREV" ]; then
systemctl reload nginx
echo "$TIMESTAMP" > "$STATE"
fi
Make it executable:
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/reload.sh
The hook runs after every successful renewal. If the cert hasn’t changed (e.g., the renewal was skipped because the cert was still fresh), the script does nothing, so the web server stays exactly as it was.
4. Set up a cron job
Certbot’s default cron job runs twice a day. On a single‑node server you can keep it, but you can also use systemd timers for finer control.
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
The timer runs certbot renew and triggers the post‑hook automatically.
5. Verify the setup
Force a renewal to test the hook:
sudo certbot renew --force-renewal
Check that systemctl status nginx shows a reload and that the new cert is in use:
openssl s_client -servername example.com -connect example.com:443 | openssl x509 -noout -dates
The notAfter date should be 90 days from now.
6. Common pitfalls
| Symptom | Likely cause | Fix |
|---|---|---|
| Web server reload fails | Incorrect service name or missing reload command | Verify systemctl reload nginx works manually. |
| Certbot keeps failing | DNS not pointing to the server during renewal | Ensure DNS A record is correct and reachable. |
| Hook never runs | Hook script not executable or wrong path | chmod +x and double‑check the path in renewal-hooks/post/ |
7. Security note
The hook script only touches the web server’s reload command; it does not expose the private key. Keep /etc/letsencrypt owned by root and restrict permissions (chmod 700 /etc/letsencrypt).
See also
- When My Ubuntu 24.04 VPS Runs Out of Disk: A Step‑by‑Step Cleanup Script That Keeps Your Apache Logs from Failing
- When a VPS Disk Fills Mid‑Night: Quick Recovery with LVM Snapshots and Automated Alerts
- Rootless Podman volumes: how to keep data safe across upgrades without root
- Fixing ProxyJump timeouts when a VPN drops your IP address.
- How to Keep a Linux Laptop Connected to a VPN When the Router Switches Between DHCP and Static IPs