Cycle's hosted MCP is live. Your AI tooling just learned DevOps.Read the announcement

Configuring Let's Encrypt with Certbot.

Let's Encrypt is a free, automated certificate authority that issues domain-validated TLS certificates over the ACME protocol. Certbot is the EFF's ACME client: it proves you control a domain, requests the certificate, installs it in your web server and renews it on a timer.

The problem the pair solves is expiry. Let's Encrypt certificates last 90 days today and are getting shorter, so a certificate installed by hand and forgotten takes the site down within a quarter. Certbot turns issuance and renewal into a scheduled job that runs without a human in the loop.

TLS certificates by default on Cycle

Certbot on one server is a solved problem. It gets more expensive as domains and servers multiply. Every host needs its own client, challenge path or DNS credentials, reload hook and check for a renewal that failed quietly.

If that's not something you want to manage yourself, Cycle handles it in the control plane. Cycle integrates with Let's Encrypt and generates certificates automatically for LINKED DNS records with TLS enabled, using either DNS or HTTP challenges. The certificates are installed on the environment's load balancer, which terminates TLS. Wildcard certificates are used when applicable.

Cycle renews certificates before they expire. If a renewal hasn't happened in time, it tries again 15, 2 and 1 days before expiry, and retries a failed attempt every 3 hours. You can also upload your own PEM certificates, though Cycle can't auto-renew those and alerts the hub owner within 30 days of expiry instead. The details are in Cycle's TLS certificate docs.

If you manage your own servers, the rest of this page walks through setting up Let's Encrypt with Certbot by hand.

How ACME validation works

ACME (RFC 8555) is the protocol between Certbot and Let's Encrypt. The client registers an account key, asks for a certificate covering a list of names and receives a challenge for each name. Once the challenges pass, it submits a certificate signing request and downloads the signed chain. The only thing Let's Encrypt checks is that you control the domain, and the challenge type decides how you prove it.

Challenge

How you prove control

What it needs

Wildcards

HTTP-01

Serve a token at http://<domain>/.well-known/acme-challenge/<token>

Port 80 reachable from the internet, DNS pointing at the server

No

DNS-01

Publish a TXT record at _acme-challenge.<domain>

API access to your DNS provider

Yes

TLS-ALPN-01

Answer a special TLS handshake on port 443

A client that supports it (Certbot doesn't natively)

No

HTTP-01 is the default for a single web server. It needs no credentials, but every server that answers for the name must be able to serve the token. Behind a load balancer, the validation request can land on a node that never saw the token file.

DNS-01 works for servers that aren't reachable from the internet, for wildcard names and for fleets where any node might answer. The cost is a DNS API token on the machine running Certbot, and a token that can edit your zone is a sensitive credential. You can limit that by pointing _acme-challenge at a separate zone with a CNAME, which Let's Encrypt follows.

Certificate lifetimes are shrinking

Let's Encrypt has announced shorter lifetimes for its default profile: 64-day certificates from February 10, 2027 and 45-day certificates from February 16, 2028. Certbot 4.0 and later renew when a third of the lifetime remains instead of at a fixed 30 days. Certbot 4.1 added ACME Renewal Information (ARI), which lets the CA tell the client when to renew. Any of your own scripts that hard-code a renewal interval need revisiting.

Before you start

The examples use one web server:

Item

Value

Host

web-1, public IP 203.0.113.10 and 2001:db8::10

Names

example.com and www.example.com, A and AAAA records pointing at web-1

OS

Ubuntu 24.04, or Debian 12 or 13

Web server

Nginx, already serving the site on port 80

You need root, ports 80 and 443 open in the host firewall and any cloud security group, and DNS already resolving publicly. Let's Encrypt validates against public DNS, so a record that hasn't propagated fails the challenge.

Step 1: Install Certbot

Certbot's own instructions recommend the snap package, which tracks the latest release and ships its renewal timer. Ubuntu ships with snapd, but on Debian install it first with sudo apt install snapd. Remove any distribution package of Certbot so the two don't conflict:

sudo apt remove certbot
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
certbot --version

If you'd rather stay on apt, sudo apt install certbot python3-certbot-nginx works on Debian and Ubuntu. Distribution packages lag upstream, so Ubuntu 24.04's apt package is a 2.x release without the renewal-threshold and ARI changes described above.

Step 2: Prepare the Nginx server block

The Nginx plugin finds the block to edit by matching server_name. If nothing matches, it falls back to the default server block, which on Ubuntu is the stock default site. Make sure one exists for every name on the certificate, for example /etc/nginx/sites-available/example.com:

server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com;
}

Enable it and reload:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Step 3: Request the certificate

Do a dry run first. It runs the full challenge against Let's Encrypt's staging environment, which has much looser rate limits, and saves nothing:

sudo certbot certonly --nginx --dry-run -d example.com -d www.example.com

When that succeeds, request the real certificate and let Certbot install it:

sudo certbot --nginx -d example.com -d www.example.com \
--agree-tos -m ops@example.com --redirect

What the flags do:

  • --nginx uses the Nginx plugin for both halves of the job. It answers HTTP-01 by temporarily adding a location to the server block, then edits the block to serve HTTPS.
  • -d lists every name on the certificate. The first name becomes the certificate name and the directory under /etc/letsencrypt/live/.
  • --redirect adds a 301 from HTTP to HTTPS. Keep port 80 open anyway, because HTTP-01 renewals still arrive there.
  • -m sets the account contact address. Let's Encrypt stopped sending expiry reminder emails in 2025, so don't treat that inbox as monitoring.

Certbot writes four files under /etc/letsencrypt/live/example.com/:

File

Contents

Typical use

fullchain.pem

Leaf certificate plus intermediate

ssl_certificate in Nginx

privkey.pem

Private key

ssl_certificate_key in Nginx

cert.pem

Leaf certificate only

Services that take the chain separately

chain.pem

Intermediate only

Services that take the chain separately

These are symlinks into /etc/letsencrypt/archive/, and Certbot repoints them on every renewal. Reference the live paths in configs, never the archive. Since Certbot 2.0, new certificates use ECDSA P-256 keys by default.

Step 4: Confirm automatic renewal

The snap installs a systemd timer that runs certbot renew twice a day. certbot renew walks every config in /etc/letsencrypt/renewal/ and only renews certificates that are due, so frequent runs are harmless.

systemctl list-timers | grep certbot

The snap's timer is snap.certbot.renew.timer. The apt package uses certbot.timer. Then simulate a full renewal:

sudo certbot renew --dry-run

The output should end with Congratulations, all simulated renewals succeeded.

Certificates installed by the Nginx plugin trigger an Nginx reload on renewal. Anything else that reads the files, such as HAProxy, needs a deploy hook. Executables in /etc/letsencrypt/renewal-hooks/deploy/ run after every successful renewal:

sudo mkdir -p /etc/haproxy/certs
sudo tee /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh >/dev/null <<'EOF'
#!/bin/sh
umask 077
name=$(basename "$RENEWED_LINEAGE")
cat "$RENEWED_LINEAGE/fullchain.pem" "$RENEWED_LINEAGE/privkey.pem" \
> "/etc/haproxy/certs/$name.pem"
systemctl reload haproxy
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh

$RENEWED_LINEAGE is the live directory of the certificate that was renewed. HAProxy wants the chain and key in one PEM file, which is why the hook concatenates them. HAProxy then loads the directory with a bind :443 ssl crt /etc/haproxy/certs/ line.

The hook only runs on future renewals, so run it once by hand to create the first file:

sudo RENEWED_LINEAGE=/etc/letsencrypt/live/example.com \
/etc/letsencrypt/renewal-hooks/deploy/haproxy.sh

Step 5: Verify the certificate

Check what Certbot manages:

sudo certbot certificates
Found the following certs:
Certificate Name: example.com
Serial Number: 4a1f...
Key Type: ECDSA
Identifiers: example.com www.example.com
Expiry Date: 2027-01-11 14:02:33+00:00 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem

Certbot 2.x from apt labels the names line Domains instead. Then check what the server presents, which differs when a service wasn't reloaded:

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -enddate

Expect a subject of CN=example.com, an issuer containing O=Let's Encrypt and a notAfter date that matches Certbot's expiry. The -servername flag sends SNI. Without it, a server hosting several sites can return its default certificate instead.

Variant: wildcard certificates with DNS-01

Wildcard names require DNS-01. Certbot ships DNS plugins for the major providers. For Cloudflare on the snap install:

sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare

Create /root/.secrets/cloudflare.ini with an API token scoped to DNS edit on this one zone:

dns_cloudflare_api_token = YOUR_SCOPED_API_TOKEN

Lock it down and request the certificate:

sudo chmod 600 /root/.secrets/cloudflare.ini
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
-d example.com -d '*.example.com'

Quote the wildcard so the shell doesn't expand it. certonly skips installation, so point ssl_certificate at the live path and add a deploy hook that reloads Nginx.

Troubleshooting

Certbot logs every run to /var/log/letsencrypt/letsencrypt.log. Timer runs on the snap install show up in journalctl -u snap.certbot.renew.service.

Symptom

Likely cause

Fix

Timeout during connect (likely firewall problem)

Port 80 blocked by the host firewall or a security group

Allow TCP 80 from the internet, not only from your office

Invalid response with a 404 for the challenge URL

The request reached a different server block or a different host

Check server_name, then check for a stale AAAA record, since Let's Encrypt prefers IPv6 when one exists

Certbot edits sites-enabled/default instead of your site, or reports Could not automatically find a matching server block

No block has a server_name matching one of the -d names

Add the name to your server block, revert the edits to the default site and rerun with --reinstall

too many certificates rate-limit error

Repeated real issuance while testing

Wait out the window and use --dry-run for tests

Renewal succeeded but clients still see the old certificate

The service reading the files was never reloaded

Add a deploy hook that reloads it

DNS-01 fails with an incorrect or missing TXT record

DNS propagation slower than Certbot's default wait

Raise --dns-cloudflare-propagation-seconds and check the token's zone scope

Running Let's Encrypt past one server

Everything above is per host. Each server running Certbot has its own account, timer, hooks and failure modes, and nothing alerts you when one of them breaks. Let's Encrypt no longer emails about expiring certificates, so expiry monitoring has to come from somewhere else. Even a cron job running openssl x509 -checkend 1209600 -noout -in fullchain.pem, which fails when fewer than 14 days remain, is better than nothing.

Load balancing complicates both challenge types. HTTP-01 needs a shared challenge directory or a rule that sends /.well-known/acme-challenge/ to the one node running Certbot. DNS-01 means putting zone-editing tokens on servers. Either way, the issued certificate then has to reach every node that terminates TLS, followed by a reload on each, which is where TLS termination design starts to matter.

Shorter lifetimes multiply every one of those events. At 45 days, a fleet with a few hundred certificates renews something every day. That pipeline of issuance, distribution, reloads and monitoring is what Cycle's control plane runs for you.

Issuing a certificate takes one command, and keeping hundreds renewed takes a system.

Cookies

Cookies Preferences

We run basic, anonymous analytics by default to measure site traffic. By clicking "Accept," you allow additional cookies for advanced app improvements and tailored advertising. Choose what you share by clicking "Customize."