DNS & TLS·8 min read·

Let's Encrypt with Certbot and Nginx on Ubuntu

Issue a Let's Encrypt certificate with certbot and nginx on Ubuntu, complete HTTP-01, and wire the live cert into a working TLS server block.

NB

Netbay Engineering

Netbay Engineering

On this page

Certbot plus nginx is still the most boring way to get a trusted certificate on a single Ubuntu VPS. Boring is the goal. You install the packages, serve port 80 for the HTTP-01 challenge, let certbot write the leaf and chain, and point nginx at live/fullchain.pem and live/privkey.pem. This post is that path with the failure points that actually happen: A record not pointing here, port 80 filtered, a default server catching the challenge, and a TLS block that still serves the snakeoil cert.

Assume Ubuntu 24.04, nginx from the distro, and a domain whose A record already matches the public IPv4 of the VPS. If DNS is wrong, stop and fix DNS first. Certbot cannot negotiate with a name that hits another machine.

HTTP-01 issuance with certbot and nginx certbot new order Let's Encrypt HTTP-01 token nginx :80 acme-challenge live/*.pem reload nginx A record must hit this VPS, port 80 must be open validators fetch http://name/.well-known/acme-challenge/token TLS starts after the files exist, not before

Install nginx and certbot, open port 80

Install nginx, certbot, and the nginx plugin. Enable the unit. Confirm something answers on port 80 from the public IP. A host firewall that allows 80 only from your laptop will pass your curl and fail Let's Encrypt, which checks from multiple vantage points.

bash
apt update
apt install -y nginx certbot python3-certbot-nginx
systemctl enable --now nginx
ss -lntp | grep -E ":80|:443"
ufw allow 80/tcp
ufw allow 443/tcp
dig +short A example.com
curl -4 -I --max-time 5 http://example.com/

The A record must equal this VPS. If curl shows another server's default page, do not run certbot yet. L3/L4 DDoS filtering on the VPS does not replace an open port 80; it only drops junk. You still need the challenge path reachable.

A port 80 server block the plugin can find

Certbot's nginx plugin looks for a server_name that matches the requested hostname. If the only server is default_server with no name, the plugin may guess wrong or inject into the wrong file. Put a dedicated HTTP block in /etc/nginx/sites-available/example.com and enable it.

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

    root /var/www/example;
    location /.well-known/acme-challenge/ {
        default_type text/plain;
    }
    location / {
        return 200 "http-ok
";
    }
}

nginx -t && systemctl reload nginx. Visit http://example.com/ and http://www.example.com/. Both names should be in DNS if you will request both on the certificate. Requesting www without an A or CNAME is a failed authorization, not a warning.

Issue with the nginx plugin

The plugin can obtain the cert and write ssl_* directives for you. Read the diff it wants to apply. On a first box, this is the usual command:

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

That runs HTTP-01, stores files under /etc/letsencrypt/live/example.com/, and rewrites the server block to listen 443 ssl with a 80-to-443 redirect. Use --dry-run first if you have already hit rate limits this week. Staging (--staging) issues untrusted certs; useful for tests, not for users.

If you prefer not to let certbot edit nginx, use certbot certonly --webroot -w /var/www/example -d example.com -d www.example.com and then set ssl_certificate and ssl_certificate_key yourself. That is more typing and easier to review in git.

After success, the live directory is a set of symlinks. Always point nginx at live/, not at archive/ with a dated filename. archive/ is the history; live/ is the current pair that renewal will retarget.

Confirm TLS and the chain nginx actually serves

Reload nginx and inspect what the client sees, not just what the file contains.

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null

You want a leaf for example.com (and www if requested) plus the Let's Encrypt intermediate in the same handshake. ssl_certificate should be fullchain.pem, not cert.pem alone. Serving cert.pem without the intermediate is the "works in some browsers, fails in others" bug. ssl_certificate_key must be privkey.pem. Permissions: private keys stay 0600 root, and the nginx worker reads them at start or reload as root.

If the handshake still presents a default self-signed cert, you have two server blocks on 443 and the default_server is not the one you edited. nginx -T | grep -E "listen|server_name|ssl_certificate" is the debug tool.

Certbot plus nginx is a short path: DNS A matches, port 80 open, server_name matches, plugin or webroot issues, nginx uses fullchain and privkey, then reload. Rate limits punish loops; dry-run and staging exist so you do not burn production issuances. You can stand up Ubuntu 24.04 on Netbay in Lucknow (DC01) in under 60 seconds and run this sequence on a real public IP — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance