Automating Let's‑Encrypt renewal on a single‑node web server without downtime

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