Fix HTTPS Not Working for Django Behind Nginx
Fix a Django site where HTTPS does not load after installing a certificate: open port 443 in the firewall and cloud security group, add a listen 443 ssl block, check DNS and the certificate, and make Django aware of HTTPS.
Problem statement
http://example.com works, Certbot said "Congratulations", yet https://example.com times out, shows "connection refused", or loads a certificate warning. Sometimes HTTPS works on the server itself (curl -k https://localhost) but not from the internet.
HTTPS needs four things to line up: traffic on port 443 must reach the server, Nginx must listen on 443 with TLS, the certificate must match the name in the browser, and Django must know the original request was HTTPS. Which of those is missing is visible from the kind of failure you see.
- Timeout: open TCP 443 in
ufwand in the cloud provider's security group or firewall. - Connection refused: add a
serverblock withlisten 443 ssl;and reload Nginx; checkss -ltnp | grep :443. - Certificate warning: the certificate does not cover the hostname, or the chain is incomplete; use
fullchain.pem. - Redirect loop or CSRF failures after HTTPS works: set
SECURE_PROXY_SSL_HEADERandX-Forwarded-Proto. - Test from outside with
curl -vI https://example.com.
Step-by-step solution
1. Classify the failure from outside the server
curl -vI https://example.com 2>&1 | head -20
Connection timed out→ packets are dropped before reaching Nginx (firewall, security group, wrong IP in DNS).Connection refused→ the server answers but nothing listens on 443.SSL certificate problemorno alternative certificate subject name matches→ TLS works but the certificate is wrong.- A response, but the browser shows warnings or loops → Django or mixed content problems.
2. Open port 443 everywhere
On the server:
sudo ufw status
sudo ufw allow 'Nginx Full' # opens 80 and 443
Then the provider's network firewall, which is separate and checked first: AWS security groups, DigitalOcean Cloud Firewalls, Hetzner Firewalls, Oracle Cloud security lists (plus its default iptables rules on the instance), and Google Cloud VPC firewall rules. Each needs an inbound rule for TCP 443 from anywhere. Forgetting this layer is the most common cause of HTTPS timeouts on a fresh server.
3. Make Nginx listen on 443
sudo ss -ltnp | grep -E ':(80|443) '
If only :80 appears, Certbot may have obtained the certificate with certonly without editing Nginx, or the edit went into a file that is not enabled. Add the HTTPS block yourself:
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
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://unix:/run/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
sudo nginx -t && sudo systemctl reload nginx
4. Check the certificate
sudo certbot certificates
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName -dates
The subjectAltName list must include the exact name in the browser (www.example.com and example.com are different names). Always use fullchain.pem, not cert.pem; without the intermediate certificate, some clients (curl, Android, Python requests) reject the connection even though desktop browsers may accept it.
5. Tell Django the request was HTTPS
Once TLS terminates at Nginx, Django receives plain HTTP from the proxy. Without telling it otherwise, request.is_secure() is false, so SECURE_SSL_REDIRECT loops and CSRF origin checks fail:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = ["https://example.com", "https://www.example.com"]
Only set SECURE_PROXY_SSL_HEADER when Nginx always sets X-Forwarded-Proto itself, as in the block above; otherwise a client could forge the header.
Verification check:
curl -sI http://example.com | grep -i location # 301 to https
curl -sI https://example.com | head -1 # HTTP/2 200
python manage.py check --deploy
Also test from a phone on mobile data, which avoids local DNS caches and corporate proxies.
Explanation
Certbot's success message only means a certificate was issued; it confirms port 80 worked for the HTTP-01 challenge, not that port 443 is reachable. Cloud providers filter traffic before it reaches the VM, so ufw allow 443 alone does nothing if the security group blocks it. And because TLS ends at Nginx, the HTTPS-ness of the request is information Nginx must forward and Django must be told to trust, which is the step that turns "HTTPS loads" into "HTTPS works".
Edge cases / notes
- IPv6. If DNS has an AAAA record but the server has no IPv6 listener or firewall rule, some clients fail while others work. Add
listen [::]:443 ssl;or remove the AAAA record. - Cloudflare. With the proxy enabled, Cloudflare connects to your origin; set SSL mode to Full (strict) and install an origin or Let's Encrypt certificate.
- Load balancers. If an ALB terminates TLS, Nginx receives HTTP and should pass the load balancer's
X-Forwarded-Protorather than$scheme. - HSTS. Add
SECURE_HSTS_SECONDSonly after HTTPS works on every subdomain you include.
Next steps
- Set up certificates properly with Configure HTTPS With Let's Encrypt for Django.
- If HTTPS loads but redirects forever, see Fix Django Too Many Redirects With HTTPS.
FAQ
Certbot succeeded, why does HTTPS time out?
Certbot validated over port 80. Port 443 is probably blocked by the cloud firewall or security group.
Do I need to restart Gunicorn after adding HTTPS?
Only after changing Django settings such as SECURE_PROXY_SSL_HEADER. Nginx needs a reload for its own changes.
Why does curl reject the certificate when the browser accepts it?
You are probably serving cert.pem without the intermediate. Use fullchain.pem.
Should Nginx or Django redirect HTTP to HTTPS?
Either works. Redirecting in Nginx is cheaper; keep SECURE_SSL_REDIRECT consistent with it.