how to install certbot for nginx on ubuntu 24.04
Use sudo apt install certbot python3-certbot-nginx for Ubuntu 24.04. Learn why snap vs apt matters for your Let's Encrypt cert and how to avoid renewal wahala.
Install Certbot: apt or snap
For Ubuntu 24.04, sudo apt install certbot python3-certbot-nginx go give you Certbot wey dey work well, and e go issue real Let's Encrypt certificates wey everybody trust. Certbot original docs dey tell you make you use snap; the difference small — snap dey follow upstream releases, but archive package dey follow wetin come with LTS and e dey get security fixes.
Pick one. If you install Certbot twice, you go get two renewal timers wey dey point to the same /etc/letsencrypt tree, and the one wey you forget go cause wahala.
The apt path:
sudo apt update
sudo apt install certbot python3-certbot-nginxE go install /usr/bin/certbot, the nginx plugin, a certbot.service + certbot.timer pair, and an /etc/cron.d/certbot entry wey no dey do anything under systemd.
The snap path:
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/certbotSnap dey carry its own timer, snap.certbot.renew.timer. Make sure say you remove the apt package before you install the snap.
After everything don finish, both one dey behave the same way. Certbot 2.x dey use ECDSA (P-256) keys by default — use --key-type rsa only if your client no fit do ECDSA. All the data dey inside /etc/letsencrypt: archive/ dey hold the real key and certificate files, live/ dey hold symlinks to the current ones, renewal/ dey hold one config file for every certificate, and accounts/ na your ACME account key.
Wetin HTTP-01 dey do, and why port 80 no be optional
HTTP-01 challenge na callback. You ask Let's Encrypt for certificate wey go cover example.com; e go check the name for public DNS, open connection to port 80 for the address wey e find, and request http://example.com/.well-known/acme-challenge/<token>. Your server must answer with the exact token content wey Certbot just write for disk. Na so the whole mechanism take work. Three things fit happen because of dis, and na dem dey cause most failed issuances.
- Port 80 must dey reachable from public internet, no be just from your laptop. If you get
ufwrule, cloud-provider security group, or VPS-console firewall wey only open 443, e go break issuance and every renewal wey follow am. - DNS must already point at dis box. The validation server go do im own lookup from outside; your
/etc/hostsentries and browser cache no mean anything to am. - If you publish AAAA record, e go try IPv6 first. Let's Encrypt go try IPv4 again if IPv6 connection fail completely — but if stale AAAA point to host wey accept connection but dey serve something else, e go give you hard failure.
Redirects dey work: validation go follow HTTP redirect go HTTPS, and e no care if the certificate for the other end missing, expired, or self-signed. Wetin e no go do na to start anywhere other than port 80. Certbot no get TLS-ALPN-01 implementation, so "just use 443" no be way out.
Choosing an authenticator: --nginx, --webroot, --standalone
--nginx na the correct default if nginx dey run already and e dey serve the domain. Certbot go read your config, put one 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.comScripted, for fresh box:
sudo certbot --nginx \
-d example.com -d www.example.com \
--agree-tos -m ops@example.com --no-eff-email \
--redirect --non-interactive--webroot na the right one if you no want Certbot touch your nginx config — for example if you dey use template, keep am for git, or push am with Ansible. Certbot go write only the challenge file inside one directory wey you don already serve.
sudo certbot certonly --webroot -w /var/www/example.com \
-d example.com -d www.example.com \
--deploy-hook "systemctl reload nginx"--standalone na the right one if nothing dey listen for port 80: like mail server, API wey only dey use 443, or first-boot script wey dey run before nginx start. Certbot go bind port 80 itself for few seconds. If nginx dey run, e go fail — you must stop nginx before you run am:
sudo certbot certonly --standalone -d mail.example.com \
--pre-hook "systemctl stop nginx" \
--post-hook "systemctl start nginx"Dem record those hooks inside the certificate renewal config, so the same stop/start go happen automatically when e dey renew.
Server block wey go work before and after certificate dey exist
Chicken-and-egg problem: nginx no go gree start if ssl_certificate dey point to file wey no dey, and Certbot no fit validate if nginx don shut down. First, make you bring site up for port 80.
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 when you check am from outside the box, then issue. 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 ACME location na wetin dey help: e dey stop return 301 block from swallow the challenge request. If you keep that location for port 80, renewals go still dey work even when the rest of the site don move to HTTPS-only.
HTTP/2 syntax depend on your nginx version, and if you mix the two forms, nginx go give startup error. Ubuntu 24.04 dey come with nginx 1.24, wey want am inline — listen 443 ssl http2;. Debian 13 dey come with newer nginx, wey want separate http2 on; directive. Check nginx -v first.
Point nginx to live/, no ever point am to archive/. The live/ symlinks dey change every time you renew; if you use hard path for archive/, you go stuck with certificate wey go expire for you.
Wildcards na DNS-01, and DNS-01 means plugin
Wildcard certificate (*.example.com) no fit pass HTTP-01 validation — no single hostname dey for you to fetch file from. DNS-01 na only way: you go prove say you control the domain by publish one _acme-challenge.example.com TXT record. Certbot go need API credentials for your DNS provider if you want make am run automatically, na why dem dey use provider plugins. The full wildcard certificate walkthrough go explain how TXT record work and the renewal wahala for manual mode; the short Cloudflare version dey follow am.
sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflareFor the apt path, na sudo apt install python3-certbot-dns-cloudflare instead. You must put credentials inside one file wey only root fit open:
# /root/.secrets/cloudflare.ini
# then: sudo chmod 600 /root/.secrets/cloudflare.ini
dns_cloudflare_api_token = your_scoped_token_hereGive the token only DNS-edit rights for that one zone. Na key for your DNS; treat am like one.
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
-d example.com -d '*.example.com'Put quote for the wildcard so your shell no go mess am up. DNS-01 also solve wetin HTTP-01 no fit do: certificates for hosts wey no get public port 80 — like internal service, one box wey you fit reach only through self-hosted WireGuard VPN on a VPS, or admin panel for private interface.
Renewal: 90 days, timer, and deploy hook
Let's Encrypt certificates dey valid for 90 days. Certbot dey do renewal when less than 30 days remain. Dis means you get 30-day window to fix any problem before the site go down. Let's Encrypt no dey send expiry-warning emails again — nobody go come tell you say your cert don expire, so you must monitor am yourself.
Check the timer wey your install carry:
systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificatescertbot renew dey check every config for /etc/letsencrypt/renewal/. E go skip anything wey still get more than 30 days, and e go renew the rest using the same flags wey you use for the first run. Na why the first run dey important: na dat one dem dey record.
To renew the file for disk alone no dey change anything — nginx go still dey serve the old certificate from memory until something tell am say e must reload. Set up one deploy hook once:
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.shAny executable wey dey renewal-hooks/deploy/ go run after any successful renewal. The --deploy-hook flag dey do the same work for one certificate, e dey store renew_hook = ... inside its renewal config. certbot --nginx dey reload for you; --webroot and --standalone setups no dey do am. If you no set hook, na why site go dey serve expired certificate even when certbot certificates dey report say the new one dey ready. Any other thing wey dey read the certificate when e dey start, e go need the same hook — containerised app like Nextcloud VPS install with Docker, TLS and backups go need its own restart or reload step inside here too.
Test how renewal dey work for real
sudo certbot renew --dry-runDis one go run full challenge against Let's Encrypt staging environment: same code path, same firewall, same DNS, no rate-limit cost, and nothing go write for disk. If e pass today, di unattended renewal for 60 days go pass too, provided say nothing change for di box underneath am.
Dry run no fit prove say your reload hook dey fire — di way dis one dey work dey change based on Certbot version. Test dat part manually: run di hook script directly, make sure say systemctl reload nginx succeed, and check sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.
The errors wey you go definitely see
Could not bind to IPv4 or IPv6. — --standalone because nginx don already hold port 80. Use --nginx or --webroot, or stop nginx before you run am. Use sudo ss -lntp | grep ':80' take check wetin hold am.
Timeout during connect (likely firewall problem) — Let's Encrypt no fit reach port 80. Check everything: sudo ufw status (open am with sudo ufw allow 'Nginx Full'), then the VPS provider firewall, then DNS. Test am from somewhere wey no be your server: curl -sSv http://example.com/.well-known/acme-challenge/test. If you get stale AAAA record, you go see dis same message.
unauthorized :: Invalid response from http://example.com/.well-known/acme-challenge/xyz: 404 — port 80 dey open, but the token no dey come out. The request enter different server block (check which one carry default_server), or the directory wey you give -w no be the one nginx dey serve. Drop one file for /var/www/example.com/.well-known/acme-challenge/test and try fetch am from outside; if e give 404, den certificate no be the problem.
DNS problem: NXDOMAIN looking up A for example.com — the name no dey resolve for public. New records wey never propagate, or record for one zone wey your registrar no dey serve.
too many certificates already issued for: example.com — na rate limit, and na dis one people dey hit when dem dey debug for loop. Let's Encrypt dey cap duplicate certificates — the same exact set of names — at five per week, and dem dey allow 50 new certificates per registered domain per week; nothing fit unblock dem except time. Debug for staging with --dry-run.
nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": No such file or directory — nginx configuration dey point to certificate wey dem never issue, or one wey you don remove with certbot delete. Comment out the TLS server block, start nginx, issue certificate, then restore the block.
open() "/etc/letsencrypt/options-ssl-nginx.conf" failed — dat file dey come with nginx plugin package. For certonly box wey no get python3-certbot-nginx, either add the plugin or change the include line with your own ssl_protocols and ssl_ciphers settings.
Owning this at scale
One certificate fit carry up to 100 names, and one single certbot --nginx -d a.example.com -d b.example.com ... dey look fine — until one stale DNS record fail validation and pull every other name for that certificate down with am. If you use separate certificates for every site, dem go fail independently, and na wetin you want when you dey host many things for one box. Once you pass small number of sites, ACME-aware front door go pay for itself: Traefik reverse proxy wey dey run multiple apps under Docker Compose go request and renew the certificates by itself, and Certbot no go need dey anywhere again.
Backup /etc/letsencrypt whole — sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt — make symlinks still dey intact. That tree hold accounts/, your ACME account key, wey you no fit regenerate exactly same way. If you move to new VPS, all you go do na: rsync the tree with -a, install Certbot, change DNS point, and run certbot renew --dry-run before you switch over.
If you rebuild the box or move to new LTS, the renewal timer no go follow you. After any migration, snapshot restore, or distro upgrade, run systemctl list-timers 'certbot*' and one --dry-run. If you skip that one, na how site dey go dark after 89 days, for 3am, for certificate wey everybody think say e dey renew itself.
All these things assume say you control the machine, wey get public IP and port 80 open to everybody — in other words, a VPS. The mechanics wey I talk above na the same for any of dem.
The same certificate steps apply for Apache instead of nginx, and when public certificate no be option, self-signed certificate for Ubuntu go work for internal services.
FAQ
I need open port 80 if my site only dey use HTTPS?
Yes, for the HTTP-01 challenge. Let's Encrypt always start dem validation request for port 80. Certbot no get TLS-ALPN-01 implementation, so if firewall only open 443, e go block first issuance and every unattended renewal wey follow am. Redirect from port 80 go HTTPS dey fine — validation go follow am. The only way to skip port 80 entirely na to use DNS-01 with provider plugin.
apt or snap — which Certbot I suppose install for nginx for Ubuntu 24.04?
Use apt. sudo apt install certbot python3-certbot-nginx go give you Certbot 2.9.0 for Ubuntu 24.04, wey current enough for everything for this guide, e dey get security patches through unattended-upgrades, and e no need snapd. Pick snap only if you need the newest release immediately or if you need DNS plugin wey dem only distribute as snap. Either way, pick only one: two installs mean two renewal timers wey point to the same /etc/letsencrypt tree, and the one wey you forget na the one go cause wahala.
Certbot fit issue wildcard certificate for nginx?
Only through DNS-01. Wildcard like *.example.com no get single hostname wey e fit use fetch challenge file, so --nginx, --webroot and --standalone no go work. Install the plugin for your DNS provider, put scoped API token inside root-only credentials file, and run certbot certonly --dns-cloudflare -d example.com -d '*.example.com', use quotes for the wildcard make shell globbing no spoil am.
Why nginx still dey serve old certificate even after renewal don work?
nginx dey keep certificate for memory and e no go notice new file for disk until e reload. certbot --nginx go reload am for you, but --webroot and --standalone runs no dey do am, so renewal fit work well while browser still dey see certificate wey wan expire. Drop executable script inside /etc/letsencrypt/renewal-hooks/deploy/ wey go run nginx -t && systemctl reload nginx, and e go fire after every successful renewal.
certbot renew --dry-run prove say renewal go work?
Mostly. E dey run real challenge against staging environment — same firewall, same DNS, same code path — without rate-limit cost and nothing dey write to disk, so if e pass, e mean say network side dey okay. But e no fit reliably prove say your deploy hook dey fire. Test that one separately: run the hook script manually and check sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.