What is it? #
Let's Encrypt issues free certificates and certbot automates obtaining and renewing them.
The prerequisites matter: DNS must already point at the server, port 80 must be reachable, and Nginx must be configured for the domain. Validation works by serving a challenge file over HTTP.
Certificates last 90 days. Renewal must be automatic, and the automation must be verified rather than assumed.
The final piece is monitoring. Even automated renewal fails sometimes, and an alert on days-to-expiry is what turns a potential outage into a routine fix.
Think of it like this #
A permit that must be renewed quarterly, where the renewal can be filed automatically.
Setting up the automatic filing is sensible. Checking once a month that it is still filing is what prevents discovering the lapse at the worst moment.
Simple example #
A domain pointing at your server, Nginx serving it over HTTP. You obtain a certificate covering the apex and www, confirm the renewal timer, and add an expiry check.
Code #
# Prerequisites — verify before running certbot
dig +short example.com # must return this server's IP
curl -I http://example.com # must reach this server's Nginx
sudo ufw status | grep 80 # port 80 must be open
# Obtain and configure automatically
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com \
--email [email protected] --agree-tos --no-eff-email --redirect
# --redirect adds the HTTP to HTTPS redirect for you
# Verify renewal is set up and working
systemctl list-timers | grep certbot # the timer should be active
sudo certbot renew --dry-run # full rehearsal, no issuance
sudo certbot certificates # what exists and when it expires
# Reload Nginx after renewal
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null <<'SH'
#!/bin/bash
systemctl reload nginx
SH
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
# What certbot produces, plus settings worth adding
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
# Add once you are confident HTTPS works everywhere — hard to undo
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
# Monitor expiry — automation can fail silently
#!/usr/bin/env bash
DOMAIN=example.com
END=<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>o</mi><mi>p</mi><mi>e</mi><mi>n</mi><mi>s</mi><mi>s</mi><mi>l</mi><msub><mi>s</mi><mi>c</mi></msub><mi>l</mi><mi>i</mi><mi>e</mi><mi>n</mi><mi>t</mi><mo>−</mo><mi>c</mi><mi>o</mi><mi>n</mi><mi>n</mi><mi>e</mi><mi>c</mi><mi>t</mi><mi mathvariant="normal">"</mi></mrow><annotation encoding="application/x-tex">(openssl s_client -connect "</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal">o</span><span class="mord mathnormal">p</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">ss</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="katex-sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">c</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">i</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="katex-base"><span class="katex-strut" style="height:0.6944em;"></span><span class="mord mathnormal">co</span><span class="mord mathnormal">nn</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">t</span><span class="mord">"</span></span></span></span>DOMAIN":443 -servername "$DOMAIN" </dev/null 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
DAYS=<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mo stretchy="false">(</mo><mo stretchy="false">(</mo></mrow><annotation encoding="application/x-tex">(( (</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(((</span></span></span></span>(date -d "<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>E</mi><mi>N</mi><mi>D</mi><mi mathvariant="normal">"</mi><mo>+</mo></mrow><annotation encoding="application/x-tex">END" +%s) -</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal" style="margin-right:0.0576em;">E</span><span class="mord mathnormal" style="margin-right:0.109em;">N</span><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="mord">"</span><span class="mord">+</span></span></span></span>(date +%s) ) / 86400 ))
echo "<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>D</mi><mi>O</mi><mi>M</mi><mi>A</mi><mi>I</mi><mi>N</mi><mi>c</mi><mi>e</mi><mi>r</mi><mi>t</mi><mi>i</mi><mi>f</mi><mi>i</mi><mi>c</mi><mi>a</mi><mi>t</mi><mi>e</mi><mi>e</mi><mi>x</mi><mi>p</mi><mi>i</mi><mi>r</mi><mi>e</mi><mi>s</mi><mi>i</mi><mi>n</mi></mrow><annotation encoding="application/x-tex">DOMAIN certificate expires in</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="mord mathnormal" style="margin-right:0.0278em;">O</span><span class="mord mathnormal" style="margin-right:0.109em;">M</span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.0785em;">I</span><span class="mord mathnormal" style="margin-right:0.109em;">N</span><span class="mord mathnormal" style="margin-right:0.0278em;">cer</span><span class="mord mathnormal">t</span><span class="mord mathnormal">i</span><span class="mord mathnormal" style="margin-right:0.1076em;">f</span><span class="mord mathnormal">i</span><span class="mord mathnormal">c</span><span class="mord mathnormal">a</span><span class="mord mathnormal">t</span><span class="mord mathnormal">ee</span><span class="mord mathnormal">x</span><span class="mord mathnormal">p</span><span class="mord mathnormal">i</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">es</span><span class="mord mathnormal">in</span></span></span></span>DAYS days"
[ "<span class="katex-error" title="ParseError: KaTeX parse error: Expected 'EOF', got '&' at position 16: DAYS" -lt 14 ] &̲& curl -fsS "ht…" style="color:#cc0000">DAYS" -lt 14 ] && curl -fsS "https://alerts.example.com/cert-expiring?days=</span>DAYS"
How it works #
Certbot proves domain control by placing a file under /.well-known/acme-challenge/ and having Let's Encrypt fetch it over HTTP. That is why DNS and port 80 must both work first — attempting it early produces a validation failure that looks mysterious.
The --nginx plugin edits your server block, adds the certificate paths and, with --redirect, the HTTP to HTTPS redirect. Reading the resulting configuration afterwards is worthwhile so you understand what changed.
Requesting both names in one command produces a certificate covering both. Omitting www is the usual cause of a name-mismatch error on whichever was left out.
The installed systemd timer runs certbot renew twice daily. The command does nothing unless a certificate is within 30 days of expiry, so frequent runs are harmless.
--dry-run rehearses the entire renewal including validation, without issuing. It is the only way to know the automation works before it matters.
The deploy hook reloads Nginx after a successful renewal. Without it, the new certificate sits on disk while the running server continues serving the old one until it happens to restart.
HSTS instructs browsers to use HTTPS for the domain for a year. It closes the plaintext first-request window and is difficult to reverse, so it belongs after everything else is confirmed working.
Real-world use #
Free automated certificates removed the last practical reason for plain HTTP. Browsers now mark HTTP as insecure and several features require HTTPS.
Certificate expiry still causes outages, almost always because renewal was never verified or the reload hook was missing. The expiry monitor is the cheap insurance.
Wildcard certificates require DNS validation and an API token for your DNS provider. They are worth it when subdomains are created frequently.
Rate limits apply to issuance, so repeated attempts while debugging can block you for a week. The staging environment exists for that.
Behind a CDN or load balancer, TLS may terminate at the edge instead. In that case renewal happens there, and the origin may use its own certificate or plain HTTP over a private network.
Common mistakes #
- Running certbot before DNS resolves to the server or while port 80 is closed.
- Requesting the apex domain but not www.
- Never testing renewal with --dry-run.
- Missing the deploy hook, so the renewed certificate is not served.
- Enabling HSTS with a long max-age before HTTPS is fully working.
Practice #
Obtain a certificate for a domain covering both the apex and www, with the HTTP redirect enabled. Confirm the renewal timer is active and run a dry-run renewal. Add the reload hook and an expiry monitoring script, then verify the served certificate with openssl.