Hands-On: HTTPS for a Live Nginx Site
The Scenario
You have an Ubuntu server where Nginx reverse-proxies an Express app on port 3000, reachable at http://api.myapp.com. In this walkthrough we run the full certificate session: verify the prerequisites properly, issue the cert with the Nginx plugin, enforce the HTTPS redirect, and confirm renewal will actually happen without us.
Step 1: Pre-Flight Checks That Save You a Failed Challenge
Most Certbot failures are diagnosable before you ever run Certbot. Three checks:
DNS actually points here:
dig +short api.myapp.com
# 203.0.113.10 <- must match your server's public IP
curl -s ifconfig.me
# 203.0.113.10
If these two outputs differ, stop — fix the A record and wait for propagation.
Ports 80 and 443 are open:
sudo ufw allow 'Nginx Full'
sudo ufw status
On AWS, also confirm the security group allows inbound 80 and 443.
Nginx answers for this exact domain:
sudo nginx -t
grep -r "server_name" /etc/nginx/sites-enabled/
# server_name api.myapp.com;
The Nginx plugin finds the right server block by matching server_name, so the domain you pass to Certbot must appear there verbatim.
Step 2: Install Certbot via Snap
Snap is the install method Certbot's maintainers recommend on modern Ubuntu — it stays current and ships the renewal timer pre-configured:
sudo snap install core && sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
certbot --version
If an older apt-installed Certbot exists, remove it first with sudo apt remove certbot so the two don't fight over renewals.
Step 3: Obtain the Certificate
sudo certbot --nginx -d api.myapp.com
Walk through the prompts:
- Email address — use a real one; expiry warnings go here if renewal ever breaks
- Agree to the Terms of Service
- Newsletter opt-in — your call
Recent Certbot versions configure the HTTP-to-HTTPS redirect automatically with the Nginx plugin. Older versions ask; if prompted, choose Redirect. Success looks like:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/api.myapp.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/api.myapp.com/privkey.pem
This certificate expires on 2026-10-04.
Deploying certificate
Successfully deployed certificate for api.myapp.com
Behind the scenes Certbot answered the HTTP-01 challenge through your running Nginx, then edited your server block: it added a listen 443 ssl block with the certificate paths and left a port 80 block whose only job is returning 301 redirects.
Step 4: Verify the Redirect and the Cert
Force of habit — verify from outside, not just in the browser.
HTTP redirects permanently:
curl -I http://api.myapp.com
# HTTP/1.1 301 Moved Permanently
# Location: https://api.myapp.com/
HTTPS terminates correctly and reaches Express:
curl https://api.myapp.com/api/hello
# {"from":"express",...}
Inspect the served certificate directly:
echo | openssl s_client -connect api.myapp.com:443 -servername api.myapp.com 2>/dev/null | openssl x509 -noout -dates -issuer
# notBefore=Jul 6 00:00:00 2026 GMT
# notAfter=Oct 4 00:00:00 2026 GMT
# issuer=C = US, O = Let's Encrypt, CN = R11
If the redirect is missing (you chose "No redirect", or an old Certbot skipped it), add it yourself in the port 80 server block:
server {
listen 80;
server_name api.myapp.com;
return 301 https://$host$request_uri;
}
Then sudo nginx -t && sudo systemctl reload nginx.
Step 5: Confirm Auto-Renewal Is Armed
The snap package installs a systemd timer that runs twice daily. Don't assume — check:
systemctl list-timers | grep certbot
# Mon 2026-07-06 21:14:00 UTC 4h left snap.certbot.renew.timer snap.certbot.renew.service
Then rehearse a renewal without touching the real certificate:
sudo certbot renew --dry-run
You want to see:
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/api.myapp.com/fullchain.pem (success)
The dry run exercises the entire pipeline — challenge, issuance against the staging CA, and the deploy step — so a pass here means the timer will succeed unattended. Because the Nginx plugin issued the cert, renewal also reloads Nginx for you; no extra cron line is needed with the snap install.
Check what's installed and when it expires at any time:
sudo certbot certificates
Step 6: Optional Hardening with HSTS
Once you're confident everything works over HTTPS, tell browsers to skip the insecure hop entirely. Inside the listen 443 ssl server block:
add_header Strict-Transport-Security "max-age=31536000" always;
sudo nginx -t && sudo systemctl reload nginx
curl -sI https://api.myapp.com | grep -i strict
# strict-transport-security: max-age=31536000
A word of caution: HSTS is a one-way door for the max-age duration — browsers will refuse plain HTTP for your domain. Only add it after the cert and renewal are proven. I hold off on the includeSubDomains flag unless every subdomain is already on HTTPS.
Step 7: Adding More Domains Later
Certs are per-hostname. When admin.myapp.com comes along, expand the existing certificate:
sudo certbot --nginx -d api.myapp.com -d admin.myapp.com --expand
Certbot reissues one cert covering both names and rewires the matching server blocks.
Recap
- Verified DNS, firewall, and
server_namebefore running anything — the three usual failure points - Installed Certbot from snap, the maintained path with the renewal timer built in
- One
certbot --nginxcommand issued the cert and deployed it into the server block - Proved the 301 redirect and inspected the live cert with
curlandopenssl - Confirmed the timer exists and passed
renew --dry-run— renewal now needs no human