HTTPS Setup (Let’s Encrypt)

The essential playbook for implementing https setup (let’s encrypt) in your SaaS.

Use Let’s Encrypt to terminate HTTPS at Nginx, redirect HTTP to HTTPS, and renew certificates automatically. For most small SaaS deployments, the reliable path is: point DNS to the server, open ports 80/443, install Nginx, issue a cert with Certbot, verify renewal, then reload Nginx safely.

Quick Fix / Quick Setup

bash
# Ubuntu/Debian + Nginx
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx

# Verify DNS first
curl -I http://yourdomain.com

# Allow web traffic
sudo ufw allow 'Nginx Full'

# Issue certificate and auto-configure Nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

# Test renewal
sudo certbot renew --dry-run

# Reload nginx
sudo systemctl reload nginx

This works when your domain already resolves to the server, Nginx is serving the correct server_name, and ports 80 and 443 are reachable from the public internet. If DNS is not ready, fix that first before running Certbot.

What’s happening

  • HTTPS requires a valid TLS certificate bound to your domain.
  • Let’s Encrypt issues domain-validated certificates, usually through an HTTP challenge on port 80.
  • Certbot can edit Nginx config automatically and install the certificate paths for you.
  • Nginx handles TLS termination; Gunicorn or your app server can continue listening on a local port such as 127.0.0.1:8000.
  • Certificates expire every 90 days, so auto-renewal must be tested before production launch.
  • browser
    Nginx TLS termination
    Gunicorn/app

    request flow diagram showing browser -> Nginx TLS termination -> Gunicorn/app.

Step-by-step implementation

1) Verify DNS points to the VPS

Check root and www records before touching Certbot.

bash
dig +short yourdomain.com
dig +short www.yourdomain.com
nslookup yourdomain.com

You should see your VPS public IP.

If DNS is not correct, fix it first. See Domain and DNS Configuration.

2) Open ports 80 and 443

If UFW is enabled:

bash
sudo ufw allow 'Nginx Full'
sudo ufw status

Also verify cloud firewall or provider security group rules allow inbound 80/tcp and 443/tcp.

3) Install Nginx and Certbot

bash
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx
sudo systemctl enable nginx
sudo systemctl start nginx

4) Create a baseline Nginx server block

Create a site config for your app:

nginx
# /etc/nginx/sites-available/yourapp
server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }
}

Enable it:

bash
sudo ln -s /etc/nginx/sites-available/yourapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

If you are also setting up the app reverse proxy, use Deploy SaaS with Nginx + Gunicorn.

5) Confirm the domain answers over HTTP

bash
curl -I http://yourdomain.com
curl -I http://www.yourdomain.com

You need a valid response from the correct host before issuing the certificate.

6) Issue the certificate

bash
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Choose the redirect option when prompted. Certbot typically updates your Nginx config automatically.

7) Validate generated TLS config

After Certbot runs, your Nginx config should include paths similar to:

nginx
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

A typical HTTPS server block looks like this:

nginx
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8000;
        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 https;
    }
}

server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

Test and reload:

bash
sudo nginx -t
sudo systemctl reload nginx

8) Test HTTPS

bash
curl -I https://yourdomain.com
curl -I https://www.yourdomain.com
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com | openssl x509 -noout -dates -issuer -subject

You want:

  • a valid certificate
  • expected hostname coverage
  • non-expired notAfter date
  • working redirect from HTTP to HTTPS

9) Test renewal before launch

bash
sudo certbot renew --dry-run
sudo certbot certificates
systemctl list-timers | grep certbot

Renewal must succeed unattended. Do not skip this.

10) Update app-level HTTPS settings

For production:

  • enable secure cookies
  • trust proxy headers
  • update generated URLs to https://
  • update OAuth callback URLs
  • update payment/webhook endpoints
  • update email links

If your app structure or env handling is inconsistent, fix that in Environment Setup on VPS and Structuring a Flask/FastAPI SaaS Project.

11) If using a CDN or proxy

Decide where TLS terminates:

  • edge only
  • edge + origin

If using Cloudflare or another CDN, ensure challenge mode and origin TLS strategy match your setup. Avoid debugging origin certs while the proxy hides the real response path.

12) Wildcard domains

For *.yourdomain.com, use DNS challenge instead of standard HTTP challenge. The normal --nginx HTTP flow is for specific hostnames like yourdomain.com and www.yourdomain.com.

DNS ready
Nginx reachable on :80
Certbot issue
redirect
renew test

Process Flow

Common causes

  • Domain DNS does not point to the target VPS.
  • Ports 80 or 443 are blocked by UFW, iptables, or cloud firewall rules.
  • Nginx server_name does not match the requested domain.
  • Another service is already binding port 80 or 443.
  • Certbot challenge path is intercepted by a redirect or wrong server block.
  • Duplicate Nginx site configs cause the wrong virtual host to answer.
  • Certificate was issued for only one hostname, but traffic also uses www or another subdomain.
  • App generates http URLs because proxy headers are missing or not trusted.
  • Mixed-content issues from frontend assets, API URLs, or hardcoded links still using http.
  • Renewal fails because DNS changed or Nginx config was modified after initial issuance.

Debugging tips

Start with DNS, ports, Nginx config, then Certbot state.

DNS and reachability

bash
dig +short yourdomain.com
dig +short www.yourdomain.com
curl -I http://yourdomain.com
curl -I https://yourdomain.com

Nginx validation

bash
sudo nginx -t
sudo systemctl status nginx
sudo ss -tulpn | grep -E ':80|:443'
sudo journalctl -u nginx -n 100 --no-pager
sudo tail -n 100 /var/log/nginx/error.log

Certbot validation

bash
sudo certbot certificates
sudo certbot renew --dry-run
sudo journalctl -u certbot -n 100 --no-pager

Certificate inspection

bash
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com | openssl x509 -noout -dates -issuer -subject

Check active Nginx site config

List enabled sites and look for duplicates:

bash
ls -la /etc/nginx/sites-enabled/
grep -R "server_name yourdomain.com" /etc/nginx/sites-enabled /etc/nginx/sites-available

Common failure patterns

Certbot says authorization failed

Usually one of:

  • DNS still points elsewhere
  • port 80 blocked
  • wrong server_name
  • another Nginx site answered the request

Browser says “Not Secure” after install

Usually one of:

  • mixed content
  • wrong hostname on cert
  • expired cert
  • CDN serving a different cert than the origin

Login/session breaks after HTTPS

Usually one of:

  • app does not trust X-Forwarded-Proto
  • secure cookie config mismatch
  • redirect loop caused by app forcing HTTPS while proxy headers are wrong

If the app fails behind Nginx after enabling HTTPS, also check 502 Bad Gateway Fix Guide.

Checklist

  • Domain resolves to the correct public IP.
  • Ports 80 and 443 are open in cloud firewall and OS firewall.
  • Nginx is serving the intended domain via server_name.
  • Certbot issued certificates for all required hostnames.
  • HTTP redirects to HTTPS.
  • App works behind Nginx over HTTPS without mixed-content errors.
  • Secure cookies and proxy headers are configured correctly.
  • certbot renew --dry-run succeeds.
  • Certificate expiry is monitored.
  • OAuth, webhook, and email callback URLs use HTTPS.

For broader release validation, use SaaS Production Checklist.

Related guides

FAQ

Do I need HTTPS on the app server too?

Usually no. For a small SaaS, terminate TLS at Nginx and proxy to Gunicorn on localhost.

Can I issue a certificate before DNS points to my server?

No for normal HTTP challenge. The domain must resolve correctly and port 80 must be reachable.

Why is Certbot using port 80 when I want HTTPS?

Let’s Encrypt commonly validates domain control over HTTP first, then issues the HTTPS certificate.

Should I force redirect from HTTP to HTTPS immediately?

Yes after certificate issuance and basic app verification.

Can I use Let’s Encrypt with a CDN like Cloudflare?

Yes, but challenge mode and origin certificate strategy must match your proxy setup.

Should I use the Nginx Certbot plugin or standalone mode?

Use the Nginx plugin if Nginx is already installed and serving the domain. Use standalone only when you can temporarily stop the existing service on port 80.

Why does my browser still show Not Secure after certificate installation?

Check for mixed content, invalid certificate hostnames, expired certs, or a CDN/proxy serving a different origin certificate than expected.

Do I need both example.com and www.example.com on the certificate?

Yes if both hostnames are used publicly. Include every hostname you redirect from or serve.

Can Let’s Encrypt be used for staging and production?

Yes. Use Let’s Encrypt staging endpoints for repeated testing to avoid rate limits, then issue the production certificate once the config is correct.

Why are secure cookies or login sessions failing after enabling HTTPS?

Your app may not trust X-Forwarded-Proto from Nginx, causing it to think requests are HTTP. Configure trusted proxy headers and secure cookie settings correctly.

Final takeaway

The fastest stable setup is DNS -> Nginx on 80/443 -> certbot --nginx -> redirect to HTTPS -> dry-run renewal.

Most failures come from DNS mismatch, blocked ports, bad server_name values, or duplicate Nginx configs.

Treat HTTPS as part of deployment, not a one-time task: verify renewals, proxy headers, secure cookies, and external callback URLs every release.