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:
- Verify you own the domain by placing a file at
/.well-known/acme-challenge/ - Request the certificate from Let's Encrypt
- Automatically modify your Nginx config to enable HTTPS
- 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.
CREESOL