Web servers

How to set up HTTPS on Nginx with Let's Encrypt and Certbot

By · Last verified

Install Certbot from snap and run certbot --nginx. Certbot obtains a free Let's Encrypt certificate for example.com and www.example.com, configures the Nginx server block and renews the certificate automatically, and Nginx redirects plain HTTP to HTTPS.

The steps apply to Ubuntu 22.04/24.04 and Debian 12/13 with any supported Nginx release and Certbot 4.1 or later (Certbot 5.x is current as of November 2026).

Prerequisites

You need a server that Let's Encrypt can reach over HTTP on port 80.

  • A Linux server with systemd and sudo access. Snap is preinstalled on Ubuntu; on Debian install it with sudo apt install snapd.
  • Nginx installed and running, with a server block for your domain.
  • DNS A (and AAAA, if you use IPv6) records for example.com and www.example.com pointing at the server, for example 203.0.113.10.
  • Inbound TCP ports 80 and 443 open in the host firewall and in any cloud security group. Let's Encrypt validates from several network locations, so do not restrict port 80 to specific source IPs.

Check DNS and the firewall before you start:

dig +short example.com A
dig +short www.example.com A
sudo ufw allow 'Nginx Full'   # only if you use ufw

Step 1: Make sure Nginx has a server block for the domain

Certbot's Nginx plugin finds the server block by its server_name, so the names must match the domains you request. A minimal HTTP-only block looks like this:

# /etc/nginx/sites-available/example.com
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

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

Enable it and test the configuration:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Step 2: Install Certbot from snap

The snap package is Certbot's recommended installation method; it bundles the Nginx plugin and a renewal timer. Remove any distribution package first so two copies do not fight over the same files.

sudo apt-get remove certbot          # ignore "not installed" errors
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
certbot --version

If you cannot use snap, Certbot also documents a pip install in a virtual environment (supported on a best-effort basis):

sudo apt update
sudo apt install python3 python3-dev python3-venv libaugeas-dev gcc
sudo python3 -m venv /opt/certbot/
sudo /opt/certbot/bin/pip install --upgrade pip
sudo /opt/certbot/bin/pip install certbot certbot-nginx
sudo ln -s /opt/certbot/bin/certbot /usr/local/bin/certbot

Step 3: Get the certificate and let Certbot configure Nginx

One command obtains the certificate with the HTTP-01 challenge, adds the TLS settings to your server block and reloads Nginx.

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

Certbot asks you to accept the Let's Encrypt subscriber agreement and offers to register an email address. The email is optional: Let's Encrypt stopped sending expiry reminder emails on June 4, 2025. For scripts, run it non-interactively:

sudo certbot --nginx --non-interactive --agree-tos \
  -d example.com -d www.example.com

If you prefer to edit Nginx yourself, use sudo certbot certonly --nginx -d example.com -d www.example.com to obtain the certificate only. The files land in /etc/letsencrypt/live/example.com/.

Step 4: Review the HTTPS server block

After Certbot finishes, the server block should reference fullchain.pem and privkey.pem and redirect HTTP to HTTPS. If you write the configuration by hand, this is a sound baseline:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;    # Nginx 1.25.1+; on older versions write "listen 443 ssl http2;" instead
    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;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;

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

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

Leave out ssl_stapling and ssl_stapling_verify: Let's Encrypt certificates no longer contain an OCSP URL. To generate cipher settings for your client base, use the TLS config generator. Apply the changes:

sudo nginx -t && sudo systemctl reload nginx

Verify

The live site should present the Let's Encrypt certificate for example.com and www.example.com, and curl -I http://example.com should return 301 with a Location: https://... header. Check it from outside the server with the SSL checker, which shows the chain, protocols and expiry date, or from any terminal:

curl ssl.sh/example.com

On the server, confirm what Certbot manages and what Nginx serves:

sudo certbot certificates
curl -sI https://example.com | head -n 1
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

Automatic renewal

Renewal is already scheduled: the snap installs the systemd timer snap.certbot.renew.timer, which runs certbot renew twice a day. Certbot 4.1 and later check ACME Renewal Information (ARI) and renew when Let's Encrypt suggests; otherwise they renew when one third of the lifetime remains. Confirm the timer and run a dry run:

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

Certificates obtained with --nginx reload Nginx automatically after renewal. If you used certonly with --webroot or --standalone, add a deploy hook so Nginx picks up the new files:

sudo certbot reconfigure --cert-name example.com --deploy-hook "systemctl reload nginx"

The pip install has no timer; add the cron line from the Certbot instructions. See Certbot renewal hooks for pre, post and deploy hooks. Nothing else needs to change when Let's Encrypt shortens its default lifetime to 64 days on February 10, 2027 and 45 days on February 16, 2028, as long as the timer keeps running. Because Let's Encrypt no longer emails you before expiry, add the domain to ssl.sh monitoring (in development) as a safety net.

Troubleshooting

Most failures come from DNS, the firewall or a mismatched server block.

  • Timeout during connect or Connection refused: port 80 is blocked or DNS points elsewhere. See certbot timeout during connect.
  • Could not automatically find a matching server block: add the exact names to server_name and run Certbot again.
  • too many certificates already issued: you hit a rate limit while testing. Use --dry-run or --test-cert (staging), and read Let's Encrypt rate limits.
  • CAA record prevents issuance: your DNS has a CAA record that does not list letsencrypt.org. See CAA record prevents issuance.
  • key values mismatch when reloading Nginx: the certificate and key files do not belong together. See nginx key values mismatch.
  • NET::ERR_CERT_COMMON_NAME_INVALID for www: the certificate does not cover that name. Run Certbot again with every -d name and answer "Expand". See NET::ERR_CERT_COMMON_NAME_INVALID.
  • Works in the browser but curl or apps fail: Nginx is serving cert.pem. See missing intermediate certificate.

FAQ

Should Nginx use cert.pem or fullchain.pem?

Use fullchain.pem for ssl_certificate. It contains your certificate followed by the Let's Encrypt intermediate. cert.pem contains only your certificate, which works in some browsers that cache intermediates but fails in curl, Java, Android apps and many API clients.

Do I still need ssl_stapling for Let's Encrypt certificates?

No. Let's Encrypt stopped including OCSP URLs in certificates in May 2025 and shut down its OCSP responders in August 2025. With ssl_stapling on; Nginx only logs a warning that the certificate has no OCSP responder URL. Remove the stapling lines.

Can certbot --nginx issue a wildcard certificate?

No. Wildcard certificates such as *.example.com require the DNS-01 challenge, which the Nginx plugin does not perform. Use a DNS plugin, for example Certbot with the Cloudflare DNS plugin, then point Nginx at the resulting files.

How do I get the 45-day tlsserver profile instead of the default?

Certbot 4.0 and later accept --preferred-profile tlsserver (or --required-profile), and Certbot 4.1 and later save the choice for renewals. See Let's Encrypt certificate profiles before switching, because renewal must run at least daily.

Is Nginx's own ACME module an alternative to Certbot?

Yes. nginx.org publishes ngx_http_acme_module (package nginx-module-acme), which obtains and renews certificates inside Nginx with the acme_issuer and acme_certificate directives. It supports HTTP-01 and TLS-ALPN-01 but not DNS-01, so wildcards still need another client. Check the module documentation for the version you install.

Sources

  1. Certbot instructions: Nginx on Linux (snap)
  2. Certbot User Guide
  3. nginx: Configuring HTTPS servers
  4. nginx: Module ngx_http_ssl_module
  5. Let's Encrypt: Ending OCSP Support in 2025
  6. Let's Encrypt: Decreasing Certificate Lifetimes to 45 Days