Skip to main content

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:

  1. Email address — use a real one; expiry warnings go here if renewal ever breaks
  2. Agree to the Terms of Service
  3. 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

  1. Verified DNS, firewall, and server_name before running anything — the three usual failure points
  2. Installed Certbot from snap, the maintained path with the renewal timer built in
  3. One certbot --nginx command issued the cert and deployed it into the server block
  4. Proved the 301 redirect and inspected the live cert with curl and openssl
  5. Confirmed the timer exists and passed renew --dry-run — renewal now needs no human