How to Fix Nginx ERR_TOO_MANY_REDIRECTS After Let’s Encrypt SSL Implementation in Django/Gunicorn Setup


2 views

When implementing Let's Encrypt SSL certificates using Certbot in a Django/Gunicorn/Nginx environment, the most common configuration issue that causes infinite redirect loops is improper HTTP-to-HTTPS redirection logic. The symptom you're seeing (ERR_TOO_MANY_REDIRECTS) occurs when the browser gets stuck bouncing between HTTP and HTTPS versions of your site.

Examining your Nginx configuration, I notice two critical issues in the server blocks:

nginx
# Problematic configuration causing redirect loops
server {
if ($host = www.example.com) {
return 301 https://$host$request_uri;
}

if ($host = example.com) {
return 301 https://$host$request_uri;
}

listen 80;
server_name www.example.com example.com;
return 404; # This line will never be reached
}

Here's the corrected version that prevents redirect loops:

nginx
# HTTP server block - proper redirection
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}

# HTTPS server block
server {
listen 443 ssl;
server_name example.com www.example.com;

# SSL configuration
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

# Django application configuration
location = /favicon.ico {
access_log off;
log_not_found off;
}

location /static/ {
root /home/myproject;
}

location / {
include proxy_params;
proxy_pass http://unix:/run/gunicorn.sock;
}
}

  • Simplified HTTP server block that always redirects to HTTPS
  • Eliminated conditional checks that could cause redirect loops
  • Consistent canonical URL handling (always redirecting to example.com)
  • Proper separation of HTTP and HTTPS server blocks

After implementing these changes, consider these best practices:

bash
# Test Nginx configuration before applying
sudo nginx -t

# Reload Nginx configuration
sudo systemctl reload nginx

# Verify HTTPS implementation
curl -I https://example.com

For Django applications, ensure your settings.py includes:

python
# Required Django security settings for HTTPS
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

If issues persist, try these diagnostic steps:

  1. Clear browser cache and cookies
  2. Test with incognito/private browsing mode
  3. Use curl or wget to inspect headers
  4. Check Nginx error logs: sudo tail -f /var/log/nginx/error.log

When implementing SSL with Let's Encrypt on a Django/Nginx/Gunicorn stack, the infinite redirect loop (ERR_TOO_MANY_REDIRECTS) typically occurs due to misconfigured server blocks. The key issue lies in how Nginx handles HTTP to HTTPS redirection while maintaining proper domain resolution.

The current setup has two problematic areas in the server blocks:

server {
    if ($host = www.example.com) {
        return 301 https://$host$request_uri;
    } # This creates a loop when already on HTTPS
    
    if ($host = example.com) {
        return 301 https://$host$request_uri;
    } # Same issue here
    
    listen 80;
    server_name www.example.com example.com;
    return 404; # This actually never executes
}

Here's the corrected configuration that properly handles SSL redirection:

# HTTP server block - handles redirect to HTTPS
server {
    listen 80;
    server_name example.com www.example.com;
    
    # Standardize on non-www or www version (choose one)
    if ($host = www.example.com) {
        return 301 https://example.com$request_uri;
    }
    
    return 301 https://$host$request_uri;
}

# HTTPS server block - main configuration
server {
    listen 443 ssl;
    server_name example.com www.example.com;
    
    # SSL configuration
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    
    # Django application configuration
    location = /favicon.ico {
        access_log off;
        log_not_found off;
    }
    
    location /static/ {
        alias /home/myproject/static/;
    }
    
    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}

The critical fixes include:

  • Separate clean server blocks for HTTP (port 80) and HTTPS (port 443)
  • Consistent domain handling (choose either www or non-www as canonical)
  • Removed conditional checks that caused infinite loops
  • Fixed static files path using 'alias' instead of 'root'

After implementing these changes:

sudo nginx -t  # Test configuration
sudo systemctl restart nginx  # Apply changes
sudo systemctl restart gunicorn  # Restart application server

Use these curl commands to verify:

curl -I http://example.com  # Should show 301 to HTTPS
curl -I https://example.com  # Should show 200 OK

1. Mixed content issues - ensure all resources (CSS, JS) use HTTPS
2. Django settings.py must include:

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

3. Cookie domain settings if using subdomains
4. HSTS headers configuration in Nginx