
All-in-One Django Deployment to AWS
In this post, we’ll take a Django project and fully deploy it to AWS with:
- A Docker image running on EC2
- A connected PostgreSQL database on RDS
- A custom domain on Route53 with https enabled
This is a complete setup to run a Django project in production for free. With that let’s get started.
Step 1: Start a django project
First, lets install django and start a project.
python3.12 -m pip install django
python3.12 -m django startproject django_ec2_complete
cd django_ec2_complete
Now that we have a Django project, let’s install a virtual environment and the necessary dependencies.
# Set up and activate the virtual environment
python3.12 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install django gunicorn psycopg2-binary
pip freeze > requirements.txt
django
is needed for Djangogunicorn
is needed to run Django in productionpsycopg2-binary
connects Django to PostgreSQL
Our Django project is up and we have the correct dependencies installed. Now let’s run Django.
python manage.py runserver
Django should now be running at http://127.0.0.1:8000/ - perfect!
Next, we need to dockerize this project so it can be deployed onto any AWS server.
Step 2: Dockerize your Django project
First we need to create a Dockerfile
with steps to build this Django project.
We can use the following code:
FROM python:3.12.2 # Use you own version
ENV PYTHONBUFFERED=1 # Output to terminal
ENV PORT 8080 # Run on port 8080
WORKDIR /app
COPY . /app/ # Copy app into working directory
RUN pip install --upgrade pip
RUN pip install -r requirements.txt # Install dependencies
# Run app with gunicorn command
CMD gunicorn django_ec2_complete.wsgi:application --bind 0.0.0.0:"${PORT}"
EXPOSE ${PORT} # Open port 8080 on container
Our Dockerfile will properly build our Django project. Let’s build and run it now.
docker build -t django-ec2-complete:latest .
docker run -p 8000:8080 django-ec2-complete:latest
You should be able to see the image exposed on port 8000. Go to http://localhost:8000/ - perfect!
To stop the image, just run:
docker ps
docker kill <image_id>
We now have a basic docker image. Let’s deploy this to Amazon ECR.
Step 3: Push your Docker image on AWS ECR
ECR will host out built image. This way the server can pull the image and run in production.
First, you’ll need to create an account or login at AWS then we should be good to setup ECR. We’ll also need to install the AWS CLI V2.
Now that we have an AWS account and CLI, lets deploy this image to ECR.
# Create an ECR registry
aws ecr create-repository --repository-name django-ec2-complete
# Boom! We just made a registry!
# Copy "repositoryUri": "620457613573.dkr.ecr.us-east-1.amazonaws.com/django-ec2-complete" value
# Login to your ECR registry
docker login -u AWS -p $(aws ecr get-login-password --region us-east-1) 620457613573.dkr.ecr.us-east-1.amazonaws.com
# Tag your built image
docker tag django-ec2-complete:latest 620457613573.dkr.ecr.us-east-1.amazonaws.com/django-ec2-complete:latest
# Push to your ECR registry
docker push 620457613573.dkr.ecr.us-east-1.amazonaws.com/django-ec2-complete:latest
Step 4: Put our ECR Image on a Server
To run Django in production, we need to setup:
- A Virtual Private Cloud (VPC) to networks and connect all resources
- Subnets (two) to host our resources in two zones (for reliability / AWS requires it)
- Security groups to define what web traffic is allowed
- Our server to run the Django image
- IAM Roles to give our server access to ECR
# Define AWS provider and set the region for resource provisioning
provider "aws" {
region = "us-east-1"
}
# Create a Virtual Private Cloud to isolate the infrastructure
resource "aws_vpc" "default" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "Django_EC2_VPC"
}
}
# Internet Gateway to allow internet access to the VPC
resource "aws_internet_gateway" "default" {
vpc_id = aws_vpc.default.id
tags = {
Name = "Django_EC2_Internet_Gateway"
}
}
# Route table for controlling traffic leaving the VPC
resource "aws_route_table" "default" {
vpc_id = aws_vpc.default.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.default.id
}
tags = {
Name = "Django_EC2_Route_Table"
}
}
# Subnet within VPC for resource allocation, in availability zone us-east-1a
resource "aws_subnet" "subnet1" {
vpc_id = aws_vpc.default.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = false
availability_zone = "us-east-1a"
tags = {
Name = "Django_EC2_Subnet_1"
}
}
# Another subnet for redundancy, in availability zone us-east-1b
resource "aws_subnet" "subnet2" {
vpc_id = aws_vpc.default.id
cidr_block = "10.0.2.0/24"
map_public_ip_on_launch = false
availability_zone = "us-east-1b"
tags = {
Name = "Django_EC2_Subnet_2"
}
}
# Associate subnets with route table for internet access
resource "aws_route_table_association" "a" {
subnet_id = aws_subnet.subnet1.id
route_table_id = aws_route_table.default.id
}
resource "aws_route_table_association" "b" {
subnet_id = aws_subnet.subnet2.id
route_table_id = aws_route_table.default.id
}
# Security group for EC2 instance
resource "aws_security_group" "ec2_sg" {
vpc_id = aws_vpc.default.id
ingress {
from_port = 22
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Only allow HTTPS traffic from everywhere
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "EC2_Security_Group"
}
}
# Define variable for RDS password to avoid hardcoding secrets
variable "secret_key" {
description = "The Secret Key for Django"
type = string
sensitive = true
}
# EC2 instance for the local web app
resource "aws_instance" "web" {
ami = "ami-0c101f26f147fa7fd" # Amazon Linux
instance_type = "t3.micro"
subnet_id = aws_subnet.subnet1.id # Place this instance in one of the private subnets
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
associate_public_ip_address = true # Assigns a public IP address to your instance
user_data_replace_on_change = true # Replace the user data when it changes
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
user_data = <<-EOF
#!/bin/bash
set -ex
yum update -y
yum install -y yum-utils
# Install Docker
yum install -y docker
service docker start
# Install AWS CLI
yum install -y aws-cli
# Authenticate to ECR
docker login -u AWS -p $(aws ecr get-login-password --region us-east-1) 620457613573.dkr.ecr.us-east-1.amazonaws.com
# Pull the Docker image from ECR
docker pull 620457613573.dkr.ecr.us-east-1.amazonaws.com/django-ec2-complete:latest
# Run the Docker image
docker run -d -p 80:8080 \
--env SECRET_KEY=${var.secret_key} \
620457613573.dkr.ecr.us-east-1.amazonaws.com/django-ec2-complete:latest
EOF
tags = {
Name = "Django_EC2_Complete_Server"
}
}
# IAM role for EC2 instance to access ECR
resource "aws_iam_role" "ec2_role" {
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Action = "sts:AssumeRole",
Principal = {
Service = "ec2.amazonaws.com",
},
Effect = "Allow",
}],
})
}
# Attach the AmazonEC2ContainerRegistryReadOnly policy to the role
resource "aws_iam_role_policy_attachment" "ecr_read" {
role = aws_iam_role.ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}
# IAM instance profile for EC2 instance
resource "aws_iam_instance_profile" "ec2_profile" {
name = "django_ec2_complete_profile"
role = aws_iam_role.ec2_role.name
}
Don’t forget to add your IP address on lines 85
, 91
. Remember to add a /32
at the end to be CIDR compatible.
Now run:
terraform init
terraform apply
Step 5: Connect a PostgreSQL Databse on AWS
Next, lets setup a new terraform file to define the database. We’ll need to create:
- A Subnet Group to connect Postgres to our subnets
- Another security group to allow SQL traffic
- A database to host PostgreSQL
Let’s create a database.tf
file to define the following:
# DB subnet group for RDS instances, using the created subnets
resource "aws_db_subnet_group" "default" {
subnet_ids = [aws_subnet.subnet1.id, aws_subnet.subnet2.id]
tags = {
Name = "Django_EC2_Subnet_Group"
}
}
# Security group for RDS, allows PostgreSQL traffic
resource "aws_security_group" "rds_sg" {
vpc_id = aws_vpc.default.id
name = "DjangoRDSSecurityGroup"
description = "Allow PostgreSQL traffic"
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Updated to "10.0.0.0/16"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"] # Updated to "10.0.0.0/16"
}
tags = {
Name = "RDS_Security_Group"
}
}
variable "db_password" {
description = "The password for the database"
type = string
sensitive = true
}
# RDS instance for Django backend, now privately accessible
resource "aws_db_instance" "default" {
allocated_storage = 20
storage_type = "gp2"
engine = "postgres"
engine_version = "16.1"
instance_class = "db.t3.micro"
identifier = "my-django-rds"
db_name = "djangodb"
username = "adam"
password = var.db_password
db_subnet_group_name = aws_db_subnet_group.default.name
vpc_security_group_ids = [aws_security_group.rds_sg.id]
skip_final_snapshot = true
publicly_accessible = true # Changed to false for private access
multi_az = false
tags = {
Name = "Django_RDS_Instance"
}
}
Run the following to set environment variables for db_password
and secret_key
export TF_VAR_secret_key=pass1234
export TF_VAR_db_password=pass1234
Re-apply terraform changes.
terraform apply --auto-approve
We can see RDS is deployed! Go to the RDS dashboard and see for yourself.
Now, lets connect Django to this new database. Go into settings.py
and add/replace the following settings:
import os
SECRET_KEY = os.getenv('SECRET_KEY')
DEBUG = False
ALLOWED_HOSTS = ['*']
# TODO: Add your domains
# If you have no domains, don't add this line
CSRF_TRUSTED_ORIGINS = ['https://lamorre.com', 'https:/www.lamorre.com']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER_NM'),
"PASSWORD": os.getenv('DB_USER_PW'),
"HOST": os.getenv('DB_IP'),
"PORT": os.getenv('DB_PORT'),
}
}
Be sure to replace CSRF_TRUSTED_ORIGINS
with the internet domain you have (for http:// and https://).
We should set these environment variables in the end of my-venv/bin/activate
export SECRET_KEY=pass1234
export DB_NAME=djangodb
export DB_USER_NM=adam
export DB_USER_PW=pass1234
export DB_IP=my-django-rds.cb2u6sse4azd.us-east-1.rds.amazonaws.com # Your DB on AWS console
export DB_PORT=5432
Run the following commands to reactivate the virtual environment with these variables active.
deactivate
source my-venv/bin/activate
Run the following commands to migrate the database and make an admin user:
python manage.py migrate
python manage.py createsuperuser
To lock everything down, you can now remove your IP address from main.tf.
Finally, let’s re-deploy this new Django code to ECR and put it on the server.
# Re-build the image (with changes)
docker build -t django-ec2-complete:latest .
# Tag the new image
docker tag django-ec2-complete:latest 620457613573.dkr.ecr.us-east-1.amazonaws.com/django-ec2-complete:latest
# Push the new image
docker push 620457613573.dkr.ecr.us-east-1.amazonaws.com/django-ec2-complete:latest
Last, edit the user_data
parameter to main.tf
to run Django with the proper environment variables:
...
resource "aws_instance" "web" {
...
user_data = <<-EOF
...
# Run the Docker image
docker run -d -p 80:8080 \
--env SECRET_KEY=${var.secret_key} \
--env DB_NAME=djangodb \
--env DB_USER_NM=adam \
--env DB_USER_PW=pass1234 \
--env DB_IP=${aws_db_instance.default.address} \
--env DB_PORT=5432 \
620457613573.dkr.ecr.us-east-1.amazonaws.com/django-ec2-complete:latest
EOF
}
Re-apply your terraform:
terraform apply --auto-approve
Boom! We should be able to see django running with the IP address from the admin dashboard!
Go to /admin
and login with the super user we created.
Step 6: Connect a Custom Domain (Optional)
If you want to connect a custom domain, all we need to do is buy a domain on Route53 in AWS and connect it with a domain.tf
terraform file.
In our domain.tf
file, we’ll set up:
- A load balancer to direct public http traffic to our server
- An ACM certificate for https://
- A Route 53 zone (the domain we bought)
- Some AWS Route 53 records - to map this domain to load balancer
With that, let’s add the domain.tf
file:
# Request a certificate for the domain and its www subdomain
resource "aws_acm_certificate" "cert" {
domain_name = "lamorre.com"
validation_method = "DNS"
subject_alternative_names = ["www.lamorre.com"]
tags = {
Name = "my_domain_certificate"
}
lifecycle {
create_before_destroy = true
}
}
# Declare the Route 53 zone for the domain
data "aws_route53_zone" "selected" {
name = "lamorre.com"
}
# Define the Route 53 records for certificate validation
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = data.aws_route53_zone.selected.zone_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
# Define the Route 53 records for the domain and its www subdomain
resource "aws_route53_record" "root_record" {
zone_id = data.aws_route53_zone.selected.zone_id
name = "lamorre.com"
type = "A"
alias {
name = aws_lb.default.dns_name
zone_id = aws_lb.default.zone_id
evaluate_target_health = true
}
}
resource "aws_route53_record" "www_record" {
zone_id = data.aws_route53_zone.selected.zone_id
name = "www.lamorre.com"
type = "A"
alias {
name = aws_lb.default.dns_name
zone_id = aws_lb.default.zone_id
evaluate_target_health = true
}
}
# Define the certificate validation resource
resource "aws_acm_certificate_validation" "cert" {
certificate_arn = aws_acm_certificate.cert.arn
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}
# Security group for ALB, allows HTTPS traffic
resource "aws_security_group" "alb_sg" {
vpc_id = aws_vpc.default.id
name = "alb-https-security-group"
description = "Allow all inbound HTTPS traffic"
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Application Load Balancer for HTTPS traffic
resource "aws_lb" "default" {
name = "django-ec2-alb-https"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb_sg.id]
subnets = [aws_subnet.subnet1.id, aws_subnet.subnet2.id]
enable_deletion_protection = false
}
# Target group for the ALB to route traffic from ALB to VPC
resource "aws_lb_target_group" "default" {
name = "django-ec2-tg-https"
port = 443
protocol = "HTTP" # Protocol used between the load balancer and targets
vpc_id = aws_vpc.default.id
}
# Attach the EC2 instance to the target group
resource "aws_lb_target_group_attachment" "default" {
target_group_arn = aws_lb_target_group.default.arn
target_id = aws_instance.web.id # Your EC2 instance ID
port = 80 # Port the EC2 instance listens on; adjust if different
}
# HTTPS listener for the ALB to route traffic to the target group
resource "aws_lb_listener" "default" {
load_balancer_arn = aws_lb.default.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08" # Default policy, adjust as needed
certificate_arn = aws_acm_certificate.cert.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.default.arn
}
}
Re-run terraform apply:
terraform apply --auto-approve
Boom, you should be able to see a Django project running (and connected to PostgreSQL and https://) on your new domain!