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 | Port 80 reachable from the internet, DNS pointing at the server | No |
DNS-01 | Publish a TXT record at | 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 |
|
Names |
|
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 certbotsudo snap install --classic certbotsudo ln -s /snap/bin/certbot /usr/local/bin/certbotcertbot --versionIf 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 nginxStep 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.comWhen 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 --redirectWhat the flags do:
--nginxuses 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.-dlists every name on the certificate. The first name becomes the certificate name and the directory under/etc/letsencrypt/live/.--redirectadds a 301 from HTTP to HTTPS. Keep port 80 open anyway, because HTTP-01 renewals still arrive there.-msets 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 |
|---|---|---|
| Leaf certificate plus intermediate |
|
| Private key |
|
| Leaf certificate only | Services that take the chain separately |
| 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 certbotThe snap's timer is snap.certbot.renew.timer. The apt package uses certbot.timer. Then simulate a full renewal:
sudo certbot renew --dry-runThe 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/certssudo tee /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh >/dev/null <<'EOF'#!/bin/shumask 077name=$(basename "$RENEWED_LINEAGE")cat "$RENEWED_LINEAGE/fullchain.pem" "$RENEWED_LINEAGE/privkey.pem" \ > "/etc/haproxy/certs/$name.pem"systemctl reload haproxyEOFsudo 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.shStep 5: Verify the certificate
Check what Certbot manages:
sudo certbot certificatesFound 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.pemCertbot 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 -enddateExpect 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=oksudo snap install certbot-dns-cloudflareCreate /root/.secrets/cloudflare.ini with an API token scoped to DNS edit on this one zone:
dns_cloudflare_api_token = YOUR_SCOPED_API_TOKENLock it down and request the certificate:
sudo chmod 600 /root/.secrets/cloudflare.inisudo 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 |
|---|---|---|
| Port 80 blocked by the host firewall or a security group | Allow TCP 80 from the internet, not only from your office |
| The request reached a different server block or a different host | Check |
Certbot edits | No block has a | Add the name to your server block, revert the edits to the default site and rerun with |
| Repeated real issuance while testing | Wait out the window and use |
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 |
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.