Run Django Behind an AWS Application Load Balancer
Configure Django correctly behind an AWS Application Load Balancer: HTTPS listeners with ACM, X-Forwarded-Proto and client IPs, ALLOWED_HOSTS for health checks, idle and keepalive timeouts, stickiness, and deregistration delay for deploys.
Problem statement
An Application Load Balancer (ALB) in front of Django on EC2 or ECS handles TLS, spreads traffic across instances, and removes unhealthy ones. Django has to agree with the ALB about several things for everything to work: which requests are HTTPS, what the client's real IP is, which hosts are allowed (including the ALB's own health checks), and how long idle connections live. Get these wrong and you see redirect loops, 400s on health checks, every request logged from the ALB's IP, or intermittent 502s.
- Create HTTPS (443) and HTTP (80, redirect to 443) listeners with an ACM certificate.
- Use an HTTP target group to Nginx or Gunicorn on the instances, health check path
/health/. - In Django:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https"). - Allow health checks: they use the instance IP as Host, so exempt
/health/from host validation or answer it in Nginx. - Get real client IPs from
X-Forwarded-For(Nginxreal_ipmodule or Gunicornforwarded_allow_ips). - Set target keepalive longer than the ALB idle timeout, and a deregistration delay that covers your longest requests.
Step-by-step solution
1. Listeners and certificate
Request a public certificate in ACM for example.com and www.example.com (DNS validation), then create:
- Listener HTTPS:443 → forward to the target group, using the ACM certificate and a modern TLS policy.
- Listener HTTP:80 → redirect to HTTPS:443 (status 301).
The ALB terminates TLS; traffic to instances is HTTP inside the VPC.
2. Target group and security groups
Create a target group with protocol HTTP, port 80 (Nginx on the instance) or 8000 (Gunicorn directly), health check path /health/, success codes 200. Security groups: the ALB allows 80 and 443 from the internet; the instances allow the target port only from the ALB's security group. That restriction is what makes forwarded headers trustworthy.
3. Tell Django about HTTPS
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"]
The ALB sets X-Forwarded-Proto: https. If Nginx sits on the instance, pass the ALB's header through rather than replacing it with Nginx's $scheme (which is http):
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
4. Pass health checks
ALB health checks send Host: <instance-private-ip>:<port>, which ALLOWED_HOSTS rejects with a 400. Options:
- Answer
/health/in Nginx:location = /health/ { access_log off; return 200 "ok"; }(checks Nginx only). - Exempt the path in a small middleware placed before
CommonMiddlewarethat returns 200 for/health/after checking the database. - Add the private IPs dynamically at startup (fetch from instance metadata) to
ALLOWED_HOSTS.
Do not use ALLOWED_HOSTS = ["*"].
5. Real client IPs
With Nginx on the instance:
set_real_ip_from 10.0.0.0/16; # your VPC CIDR
real_ip_header X-Forwarded-For;
real_ip_recursive on;
With Gunicorn directly, set --forwarded-allow-ips to the VPC range so it trusts the header, and read REMOTE_ADDR in Django. Rate limiting, audit logs, and django-axes all depend on this.
6. Timeouts and deploys
- Idle timeout on the ALB defaults to 60 seconds. Keep target keepalive longer (Nginx
keepalive_timeout 75s) to avoid intermittent 502s. - Deregistration delay (default 300 s) is how long the ALB waits for in-flight requests on a deregistering target. Set it just above your longest normal request, for example 30-60 s, so deploys are not slowed needlessly.
- Stickiness is usually unnecessary for Django with database or Redis sessions; leave it off.
Verification check: curl -I http://example.com returns 301 to HTTPS, curl -I https://example.com returns 200, targets show healthy in the console, Django logs show real client IPs, and python manage.py check --deploy passes.
Explanation
An ALB is a layer 7 reverse proxy managed by AWS. Because it terminates TLS and opens new connections to targets, Django loses direct knowledge of the protocol and client address, which the ALB restores through X-Forwarded-* headers. Django deliberately ignores those headers until told which ones to trust, since a client could otherwise forge them. The remaining settings align connection lifetimes and health checking so that the ALB's view of target health matches reality.
Edge cases / notes
- WebSockets work through ALBs; raise the idle timeout or send pings for long-lived connections.
- WAF. Attach AWS WAF to the ALB for managed rule sets and rate limiting.
- Large uploads stream through the ALB; Nginx
client_max_body_sizeon targets still applies. - IPv6. Dualstack ALBs accept IPv6 clients;
X-Forwarded-Forthen contains IPv6 addresses.
Next steps
- Diagnose intermittent errors with Fix AWS Application Load Balancer 502s for Django.
- If health checks fail, see Fix Django Health Check Failing on a Load Balancer.
FAQ
Do I need Nginx on the instances behind an ALB?
Not strictly; the ALB can target Gunicorn directly. Nginx still helps with buffering, static files, and request limits.
Why does Django think requests are HTTP behind the ALB?
TLS ends at the ALB. Set SECURE_PROXY_SSL_HEADER so Django trusts X-Forwarded-Proto.
Why do ALB health checks get 400?
They send the instance IP as Host. Answer /health/ in Nginx or exempt it from host validation.
Should I enable sticky sessions?
Usually not. Django sessions in the database or Redis work on any target.