Deployment
#django
#aws-ec2
#gunicorn
#nginx

Deploy Django on AWS EC2 Step by Step

To deploy Django on AWS EC2 reliably, you need more than python manage.py runserver.

Problem statement

To deploy Django on AWS EC2 reliably, you need more than python manage.py runserver. A production setup has to handle process supervision, reverse proxying, static files, database migrations, TLS, secrets, restarts, and rollback.

The common failure modes are predictable: Gunicorn is running but Nginx cannot reach it, static files return 404, ALLOWED_HOSTS blocks requests, migrations break the release, or HTTPS is enabled but Django still thinks requests are insecure. A good EC2 deployment avoids those issues with a repeatable structure and clear verification at each step.

Quick answer

A solid baseline for a Django AWS EC2 deployment is:

  • Ubuntu LTS on EC2
  • Python virtualenv
  • Gunicorn as the app server
  • Nginx as the reverse proxy
  • systemd for service management
  • PostgreSQL for production data
  • optional Redis for caching or background jobs
  • Let's Encrypt for TLS

High-level order:

  1. Provision Ubuntu EC2
  2. Restrict network access and secure SSH
  3. Install Python, Nginx, and system packages
  4. Set up PostgreSQL if you are running it on the instance
  5. Upload the app into a release-based directory layout
  6. Create a virtualenv and install dependencies
  7. Configure Django production settings and secrets
  8. Run checks, migrations, and collectstatic
  9. Configure Gunicorn with systemd
  10. Configure Nginx
  11. Enable HTTPS
  12. Verify the deployment
  13. Keep rollback paths for code and backups for data

Step-by-step solution

1) Provision the AWS EC2 instance

Use Ubuntu 22.04 LTS or 24.04 LTS. For a small app, t3.small or t3.medium is a reasonable starting point. Scale based on memory pressure, worker count, and database load.

In the EC2 security group, allow only:

  • 22 from your admin IP range
  • 80 from the internet
  • 443 from the internet

If this server will keep local uploads or a local database, size the EBS volume accordingly and enable snapshots.

Point your domain or subdomain to the instance public IP. An Elastic IP is useful if you want a stable address across instance stop/start cycles.

Verify

ssh -i your-key.pem ubuntu@your-server-ip

2) Secure the server before deploying Django

Create a non-root sudo user and use key-based SSH.

sudo adduser deploy
sudo usermod -aG sudo deploy
sudo mkdir -p /home/deploy/.ssh
sudo cp /home/ubuntu/.ssh/authorized_keys /home/deploy/.ssh/
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys

Now install basic protections and update packages:

sudo apt update && sudo apt upgrade -y
sudo apt install -y ufw fail2ban
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable

If you control all SSH clients and have verified key login works for deploy, disable password auth and root SSH login in /etc/ssh/sshd_config:

PasswordAuthentication no
PermitRootLogin no

Then reload SSH:

sudo systemctl reload ssh

Verify

ssh -i your-key.pem deploy@your-server-ip
sudo ufw status

Rollback note: do not close your current SSH session before confirming the new user can log in.

3) Install runtime and system packages

Install Python, venv tools, Nginx, Git, and build dependencies commonly needed by Django packages.

sudo apt install -y python3 python3-venv python3-pip \
    nginx git build-essential libpq-dev pkg-config

If your app uses PostgreSQL locally on the server:

sudo apt install -y postgresql postgresql-contrib

If your app uses Redis:

sudo apt install -y redis-server

For many production setups, a managed database is better than a local PostgreSQL install. But for a single EC2 deployment, local PostgreSQL is still common and simpler to start with.

Verify

python3 --version
nginx -v
git --version

4) Set up PostgreSQL if it will run on this EC2 instance

If you are using RDS or another managed database, skip this step and use that connection string in your environment file.

For local PostgreSQL, create the database and user before running Django migrations:

sudo -u postgres psql
CREATE DATABASE myapp;
CREATE USER myappuser WITH PASSWORD 'strongpassword';
ALTER ROLE myappuser SET client_encoding TO 'utf8';
ALTER ROLE myappuser SET default_transaction_isolation TO 'read committed';
ALTER ROLE myappuser SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE myapp TO myappuser;
\q

Verify

psql "postgresql://myappuser:strongpassword@127.0.0.1:5432/myapp" -c '\conninfo'

If local PostgreSQL is only for this app, make sure you also include it in your backup plan.

5) Upload the Django app with a rollback-friendly layout

Create a release-based directory structure:

sudo mkdir -p /srv/myapp/releases /srv/myapp/shared /srv/myapp/shared/static /srv/myapp/shared/media
sudo chown -R deploy:deploy /srv/myapp

Example layout:

  • /srv/myapp/releases/20260424-120000
  • /srv/myapp/current -> symlink to active release
  • /srv/myapp/shared for persistent data

Deploy from Git or a release artifact. Example with Git:

cd /srv/myapp/releases
git clone git@github.com:your-org/your-repo.git 20260424-120000
ln -sfn /srv/myapp/releases/20260424-120000 /srv/myapp/current

If the repo is private, use a deploy key or another reproducible method. Avoid editing code directly on the server.

Verify

ls -l /srv/myapp

6) Create the virtual environment and install dependencies

cd /srv/myapp/current
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt

Confirm Gunicorn is installed:

which gunicorn
gunicorn --version

Because this guide keeps .venv inside each release, every new release needs its own dependency install before you switch the /srv/myapp/current symlink. If you roll back to an older release, that release must still have a valid virtualenv and dependencies.

If your project is ASGI-first, Uvicorn may be more appropriate, but Gunicorn is a strong default for standard Django WSGI deployments.

7) Configure Django production settings

Store secrets outside the repo. One simple pattern is an env file referenced by systemd.

Create /etc/myapp.env:

sudo nano /etc/myapp.env

Example:

DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_SECRET_KEY=replace-me
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
DJANGO_CSRF_TRUSTED_ORIGINS=https://example.com,https://www.example.com
DATABASE_URL=postgresql://myappuser:strongpassword@127.0.0.1:5432/myapp
REDIS_URL=redis://127.0.0.1:6379/0

Secure it:

sudo chown root:deploy /etc/myapp.env
sudo chmod 640 /etc/myapp.env

Make sure only the Gunicorn service user can read this file; do not make it world-readable.

In Django production settings, make sure you set:

DEBUG = False
ALLOWED_HOSTS = ["example.com", "www.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com", "https://www.example.com"]

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

Enable SECURE_SSL_REDIRECT = True only after HTTPS is working correctly, or you can create redirect loops or make the site inaccessible during initial setup.

If you load hosts or origins from environment variables, parse them into Python lists rather than passing a raw comma-separated string directly into Django settings.

For static and media:

STATIC_URL = "/static/"
STATIC_ROOT = "/srv/myapp/shared/static"

MEDIA_URL = "/media/"
MEDIA_ROOT = "/srv/myapp/shared/media"

Run a deployment check:

cd /srv/myapp/current
source .venv/bin/activate
python manage.py check --deploy

Verify

Fix any warnings that are relevant to your setup before continuing.

8) Run release commands

Run checks and release commands from the active release:

cd /srv/myapp/current
source .venv/bin/activate
python manage.py check --deploy
python manage.py migrate
python manage.py collectstatic --noinput

Create a superuser only if you actually need admin access:

python manage.py createsuperuser

Rollback note: application rollback is easy; database rollback is not. Before risky schema changes, take a database backup and avoid destructive migrations without a recovery plan.

9) Configure Gunicorn with systemd

Create /etc/systemd/system/gunicorn-myapp.service:

[Unit]
Description=Gunicorn for myapp
After=network.target

[Service]
User=deploy
Group=www-data
WorkingDirectory=/srv/myapp/current
EnvironmentFile=/etc/myapp.env
RuntimeDirectory=gunicorn-myapp
RuntimeDirectoryMode=0755
ExecStart=/srv/myapp/current/.venv/bin/gunicorn \
    --workers 3 \
    --bind unix:/run/gunicorn-myapp/gunicorn.sock \
    config.wsgi:application
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable gunicorn-myapp
sudo systemctl start gunicorn-myapp
sudo systemctl status gunicorn-myapp

Inspect logs if needed:

journalctl -u gunicorn-myapp -n 50 --no-pager

Verify

sudo ls -l /run/gunicorn-myapp/gunicorn.sock

10) Configure Nginx as the reverse proxy

Create /etc/nginx/sites-available/myapp:

server {
    listen 80;
    server_name example.com www.example.com;

    client_max_body_size 20M;

    location /static/ {
        alias /srv/myapp/shared/static/;
    }

    location /media/ {
        alias /srv/myapp/shared/media/;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn-myapp/gunicorn.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Enable the site:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Verify

curl -I http://example.com
systemctl status nginx

If you get a 502 here, check socket permissions, Gunicorn status, and the Nginx error log before moving on.

11) Enable HTTPS

Install Certbot:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

This can update the Nginx config and add HTTP-to-HTTPS redirects.

Verify renewal:

systemctl list-timers | grep certbot
sudo certbot renew --dry-run

Once HTTPS is working correctly, enable or keep SECURE_SSL_REDIRECT = True in Django and reload your app service if needed.

Then confirm Django sees secure requests correctly by testing login, forms, and redirects over HTTPS.

Verify

curl -I https://example.com

12) Verify the deployment end to end

Functional checks:

  • homepage loads
  • admin loads if enabled
  • static assets return 200
  • forms submit without CSRF errors
  • database-backed pages work

Service checks:

systemctl status gunicorn-myapp
systemctl status nginx
sudo ss -ltnp
journalctl -u gunicorn-myapp -n 50 --no-pager
sudo nginx -t

Also verify:

  • DEBUG=False
  • ALLOWED_HOSTS matches the real domain
  • no secrets are stored in Git
  • python manage.py check --deploy is clean or understood

13) Rollback and recovery plan

Keep the previous release directory. To roll back code:

ln -sfn /srv/myapp/releases/previous-release /srv/myapp/current
sudo systemctl restart gunicorn-myapp

Then verify the site and logs again.

Database rollback is harder. If a migration changed schema incompatibly, switching code back may not be enough. Before risky releases, back up the database and uploaded media.

When to script this

Once you have repeated this process more than a few times, the mechanical parts should become a script or template: package install, directory setup, env file placement, systemd unit creation, Nginx config, release switching, and post-deploy checks. Manual setup is useful first because it makes failures easier to understand. After that, automation reduces drift and rollback mistakes.

Explanation

Gunicorn + Nginx + systemd is a strong baseline for deploying Django on AWS EC2 because it is simple, observable, and widely used.

  • Gunicorn runs the Django app process
  • Nginx handles client connections, static files, and proxying
  • systemd restarts Gunicorn on failure and starts it on boot

This stack is a good default for a single EC2 instance. Move beyond it when you need one or more of:

  • higher traffic requiring horizontal scaling
  • zero-downtime deploys
  • separate worker services
  • managed PostgreSQL and Redis
  • a load balancer in front of multiple app nodes

Edge cases and notes

Common EC2 deployment issues:

  • security group allows 22 but not 80 or 443
  • Nginx cannot access the Gunicorn socket due to permission mismatch
  • DNS still points to an old IP
  • ALLOWED_HOSTS does not include the final hostname
  • collectstatic ran into the wrong path
  • CSRF_TRUSTED_ORIGINS is missing https://...
  • redirect loops happen if SECURE_PROXY_SSL_HEADER is missing behind TLS termination
  • SECURE_SSL_REDIRECT was enabled before HTTPS was actually working

If your app stores user uploads locally, remember that instance replacement or disk failure can lose them unless you back up media. For larger apps, object storage is often safer than local disk.

This guide is intentionally non-Docker. If your stack is container-based, use a separate deployment path rather than mixing host-level and container-level process management.

Before deploying, use the Django Deployment Checklist for Production to confirm settings, secrets, and backup readiness.

If you want more detail on the app server and reverse proxy side, see Deploy Django with Gunicorn and Nginx on Ubuntu.

If your project is ASGI-based, read Deploy Django ASGI with Uvicorn and Nginx.

If you want a simpler HTTPS setup on a small server, see Deploy Django with Caddy and Automatic HTTPS.

FAQ

How much EC2 do I need for a small Django app?

For a small internal app or low-traffic site, t3.small is often enough to start. Watch memory, swap use, response times, and database load before deciding whether to move to a larger instance.

Should I use SQLite on EC2 for production?

Usually no. SQLite can work for very small single-process workloads, but PostgreSQL is the normal production choice for Django on EC2 because it handles concurrency, backups, and operational growth much better.

Can I deploy Django on EC2 without Nginx?

Yes, but it is usually not the best production choice. Nginx gives you better static file handling, TLS integration, buffering, and a stable reverse proxy layer in front of Gunicorn.

How do I update the app after the first deployment?

Create a new release directory, install dependencies in that release, run checks, run migrate and collectstatic, switch the /srv/myapp/current symlink, and restart Gunicorn. Keep the previous release intact so you can roll back code quickly if needed.

What should I back up on a single-server EC2 deployment?

At minimum:

  • PostgreSQL database
  • uploaded media files if stored locally
  • critical config such as env files and Nginx/systemd definitions
  • EBS snapshots if you rely on local state

For schema-changing releases, take a fresh database backup before running migrations.

2026 ยท django-deployment.com - Django Deployment knowledge base