Skip to main content

SSL with Nginx & Certbot

How HTTPS Works (in 30 seconds)

When a browser connects to your site over HTTPS:

  1. Your server presents an SSL certificate — a digitally signed document proving you own the domain
  2. The browser verifies the certificate was signed by a trusted Certificate Authority (CA)
  3. Both sides agree on an encryption key and the connection is encrypted

Let's Encrypt is a free, trusted CA. Certbot is the tool that requests certificates from Let's Encrypt, proves you control the domain (by temporarily serving a file on port 80), and configures Nginx to use the cert automatically.

Prerequisites Checklist

Before running Certbot, confirm:

  • Your domain's DNS A record points to your server's IP (e.g., example.com → 1.2.3.4)
  • Port 80 is open in your security group / firewall (Certbot needs it to verify ownership)
  • Port 443 is open (for HTTPS traffic)
  • Nginx is installed and running (sudo systemctl status nginx)
  • Your Nginx server block has server_name example.com www.example.com;

Step 1 — Install Certbot

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

The python3-certbot-nginx plugin lets Certbot read and modify your Nginx config automatically.

Step 2 — Obtain the Certificate

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

Certbot will:

  1. Ask for your email (for renewal reminders)
  2. Ask you to agree to Let's Encrypt's Terms of Service
  3. Perform the HTTP-01 challenge to verify domain ownership
  4. Obtain the certificate
  5. Automatically modify your Nginx config to enable HTTPS

If you want to see what it would do without actually changing anything:

sudo certbot --nginx -d example.com --dry-run

Step 3 — What Certbot Added to Nginx

After running, your Nginx server block will look something like:

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;
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_cache_bypass $http_upgrade;
}
}

server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri; # redirect HTTP → HTTPS
}

The second server block (port 80) redirects all HTTP traffic to HTTPS permanently (301).

Step 4 — Test the Configuration

sudo nginx -t # check for syntax errors
sudo systemctl reload nginx

Visit https://example.com in your browser. You should see the padlock icon with no warnings.

Also verify the redirect works:

curl -I http://example.com
# HTTP/1.1 301 Moved Permanently
# Location: https://example.com/

Step 5 — Automatic Renewal with Cron

Let's Encrypt certificates expire every 90 days. Certbot includes a renewal command (certbot renew) that checks all installed certs and renews any expiring within 30 days. You need to run this automatically.

Option A — Cron Job (classic, explicit)

Open the root crontab:

sudo crontab -e

Add this line:

0 3 * * * certbot renew --quiet && systemctl reload nginx

Breaking it down:

  • 0 3 * * * — runs every day at 3:00 AM
  • certbot renew — checks and renews expiring certificates
  • --quiet — suppresses output unless there's an error
  • && systemctl reload nginx — reloads Nginx to pick up the new cert after renewal

Option B — Systemd Timer (modern, preferred on Ubuntu 20.04+)

Ubuntu installs a systemd timer automatically when you install Certbot via snap (the newer install method). Check if it's already running:

sudo systemctl status snap.certbot.renew.timer
# or
sudo systemctl status certbot.timer

If the timer is active, you're done — no cron job needed. If you used apt to install Certbot (as in this guide), add the cron job from Option A.

Verify the Renewal Works

Test the renewal process without actually renewing (dry run):

sudo certbot renew --dry-run

You should see:

Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/example.com/fullchain.pem (success)

If this passes, your renewal is set up correctly.

Certificate Files Reference

Certbot stores certificates in /etc/letsencrypt/live/<domain>/:

FilePurpose
fullchain.pemYour cert + the intermediate CA chain (use this in Nginx)
privkey.pemYour private key (never share this)
cert.pemYour cert only (without chain — rarely needed)
chain.pemThe intermediate chain only

Common Issues

IMPORTANT NOTES: Failed to renew certificate — usually means port 80 is blocked or Nginx isn't running. The HTTP-01 challenge requires port 80 to be reachable.

certificate not yet due for renewal — Certbot only renews if expiry is within 30 days. Run certbot certificates to see the expiry date.

Certificate works but old cert still showing — reload Nginx after renewal: sudo systemctl reload nginx.