Using Let's Encrypt in Production: Best Practices

Best practices for running Let's Encrypt in production: Certbot configuration, renewal automation, monitoring, rate limits, and failure handling.

Let's Encrypt is the most popular certificate authority in the world, powering over 300 million websites with free, automated TLS certificates. For most production workloads, it's the right choice. But "free and automated" doesn't mean "set up once and forget." Running Let's Encrypt reliably in production requires understanding its constraints, configuring your automation correctly, and monitoring the results.

Here's how to do it right.

Choosing an ACME Client

Let's Encrypt uses the ACME (Automatic Certificate Management Environment) protocol. You need an ACME client to request and renew certificates. The choice of client matters more than most people think.

Certbot

The official Let's Encrypt client. It's the most widely documented, has plugins for most web servers, and is the default recommendation.

# Install Certbot (Ubuntu/Debian)
sudo apt install certbot python3-certbot-nginx

# Obtain a certificate with automatic Nginx configuration
sudo certbot --nginx -d example.com -d www.example.com

# Obtain a certificate without modifying web server config
sudo certbot certonly --webroot -w /var/www/html -d example.com

Best for: Traditional server setups (Nginx, Apache) on Linux. Most tutorials and documentation reference Certbot.

acme.sh

A pure shell script ACME client. No dependencies beyond curl and openssl. Supports more DNS providers than Certbot and is popular in Docker and minimal environments.

# Install acme.sh
curl https://get.acme.sh | sh

# Issue a certificate using DNS validation
acme.sh --issue --dns dns_cf -d example.com -d "*.example.com"

# Issue using webroot
acme.sh --issue -w /var/www/html -d example.com

Best for: Environments where you want minimal dependencies, need DNS-01 validation with specific providers, or run in containers.

Caddy (Built-in ACME)

Caddy is a web server with automatic HTTPS built in. It obtains and renews Let's Encrypt certificates with zero configuration. Just specify your domain in the Caddyfile.

example.com {
    reverse_proxy localhost:3000
}

That's it. Caddy handles certificate issuance, renewal, and OCSP stapling automatically.

Best for: New deployments where you can choose your web server. Caddy eliminates certificate management as a concern entirely.

Traefik (Built-in ACME)

Traefik is a reverse proxy popular in container and Kubernetes environments. Like Caddy, it has built-in ACME support and can obtain Let's Encrypt certificates automatically.

Best for: Docker and Kubernetes environments where Traefik is already the ingress controller.

Match the client to your stack

Don't default to Certbot just because it's the most common. If you run Caddy or Traefik, use their built-in ACME support -- it's simpler and has fewer moving parts. Use acme.sh when you need DNS-01 validation with specific providers.

DNS-01 vs HTTP-01 Challenge

Let's Encrypt verifies domain ownership through challenges. The two main types have different trade-offs.

HTTP-01

The ACME client places a file at http://yourdomain.com/.well-known/acme-challenge/TOKEN. Let's Encrypt fetches this file to verify you control the domain.

Pros:

  • Simple setup, no DNS API needed
  • Works with any hosting that allows file serving on port 80

Cons:

  • Requires port 80 to be accessible from the internet
  • Doesn't work behind CDNs that cache challenge responses
  • Cannot issue wildcard certificates
  • Doesn't work if your DNS points to a different server than where Certbot runs

DNS-01

The ACME client creates a DNS TXT record (_acme-challenge.yourdomain.com). Let's Encrypt queries DNS to verify you control the domain.

Pros:

  • Works behind CDNs, load balancers, and firewalls
  • Can issue wildcard certificates
  • Doesn't require the web server to be running
  • Works even when the domain points elsewhere

Cons:

  • Requires API access to your DNS provider
  • DNS propagation delays can cause validation failures
  • DNS API credentials need to be stored securely on the server
FeatureHTTP-01DNS-01
Wildcard supportNoYes
Works behind CDNOften notYes
Requires port 80YesNo
DNS API neededNoYes
Setup complexityLowMedium

Recommendation: Use HTTP-01 for simple setups where the server is directly accessible. Use DNS-01 when you need wildcards, operate behind a CDN, or can't expose port 80.

Renewal Automation

Let's Encrypt certificates are valid for 90 days. Renewal should happen automatically at around 30 days before expiry (Certbot's default). Here's how to make it reliable.

Systemd Timers (Preferred on Modern Linux)

Most Certbot installations create a systemd timer automatically. Verify it exists and is running:

# Check if the timer is active
systemctl list-timers | grep certbot

# If missing, enable it
sudo systemctl enable --now certbot.timer

The default timer runs twice daily, which is more than enough. Certbot only actually renews when a certificate is within 30 days of expiry.

Cron Jobs

If you're not using systemd, a cron job works:

# Add to crontab (runs twice daily at random times)
0 0,12 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"

Pre and Post Hooks

Hooks run before and after certificate renewal. They're essential for deploying renewed certificates.

# Certbot renewal with hooks
certbot renew \
  --pre-hook "systemctl stop nginx" \
  --post-hook "systemctl start nginx" \
  --deploy-hook "systemctl reload nginx"
  • pre-hook: Runs before renewal attempt. Use to stop services that bind port 80.
  • post-hook: Runs after renewal attempt (regardless of success/failure). Use to restart stopped services.
  • deploy-hook: Runs only after a successful renewal. Use to reload the web server, copy certificates, or notify other systems.

deploy-hook vs post-hook

Use --deploy-hook (not --post-hook) for reloading your web server. Post-hook runs even when no renewal happened. Deploy-hook only runs when a certificate was actually renewed. This avoids unnecessary service interruptions.

Automation isn't enough -- add monitoring

Certbot failures are silent. Get alerts when certificates approach expiry despite auto-renewal.

Rate Limits and How to Avoid Them

Let's Encrypt enforces rate limits to prevent abuse. Hitting them in production can leave you unable to renew.

Key limits:

  • 50 certificates per registered domain per week (e.g., 50 certs for subdomains of example.com)
  • 5 duplicate certificates per week (same exact set of domain names)
  • 300 new orders per account per 3 hours
  • 5 failed validations per hostname per hour

How to stay within limits:

  1. Use the staging environment for testing. The staging server has much higher rate limits and issues untrusted certificates (fine for testing).
certbot certonly --staging -d example.com
  1. Combine domains into fewer certificates. Instead of separate certificates for each subdomain, use SAN certificates:
certbot certonly -d example.com -d www.example.com -d api.example.com
  1. Don't retry aggressively. If renewal fails, Certbot backs off by default. Don't run it in a tight loop trying to force it through.

  2. Use --expand to add domains to an existing certificate instead of requesting a new one:

certbot certonly --expand -d example.com -d www.example.com -d new-subdomain.example.com

Handling Renewal Failures

When renewal fails, you need to know about it and fix it fast. Here's a systematic approach.

Common Failure Causes and Fixes

DNS validation fails:

  • Verify DNS API credentials haven't expired or been rotated
  • Check that the DNS provider plugin is up to date
  • Test DNS propagation: dig TXT _acme-challenge.example.com

HTTP validation fails:

  • Ensure port 80 is open and reachable from the internet
  • Check that .well-known/acme-challenge/ path isn't blocked by your web server config or WAF
  • Verify the webroot directory is correct

Permission errors:

  • Certbot needs write access to /etc/letsencrypt/
  • Web server reload needs appropriate permissions (usually root or sudo)
  • Check that the user running the cron/timer has correct permissions

Rate limits hit:

  • Wait for the rate limit window to reset (1 week for most limits)
  • Use the staging environment while debugging
  • Consider using a different ACME account if available

Testing Renewal

Always test after any server changes:

# Dry run - simulates renewal without actually requesting a certificate
sudo certbot renew --dry-run

If the dry run succeeds, your renewal process is working. Run this after every server migration, OS upgrade, or web server configuration change.

Multi-Server Setups

When multiple servers need the same certificate (behind a load balancer, for example), you have several options.

Centralized Certificate Server

One server handles all certificate issuance and renewal. Other servers pull certificates from a shared location (NFS mount, S3 bucket, HashiCorp Vault).

Pros: Single point of management, easy to monitor. Cons: Single point of failure, requires secure certificate distribution.

DNS-01 on Any Server

Since DNS-01 validation doesn't require the web server, you can run it from any machine with DNS API access. The certificate is then deployed to all servers.

Cloud-Managed Certificates

AWS Certificate Manager, Google Cloud Managed Certificates, and Azure provide free certificates that are automatically deployed to load balancers. If you're already using cloud load balancers, this is often the simplest path.

The trade-off: You're locked into the cloud provider's certificate management. The certificates only work with that provider's services (you can't download an ACM certificate and put it on a bare server).

Monitoring: The Non-Negotiable Safety Net

Everything above can fail silently. The cron job gets removed during a server migration. DNS credentials expire. A firewall change blocks port 80. A rate limit prevents renewal. The certificate file gets renewed but the web server isn't reloaded.

Monitoring is the one thing that catches all of these. It doesn't check your automation -- it checks the result. It connects to your domain, inspects the certificate being served, and tells you when it's approaching expiry.

What to monitor:

  • Days until expiry for every domain (alert at 30, 14, 7, 3, 1 days)
  • Certificate chain validity (missing intermediates)
  • Certificate changes (unexpected issuer or SANs)
  • All environments: production, staging, internal tools

The rule: If a domain serves HTTPS traffic, it should be monitored. No exceptions. The certificate you're not monitoring is the one that will expire on a holiday weekend.

Let's Encrypt is an incredible service -- free, automated, and reliable. But reliability has limits. Auto-renewal is your primary defense. Monitoring is the safety net that catches everything else. Use both.


Let's Encrypt handles the certificates. Monitoring handles the "what if."

Never miss an SSL certificate expiry

Monitor your certificates and get alerts before they expire. Free for up to 3 certificates.