Let’s Encrypt issues free, trusted TLS certificates valid for 90 days. Certbot obtains them, configures Nginx, and renews them automatically. On a server with a public IP and a domain, the whole process takes about two minutes.

Before you start

  • Nginx is installed and serving your domain over HTTP — see installing Nginx on Ubuntu.
  • The domain’s A record (and AAAA, if you have one) points to this server. Check with dig +short example.com.
  • Ports 80 and 443 are open in your firewall (sudo ufw allow 'Nginx Full').
  • Your server block has the right server_name, e.g. server_name example.com www.example.com;.

A stale AAAA record pointing elsewhere is a common cause of failed validation: Let’s Encrypt prefers IPv6 when it exists.

1. Install Certbot

On Ubuntu and Debian:

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

The Certbot project also distributes a snap (sudo snap install --classic certbot), which tracks newer versions. Either works; pick one and don’t install both.

2. Get a certificate

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

Certbot asks for an email address (for expiry warnings and security notices) and agreement to the terms. It then:

  1. Proves you control the domain via an HTTP-01 challenge — Let’s Encrypt fetches a temporary file from http://example.com/.well-known/acme-challenge/.
  2. Saves the certificate to /etc/letsencrypt/live/example.com/.
  3. Edits your server block to listen 443 ssl with the certificate paths.
  4. Adds an HTTP → HTTPS redirect (current Certbot versions do this by default).

Visit https://example.com — the padlock should appear.

3. What Certbot changed

Your server block now contains lines like:

listen 443 ssl;
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;

plus a separate port-80 block that returns a 301 to HTTPS. The files under live/ are symlinks to the newest certificate, so renewals don’t require config changes.

Use fullchain.pem (certificate + intermediate), not cert.pem. Serving only the leaf certificate works in some browsers and fails in others — see SSL certificate chain errors.

4. Automatic renewal

Certbot installs a systemd timer (or a cron job) that runs twice a day and renews certificates within 30 days of expiry:

systemctl list-timers | grep certbot
sudo certbot renew --dry-run

The dry run talks to Let’s Encrypt’s staging server and confirms renewal will work. Run it whenever you change your Nginx config in ways that might affect /.well-known/.

Certbot reloads Nginx after renewal through its Nginx plugin. If you obtained a certificate another way (for example --webroot), add a deploy hook so Nginx picks up the new file:

echo -e '#!/bin/sh\nsystemctl reload nginx' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

5. Useful Certbot commands

sudo certbot certificates                 # list certificates and expiry dates
sudo certbot --nginx -d new.example.com   # add another site
sudo certbot --nginx --expand -d example.com -d www.example.com -d shop.example.com   # add a name to an existing cert
sudo certbot delete --cert-name old.example.com
sudo certbot renew --force-renewal        # renew now (respect rate limits!)

Harden the HTTPS config (optional)

Certbot’s defaults are sound. Two common additions inside the HTTPS server block:

add_header Strict-Transport-Security "max-age=31536000" always;
http2 on;     # Nginx 1.25.1+; on older versions use "listen 443 ssl http2;"

Only enable HSTS once you’re sure the site (and any subdomains you include) will stay on HTTPS. See HSTS explained before adding includeSubDomains or preload.

Troubleshooting

Timeout during connect (likely firewall problem) — Let’s Encrypt couldn’t reach port 80. Check UFW, any provider-side firewall, and that Nginx listens on port 80.

Invalid response from http://example.com/.well-known/acme-challenge/…: 404 — the request reached a different server block, or a location rule intercepts /.well-known/. Confirm with sudo nginx -T which block has your server_name.

DNS problem: NXDOMAIN looking up A for example.com — the DNS record doesn’t exist yet or hasn’t propagated. Wait and check with dig.

too many certificates already issued — you’ve hit a rate limit (for example, 5 duplicate certificates per week). Test with --dry-run or --staging while experimenting.

Behind Cloudflare’s proxy — HTTP-01 still works as long as Cloudflare forwards port 80 traffic to your server. If you use the orange cloud with “Always Use HTTPS”, validation still succeeds via redirect handling in most setups; if not, use the DNS challenge.

Wildcard certificates

A certificate for *.example.com requires the DNS-01 challenge: you prove control by creating a TXT record. Certbot supports this with DNS-provider plugins — see wildcard certificates with Certbot.

Using Apache?

The process is the same with the Apache plugin: Let’s Encrypt with Certbot on Apache.

HTTPS is free and automatic; there’s no reason to run any site without it. Every VPS.DO server has a dedicated public IPv4 address, which is all Let’s Encrypt needs.

  • SSL
  • Nginx
  • Let's Encrypt