SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-29

Install Certbot for nginx on Ubuntu 24.04

Run sudo apt install certbot python3-certbot-nginx, then certbot --nginx for Let's Encrypt. Know apt versus snap, plus why port 80 renewal fit timeout.

Install Certbot: apt or snap

For Ubuntu 24.04, sudo apt install certbot python3-certbot-nginx dey give you working Certbot wey fit issue real Let's Encrypt certificates wey public systems trust. Certbot upstream docs dey direct you to use snap instead. The difference small: snap dey follow upstream releases, while archive package dey follow wetin ship with the LTS and dey receive security fixes.

Choose one. If you install two copies of Certbot, both go get renewal timers wey target the same /etc/letsencrypt tree. The copy wey you forget about na the one wey go surprise you.

The apt method:

sudo apt update
sudo apt install certbot python3-certbot-nginx

This one installs /usr/bin/certbot, the nginx plugin, one certbot.service + certbot.timer pair, and one /etc/cron.d/certbot entry wey no dey do anything under systemd.

The snap method:

sudo apt remove certbot python3-certbot-nginx
sudo snap install core && sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

The snap ship with its own timer, snap.certbot.renew.timer. Remove the apt package before you install the snap.

After installation, both methods dey behave the same way. Certbot 2.x dey use ECDSA (P-256) keys by default. Use --key-type rsa only for client wey no fit use ECDSA. All state dey under /etc/letsencrypt: archive/ dey hold the real key and certificate files, live/ dey point with symlinks to the current files, renewal/ get one config file for each certificate, and accounts/ hold your ACME account key.

Wetin HTTP-01 really dey do, and why port 80 no be optional

HTTP-01 challenge na callback. You ask Let's Encrypt for certificate wey cover example.com; e resolve the name for public DNS, open connection to port 80 for the address wey e find, then request http://example.com/.well-known/acme-challenge/<token>. Your server answer with the exact token content wey Certbot just write to disk. Na the whole mechanism be that. Three things follow from am, and dem explain most failed certificate issuance.

  • Port 80 must dey reachable from public internet, no be only from your laptop. A ufw rule, cloud-provider security group, or VPS-console firewall wey open only 443 go break issuance and every renewal after am.
  • DNS must don point to this box already. Validation server do its own lookup from outside; your /etc/hosts entries and browser cache no mean anything to am.
  • If you publish an AAAA record, IPv6 go dey tried first. Let's Encrypt go retry over IPv4 when IPv6 connection fail completely, but stale AAAA wey point to host wey accepts the connection and serve another thing go give you hard failure.

Redirects dey allowed: validation follow HTTP redirect to HTTPS, and e no care whether certificate for the other end dey missing, expired, or self-signed. But e no go start from anywhere apart from port 80. Certbot no get TLS-ALPN-01 implementation, so “just use 443” no be escape route.

How to choose authenticator: --nginx, --webroot, --standalone

--nginx na the correct default when nginx dey run and e already dey serve the domain. Certbot go parse your config, put temporary challenge location, reload nginx, validate, then write the TLS directives inside your server block. No downtime.

sudo certbot --nginx -d example.com -d www.example.com

For a fresh box wey script go configure:

sudo certbot --nginx \
  -d example.com -d www.example.com \
  --agree-tos -m ops@example.com --no-eff-email \
  --redirect --non-interactive

--webroot dey correct when you no want Certbot come near your nginx config. This one fit be config wey you generate from template, keep for git, or deploy with Ansible. Certbot go write only the challenge file inside directory wey you already dey serve.

sudo certbot certonly --webroot -w /var/www/example.com \
  -d example.com -d www.example.com \
  --deploy-hook "systemctl reload nginx"

--standalone dey correct when nothing dey listen on port 80: like mail server, API wey only dey speak 443, or first-boot script wey dey run before nginx exist. Certbot go bind port 80 by itself for some seconds. If nginx dey run, this command go fail, so stop nginx around the run:

sudo certbot certonly --standalone -d mail.example.com \
  --pre-hook "systemctl stop nginx" \
  --post-hook "systemctl start nginx"

Those hooks dey recorded inside the certificate renewal config, so the same stop/start go happen automatically during renewal.

Server block wey go work before and after certificate dey exist

Na chicken-and-egg problem: nginx no gree start when ssl_certificate point to file wey no exist, and Certbot no fit validate while nginx dey down. Bring the site up for port 80 first.

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

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

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
        default_type "text/plain";
        try_files $uri =404;
    }

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

Run sudo nginx -t && sudo systemctl reload nginx, confirm say curl -I http://example.com/ dey answer from outside the box, then issue certificate. After that:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
        default_type "text/plain";
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    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;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

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

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

The ^~ prefix for the ACME location dey useful because e stop return 301 block from swallowing the challenge request. If you keep that location for port 80, renewals go continue work after the rest of the site become HTTPS-only.

Both blocks above dey serve files from disk; if nginx dey front an application instead, location / go become proxy_pass block. The reverse proxy server block, line by line explain the headers wey that app need, while the ACME location and TLS directives remain exactly as dem be.

HTTP/2 syntax depend on your nginx version, and if you mix the two forms, nginx go show startup error. Ubuntu 24.04 ships nginx 1.24, wey want am inline, listen 443 ssl http2;. Debian 13 ships newer nginx, wey want separate http2 on; directive. Check nginx -v first.

Point nginx to live/, never to archive/. The live/ symlinks dey get repointed for every renewal; hard path inside archive/ go pin you to certificate wey go expire without you knowing.

Wildcards mean DNS-01, and DNS-01 mean plugin

Wildcard certificate (*.example.com) no fit pass validation through HTTP-01, because no single hostname dey wey you fit fetch file from. DNS-01 na the only way: you go prove say you control the domain by publishing _acme-challenge.example.com TXT record. Certbot need API credentials for your DNS provider to do am without person intervention. Na why provider plugins dey exist. The full wildcard certificate walkthrough explain how TXT record work and the renewal problem for manual mode; the short Cloudflare version dey below.

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

For apt path, na sudo apt install python3-certbot-dns-cloudflare instead. Put credentials for file wey only root fit read:

# /root/.secrets/cloudflare.ini
# then: sudo chmod 600 /root/.secrets/cloudflare.ini
dns_cloudflare_api_token = your_scoped_token_here

Limit the token to DNS-edit rights for that one zone. Na key for your DNS, so treat am like one.

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

Put quote around wildcard so your shell no glob am. DNS-01 still solve wetin HTTP-01 no fit solve: certificates for hosts wey no get public port 80, internal service, machine wey you fit reach only through self-hosted WireGuard VPN on a VPS, or admin panel wey dey private interface.

Renewal: 90 days, timer, and deploy hook

Let's Encrypt certificates dey valid for 90 days. Certbot dey renew dem when fewer than 30 days remain. This give you 30 days to fix failed renewal before e become outage. Let's Encrypt no dey send expiry-warning emails again. Nobody go remind you, so na you go monitor am now.

Check the timer wey your install ship with:

systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificates

certbot renew dey check every config for /etc/letsencrypt/renewal/. E skip any one wey get more than 30 days before expiry, then renew the rest with exactly the flags wey the original run use. Na why the first run important: na that one dem record.

Renewing the file for disk no change anything by itself. nginx go continue serve the old certificate from memory until something tell am to reload. Set deploy hook one time:

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
set -e
nginx -t && systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Anything executable for renewal-hooks/deploy/ go run after any renewal wey succeed. The --deploy-hook flag do the same work for one certificate, and e store renew_hook = ... for its renewal config. certbot --nginx dey reload am for you; --webroot and --standalone setups no dey do that. If hook no dey, na exactly why site fit serve expired certificate while certbot certificates dey report fresh one without wahala. Anything else wey dey read certificate when e start need the same hook. A containerised app like a Nextcloud VPS install with Docker, TLS and backups need its own restart or reload step, and you need wire am here too.

Real renewal test

sudo certbot renew --dry-run

Dis one dey run the complete challenge against Let's Encrypt staging environment: na the same code path, same firewall, same DNS, and e no use rate limit. E no write anything to disk. If e pass today, unattended renewal for 60 days go pass too, as long as nothing change for the server.

Dry run no prove say your reload hook go run. The behaviour fit vary according to Certbot version. Test dat part by hand: run the hook script directly, confirm say systemctl reload nginx succeed, and check sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.

Errors wey you go actually encounter

Could not bind to IPv4 or IPv6., --standalone while nginx already dey hold port 80. Use --nginx or --webroot, or stop nginx for the run. Confirm which process dey hold am with sudo ss -lntp | grep ':80'.

Timeout during connect (likely firewall problem), Let's Encrypt no fit reach port 80. Check from outside go inside: sudo ufw status (open am with sudo ufw allow 'Nginx Full'), then the VPS provider own firewall, then DNS. Test from somewhere wey no be your server: curl -sSv http://example.com/.well-known/acme-challenge/test. Stale AAAA record fit produce this same message.

unauthorized :: Invalid response from http://example.com/.well-known/acme-challenge/xyz: 404, port 80 dey reachable, but the token no dey serve. The request enter another server block (check which one own default_server), or the directory wey you pass to -w no be the one nginx dey serve. Put file for /var/www/example.com/.well-known/acme-challenge/test and fetch am from outside; if e return 404, certificate no be the problem.

DNS problem: NXDOMAIN looking up A for example.com, the name no resolve publicly. E fit be new records wey never propagate, or record for zone wey your registrar no dey serve.

too many certificates already issued for: example.com, na rate limit, and na the one people dey hit when dem dey debug inside loop. Let's Encrypt dey limit duplicate certificates, meaning the same exact set of names, to five per week. E also allow 50 new certificates per registered domain per week separately; na only time fit remove either limit. Debug against staging with --dry-run.

nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": No such file or directory, nginx dey configured for certificate wey dem never issue, or certificate wey dem remove with certbot delete. Comment out the TLS server block, start nginx, issue the certificate, then restore the block.

open() "/etc/letsencrypt/options-ssl-nginx.conf" failed, that file dey come with the nginx plugin package. For a certonly box wey no get python3-certbot-nginx, either add the plugin or replace the include line with your own ssl_protocols and ssl_ciphers settings.

Managing am for scale

One certificate fit carry up to 100 names, and one certbot --nginx -d a.example.com -d b.example.com ... fit look tempting, until one old DNS record fail validation and make every other name for that certificate fail too. Separate certificates for each site dey fail independently, and na wetin you want for one server wey dey host more than small number of things. Once sites don pass a few, ACME-aware front door go become useful: one Traefik reverse proxy wey dey run multiple apps under Docker Compose go request and renew the certificates by itself, so Certbot no dey needed again. Which proxy you go use for that front door na separate decision, and comparing Nginx with Caddy and Traefik mostly depend on how much certificate work and per-app configuration you want the proxy to handle for you.

Back up /etc/letsencrypt complete, sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt, and keep symlinks intact. That tree hold accounts/, your ACME account key, and you no fit regenerate the same key again. If you dey move to new VPS, the steps na: use rsync to copy the tree with -a, install Certbot, point DNS to the new server, then run certbot renew --dry-run before you switch traffic.

If you rebuild the server or move to new LTS, the renewal timer no dey follow you. After any migration, snapshot restore, or distro upgrade, run systemctl list-timers 'certbot*' and one --dry-run. If you skip this, the site fit go offline 89 days later, for 3am, because everybody assume say the certificate dey renew by itself.

All this assume say na machine wey you control, with public IP and port 80 open to everybody for internet — VPS, in other words. The steps above dey work the same way for any of dem.

The same certificate steps apply for Apache instead of nginx, and if public certificate no dey possible, self-signed certificate for Ubuntu fit cover internal services.

FAQ

Port 80 dey need remain open if na HTTPS only my site dey serve?

Yes, because of the HTTP-01 challenge. Let's Encrypt always dey start validation request for port 80. Certbot no get TLS-ALPN-01 implementation, so firewall wey open only 443 go block both the first certificate issuance and every unattended renewal after that. Redirect from port 80 go HTTPS dey okay; validation go follow am. The only way to skip port 80 completely na to use DNS-01 with provider plugin.

apt or snap, which Certbot I suppose install for nginx on Ubuntu 24.04?

Use apt. sudo apt install certbot python3-certbot-nginx dey give you Certbot 2.9.0 for Ubuntu 24.04. This version current enough for everything wey this guide cover, e dey receive security patches through unattended-upgrades, and e no require snapd. Choose snap only if you need the newest release immediately or DNS plugin wey dem distribute exclusively as snap. Either way, choose exactly one: two installations mean two renewal timers dey point to the same /etc/letsencrypt tree, and na the one you forget go cause problem.

Certbot fit issue wildcard certificate for nginx?

Only through DNS-01. Wildcard like *.example.com no get one specific hostname wey challenge file fit come from, so --nginx, --webroot and --standalone no dey possible. Install the plugin for your DNS provider, put scoped API token inside credentials file wey only root fit read, then run certbot certonly --dns-cloudflare -d example.com -d '*.example.com'. Put quote around the wildcard so shell no glob am.

Why nginx still dey serve the old certificate after renewal succeed?

nginx dey keep certificate for memory. E no notice the new file for disk until e reload. certbot --nginx dey reload am automatically, but --webroot and --standalone runs no dey do that. So renewal fit succeed while browser still dey see certificate wey dey expire. Put executable script inside /etc/letsencrypt/renewal-hooks/deploy/ wey runs nginx -t && systemctl reload nginx. The script go run after every successful renewal.

certbot renew --dry-run prove say renewal go work?

Mostly. E runs the real challenge against staging environment, with the same firewall, same DNS, and same code path. E no use rate-limit quota and e no write anything to disk, so if e pass, the network side dey okay. But e no reliably prove say your deploy hook go run. Test that one separately: run the hook script by hand and check sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.