Certbot Setup on Ubuntu: 13 Steps, 40 Min [2026]

Certbot is still the default way to get a free TLS certificate onto an Ubuntu server, and 2026 is the year its job description changed. Let’s Encrypt will switch its default certificate lifetime from 90 days to 64 days on February 10, 2027, according to its October 7, 2026 announcement, and operators are already auditing their configs for hard-coded renewal assumptions. A Certbot setup that only works because someone renews by hand every couple of months will fail the first time the clock gets shorter.

This tutorial walks through a complete Certbot setup on Ubuntu 26.04 LTS with nginx in 13 steps and roughly 40 minutes. You will point DNS at a server, install nginx, install Certbot from snap, request a staging certificate, switch to a real one, harden the TLS config, automate renewal with a deploy hook, test the whole chain, and add expiry monitoring that does not depend on email reminders. A copy-paste provisioning script at the end ties it together.

If you would rather skip Certbot entirely, a server that handles certificates internally is a fair alternative, and our Caddy and Nginx comparison covers that trade-off with benchmark numbers. Stay with nginx and Certbot when you already run nginx, need its module ecosystem, or manage a fleet where the config format matters more than convenience.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

What Changed for Certbot and Let’s Encrypt in 2026

Three shifts matter for anyone writing a Certbot tutorial or running a Certbot server today. First, certificates are getting shorter. Let’s Encrypt announced on October 7, 2026 that certificates issued or renewed on or after February 10, 2027 default to 64 days. Existing valid certificates are not revoked, and subscribers can opt into 45-day or 6-day lifetimes instead. The company also said it plans to cut authorization reuse from 30 days to 10 days alongside the change, with a further reduction to seven hours planned for 2028.

Second, Let’s Encrypt now speaks in profiles. The classic profile is the default, tlsserver is documented as the profile for automated TLS server certificates with a 45-day lifetime, and the old tlsclient profile stopped being available on July 8, 2026. Certbot 4.0 added the --preferred-profile option in 2025, so you can ask for a profile by name and fall back gracefully when the server does not offer it.

Third, validation is getting stricter. Let’s Encrypt began using six remote network perspectives on its staging environment on October 27, 2026. Remote perspectives mean more vantage points must reach your port 80 challenge. A firewall that geo-blocks traffic will fail more often than it used to.

The ACME endpoints and issuance chains are not changing, and Let’s Encrypt does not expect its rate limits to change with the new lifetime. Let’s Encrypt still charges nothing for certificates, and no paid Certbot plan turned up in the official sources checked on October 28, 2026. Free does not mean permanent, because certificate authorities come and go. The recent news that eMudhra is leaving the global TLS certificate business is a reminder to keep your issuance pipeline portable. Certbot speaks ACME, so the same automation can point at another ACME provider later with a one-line change.

DateChangeWhat it means for your server
March 11, 2026Certbot 5.3 adds the –ip-address option for Let’s Encrypt IP address certificatesPublic IPs can get certificates, but nginx needs manual config
July 8, 2026The tlsclient profile stops being availableDo not request it in any script
October 7, 2026Let’s Encrypt announces a 64-day default starting February 10, 2027Renewal must be fully automatic
October 14, 202664-day certificates go live on the classic profile in stagingTest renewals against staging now
October 27, 2026Staging uses six remote validation perspectivesKeep port 80 open to the whole internet
February 10, 202764-day default begins in productionNew and renewed certificates get shorter

Let’s Encrypt says the last 90-day certificate is expected to expire on May 11, 2027. After that date, every certificate on the default profile will follow the shorter schedule. One more change affects monitoring. Let’s Encrypt stopped sending certificate expiry reminder emails, and community members have already started publishing free external expiry monitors to fill the gap. Step 12 below builds a local one that needs no third party.

Prerequisites and Versions for a Certbot Setup

You need a server you control, a domain name you own, and about 40 minutes. The commands below assume a fresh Ubuntu Server install with a sudo user. Nothing here needs a paid plan, and the whole stack costs only the server and the domain.

ComponentVersion or requirementNotes
Ubuntu Server26.04 LTS (April 2026 release), including the 26.04.1 point releaseUbuntu 26.10 is newer but is a short-term release
Certbot5.4 or later (the Certbot 5.8.0 documentation is current)Installed from snap so it tracks upstream releases
nginxCurrent package from the Ubuntu repositoryAny supported nginx works with the Certbot nginx plugin
DomainA and optionally AAAA records you can editA registrar or DNS host with an API helps for DNS-01
NetworkInbound TCP ports 80 and 443 reachable from the internetPort 80 must stay open for HTTP-01 validation and renewals
AccountAn email address for expiry and security noticesUsed only for ACME account contact

Ubuntu 26.04 LTS is the right baseline for a production web server. Canonical followed it with 26.04.1 as the stabilized point release, and the Ubuntu release notes list the current state of each branch. Ubuntu 26.10, codenamed Stonking Stingray, is already out. It is newer, but it is a short-term release that is reportedly supported only until July 2027. Pick it only if you specifically need its newer kernel and platform features.

For nginx, the Ubuntu repository package is fine for this tutorial. The nginx project’s own release blog reportedly lists 1.31.7 as the newest open source release as of October 14, 2026, but your server will run whatever version Ubuntu ships, and Certbot works with it. If you need a newer nginx for HTTP/3 or other features, add the official nginx repository later. That choice does not change any step below.

A note on Python. Certbot’s snap bundles its own Python, so you never touch the system interpreter. That matters because Python 3.10 is at end of life as of October 2026, and the Certbot repository carries a change to deprecate Python 3.10 support. Users of the pip install path on older systems will feel that deprecation first. Snap users will not.

You also need a way to test from outside your own network. A phone on mobile data, a second VPS, or an online TLS checker works. Testing from the server itself hides firewall and DNS mistakes, and those two problems cause most failed first attempts.

Steps 1 and 2: Point DNS and Open the Firewall

Let’s Encrypt validates that you control a domain before it issues anything. For the most common challenge type, HTTP-01, it fetches a small file from port 80 of the name you requested. Before you install a single package, make sure the name actually points at this server and the ports are open.

Step 1: Create DNS records. In your DNS provider, create an A record for example.com and another for www.example.com pointing at your server’s public IPv4 address. If the server has IPv6, add AAAA records too, but only if nginx will really listen on IPv6. A stale AAAA record that points nowhere is the single most common cause of mysterious validation failures, because Let’s Encrypt prefers IPv6 when it finds a record.

Check that the records have propagated from your own machine, not from the server:

dig +short example.com A
dig +short www.example.com A
dig +short example.com AAAA

You should see your server’s address for the A lookups and either your IPv6 address or nothing for the AAAA lookup. If the lookup returns an old address, wait for the previous record’s TTL to expire before you continue. Rushing here wastes failed validation attempts, and those count toward rate limits.

Step 2: Update the server and open ports. Log in over SSH, apply pending updates, and enable the firewall with rules for SSH and web traffic. Allow SSH before you enable UFW, or you lock yourself out.

sudo apt update && sudo apt upgrade -y
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Expected output from the last command looks like this:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp (OpenSSH)           ALLOW IN    Anywhere
80/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere

If your cloud provider has its own network firewall (a security group, for example), open ports 80 and 443 there too. UFW only controls the host. A cloud-level rule that blocks port 80 will break validation even when UFW looks perfect.

Keep port 80 open permanently, not just during issuance. Renewals use the same challenge, and with the remote validation perspectives rolling out, a firewall that allows only a short list of source addresses will start failing renewals at random. Redirecting port 80 to HTTPS is fine, because Let’s Encrypt follows redirects during HTTP-01 validation.

Steps 3 and 4: Install nginx and Publish a Test Site

Step 3: Install nginx. Install the package and confirm the service is running. Ubuntu starts nginx automatically after installation.

sudo apt install -y nginx
sudo systemctl enable --now nginx
systemctl is-active nginx
nginx -v

The is-active check prints active, and nginx -v prints the packaged version string. Browse to http://example.com from another device. You should see the default nginx welcome page. If you do not, stop here and fix DNS or the firewall, because Certbot cannot succeed when the plain HTTP site is unreachable.

Step 4: Create a server block for your domain. The Certbot nginx plugin finds your site by matching the server_name directive, so give it a real server block rather than editing the default one. Create a web root and a placeholder page first.

sudo mkdir -p /var/www/example.com/html
sudo chown -R $USER:$USER /var/www/example.com/html
echo '<h1>example.com is alive</h1>' > /var/www/example.com/html/index.html

Now write the server block. This is the config Certbot will edit when it installs your certificate, so keep it simple.

# /etc/nginx/sites-available/example.com
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /var/www/example.com/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Enable it, remove the default site so it does not shadow your domain, test the syntax, and reload:

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

A healthy syntax test prints two lines: syntax is ok and test is successful. Then confirm the site answers on the domain name, from outside:

curl -I http://example.com

You want an HTTP/1.1 200 OK response with a Server: nginx header. If you get a 404 or the default page, the server_name does not match what you typed, or the symlink points at the wrong file. Fix that now. Debugging a certificate request is far harder than debugging a plain web server.

Steps 5 and 6: Install Certbot From Snap and Choose a Profile

Step 5: Install Certbot. Remove any distro-packaged Certbot first so two copies do not fight over the same renewal timer, then install the snap and link the binary onto your path.

sudo apt remove -y certbot python3-certbot-nginx
sudo snap install --classic certbot
sudo ln -sf /snap/bin/certbot /usr/bin/certbot
certbot --version

The version check prints a line such as certbot 5.8.0 or newer. Anything at 5.4 or later supports IP address certificates and the profile options used later in this tutorial.

Why snap and not apt

The snap tracks upstream Certbot releases directly, so you get new ACME features without waiting for a distro backport. That matters in a year when lifetimes and profiles keep changing. The apt package pins whatever version Ubuntu froze at release, and it keeps that version for the life of the LTS. The snap also ships its own Python, which avoids the system Python deprecation churn described earlier. The trade-off is a slightly larger footprint and the snap confinement rules, which you will meet again when installing DNS plugins in Step 13.

Which profile to request

Step 6: Decide on a profile. Let’s Encrypt offers a default and a handful of named profiles. For a standard website, you have three practical choices:

  • classic is the default. Today it issues 90-day certificates, and from February 10, 2027 it issues 64-day certificates.
  • tlsserver is documented for automated TLS server certificates and issues 45-day certificates. Pick it if you want the shortest lifetime your automation can handle right now.
  • shortlived issues six-day certificates. It is the profile used for IP address certificates, and it is only sensible when renewal is fully automatic and monitored.

Start with classic if your renewal automation is new. Move to tlsserver after Step 11 proves renewals work. The point of shorter lifetimes is that a leaked key stays useful to an attacker for less time, and Let’s Encrypt cites reduced key compromise and misissuance risk as the reason for the change. You do not have to chase the shortest option on day one. You do have to make sure automation is boring and tested.

Certbot records the profile you used in each certificate’s renewal file, so a later renewal asks for the same profile. If a profile disappears, as tlsclient did on July 8, 2026, renewals that depend on it fail. Keep an eye on the Let’s Encrypt profiles page, and browse the rest of our cybersecurity guides when you want to harden the other layers of the same server.

Step 7: Request a Staging Certificate, Then the Real One

Let’s Encrypt enforces rate limits on production issuance. A typo in a domain name or a broken firewall rule can burn through them quickly, and you then wait before you can try again. The staging environment has far looser limits and issues certificates that browsers do not trust, which is exactly what you want while you test.

Step 7a: Run against staging. The --nginx plugin edits your server block for you, adds the certificate paths, and sets up a redirect.

sudo certbot --nginx --test-cert \
  -d example.com -d www.example.com \
  --email [email protected] --agree-tos --no-eff-email \
  --redirect --non-interactive

On success, Certbot prints a summary. Typical output looks like this:

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Account registered.
Requesting a certificate for example.com and www.example.com

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2027-01-26.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

Deploying certificate
Successfully deployed certificate for example.com to /etc/nginx/sites-enabled/example.com
Successfully deployed certificate for www.example.com to /etc/nginx/sites-enabled/example.com
Congratulations! You have successfully enabled HTTPS on https://example.com and https://www.example.com

The expiry date in your own output depends on the day of issue and the profile. Until the 64-day default arrives on February 10, 2027, a certificate from the classic profile is still valid for 90 days. Staging certificates show the same shape of output, but the issuer is a fake intermediate, and browsers will warn.

Step 7b: Delete the staging certificate and request the real one. Remove the test lineage so the production request does not collide with it, then run the same command without --test-cert.

sudo certbot delete --cert-name example.com
sudo certbot --nginx \
  -d example.com -d www.example.com \
  --email [email protected] --agree-tos --no-eff-email \
  --redirect --non-interactive

If you want the 45-day profile from the start, add --preferred-profile tlsserver to the production command. Certbot sends your preference and uses the default profile if the server does not offer the one you asked for. Use --required-profile instead if you would rather fail loudly than fall back.

Confirm what you have:

sudo certbot certificates
Found the following certs:
  Certificate Name: example.com
    Serial Number: 4a9c...e17f
    Key Type: ECDSA
    Domains: example.com www.example.com
    Expiry Date: 2027-01-26 09:12:41+00:00 (VALID: 89 days)
    Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem

Write down the certificate name. Every later command, from renewal tests to deletion, refers to it. Also notice that the certificate lives under /etc/letsencrypt/live/ as symlinks. Point nginx at those symlinks, never at the numbered files in archive/, or renewals will deploy a new certificate that nginx never loads.

Steps 8 and 9: Verify the Chain and Tighten nginx TLS

Step 8: Check the certificate from outside. Run openssl from your laptop, not the server. It connects the same way a browser does and prints the issuer, the subject, and the validity dates.

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates
curl -I https://example.com
curl -I http://example.com

The first command should show Let’s Encrypt as the issuer and your domain as the subject, with a notBefore of today and a notAfter matching your certificate. The second curl returns 200 over HTTPS. The third returns a 301 redirect to the HTTPS URL. If the redirect is missing, rerun Certbot with --redirect, or add the redirect block by hand.

Step 9: Review what Certbot wrote. Open the server block and read it. Certbot adds lines marked with a comment saying they are managed by Certbot, including an include of options-ssl-nginx.conf and a Diffie-Hellman parameters file. Do not delete those lines. You can still add your own headers and protocol settings above or below them.

server {
    listen [::]:443 ssl ipv6only=on;
    listen 443 ssl;
    http2 on;
    server_name example.com www.example.com;

    root /var/www/example.com/html;
    index index.html;

    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;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;

    location / {
        try_files $uri $uri/ =404;
    }
}

Two cautions on those headers. HSTS tells browsers to refuse plain HTTP for the whole max-age period, so add it only after you are certain HTTPS works on every subdomain you care about. Start with a short value such as 300 seconds while you test, then raise it. Also skip the includeSubDomains and preload flags until you have audited every subdomain, since a preload listing is hard to undo.

Test and reload after every edit:

sudo nginx -t && sudo systemctl reload nginx

Certificates protect traffic in transit, and nothing else. A compromised web process still reads your private key if the permissions allow it. Confining nginx with a mandatory access control profile limits that damage, and our AppArmor profile for nginx on Ubuntu walks through the setup step by step. Keep /etc/letsencrypt/archive and /etc/letsencrypt/live readable by root only, which is the default.

Step 10: Automate Renewal With a Deploy Hook

A certificate that renews on its own is the entire point of this exercise. The snap installs a systemd timer that runs certbot renew twice a day. The command is cheap, because it only renews certificates that are close to expiry. Confirm the timer exists and has a next run time:

systemctl list-timers --all | grep -i certbot
Wed 2026-10-28 23:41:00 UTC  2h 40min  Wed 2026-10-28 11:41:00 UTC  9h ago  snap.certbot.renew.timer  snap.certbot.renew.service

Note the timer name. On the snap install it is snap.certbot.renew.timer. On the apt package it is certbot.timer. If you see neither, the timer is not installed, and you have to fix that before going any further.

The nginx plugin reloads nginx itself after a successful renewal, but you should not rely on that alone. If you ever switch to webroot or DNS validation, or if you copy certificates to another service such as a mail server, nothing reloads automatically. A deploy hook solves all of those cases. Certbot runs every executable file in the deploy hook directory after each successful renewal, and only then.

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null <<'EOF'
#!/bin/sh
set -eu
/usr/sbin/nginx -t
/bin/systemctl reload nginx
logger -t certbot-deploy "nginx reloaded after renewing $RENEWED_LINEAGE"
EOF
sudo chmod 750 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

The hook runs a syntax test first. If a bad config slipped in since the last reload, the test fails, the hook exits non-zero, and nginx keeps serving the old good config instead of crashing on reload. Certbot exposes RENEWED_LINEAGE to the hook, which holds the path of the renewed certificate, so you can log or copy files per domain.

Two more hook directories exist. Files in pre run before every renewal attempt, and files in post run after every attempt, whether it succeeded or failed. Use them for the standalone authenticator, where you must stop and restart a web server. For the nginx plugin you rarely need either.

Steps 11 and 12: Dry-Run Renewals and Monitor Expiry

Step 11: Test renewal without waiting 60 days. The dry run contacts the Let’s Encrypt staging server, walks the whole renewal path including your hooks, and throws the result away.

sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Account registered.
Simulating renewal of an existing certificate for example.com and www.example.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/example.com/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Run this after every infrastructure change: a new firewall rule, a CDN, a proxy, a changed redirect. Run it again in the weeks before February 10, 2027. Certbot skips deploy hooks during a dry run, since no certificate actually changes. Test the hook script directly with sudo RENEWED_LINEAGE=/etc/letsencrypt/live/example.com /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh to prove it works.

Step 12: Add monitoring that does not use email. Let’s Encrypt no longer emails expiry reminders, so a silent renewal failure now surfaces only when browsers start showing errors. Build a small local check and run it daily. This script reads the live certificate from the running server, which tests what visitors actually get, not what sits on disk.

sudo tee /usr/local/bin/check-cert.sh > /dev/null <<'EOF'
#!/bin/sh
# usage: check-cert.sh example.com [warn_days]
HOST="$1"
WARN_DAYS="${2:-14}"
SECONDS_LEFT=$((WARN_DAYS * 86400))

if echo | openssl s_client -connect "$HOST:443" -servername "$HOST" 2>/dev/null \
  | openssl x509 -noout -checkend "$SECONDS_LEFT" > /dev/null; then
  echo "OK: $HOST certificate valid for more than $WARN_DAYS days"
else
  echo "ALERT: $HOST certificate expires within $WARN_DAYS days or is unreachable"
  exit 1
fi
EOF
sudo chmod 755 /usr/local/bin/check-cert.sh
/usr/local/bin/check-cert.sh example.com 14

Pick the warning threshold with your lifetime in mind. A 14-day warning on a 90-day certificate leaves plenty of room, while the same threshold on a 45-day certificate leaves less than a third of the lifetime to react. For 45-day certificates, a 10-day threshold works. For six-day certificates, alert on anything under two days and treat every alert as urgent.

Hook the script into whatever you already use for alerts. A cron entry that mails output on failure is the simplest option:

echo '15 7 * * * root /usr/local/bin/check-cert.sh example.com 14 || logger -t cert-monitor "certificate check failed for example.com"' | sudo tee /etc/cron.d/check-cert

Run the check from a different machine than the web server if you can. A monitor that lives on the box it watches dies with the box. Third-party certificate monitors exist, including free community projects, but a self-hosted check has no account to manage and no data leaving your network.

Step 13: Wildcards, DNS-01 and IP Address Certificates

The HTTP-01 challenge you used so far cannot issue wildcard certificates, and it cannot work for servers that are not reachable on port 80. For both cases you need DNS-01, where Certbot proves control by creating a TXT record through your DNS provider’s API. This step uses Cloudflare as the example, and the same pattern applies to other providers that publish a Certbot plugin.

The snap confines plugins, so you must explicitly trust and connect them. Install the plugin, create a credentials file with a narrowly scoped API token, and lock down its permissions.

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

sudo mkdir -p /etc/letsencrypt/secrets
sudo tee /etc/letsencrypt/secrets/cloudflare.ini > /dev/null <<'EOF'
dns_cloudflare_api_token = REPLACE_WITH_A_ZONE_DNS_EDIT_TOKEN
EOF
sudo chmod 600 /etc/letsencrypt/secrets/cloudflare.ini

Create the API token with permission to edit DNS records for the one zone only, not your whole account. Then request the wildcard certificate:

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/secrets/cloudflare.ini \
  -d example.com -d '*.example.com' \
  --email [email protected] --agree-tos --no-eff-email

Because certonly only obtains the certificate and does not edit nginx, you point the ssl_certificate lines at the new lineage yourself and rely on your deploy hook for reloads. That is the reason Step 10 matters. Add --preferred-profile tlsserver if you want the 45-day lifetime.

A newer DNS validation style is also under discussion. The Let’s Encrypt community forum has a thread on Google supporting dns-persist-01 in production, and Certbot’s repository has a dns-persist plugin in active development as of early October 2026. It aims to let you set one persistent DNS record instead of rewriting TXT records at every renewal. Treat it as something to watch, not something to deploy today, until a release ships in the Certbot version you run.

IP address certificates are the other option. Since March 11, 2026, Certbot can obtain Let’s Encrypt certificates for public IP addresses through the --ip-address option, which needs Certbot 5.3, and webroot validation needs 5.4 or later. These certificates use the six-day shortlived profile. The installers for web servers did not yet support IP address certificates in the March guidance, so nginx must be configured by hand and refreshed through a deploy hook. Check the flags for your version before you run it:

certbot certonly --help | grep -i -A2 'ip-address'
sudo certbot certonly --webroot -w /var/www/example.com/html \
  --ip-address 203.0.113.10 --preferred-profile shortlived \
  --email [email protected] --agree-tos --no-eff-email

The address 203.0.113.10 belongs to a documentation range and will not validate, so replace it with your own public address. Use IP certificates only for services with no domain name, such as an appliance or a lab host, and monitor them hard because six days leaves almost no slack. EFF reported in its March announcement that Certbot runs on more than 4 million web servers and maintains HTTPS certificates for more than 31 million websites, which tells you how much operational experience stands behind the default path.

Complete Working Project: One-Script TLS Provisioning

Here is the whole tutorial as a single idempotent script. It installs nginx and Certbot, writes the server block, requests a certificate, installs the deploy hook, and runs a dry run. Save it as provision-tls.sh, set the three variables at the top, and run it as root on a fresh Ubuntu 26.04 LTS server whose DNS already points at the machine.

#!/bin/bash
set -euo pipefail

DOMAIN="example.com"
EMAIL="[email protected]"
STAGING="yes"   # set to "no" once the staging run succeeds

if [ "$(id -u)" -ne 0 ]; then
  echo "Run as root" >&2
  exit 1
fi

echo "[1/7] Packages and firewall"
apt-get update -y
apt-get install -y nginx dnsutils curl
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable

echo "[2/7] DNS sanity check"
IP_DNS="$(dig +short "$DOMAIN" A | tail -n1)"
IP_ME="$(curl -fsS https://ifconfig.me)"
if [ "$IP_DNS" != "$IP_ME" ]; then
  echo "DNS for $DOMAIN points at $IP_DNS but this server is $IP_ME" >&2
  exit 1
fi

echo "[3/7] Site files"
mkdir -p "/var/www/$DOMAIN/html"
echo "<h1>$DOMAIN is alive</h1>" > "/var/www/$DOMAIN/html/index.html"
cat > "/etc/nginx/sites-available/$DOMAIN" <<CONF
server {
    listen 80;
    listen [::]:80;
    server_name $DOMAIN www.$DOMAIN;
    root /var/www/$DOMAIN/html;
    index index.html;
    location / { try_files \$uri \$uri/ =404; }
}
CONF
ln -sf "/etc/nginx/sites-available/$DOMAIN" "/etc/nginx/sites-enabled/$DOMAIN"
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl reload nginx

echo "[4/7] Certbot from snap"
apt-get remove -y certbot python3-certbot-nginx || true
snap install --classic certbot
ln -sf /snap/bin/certbot /usr/bin/certbot

echo "[5/7] Request certificate"
EXTRA=""
if [ "$STAGING" = "yes" ]; then EXTRA="--test-cert"; fi
certbot --nginx $EXTRA \
  -d "$DOMAIN" -d "www.$DOMAIN" \
  --email "$EMAIL" --agree-tos --no-eff-email \
  --redirect --non-interactive

echo "[6/7] Deploy hook"
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'HOOK'
#!/bin/sh
set -eu
/usr/sbin/nginx -t
/bin/systemctl reload nginx
logger -t certbot-deploy "nginx reloaded after renewing $RENEWED_LINEAGE"
HOOK
chmod 750 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

echo "[7/7] Dry run and timer"
certbot renew --dry-run
systemctl list-timers --all | grep -i certbot || echo "WARNING: no certbot timer found"
echo "Done. If STAGING=yes, delete the test cert and rerun with STAGING=no."

When the staging run passes, run certbot delete --cert-name example.com, change STAGING to no, and run the script again. Because every step either overwrites a file or ignores an existing resource, a second run does no harm. The DNS check in step two catches the most frequent beginner mistake before it burns a validation attempt.

Finish by running the monitor from Step 12 against the new site and writing the domain, the certificate name, the profile you chose, and the date of your last dry run into your runbook. When February 10, 2027 arrives, that note tells whoever is on call exactly what to verify.

Common Pitfalls That Break a Certbot Setup

These six mistakes account for most failed certificate requests and most surprise expirations. Each one is easy to avoid once you know it exists.

  • Stale AAAA records. Let’s Encrypt prefers IPv6 when it finds a record. If your AAAA record points at an old host, validation hits the wrong machine and fails, even though IPv4 is perfect. Delete the record or make nginx answer on IPv6.
  • Skipping staging. Debugging against production burns rate limits. Always run with --test-cert first, then delete the test lineage before the real request.
  • Closing port 80 after issuance. Security scanners flag open HTTP ports, so people close them. Renewals then fail silently on the next cycle. Keep port 80 open and redirect it.
  • Running two Certbot installs. A leftover apt Certbot and a new snap Certbot each install a timer and each keep their own account and renewal files. Remove the apt package, and confirm only one timer exists.
  • Assuming 90 days forever. Scripts that renew at day 60 or alert at day 30 break when lifetimes drop. Base thresholds on a fraction of the lifetime, or on fixed days remaining, and test with the 45-day profile.
  • Forgetting the reload. A renewed certificate on disk does nothing if nginx still serves the old one from memory. Use the deploy hook, and check the live certificate dates from outside, as in Step 8.

A seventh trap is worth a line. Rate limits are per registered domain and per exact set of names. Requesting the same set of names repeatedly, for example in a loop that calls --force-renewal on every deploy, hits the duplicate certificate limit fast. Use the dry run for testing and let the timer handle real renewals.

Troubleshooting Certbot Errors

When a request fails, read the last 30 lines of /var/log/letsencrypt/letsencrypt.log first. The error text there is more specific than the summary on screen. This table covers the ten problems readers report most often.

Symptom or messageLikely causeFix
Timeout during connect (likely firewall problem)Port 80 blocked by UFW, a cloud security group, or an ISPOpen TCP 80 at every layer and test from a phone on mobile data
Invalid response, 404 on the challenge pathWrong server_name, wrong root, or a different server block answersMatch server_name to the domain and run nginx -T to find the winning block
DNS problem: NXDOMAIN looking up ARecord missing or not yet propagatedCreate the record and wait for the old TTL to expire
Could not connect to the IPv6 addressStale or unreachable AAAA recordRemove the AAAA record or configure nginx to listen on IPv6
Too many certificates already issued for this exact set of namesDuplicate certificate rate limitWait out the limit and use staging or dry runs for testing
Too many failed authorizations recentlyRepeated failed validationsFix the root cause, then wait before retrying
Could not find a matching server blockThe nginx plugin cannot map the domain to a server_nameAdd the domain to server_name and reload, or use certonly with webroot
The nginx plugin is not workingCertbot cannot read or parse the nginx configRun nginx -t and fix the syntax, then try again
Renewal works by hand but not from the timerMissing PATH, wrong user, or a second Certbot installRun systemctl status snap.certbot.renew.service and remove duplicate installs
Browser shows the old certificate after renewalnginx never reloadedAdd the deploy hook from Step 10 and reload nginx
Plugin missing after a snap refreshSnap plugin not connectedRun snap connect certbot:plugin certbot-dns-cloudflare again

Two diagnostic habits save time. First, run sudo nginx -T to print the full effective config, because the server block that answers a request is often not the one you think. Second, test the challenge path yourself before you blame Let’s Encrypt. Create a file at /var/www/example.com/html/.well-known/acme-challenge/test.txt and fetch it from outside with curl. If you cannot fetch it, Let’s Encrypt cannot either.

Advanced Tips for Certbot and nginx

Once the basics run clean, a few refinements make the setup safer and more portable.

Use a separate certificate per risk boundary. One certificate that lists twenty hostnames means a single renewal failure takes all twenty down. Split unrelated sites into separate lineages so one broken DNS record does not block everything.

Pin the key type. Certbot defaults to ECDSA keys in current releases, which are smaller and faster than RSA. If an old client needs RSA, request it explicitly with --key-type rsa for that one lineage rather than weakening the whole fleet.

Move shorter. After two clean months of timer-driven renewals, switch a low-risk site to the 45-day profile by editing its renewal file or reissuing with --preferred-profile tlsserver. You learn how your monitoring behaves at shorter lifetimes before February 10, 2027 forces the issue on every site.

Back up the right directory. Back up /etc/letsencrypt as a whole, because it holds the account key, renewal files, and hooks. Losing it means re-issuing everything. Losing the private keys means you must revoke and reissue, so encrypt the backup.

Keep a second ACME option ready. Certificate authorities change, as the eMudhra exit showed. Because Certbot uses standard ACME, you can add a second ACME server with --server and keep a break-glass lineage ready. Test it quarterly so you know the fallback works before you need it.

Run Certbot in a container when the host is locked down. The official Certbot image works with the webroot and DNS authenticators, and it suits hosts where snap is unavailable. Mount /etc/letsencrypt as a volume, run the renew command from a scheduled job, and send a reload signal to your nginx container from the deploy hook.

Separate duties. Run nginx as an unprivileged user, keep private keys root-only, and give DNS API tokens the narrowest scope your provider allows. A token that can edit one zone is a small risk. A token that can edit your whole account is a large one.

FAQ

How long does a Certbot setup on Ubuntu take?

A clean run takes about 40 minutes if DNS is already pointing at the server. Most of that time goes to DNS propagation, the staging test, and the dry run. The provisioning script above does the mechanical parts in a few minutes once your variables are set.

Does Certbot cost anything?

No. Certbot is free, open source software, and Let’s Encrypt issues certificates without a price. No paid Certbot or Let’s Encrypt plan turned up in the official sources checked on October 28, 2026. You pay only for the server and the domain name.

What changes on February 10, 2027?

Let’s Encrypt’s default certificate lifetime drops from 90 days to 64 days for certificates issued or renewed on or after that date. Existing valid certificates are not revoked. You can already choose 45-day or 6-day lifetimes through profiles, and Let’s Encrypt tested the 64-day default on its staging environment starting October 14, 2026. If your renewal runs on the systemd timer and your monitoring uses days remaining, you need no changes.

Should I install Certbot with snap or apt?

Use snap. It tracks upstream releases, ships its own Python, and gets new ACME features, including profile support, without waiting for a distro backport. The apt package stays frozen at whichever version Ubuntu shipped. Never keep both installed, because two timers and two sets of renewal files cause confusing failures.

Why does Certbot fail with a timeout or a connection error?

Almost always the cause is the network path, not Certbot. Check that port 80 is open in UFW and in any cloud firewall, that your A record points at the right server, and that any AAAA record is valid. Let’s Encrypt began testing validation from six remote perspectives on staging on October 27, 2026, so a firewall that only allows some regions will fail more often.

Can I get a wildcard certificate with the nginx plugin?

No. Wildcards require the DNS-01 challenge, which needs a DNS plugin or a manual TXT record workflow. Use certbot certonly with a DNS plugin for your provider, point nginx at the new certificate files, and rely on a deploy hook to reload nginx after each renewal.

Can Certbot issue a certificate for an IP address?

Yes, since March 11, 2026. Certbot 5.3 added the --ip-address option, and webroot validation needs Certbot 5.4 or later. These certificates use the six-day shortlived profile, and the web server installers did not yet configure nginx for them, so you point nginx at the files yourself and use a deploy hook.

How do I know my renewal will actually work?

Run sudo certbot renew --dry-run after every infrastructure change, confirm a timer exists with systemctl list-timers, and run the daily expiry check from Step 12. Let’s Encrypt no longer sends expiry reminder emails, so a local check is your safety net.

Elias Virtanen

Elias Virtanen

Cybersecurity Analyst

Elias Virtanen is the Cybersecurity Analyst at Tech Insider, bringing hands-on expertise from his background in penetration testing and security consulting. He previously worked as a security researcher at F-Secure in Helsinki, where he focused on threat intelligence and vulnerability disclosure. Elias covers ransomware trends, zero-trust architecture, and the evolving regulatory landscape including NIS2 and the EU Cyber Resilience Act. He holds a CISSP certification and an MSc in Information Security from Aalto University.

View all articles