Deploy Django on AWS Elastic Beanstalk
Deploy Django to AWS Elastic Beanstalk with the EB CLI, Gunicorn, RDS PostgreSQL, leader-only migrations, static file mapping, and working health checks.
Problem statement
AWS Elastic Beanstalk creates the EC2 instances, Auto Scaling group, load balancer, and Nginx proxy for you, then deploys your application bundle onto them. For Django, the official tutorial gets a demo project online, but it uses SQLite, hardcodes ALLOWED_HOSTS, and leaves out problems that appear as soon as you run a real app:
- the load balancer health check fails because Django rejects the instance IP as a
Host - migrations run on every instance at the same time
- static files are served by Django instead of the proxy
- the database lives inside the environment and is deleted with it
This guide deploys Django to the Amazon Linux 2023 Python platform with a separately managed RDS PostgreSQL database, a Procfile-controlled Gunicorn process, leader-only migrations, and a health check that actually passes.
Quick answer
- Add
requirements.txt, aProcfilewith a Gunicornwebcommand, and.ebextensionsconfig files. - Map
/staticto your collected static directory with theaws:elasticbeanstalk:environment:proxy:staticfilesnamespace. - Run
migrateincontainer_commandswithleader_only: true. - Create RDS PostgreSQL outside the environment and pass credentials as environment properties.
- Add a
/healthz/endpoint and allow the health check to passALLOWED_HOSTSvalidation. eb init,eb create,eb setenv, theneb deploy.- Verify with
eb health,eb logs, and real HTTPS requests.
Step-by-step solution
1. Project files
.
├── .ebextensions/
│ ├── 01_django.config
│ └── 02_migrate.config
├── .ebignore
├── Procfile
├── manage.py
├── requirements.txt
└── config/
├── settings.py
├── urls.py
└── wsgi.py
requirements.txt needs at least:
Django==5.2.6
gunicorn==23.0.0
psycopg[binary]==3.2.10
2. Define the web process with a Procfile
On Amazon Linux 2 and later Python platforms, Elastic Beanstalk runs Gunicorn for you using WSGIPath, but a Procfile gives you explicit control over workers and timeouts:
web: gunicorn --bind 127.0.0.1:8000 --workers 3 --threads 2 --timeout 60 --access-logfile - config.wsgi:application
The platform's Nginx proxy forwards requests to port 8000 by default, so bind Gunicorn there. When a Procfile exists, it takes precedence over WSGIPath.
3. Configure environment options
.ebextensions/01_django.config:
option_settings:
aws:elasticbeanstalk:application:environment:
DJANGO_SETTINGS_MODULE: config.settings
aws:elasticbeanstalk:environment:proxy:staticfiles:
/static: staticfiles
aws:elasticbeanstalk:application:
Application Healthcheck URL: /healthz/
DJANGO_SETTINGS_MODULEis needed somanage.pycommands incontainer_commandscan load settings.- The
staticfilesnamespace tells the Nginx proxy to serve/staticdirectly from thestaticfilesdirectory in your bundle, so Django never handles those requests. Application Healthcheck URLsets the path used for enhanced health reporting and the load balancer health check.
4. Run migrations once per deploy
.ebextensions/02_migrate.config:
container_commands:
01_collectstatic:
command: "source /var/app/venv/*/bin/activate && python3 manage.py collectstatic --noinput"
02_migrate:
command: "source /var/app/venv/*/bin/activate && python3 manage.py migrate --noinput"
leader_only: true
container_commands run after the application bundle is extracted but before the new version starts serving. leader_only: true limits the migration to a single instance, so several instances do not race to apply the same migration. collectstatic runs on every instance because each one serves its own copy of the static files.
Migrations still run while old instances may be serving traffic during rolling deploys, so keep them backward compatible with the previous release.
5. Settings that fit Elastic Beanstalk
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = False
ALLOWED_HOSTS = [
h.strip() for h in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",") if h.strip()
]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["RDS_DB_NAME"],
"USER": os.environ["RDS_USERNAME"],
"PASSWORD": os.environ["RDS_PASSWORD"],
"HOST": os.environ["RDS_HOSTNAME"],
"PORT": os.environ.get("RDS_PORT", "5432"),
"CONN_MAX_AGE": 60,
"OPTIONS": {"sslmode": "require"},
}
}
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
The RDS_* names match what Elastic Beanstalk sets when a database is coupled to the environment, which makes it easy to switch later. Here you set them yourself because the database is created separately.
6. Make the health check pass
The Application Load Balancer sends health checks directly to each instance. Depending on configuration, the Host header can be the instance's private IP address, which is not in ALLOWED_HOSTS. Django then returns 400 Bad Request, the target is marked unhealthy, and the environment turns red even though the app works.
Answer the health check before host validation runs, with a tiny middleware placed first:
# config/healthcheck.py
from django.http import HttpResponse
class HealthCheckMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path == "/healthz/":
return HttpResponse("ok", content_type="text/plain")
return self.get_response(request)
MIDDLEWARE = [
"config.healthcheck.HealthCheckMiddleware",
"django.middleware.security.SecurityMiddleware",
# ...
]
ALLOWED_HOSTS is enforced when code calls request.get_host(), which this middleware never does, so it responds without weakening host validation for real traffic. Keep the response static: do not expose version details or database state on an unauthenticated URL.
7. Create the RDS database outside the environment
Create an RDS PostgreSQL instance in the same VPC through the console or infrastructure as code. Keep it out of the Beanstalk environment definition, because a database added inside the environment is tied to its lifecycle and deleted when you terminate the environment.
Allow inbound PostgreSQL (port 5432) on the RDS security group only from the security group used by the Beanstalk instances.
8. Create the environment
pip install awsebcli
eb init -p python-3.13 my-django-app --region eu-west-1
eb create django-prod --elb-type application
Check which Python platform branches exist in your region with eb platform list and choose the one you test against. eb create provisions the load balancer, Auto Scaling group, and instances and performs the first deploy. The first deploy fails if required environment variables are missing, so set them immediately:
eb setenv \
DJANGO_SECRET_KEY='generated-secret' \
DJANGO_ALLOWED_HOSTS='django-prod.eba-abc123.eu-west-1.elasticbeanstalk.com,www.example.com' \
RDS_HOSTNAME='mydb.abcdefghij.eu-west-1.rds.amazonaws.com' \
RDS_DB_NAME=django \
RDS_USERNAME=django \
RDS_PASSWORD='generated-db-password'
Environment properties are visible to anyone with Beanstalk console access. For stricter secret handling, store credentials in AWS Secrets Manager and load them at startup.
9. Deploy updates
Keep local-only files out of the bundle with .ebignore:
.venv/
.git/
*.sqlite3
.env
__pycache__/
Then deploy:
eb deploy django-prod
When .ebignore exists, the EB CLI uses it instead of .gitignore and deploys the working directory contents, including uncommitted changes. Deploy from a clean checkout in CI to avoid shipping local edits.
10. Add HTTPS
Request a certificate in AWS Certificate Manager, then add an HTTPS listener to the load balancer, for example in .ebextensions/03_https.config:
option_settings:
aws:elbv2:listener:443:
ListenerEnabled: 'true'
Protocol: HTTPS
SSLCertificateArns: arn:aws:acm:eu-west-1:123456789012:certificate/example-id
Redirect HTTP to HTTPS at the load balancer or with Nginx, and set SECURE_SSL_REDIRECT = True only after you confirm X-Forwarded-Proto arrives as expected.
11. Verify
eb status
eb health --refresh
eb logs --all
curl -I https://www.example.com/
curl -I https://www.example.com/static/admin/css/base.css
Verification check:
eb healthshows all instancesOk/static/responses come from Nginx (no Django middleware headers)eb logsshows exactly onemigraterun per deploy, on the leader- the admin loads with styling over HTTPS
Rollback
Every deploy creates an application version. Roll back by deploying a previous version label:
eb appversion
eb deploy django-prod --version app-240901_120000
Database migrations are not rolled back. Take an RDS snapshot before risky migrations and prefer backward-compatible changes so the previous version can run against the new schema.
Explanation
Elastic Beanstalk is a deployment orchestrator over standard AWS resources. On each deploy it extracts your bundle into a staging directory, installs requirements.txt into a virtual environment under /var/app/venv/, runs container_commands, and then swaps the new version in and restarts the web process from your Procfile behind Nginx.
The load balancer health check sits outside that flow. It talks to instances directly, so anything Django does with the Host header affects whether instances are considered healthy. Handling the health check path before host validation solves that without adding private IPs to ALLOWED_HOSTS.
Edge cases / notes
Deploys time out
Long container_commands (big migrations, slow collectstatic to S3) can exceed the deploy timeout. Move heavy data migrations to a separate management command and increase the command timeout option only if needed.
Rolling deployments and capacity
Rolling deploys take some instances out of service. Use "Rolling with additional batch" or "Immutable" deployment policies in production so capacity does not drop during a release.
Celery workers
A web environment should not also run Celery. Use a separate worker environment or a separate process platform such as ECS for background workers.
Troubleshooting failed deployments
Elastic Beanstalk rolls a failed deploy back to the previous version in many cases, which can make the root cause easy to miss. Always pull the logs from the failed attempt:
eb events --follow
eb logs --all
The bundle eb logs --all downloads contains, among others:
/var/log/eb-engine.log: each deployment step, includingcontainer_commandsoutput and failures/var/log/web.stdout.log: output from yourwebprocess (Gunicorn and Django)/var/log/nginx/error.log: proxy errors such asconnect() failed (111: Connection refused), which mean Gunicorn is not listening on port 8000
Match the symptom to the step:
| Symptom | Likely cause |
|---|---|
Command 02_migrate failed in eb-engine.log | migration error, database unreachable from instances, or missing DJANGO_SETTINGS_MODULE |
502 Bad Gateway | Gunicorn crashed at import or bound to a different port |
Environment health Severe with 4xx responses | health check returning 400 due to ALLOWED_HOSTS |
Static files 404 | staticfiles mapping path does not match STATIC_ROOT, or collectstatic did not run |
Connecting to an instance
For deeper debugging, SSH into an instance (if you configured a key pair) and load the environment the platform uses:
eb ssh django-prod
sudo /opt/elasticbeanstalk/bin/get-config environment
source /var/app/venv/*/bin/activate
cd /var/app/current
get-config environment prints the environment properties as JSON, which helps confirm what the app actually receives. Treat instances as disposable: fix problems in configuration files and redeploy rather than editing files in place, because Auto Scaling replaces instances.
Common mistakes
- Running
migratewithoutleader_only. Every instance runs it concurrently during scale-out or rolling deploys. - Putting secrets in
.ebextensions. Those files are committed to the repository. Useeb setenvor Secrets Manager. - Forgetting that
.ebignorereplaces.gitignore. Once.ebignoreexists, anything not listed in it is uploaded, including local.envfiles. - Large application bundles. Uploading
node_modulesor media directories slows every deploy and may exceed bundle size limits.
Instance and worker sizing
Each instance runs the Procfile's Gunicorn command. Size --workers to the instance type's vCPUs and memory, and scale out with the Auto Scaling group rather than giant instances. Configure scaling triggers on CPU or request count, and keep a minimum of two instances in production so a single instance replacement does not cause downtime.
Next steps
- Compare with a self-managed server in Deploy Django on AWS EC2 Step by Step.
- If instances keep going unhealthy, see Fix Load Balancer Health Checks Failing for Django.
- For container-based deploys, see Deploy Django on AWS ECS Fargate.
FAQ
Should I attach the RDS database to the Elastic Beanstalk environment?
Not for production. A database created inside the environment is terminated with the environment. Create RDS separately and connect with environment properties or Secrets Manager.
Why is my Elastic Beanstalk environment red when the site works?
Usually the load balancer health check receives 400 Bad Request because its Host header is not in ALLOWED_HOSTS. Serve the health check path before host validation, or point the check at a path that returns 200 for that request.
How do I run Django migrations on Elastic Beanstalk?
Use container_commands in an .ebextensions file with leader_only: true, after activating the platform virtual environment. That runs the migration once per deploy before the new version starts.
Where does Elastic Beanstalk install my Python packages?
On Amazon Linux 2 and 2023 Python platforms, dependencies from requirements.txt are installed into a virtual environment under /var/app/venv/, which is why commands activate /var/app/venv/*/bin/activate first.
Does Elastic Beanstalk serve Django static files?
It can. Map /static to your STATIC_ROOT directory with the aws:elasticbeanstalk:environment:proxy:staticfiles option so the Nginx proxy serves the files, and run collectstatic during deploy.