Reference Guide Detailed deployment notes with production context and concrete examples.

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

bash
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

  1. Confirm the domain points to the server
    Run:
    bash
    dig +short example.com
    dig +short www.example.com
    

    Verify the returned IP matches your server’s public IP.
  2. Open required firewall ports
    If using UFW:
    bash
    sudo ufw allow 80,443/tcp
    sudo ufw status
    

    If using a cloud firewall or security group, also allow inbound TCP 80 and 443 there.
  3. Install Nginx and Certbot
    bash
    sudo apt update
    sudo apt install -y nginx certbot python3-certbot-nginx
    
  4. Create or verify the Nginx server block for HTTP
    Example file: /etc/nginx/sites-available/flaskapp
    nginx
    server {
        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).
  5. Enable the site
    bash
    sudo ln -s /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/
    

    If the default site conflicts, remove it:
    bash
    sudo rm -f /etc/nginx/sites-enabled/default
    
  6. Validate and reload Nginx
    bash
    sudo nginx -t
    sudo systemctl reload nginx
    

    Test plain HTTP before requesting a certificate:
    bash
    curl -I http://example.com
    
  7. Request and install the certificate with Certbot
    bash
    sudo certbot --nginx -d example.com -d www.example.com
    

    Choose the redirect option when prompted.
  8. Verify the generated TLS configuration
    Certbot should update your Nginx config to include a TLS server block similar to:
    nginx
    server {
        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;
        }
    }
    
  9. Force HTTPS if Certbot did not add the redirect
    Use a dedicated port 80 redirect block:
    nginx
    server {
        listen 80;
        server_name example.com www.example.com;
        return 301 https://$host$request_uri;
    }
    
  10. Reload and test HTTPS
    bash
    sudo nginx -t
    sudo systemctl reload nginx
    curl -I https://example.com
    

    Confirm in a browser that the certificate is valid and trusted.
  11. Verify automatic renewal
    Check the systemd timer:
    bash
    systemctl list-timers | grep certbot
    sudo systemctl status certbot.timer
    

    Test renewal without making changes:
    bash
    sudo certbot renew --dry-run
    
  12. Ensure Flask trusts the forwarded HTTPS scheme
    If Flask generates wrong redirect URLs or enters redirect loops, make sure Nginx sends:
    nginx
    proxy_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_name mismatch → Certbot modifies or validates the wrong server block → Set server_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 80 at 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 80 redirect block or use the Certbot redirect option.
  • Proxy headers missing → Flask generates insecure URLs or redirect loops behind HTTPS → Send X-Forwarded-Proto and configure proxy middleware correctly.
  • Renewal not configured or failing → Certificate expires after initial setup → Verify certbot.timer and run certbot 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 80 handling during issuance and confirm /.well-known/acme-challenge/ is reachable.

Debugging Section

Check the following commands and outputs:

bash
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:

  • dig returns the correct public IP for all hostnames.
  • curl -I http://example.com returns 200, 301, or 302, not a timeout or wrong host response.
  • curl -I https://example.com returns a valid HTTPS response.
  • nginx -t shows syntax is ok and test is successful.
  • nginx -T shows the expected server_name, redirect block, and certificate paths.
  • /var/log/letsencrypt/letsencrypt.log contains the reason Certbot failed if issuance or renewal breaks.
  • ss -tulpn confirms Nginx is listening on :80 and :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 80 and 443 are open in OS and cloud firewall rules
  • Nginx server_name matches the domain exactly
  • nginx -t passes with no errors
  • certbot --nginx completed successfully
  • HTTP requests redirect to HTTPS
  • HTTPS certificate is valid and trusted in the browser
  • certbot.timer or renewal cron is active
  • certbot renew --dry-run succeeds
  • Flask receives correct forwarded scheme headers if required

For a broader production validation pass, use 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.