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 no wide: snap dey follow upstream releases, while archive package dey follow wetin ship with the LTS and dey get security fixes.
Choose one. If you install two copies of Certbot, dem go create two renewal timers wey dey 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-nginxThis one installs /usr/bin/certbot, the nginx plugin, a certbot.service + certbot.timer pair, and a /etc/cron.d/certbot entry wey no 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/certbotThe snap get its own timer, snap.certbot.renew.timer. Remove the apt package before you install the snap.
After installation, both methods dey behave the same, except for the exact time wey dem decide to renew certificates. See Renewal below. Certbot 2.x dey use ECDSA (P-256) keys by default. Use --key-type rsa only for client wey no fit handle ECDSA. All state dey under /etc/letsencrypt: archive/ get the actual key and certificate files, live/ points with symlinks to the current files, renewal/ get one config file for each certificate, and accounts/ na your ACME account key.
Wetín 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 consequences dey come from am, and dem explain most failed certificate issuances.
- Port 80 must dey reachable from public internet, no be only from your laptop. A
ufwrule, cloud-provider security group, or VPS-console firewall wey open only 443 go break issuance and every renewal wey follow. - DNS must don already point to this box. Validation server do its own lookup from outside; your
/etc/hostsentries and browser cache no mean anything to am. - If you publish an AAAA record, IPv6 go try first. Let's Encrypt go retry over IPv4 only when the IPv6 connection timeout. If IPv6 connection refuse, e no go retry, and stale AAAA wey point to host wey accept the connection and serve another thing go give you hard failure.
Redirects dey allowed: validation follow HTTP redirect go HTTPS, and e no matter 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 way out.
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.comFor 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 be this: nginx no go start if ssl_certificate dey 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. Afterwards:
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 longer ACME prefix don already beat location /, so return 301 no go ever see challenge request. The ^~ modifier dey useful for another place: once that prefix match, nginx go skip regex locations completely. So rule wey dem add later, like location ~ /\. { deny all; }, no fit block the challenge. If you keep that location for port 80, renewals go still work after the rest of the site become HTTPS-only.
Both blocks above dey serve files from disk. If nginx dey instead front an application, location / go become a proxy_pass block. the reverse proxy server block, line by line explain the headers wey that app need, while the ACME location and the TLS directives remain exactly as dem be.
HTTP/2 syntax depend on your nginx version. Ubuntu 24.04 dey ship nginx 1.24, wey want am inline as listen 443 ssl http2;, and e go fail to start if you give am the separate http2 on; directive. Debian 13 dey ship nginx 1.26, wey want http2 on; and only warn say the inline form don deprecate. Check nginx -v first.
Point nginx to live/, never point am to archive/. The live/ symlinks dey get repointed every renewal. Hard path wey enter archive/ go pin you to certificate wey go expire while you still dey use am.
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-cloudflareFor 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_hereLimit 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, deploy hook
Let's Encrypt certificates valid for 90 days. The apt package (Certbot 2.9.0) dey renew dem when less than 30 days remain. The snap dey follow current Certbot. Since version 4.1, e also dey ask Let's Encrypt for suggested renewal time through ACME Renewal Info (ARI), and e fit renew earlier. For Let's Encrypt certificate, this normally mean around two-thirds of the certificate lifetime. Anyhow, you go get roughly 30-day window where broken renewal still na problem wey you fit fix, instead of outage. From next year, renewals go dey happen more often. Let's Encrypt default profile go move to 64-day certificates on February 10, 2027, and to 45-day certificates on February 16, 2028. Let's Encrypt no dey send expiry-warning emails again. Nobody go tap you to remind you. Monitoring don become your responsibility now.
Check the timer wey your install ship with:
systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificatescertbot renew dey check every config inside /etc/letsencrypt/renewal/. E skip anything wey never due, then renew the rest with the options wey that config save. Those ones na the flags from the original run, unless you don change dem later with certbot reconfigure. Na why the first run matter: na that one system 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 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.shAnything executable inside renewal-hooks/deploy/ go run after any successful renewal. The --deploy-hook flag do the same work for one certificate, and e save renew_hook = ... inside the renewal config. certbot --nginx dey reload am for you. --webroot and --standalone setups no dey do this. Missing hook na exactly why site fit still serve expired certificate while certbot certificates dey report fresh one without problem. Anything else wey dey read certificate when e start need the same hook. Containerised app like Nextcloud VPS install with Docker, TLS and backups need its own restart or reload step wired here too.
Renewal test wey real
sudo certbot renew --dry-runDis one run the full challenge against Let's Encrypt's staging environment: e dey use the same code path, the same firewall, and the same DNS. Nothing go count against your production rate limits; staging get its own limits, and dem dey far more generous. E no save any certificate for disk. If e pass today, unattended renewal for 60 days go pass too, as long as nothing change underneath the box.
Dry run no prove say your reload hook go run, because behaviour fit vary according to Certbot version. Test dat part by hand: execute the hook script directly, confirm say systemctl reload nginx succeed, then check sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.
Wetin errors you go actually meet
Could not bind to IPv4 or IPv6., --standalone while nginx still dey hold port 80. Use --nginx or --webroot, or stop nginx for the run. Confirm wetin dey hold the port 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 check 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. An old AAAA record fit cause 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 served. 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 one 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 a record for zone wey your registrar no dey serve.
too many certificates (5) already issued ..., na rate limit, and na this one people dey hit when dem dey debug in a loop. The message name the limit and e end with a retry after time. Let's Encrypt limit duplicate certificates, meaning the same exact set of names, to five per week, and e no get override for that one, so na only time fit unblock am. Separately, e allow 50 new certificates per registered domain per week; you fit raise that limit by requesting an override, a request for the exact same set of names as an existing certificate counts as renewal and no dey enter that limit, and renewals coordinated through ARI no dey enter any 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 get configuration 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.
Owning this at scale
One certificate fit carry up to 100 names. One single certbot --nginx -d a.example.com -d b.example.com ... fit look tempting, until one old DNS record fail validation and bring down every other name for that certificate. Separate certificates for each site dey fail independently. Na this one you want for server wey dey host more than two things. Once sites don pass small number, ACME-aware front door go become useful: a Traefik reverse proxy wey dey run multiple apps under Docker Compose go request and renew the certificates by itself, and Certbot no go dey needed again. Which proxy you choose for that front door na separate decision. 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, with sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt and symlinks intact. That directory tree contain accounts/, your ACME account key. You no fit regenerate the same key exactly. If you move go new VPS, the process become: use -a to sync the tree, 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 go new LTS release, 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 this, the site fit go offline 89 days later, for 3am, because certificate wey everybody assume say e dey renew by itself no renew.
The HTTP-01 routes above assume say na machine wey you control, with public IP and port 80 open to everybody. In other words, na VPS. DNS-01 no need either of these. The steps above work the same way for any of dem.
The same certificate steps apply for Apache instead of nginx. If public certificate no dey possible, self-signed certificate for Ubuntu fit cover internal services.
FAQ
I need to leave port 80 open if my site dey serve only HTTPS?
Yes, because of the HTTP-01 challenge. Let's Encrypt always dey start validation request for port 80, and 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 avoid port 80 completely na DNS-01, either with provider plugin or with Certbot --manual --preferred-challenges dns. For this one, you go add the TXT record yourself, and renewal no go remain automatic unless you provide --manual-auth-hook script.
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. E current enough for everything for this guide, 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 wey dey point to the same /etc/letsencrypt tree, and na the one wey you forget go cause problem.
Certbot fit issue wildcard certificate for nginx?
Only through DNS-01. Wildcard like *.example.com no get one hostname wey challenge file fit come from, so --nginx, --webroot and --standalone no work. 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 keeps certificate for memory and e no notice the new file for disk until e reload. certbot --nginx reloads am for you, but --webroot and --standalone runs no reload am. 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, and e go run after every successful renewal.
certbot renew --dry-run dey 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 touch your production rate limits and e no save any certificate to disk, so if e pass, the network part dey okay. But e no reliably prove say your deploy hook go run. Test that separately: run the hook script by hand and check sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.