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
# 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 nginxThis 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.
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.
dig +short yourdomain.com
dig +short www.yourdomain.com
nslookup yourdomain.comYou 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:
sudo ufw allow 'Nginx Full'
sudo ufw statusAlso verify cloud firewall or provider security group rules allow inbound 80/tcp and 443/tcp.
3) Install Nginx and Certbot
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx
sudo systemctl enable nginx
sudo systemctl start nginx4) Create a baseline Nginx server block
Create a site config for your app:
# /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:
sudo ln -s /etc/nginx/sites-available/yourapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxIf you are also setting up the app reverse proxy, use Deploy SaaS with Nginx + Gunicorn.
5) Confirm the domain answers over HTTP
curl -I http://yourdomain.com
curl -I http://www.yourdomain.comYou need a valid response from the correct host before issuing the certificate.
6) Issue the certificate
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.comChoose 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:
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:
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:
sudo nginx -t
sudo systemctl reload nginx8) Test HTTPS
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 -subjectYou want:
- a valid certificate
- expected hostname coverage
- non-expired
notAfterdate - working redirect from HTTP to HTTPS
9) Test renewal before launch
sudo certbot renew --dry-run
sudo certbot certificates
systemctl list-timers | grep certbotRenewal 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.
Process Flow
Common causes
- Domain DNS does not point to the target VPS.
- Ports
80or443are blocked by UFW, iptables, or cloud firewall rules. - Nginx
server_namedoes not match the requested domain. - Another service is already binding port
80or443. - 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
wwwor another subdomain. - App generates
httpURLs 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
dig +short yourdomain.com
dig +short www.yourdomain.com
curl -I http://yourdomain.com
curl -I https://yourdomain.comNginx validation
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.logCertbot validation
sudo certbot certificates
sudo certbot renew --dry-run
sudo journalctl -u certbot -n 100 --no-pagerCertificate inspection
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com | openssl x509 -noout -dates -issuer -subjectCheck active Nginx site config
List enabled sites and look for duplicates:
ls -la /etc/nginx/sites-enabled/
grep -R "server_name yourdomain.com" /etc/nginx/sites-enabled /etc/nginx/sites-availableCommon failure patterns
Certbot says authorization failed
Usually one of:
- DNS still points elsewhere
- port
80blocked - 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
80and443are 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-runsucceeds. - ✓ Certificate expiry is monitored.
- ✓ OAuth, webhook, and email callback URLs use HTTPS.
For broader release validation, use SaaS Production Checklist.
Related guides
- Domain and DNS Configuration
- Deploy SaaS with Nginx + Gunicorn
- Environment Setup on VPS
- Static and Media File Handling
- 502 Bad Gateway Fix Guide
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.