Free HTTPS with Nginx and Let's Encrypt in 2026
A clean Nginx + Certbot setup that auto-renews, scores A+ on SSL Labs, and survives the next protocol deprecation.

HTTPS is table stakes. Browsers shame plain HTTP, search engines penalise it, and Let's Encrypt is free. There is no longer an excuse. Here is the modern setup, current as of 2026.
Install Nginx
sudo apt update
sudo apt install -y nginx
sudo systemctl enable --now nginx
Confirm with curl -I http://localhost. You should see HTTP/1.1 200 OK.
Create the server block
Certbot's nginx plugin finds the right virtual host by matching server_name, and a fresh Nginx install only ships the catch-all default. Skip this step and the certificate request later in this guide fails with "could not automatically find a matching server block". Create a web root and a placeholder page first:
sudo mkdir -p /var/www/example.com
echo 'example.com is alive' | sudo tee /var/www/example.com/index.html
Then create /etc/nginx/sites-available/example.com with a plain HTTP block for now. Certbot upgrades it in a later step:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.html;
}
Enable it, check the syntax, and reload:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Running curl -H "Host: example.com" http://localhost should now return the placeholder page.
Point DNS first
Before requesting a certificate, every name you put on it must resolve to the VPS. The certbot command later in this guide covers both example.com and www.example.com, so check both from your laptop:
dig +short example.com
dig +short www.example.com
Both must return your VPS IP. If either does not, fix DNS before continuing. Let's Encrypt will reject ACME challenges otherwise, and a missing www record fails the whole request even when the bare domain is fine.
Install Certbot
Use the snap distribution. Apt has Certbot but lags on plugin support.
sudo apt install -y snapd
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Open the firewall
The ACME challenge arrives over plain HTTP on port 80, and the finished site serves on port 443. If UFW is active with only SSH allowed, the challenge fails and your HTTPS site is unreachable even after issuance. Allow both ports through Nginx's bundled profile:
sudo ufw allow 'Nginx Full'
If UFW is inactive on your box, the command just records the rule and changes nothing else, so it is safe to run either way. If your provider puts its own firewall or security group in front of the VPS, open ports 80 and 443 there too, or the certificate request below will time out.
Request the certificate
Certbot's nginx plugin edits your config and reloads in one shot:
sudo certbot --nginx -d example.com -d www.example.com
Pick the redirect option when prompted. Forcing HTTPS at the edge is the easy default.
Harden the server block
Certbot's defaults are okay. The config below is better. One line in it deserves a pause before you copy anything: the Strict-Transport-Security (HSTS) header. HSTS tells browsers to refuse plain HTTP for your domain for as long as max-age says, and browsers cache it, so there is no practical way to take it back early. Jump straight to a one-year value with includeSubDomains and any subdomain not served over HTTPS, or a certificate that later fails to renew, locks visitors out until the year runs down. The block below starts with a five-minute max-age and no includeSubDomains so a mistake expires quickly. Open /etc/nginx/sites-available/example.com, which certbot rewrote in the previous step, and replace its entire contents with the two blocks below:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
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_prefer_server_ciphers off;
ssl_ciphers ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=300" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=()" always;
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;
}
Reload:
sudo nginx -t && sudo systemctl reload nginx
Live with the short HSTS value for a few days. Once the renewal dry run in the next section passes and every subdomain you serve answers on valid HTTPS, raise the header to max-age=31536000; includeSubDomains and reload again. That long value is the one SSL Labs wants to see for an A+.
Auto-renewal
The snap install registers a systemd timer. Verify it:
systemctl list-timers | grep certbot
Force a dry run to catch issues before they bite:
sudo certbot renew --dry-run
If that succeeds, you can stop thinking about certificates for ninety days at a time.
Test
Run the SSL Labs scan against your domain. The config above scores A+ on a stock Ubuntu 24.04 box once HSTS is raised to the one-year value; with the five-minute trial value expect an A. If you score lower than that, the most common culprits are: still listening on TLSv1.0/1.1, missing HSTS, or a weak DH parameter file from an older guide.
That's the whole pipeline: one server block, one certbot run, a hardened config, and a renewal timer that runs forever.
Continue reading