Automate SSL Certificate Renewal with Certbot and NGINX

Photo by Unsplash

Photo by Unsplash
Manually managing SSL certificates is one of those tasks that feels simple until a certificate expires in production at 2 AM on a Sunday. Let's Encrypt and Certbot automate the entire lifecycle — issuance, validation, installation, and renewal — so that SSL certificate expiry simply stops being a concern. After setting this up across every DigitalOcean Droplet and GCP Compute Engine instance we manage at Commsult Indonesia, I have not manually renewed a certificate in over two years. This guide covers the complete setup, the NGINX configuration for strong TLS, and the automated renewal pipeline.
Let's Encrypt is a free Certificate Authority (CA) that issues Domain Validation (DV) certificates valid for 90 days. The short validity window is intentional — it enforces automation and limits the damage window for compromised certificates. Certbot is the official ACME client that communicates with Let's Encrypt's API. ACME (Automatic Certificate Management Environment) is the protocol that proves you control a domain before the CA issues a certificate. Certbot supports two challenge types: HTTP-01 (creates a temporary file at /.well-known/acme-challenge/ on your web server) and DNS-01 (creates a temporary DNS TXT record). HTTP-01 is simpler and works for most setups.
A Let's Encrypt certificate lasts 90 days. Certbot's recommended renewal approach is to run `certbot renew` twice daily (via systemd timer or cron). The command checks whether any certificate is within 30 days of expiry — if not, it does nothing. If a renewal is needed, it re-runs the ACME challenge, obtains a new certificate, and runs any configured deploy hooks (such as reloading NGINX). This approach means certificates are typically renewed when they have 30 days remaining, giving you a 30-day buffer even if the renewal process fails for a few days.
Let's Encrypt enforces rate limits to prevent abuse. The most relevant for developers: 50 certificates per registered domain per week, and 5 failed validation attempts per hour per account per hostname. These limits matter during initial setup when you may be iterating on your configuration. Always use `--dry-run` flag when testing your certbot commands — dry runs use the Let's Encrypt staging environment (separate rate limits) and do not issue real certificates. Only remove `--dry-run` when you are confident the configuration is correct.
┌────────────────────────────────────────────────────────────────┐
│ Certbot Auto-Renewal Architecture │
│ │
│ systemd timer (twice daily) │
│ │ │
│ ▼ │
│ certbot renew │
│ │ │
│ ├── cert expiry > 30 days? → skip (no-op) │
│ │ │
│ └── cert expiry ≤ 30 days? │
│ │ │
│ ▼ │
│ ACME Challenge │
│ (HTTP-01 or DNS-01) │
│ │ │
│ ▼ │
│ Let's Encrypt CA ──► new cert issued │
│ │ │
│ ▼ │
│ Deploy hook: │
│ nginx -s reload │
│ │ │
│ ▼ │
│ cert valid for 90 days ✓ │
└────────────────────────────────────────────────────────────────┘If your server is behind a cloud firewall or DigitalOcean's Droplet firewall, ensure port 80 (HTTP) is open during the ACME HTTP-01 challenge. Certbot's NGINX plugin temporarily serves the challenge file on port 80. After the challenge completes successfully, you can restrict port 80 to only redirect to HTTPS. Certbot will continue to handle renewals automatically because the NGINX plugin manages the temporary port-80 exposure during the renewal process.
The NGINX plugin for Certbot is the recommended approach — it automatically edits your NGINX configuration to add the certificate paths, enable HTTPS, and add the HTTP-to-HTTPS redirect. After running `certbot --nginx`, your NGINX configuration will have `ssl_certificate` and `ssl_certificate_key` directives pointing to `/etc/letsencrypt/live/<domain>/`. The plugin also adds `include /etc/letsencrypt/options-ssl-nginx.conf` which provides a secure default cipher suite and protocol configuration maintained by Let's Encrypt.
After Certbot renews a certificate, NGINX needs to reload to pick up the new certificate. Without a deploy hook, the old certificate stays loaded in memory until NGINX is restarted manually or the server reboots. Certbot runs scripts in `/etc/letsencrypt/renewal-hooks/deploy/` after every successful renewal. Create a script that runs `nginx -t && systemctl reload nginx` — the config test ensures a broken NGINX configuration does not take down your server during an automated renewal.
# 1. Install Certbot with NGINX plugin (Ubuntu 22.04)
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
# 2. Obtain and auto-configure NGINX certificate
sudo certbot --nginx -d example.com -d www.example.com --email your@email.com --agree-tos --no-eff-email --redirect
# 3. Verify auto-renewal timer is active
sudo systemctl status certbot.timer
# ● certbot.timer - Run certbot twice daily
# Active: active (waiting)
# 4. Dry run to confirm renewal works
sudo certbot renew --dry-run
# 5. Add a deploy hook to reload NGINX after renewal
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'EOF'
#!/bin/bash
nginx -t && systemctl reload nginx
echo "NGINX reloaded after cert renewal at $(date)" >> /var/log/certbot-deploy.log
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
# 6. Harden NGINX TLS config (add to server block)
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384;
# ssl_prefer_server_ciphers off;
# ssl_session_cache shared:SSL:10m;
# ssl_session_timeout 1d;
# add_header Strict-Transport-Security "max-age=63072000" always;
# 7. Check certificate expiry manually
sudo certbot certificatesThe default Certbot NGINX configuration is secure but not optimal for an A+ rating on SSL Labs. To achieve maximum security score: disable TLS 1.0 and 1.1 (keep only TLSv1.2 and TLSv1.3), configure a strong cipher suite prioritizing ECDHE with forward secrecy, enable HSTS with a 2-year max-age and includeSubDomains, add OCSP stapling to reduce latency on TLS handshakes, and configure a Diffie-Hellman parameter file (2048-bit minimum, 4096-bit preferred). Generate the DH params file with `openssl dhparam -out /etc/nginx/dhparam.pem 2048` — this takes 1-2 minutes on a typical VPS.
The most common mistake when setting up Certbot is running the initial `certbot certonly` or `certbot --nginx` command when port 80 is blocked. The HTTP-01 challenge fails silently from the user's perspective — you get a rate-limit hit and have to wait an hour before retrying. Check that `curl http://your-domain.com/.well-known/acme-challenge/test` returns something (even a 404 from NGINX) before running Certbot. Also, never delete `/etc/letsencrypt/` — this directory contains your account key and certificate renewal configuration. Back it up to DigitalOcean Spaces or GCP Cloud Storage.
For environments with multiple subdomains (api.example.com, app.example.com, admin.example.com), a wildcard certificate (`*.example.com`) is more efficient than individual certificates per subdomain. Wildcard certificates require the DNS-01 challenge, which proves domain ownership by creating a DNS TXT record rather than serving a file over HTTP. For DigitalOcean DNS, install the `python3-certbot-dns-digitalocean` plugin and create a credentials file with your DigitalOcean API token. The challenge and renewal are then fully automated without any HTTP access needed.
Even with automated renewal, you should monitor certificate expiry as a safety net. Add a Prometheus Blackbox Exporter probe that checks TLS certificate expiry for each domain and alert when expiry is less than 21 days away. Alternatively, Let's Encrypt sends expiry reminder emails to the address registered with Certbot. For a belt-and-suspenders approach, use a service like certificate.transparency.dev or an uptime monitor with TLS expiry checking (UptimeRobot's free tier includes this feature) as a secondary notification channel outside your monitoring stack.