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.

Elastic Beanstalk deploy floweb deployapplication bundlePlatform buildrequirements, staticcontainer_commandsleader-only migrateGunicornbehind NginxBeanstalk orchestrates standard AWS resources; RDS stays outside the environment.

Quick answer

  1. Add requirements.txt, a Procfile with a Gunicorn web command, and .ebextensions config files.
  2. Map /static to your collected static directory with the aws:elasticbeanstalk:environment:proxy:staticfiles namespace.
  3. Run migrate in container_commands with leader_only: true.
  4. Create RDS PostgreSQL outside the environment and pass credentials as environment properties.
  5. Add a /healthz/ endpoint and allow the health check to pass ALLOWED_HOSTS validation.
  6. eb init, eb create, eb setenv, then eb deploy.
  7. 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_MODULE is needed so manage.py commands in container_commands can load settings.
  • The staticfiles namespace tells the Nginx proxy to serve /static directly from the staticfiles directory in your bundle, so Django never handles those requests.
  • Application Healthcheck URL sets 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
commands that run on each deploycontainer_commands:01_collectstatic:command: "… manage.py collectstatic --noinput"02_migrate:command: "… manage.py migrate --noinput"leader_only: true1231Runs on every instance: each serves its own static copy.2Names sort alphabetically, so the prefix sets the order.3One instance migrates, so several do not race.These run after extraction and before the new version serves trafficdjango-deployment.com
A failing command aborts the deploy, which is the behaviour you want for migrations.

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.

the health check is not your domainFailingALB checks / with the instance IP asHost✕Django returns 400 DisallowedHost✕environment turns red, deploys roll backPassing✓a cheap /healthz/ view✓host allowed, or middleware answersfirst✓health path set on the target groupNever fix this with ALLOWED_HOSTS = ["*"]django-deployment.com
The health view should not touch the database unless you want database outages to cycle instances.

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__/
deployment policies, side by sideDowntimeCapacity during deployAll at once✕yes✕none while it runsRolling✓no!reduced by one batchRolling + batch✓no✓fullImmutable✓no✓full, new instancesImmutable is the slowest and the easiest to roll backdjango-deployment.com
Mixed versions serve traffic during rolling deploys, so migrations must suit both.

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 health shows all instances Ok
  • /static/ responses come from Nginx (no Django middleware headers)
  • eb logs shows exactly one migrate run 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
where a failed deploy usually brokeSymptomUsual causeCommand 02_migrate faileddatabase unreachable,or env vars missingEnvironment health Severehealth check gets 400 or 500ModuleNotFoundError in web.stdout.logProcfile module path or requirementsDeploy times outslow migration or large bundleeb logs collects every log file from every instancedjango-deployment.com
Start with eb-engine.log for deploy steps and web.stdout.log for Django itself.

The bundle eb logs --all downloads contains, among others:

  • /var/log/eb-engine.log: each deployment step, including container_commands output and failures
  • /var/log/web.stdout.log: output from your web process (Gunicorn and Django)
  • /var/log/nginx/error.log: proxy errors such as connect() failed (111: Connection refused), which mean Gunicorn is not listening on port 8000

Match the symptom to the step:

SymptomLikely cause
Command 02_migrate failed in eb-engine.logmigration error, database unreachable from instances, or missing DJANGO_SETTINGS_MODULE
502 Bad GatewayGunicorn crashed at import or bound to a different port
Environment health Severe with 4xx responseshealth check returning 400 due to ALLOWED_HOSTS
Static files 404staticfiles 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 migrate without leader_only. Every instance runs it concurrently during scale-out or rolling deploys.
  • Putting secrets in .ebextensions. Those files are committed to the repository. Use eb setenv or Secrets Manager.
  • Forgetting that .ebignore replaces .gitignore. Once .ebignore exists, anything not listed in it is uploaded, including local .env files.
  • Large application bundles. Uploading node_modules or 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

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.

Keep going