How to Set Up SSL with Let's Encrypt on Nginx (Ubuntu 2026)

Every website needs HTTPS in 2026 — browsers flag HTTP sites as "Not Secure," search engines penalise them, and browsers increasingly block mixed content and camera/microphone access on non-HTTPS pages. Let's Encrypt provides free, trusted SSL certificates with automated renewal. This guide walks through the complete setup from zero to a working HTTPS site on Nginx.

Prerequisites

  • A VPS or server running Ubuntu 22.04 / 24.04
  • A domain name pointed to your server's IP (A record in DNS)
  • Nginx installed and serving your site on port 80
  • Root or sudo access

Verify DNS is propagated before starting:

dig example.com A +short      # should return your server's IP
ping example.com              # should reach your server

Step 1: Install Nginx (if not already installed)

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

# Open firewall
sudo ufw allow 'Nginx Full'   # allows ports 80 and 443
sudo ufw allow OpenSSH        # keep SSH access open
sudo ufw enable
sudo ufw status

Step 2: Create a Basic Nginx Server Block

sudo nano /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 index.php;

    location / {
        try_files $uri $uri/ =404;
    }
}
# Enable the site
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

# Test Nginx config
sudo nginx -t

# Reload
sudo systemctl reload nginx

Confirm your site loads over HTTP before proceeding: curl -I http://example.com should return a 200 response.

Step 3: Install Certbot

sudo apt install certbot python3-certbot-nginx -y

Step 4: Obtain the SSL Certificate

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

Certbot will ask for an email address (for renewal alerts), prompt you to agree to the Terms of Service, and then:

  1. Verify you own the domain by placing a file at /.well-known/acme-challenge/
  2. Request the certificate from Let's Encrypt
  3. Automatically modify your Nginx config to enable HTTPS
  4. Offer to redirect all HTTP traffic to HTTPS (say yes)

If successful, you'll see a message like:

Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem

What Your Nginx Config Looks Like After Certbot

Certbot modifies your server block automatically:

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

    # Certbot added this — HTTP redirects to HTTPS
    return 301 https://$host$request_uri;
}

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

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

    # Certbot-managed SSL config
    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;

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

Step 5: Verify Auto-Renewal

Let's Encrypt certificates expire every 90 days. Certbot installs a systemd timer (or cron job) that auto-renews certificates at least 30 days before expiry:

# Check the auto-renewal timer
sudo systemctl status certbot.timer

# Test renewal (dry run — doesn't actually renew)
sudo certbot renew --dry-run

# If the dry run succeeds, auto-renewal is working

Certbot's timer runs twice daily. As long as ports 80 and 443 are open and the domain resolves to your server, renewal is fully automatic.

Step 6: Harden SSL Configuration

Certbot's default config is good, but you can improve it with modern cipher settings and HSTS:

server {
    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;

    # Modern TLS only
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    # HSTS — tells browsers to always use HTTPS (includeSubDomains if safe)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Prevent MIME sniffing
    add_header X-Content-Type-Options nosniff always;

    # Clickjacking protection
    add_header X-Frame-Options SAMEORIGIN always;

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

    # OCSP stapling (speeds up handshake by caching cert status)
    ssl_stapling        on;
    ssl_stapling_verify on;
    resolver            8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout    5s;
}

Nginx as a Reverse Proxy for Node.js (with SSL)

If you're running a Node.js app on port 3000, Nginx handles SSL and forwards to your app:

server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass         http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection 'upgrade';
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

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

Tell your Node.js app to trust the Nginx proxy (so req.ip shows the real client IP, not 127.0.0.1):

// Express
app.set('trust proxy', 1)

// Fastify
await app.register(import('@fastify/express'))
// or configure trustProxy in Fastify options

Multiple Domains on One Server

# Get a certificate for multiple domains at once
sudo certbot --nginx -d example.com -d www.example.com -d api.example.com

# Or get separate certificates
sudo certbot --nginx -d blog.example.com
sudo certbot --nginx -d shop.example.com

# List all certificates
sudo certbot certificates

Wildcard Certificate (*.example.com)

Wildcard certificates cover all subdomains. They require DNS validation (not HTTP), so you need to add a DNS TXT record:

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

# Certbot will ask you to add a DNS TXT record:
# _acme-challenge.example.com → [provided value]
# Add it in your DNS control panel, wait 1-2 minutes, then press Enter

For automated wildcard renewal, use a DNS plugin for your registrar (Certbot has plugins for Cloudflare, Route53, DigitalOcean DNS, etc.).

Troubleshooting Common Issues

# "Connection refused" during cert issuance
sudo ufw allow 80   # port 80 must be open for HTTP challenge

# Certificate not found / wrong path
sudo certbot certificates   # shows exact paths

# Nginx not reloading after renewal
# Certbot's nginx plugin handles this, but verify:
sudo systemctl reload nginx

# Check Nginx config for errors
sudo nginx -t

# Check renewal logs
sudo journalctl -u certbot.service
sudo cat /var/log/letsencrypt/letsencrypt.log

# Rate limit hit (Let's Encrypt limits: 5 certs/domain/week)
# Use --staging flag while testing to avoid hitting production rate limits
sudo certbot --nginx --staging -d example.com

Check Your SSL Grade

After setup, verify your configuration at SSL Labs. A properly configured Nginx + Let's Encrypt setup should score A or A+. If you score lower, the detailed report shows exactly what to fix.

Summary

The core flow: install Nginx, point your domain's DNS to the server, run certbot --nginx -d yourdomain.com, and accept the HTTP-to-HTTPS redirect. Auto-renewal is handled automatically by Certbot's systemd timer. The whole process takes about 10 minutes on a fresh server. Add HSTS and OCSP stapling for the best possible SSL rating.

Frequently Asked Questions

Is Let's Encrypt truly free? Are there limitations?

Yes — Let's Encrypt is completely free, run by a non-profit (ISRG). Limitations: certificates expire every 90 days (auto-renewal handles this), rate limits apply (5 certificates per registered domain per week — not a concern for normal use), and they only issue Domain Validation (DV) certificates (no Organization Validation or Extended Validation). For standard websites and APIs, DV certificates are sufficient. EV certificates (which show the company name in some older browsers' address bars) require a paid CA and manual validation process.

What happens if auto-renewal fails?

Certbot will send an email to the address you registered with when your certificate is 30 days, 14 days, and 7 days from expiry. If renewal keeps failing: check that port 80 is open (HTTP challenge requires it), verify the domain still resolves to your server, and check the Certbot logs at /var/log/letsencrypt/letsencrypt.log. Common causes: firewall blocking port 80, DNS no longer pointing to the server, or Nginx config error preventing Certbot from writing challenge files.

Can I use Let's Encrypt on shared hosting?

Most modern shared hosting providers (cPanel-based hosts, Hostinger, SiteGround, etc.) include Let's Encrypt integration directly in their control panel — you can install an SSL certificate with one click without touching the command line. The manual Certbot setup in this guide is for VPS or dedicated servers where you have root access. If your host doesn't support Let's Encrypt, most offer free AutoSSL (cPanel's built-in equivalent) or you can purchase a cheap DV certificate from providers like Namecheap (~$5/year).

What is HSTS and should I enable it?

HSTS (HTTP Strict Transport Security) is a response header that tells browsers to always connect to your domain over HTTPS — even if the user types http:// or clicks an HTTP link. After the first HTTPS visit, the browser enforces HTTPS itself without needing a server redirect. max-age=31536000 (1 year) is the standard. Be careful with includeSubDomains — only add it if all your subdomains also have valid HTTPS. Once enabled with a long max-age, it's hard to undo (you'd need users to wait for the cache to expire). Test with a short max-age first.