· 8 min read

Nginx Let's Encrypt SSL: Certbot, Auto-Renewal, and TLS Setup

To set up Nginx with a free Let's Encrypt SSL certificate, install Nginx, point your DNS at the server, run certbot with the Nginx plugin, and confirm the renewal timer works. The whole process takes about ten minutes on a clean Linux server, and the rest of this guide covers the parts people usually get wrong: the HTTP-to-HTTPS redirect, renewal hooks, and a TLS configuration that holds up to modern scanners.

The examples use Ubuntu 22.04/24.04 and Debian-style paths. AlmaLinux and Rocky Linux differences are noted where they matter. We use example.com and the documentation address 203.0.113.10 throughout. Replace them with your own values.

Prerequisites

Before you request a certificate, check these four things. Most failed issuance attempts come from one of them.

Verify DNS from your own machine:

dig +short A example.com
dig +short AAAA example.com

If the AAAA record points somewhere that does not serve your site, Let's Encrypt may try IPv6 and fail. Remove the record or fix it.

Step 1: Install Nginx

On Ubuntu or Debian:

sudo apt update
sudo apt install -y nginx
sudo systemctl enable --now nginx

On AlmaLinux or Rocky Linux:

sudo dnf install -y nginx
sudo systemctl enable --now nginx

Open the firewall. With UFW:

sudo ufw allow 'Nginx Full'
sudo ufw status

With firewalld:

sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload

Step 2: Create a Server Block

Create /etc/nginx/sites-available/example.com (on RHEL-family systems use /etc/nginx/conf.d/example.com.conf instead):

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

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

    location / {
        try_files $uri $uri/ =404;
    }
}

Create the web root, enable the site, test, and reload:

sudo mkdir -p /var/www/example.com/html
echo "<h1>It works</h1>" | sudo tee /var/www/example.com/html/index.html
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Make sure curl -I http://example.com returns 200 OK before moving on. The server_name line matters: certbot's Nginx plugin uses it to find which block to edit.

Step 3: Install Certbot

The Certbot project recommends the snap package because it stays current:

sudo apt install -y snapd
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot

If you prefer distribution packages, sudo apt install certbot python3-certbot-nginx works, but the version may lag behind. On RHEL-family systems, certbot is typically installed via snap or from EPEL.

Step 4: Get the Certificate

Run certbot with the Nginx plugin:

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

Certbot will ask for an email address (used for expiry warnings) and ask you to accept the terms of service. It then validates both names, downloads the certificate, edits your server block, and reloads Nginx.

Files are stored under /etc/letsencrypt/live/example.com/:

File Purpose
fullchain.pem Your certificate plus intermediates. Use this for ssl_certificate.
privkey.pem Private key. Never share it or commit it to a repo.
chain.pem Intermediates only.

Webroot or certonly mode

If you do not want certbot touching your config, get the certificate only and edit Nginx yourself:

sudo certbot certonly --webroot -w /var/www/example.com/html \
  -d example.com -d www.example.com

This is the better choice for configuration-managed servers (Ansible, Salt) where automatic edits would be overwritten.

Step 5: Redirect HTTP to HTTPS

Certbot normally adds a redirect when you choose it. If you manage the config manually, write two separate server blocks. A clean, explicit layout looks like this:

# HTTP: redirect everything except ACME challenges to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/example.com/html;
    }

    location / {
        return 301 https://example.com$request_uri;
    }
}

# HTTPS
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    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;

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

Notes:

Step 6: Secure TLS Settings

Certbot installs a baseline in /etc/letsencrypt/options-ssl-nginx.conf. If you want to control it yourself, put these directives in the HTTPS server block or a shared snippet:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;

ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

add_header X-Content-Type-Options nosniff always;
add_header Strict-Transport-Security "max-age=31536000" always;

What each part does:

A note on OCSP stapling: Let's Encrypt has been moving away from OCSP toward CRLs. Check the current Let's Encrypt announcements before adding ssl_stapling on; for their certificates. It is not required for a secure setup.

Always validate and reload:

sudo nginx -t && sudo systemctl reload nginx

Step 7: Automatic Renewal

Let's Encrypt certificates are valid for 90 days. The certbot package installs a systemd timer that checks twice a day and renews certificates that are close to expiry. Confirm it exists:

systemctl list-timers | grep certbot

Then test the full renewal flow without issuing a real certificate:

sudo certbot renew --dry-run

If the dry run passes, renewal will work unattended. To make sure Nginx picks up the new certificate, add a deploy hook:

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'EOF'
#!/bin/sh
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

The plugin-based flow usually reloads Nginx on its own, but the hook protects you in certonly and webroot setups. Hook scripts must be executable, or they are silently skipped.

Wildcard Certificates

Wildcards (*.example.com) require the DNS-01 challenge, because HTTP-01 cannot prove control of every subdomain. You need either a certbot DNS plugin for your DNS provider or a manual TXT record:

sudo certbot certonly --manual --preferred-challenges dns \
  -d example.com -d '*.example.com'

Manual mode does not renew automatically. For production, use a DNS plugin with API credentials limited to the zone.

Verify the Result

curl -I http://example.com          # expect 301 to https
curl -I https://example.com         # expect 200
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates -issuer
sudo certbot certificates

Also run the domain through an external TLS scanner such as SSL Labs to confirm the chain and protocol list from the outside.

Common Mistakes and Fixes

Where Your Server Fits

Certificates only protect traffic in transit. A public web server also needs a sensible network layer: restricted management ports, rate limiting, and a plan for volumetric attacks. If you are hosting on a VDS with full root access, you can run this exact stack and control every Nginx and firewall setting yourself. For public sites that attract hostile traffic, consider adding DDoS protection and a managed firewall in front of the origin, so TLS terminates on your server but junk traffic is filtered before it arrives.

Quick Checklist

  1. DNS A/AAAA records point to the server.
  2. Ports 80 and 443 are open.
  3. certbot --nginx succeeded for all hostnames.
  4. HTTP returns a 301 to the canonical HTTPS URL.
  5. TLS 1.2 and 1.3 only, session tickets off, HSTS added carefully.
  6. certbot renew --dry-run passes and a reload hook exists.
  7. External scan shows a valid chain.

Frequently asked questions

Is Let's Encrypt really free for commercial use?

Yes. Let's Encrypt issues domain-validated certificates at no charge, including for commercial sites. It is run by a nonprofit, and you only pay for your own server and domain.

How often does certbot renew certificates?

Certificates last 90 days. The certbot systemd timer (or cron job) runs twice daily and renews any certificate that is within about 30 days of expiry. Run certbot renew --dry-run to confirm it works.

Can I get a Let's Encrypt certificate for an IP address or internal hostname?

Standard Let's Encrypt issuance requires a publicly resolvable domain name that you control. Internal hostnames can still get certificates through the DNS-01 challenge if the domain is public, but private-only names cannot be validated.

Why does certbot fail even though my site loads in a browser?

The usual causes are port 80 blocked from outside, an AAAA record pointing to the wrong host, or a CDN or proxy intercepting the challenge request. Test with curl from another network and check dig output for both A and AAAA records.

Need a root-access server for Nginx? Explore Nixcon BGP-enabled VDS.Learn more