How to Set Up HTTPS for Flask (Nginx + Let’s Encrypt)
If you're trying to enable HTTPS for a Flask app in production, this guide shows you how to configure Nginx with Let's Encrypt step-by-step. The result is a valid TLS certificate, automatic HTTP-to-HTTPS redirect, and a renewable production setup.
Quick Fix / Quick Setup
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo systemctl status certbot.timer
sudo nginx -t && sudo systemctl reload nginx
curl -I http://example.com
curl -I https://example.com
Replace example.com and www.example.com with your real domain. This works when DNS already points to the server, Nginx serves the domain correctly on port 80, and ports 80 and 443 are open.
What’s Happening
HTTPS for Flask is usually terminated at Nginx, not inside Flask or Gunicorn. Let’s Encrypt issues certificates only after validating domain ownership, typically over HTTP on port 80. Certbot updates the Nginx server block to use the certificate files and can add the HTTP-to-HTTPS redirect automatically. If DNS, firewall rules, or server_name are wrong, issuance and renewal fail.
Step-by-Step Guide
- Confirm the domain points to the server
Run:bashdig +short example.com dig +short www.example.com
Verify the returned IP matches your server’s public IP. - Open required firewall ports
If using UFW:bashsudo ufw allow 80,443/tcp sudo ufw status
If using a cloud firewall or security group, also allow inbound TCP80and443there. - Install Nginx and Certbotbash
sudo apt update sudo apt install -y nginx certbot python3-certbot-nginx - Create or verify the Nginx server block for HTTP
Example file:/etc/nginx/sites-available/flaskappnginxserver { listen 80; server_name example.com www.example.com; location / { proxy_pass http://127.0.0.1:8000; include proxy_params; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
If your Flask app is served through Gunicorn, make sure Gunicorn is already working first. If not, use Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide) or Deploy Flask on Ubuntu VPS (Step-by-Step). - Enable the sitebash
sudo ln -s /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/
If the default site conflicts, remove it:bashsudo rm -f /etc/nginx/sites-enabled/default - Validate and reload Nginxbash
sudo nginx -t sudo systemctl reload nginx
Test plain HTTP before requesting a certificate:bashcurl -I http://example.com - Request and install the certificate with Certbotbash
sudo certbot --nginx -d example.com -d www.example.com
Choose the redirect option when prompted. - Verify the generated TLS configuration
Certbot should update your Nginx config to include a TLS server block similar to:nginxserver { listen 443 ssl; 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; location / { proxy_pass http://127.0.0.1:8000; include proxy_params; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } - Force HTTPS if Certbot did not add the redirect
Use a dedicated port80redirect block:nginxserver { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri; } - Reload and test HTTPSbash
sudo nginx -t sudo systemctl reload nginx curl -I https://example.com
Confirm in a browser that the certificate is valid and trusted. - Verify automatic renewal
Check the systemd timer:bashsystemctl list-timers | grep certbot sudo systemctl status certbot.timer
Test renewal without making changes:bashsudo certbot renew --dry-run - Ensure Flask trusts the forwarded HTTPS scheme
If Flask generates wrong redirect URLs or enters redirect loops, make sure Nginx sends:nginxproxy_set_header X-Forwarded-Proto $scheme;
If needed, configure proxy header handling in your Flask or WSGI stack so the app correctly sees the original request scheme as HTTPS.
Common Causes
- DNS not pointing to the correct server IP → Let’s Encrypt validation reaches the wrong host or times out → Update A/AAAA records and wait for propagation.
- Nginx
server_namemismatch → Certbot modifies or validates the wrong server block → Setserver_name example.com www.example.com;on the intended site and reload Nginx. - Port 80 blocked by firewall → HTTP challenge fails before certificate issuance → Open inbound TCP
80at the OS and cloud firewall. - Port 443 blocked → HTTPS works internally but is unreachable externally → Open inbound TCP
443. - Default Nginx site taking precedence → Requests hit the wrong server block → Remove or disable conflicting configs and confirm with
nginx -T. - Invalid Nginx config → Certbot cannot install certificate changes safely → Fix syntax errors, test with
nginx -t, then rerun Certbot. - Wrong certificate paths or stale config → Nginx serves an old or missing certificate → Verify
/etc/letsencrypt/live/<domain>/paths and reload Nginx. - No HTTP-to-HTTPS redirect → Site loads on both protocols or remains on HTTP → Add a port
80redirect block or use the Certbot redirect option. - Proxy headers missing → Flask generates insecure URLs or redirect loops behind HTTPS → Send
X-Forwarded-Protoand configure proxy middleware correctly. - Renewal not configured or failing → Certificate expires after initial setup → Verify
certbot.timerand runcertbot renew --dry-run. - AAAA record points to a different server → Validation or browser traffic uses IPv6 and fails unexpectedly → Fix or remove the incorrect AAAA record.
- Certbot challenge path intercepted by custom routing or redirect logic → ACME validation fails → Simplify port
80handling during issuance and confirm/.well-known/acme-challenge/is reachable.
Debugging Section
Check the following commands and outputs:
dig +short example.com
dig +short www.example.com
curl -I http://example.com
curl -I https://example.com
sudo nginx -t
sudo nginx -T | less
sudo systemctl status nginx
sudo ss -tulpn | grep -E ':80|:443'
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log
sudo certbot certificates
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run
sudo systemctl status certbot.timer
journalctl -u nginx --no-pager -n 100
sudo grep -R "server_name\|ssl_certificate\|listen 443" /etc/nginx/sites-enabled /etc/nginx/conf.d
What to look for:
digreturns the correct public IP for all hostnames.curl -I http://example.comreturns200,301, or302, not a timeout or wrong host response.curl -I https://example.comreturns a valid HTTPS response.nginx -tshowssyntax is okandtest is successful.nginx -Tshows the expectedserver_name, redirect block, and certificate paths./var/log/letsencrypt/letsencrypt.logcontains the reason Certbot failed if issuance or renewal breaks.ss -tulpnconfirms Nginx is listening on:80and:443.
If HTTPS stopped working after a previous successful setup, see Flask HTTPS Not Working After Certbot.
Checklist
- Domain A/AAAA records point to the correct server IP
- Ports
80and443are open in OS and cloud firewall rules - Nginx
server_namematches the domain exactly -
nginx -tpasses with no errors -
certbot --nginxcompleted successfully - HTTP requests redirect to HTTPS
- HTTPS certificate is valid and trusted in the browser
-
certbot.timeror renewal cron is active -
certbot renew --dry-runsucceeds - Flask receives correct forwarded scheme headers if required
For a broader production validation pass, use Flask Production Checklist (Everything You Must Do).
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Deploy Flask on Ubuntu VPS (Step-by-Step)
- Flask HTTPS Not Working After Certbot
- Flask Production Checklist (Everything You Must Do)
FAQ
Q: Should I enable HTTPS in Flask itself?
A: No. Use Nginx to terminate TLS and proxy to Gunicorn or the Flask app internally.
Q: Why does Certbot say it cannot authenticate the domain?
A: The domain likely does not resolve to this server, port 80 is blocked, or Nginx is serving the wrong virtual host.
Q: Can I skip www and secure only the apex domain?
A: Yes. Request a certificate only for the hostnames you actually serve.
Q: Why am I getting redirect loops after enabling HTTPS?
A: Nginx may be redirecting incorrectly, or Flask may not trust forwarded HTTPS headers and thinks requests are plain HTTP.
Q: How do I know renewal will work later?
A: Check certbot.timer and run sudo certbot renew --dry-run successfully.
Final Takeaway
For Flask in production, HTTPS is an Nginx and certificate management task. If DNS, server_name, port access, and Certbot renewal are all correct, HTTPS setup is straightforward and stable.