Deploying Django to AWS with Docker and Let's Encrypt

Last updated June 29th, 2020

In this tutorial, we'll deploy a Django app to AWS EC2 with Docker. The app will run behind an HTTPS Nginx proxy with Let's Encrypt SSL certificates. We'll use AWS RDS to serve our Postgres database along with AWS ECR to store and manage our Docker images.

Django on Docker Series:

  1. Dockerizing Django with Postgres, Gunicorn, and Nginx
  2. Securing a Containerized Django Application with Let's Encrypt
  3. Deploying Django to AWS with Docker and Let's encrypt (this article!)

Contents

Objectives

By the end of this tutorial, you will be able to:

  1. Set up a new EC2 instance
  2. Install Docker on an EC2 instance
  3. Configure and use an Elastic IP address
  4. Set up an IAM role
  5. Utilize Amazon Elastic Container Registry (ECR) image registry to store built images
  6. Configure AWS RDS for data persistence
  7. Configure an AWS Security Group
  8. Deploy Django to AWS EC2 with Docker
  9. Run the Django app behind an HTTPS Nginx proxy with Let's Encrypt SSL certificates

Prerequisites

This post builds on the Dockerizing Django with Postgres, Gunicorn, and Nginx and Securing a Containerized Django Application with Let's Encrypt posts.

It assumes that you can:

  1. Containerize a Django app along with Postgres, Nginx, and Gunicorn
  2. Secure a containerized Django app running behind an HTTPS Nginx proxy with Let's Encrypt SSL certificates
  3. Use SSH and SCP to connect to remote server.

AWS EC2

First, create an AWS account if you don't already have one.

Next, navigate to the EC2 console and click Launch instance:

EC2 Home

Select Ubuntu Server 18.04 LTS (HVM) for server image (AMI):

Select AMI

In the next step, stick with the t2.micro instance. Click on Next: Configure Instance Details:

EC2 instance type

At the Configure Instance Details step, leave everything as it is to keep things simple. Then click Next a few times until you're at the Configure Security Group step.

With Create a new security group selected, set the name and description to django-ec2 and add two rules:

  • HTTP -> Anywhere
  • HTTPS -> Anywhere

EC2 configure security groups

These rules are needed to issue certificates and to access the app.

Security group inbound rules are used to limit access to your instance from the internet. You should allow HTTP and HTTPS traffic from anywhere for instances hosting web apps. SSH must be allowed for you to connect to the instance for set up and deployment.

Click Review and Launch. On the next screen, click Launch.

You will be prompted to select a key pair. You need it for SSH connection to your instance. Select Create a new key pair and name it djangoletsencrypt. Then click Download key pair. After the key pair is downloaded, click on Launch Instances:

EC2 Add key pair

It will take a few minutes for the instance to spin up. In the meantime, let's configure an IAM user.

Configure EC2 Instance

Install Docker

Navigate back to the EC2 console, select the newly created instance, and grab the public IP address:

EC2 Public IP

Connect to your EC2 instance using the .pem key that we downloaded in the "AWS EC2" step.

$ ssh -i /path/to/your/djangoletsencrypt.pem [email protected]

Your .pem was probably downloaded into path like ~/Downloads/djangoletsencrypt.pem. If you're not sure where to store it, move it into the ~/.ssh directory. You may have to also change the permissions -- i.e., chmod 400 -i /path/to/your/djangoletsencrypt.pem.

Start by installing the latest version of Docker and version 1.25.5 of Docker Compose:

$ sudo apt update
$ sudo apt install apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
$ sudo apt update
$ sudo apt install docker-ce
$ sudo usermod -aG docker ${USER}
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.25.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose

Install AWS CLI

First, install unzip:

$ sudo apt install unzip

Download AWS CLI ZIP:

$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"

Unzip its content:

$ unzip awscliv2.zip

Install AWS CLI:

$ sudo ./aws/install

Verify installation:

$ aws --version

Elastic IP

By default, instances receive new public IP address every time they start and re-start.

Elastic IP allows you to allocate static IPs for your EC2 instances, so the IP stays the same all the time and can be re-associated between instances. It's recommended to use one for your production setup.

Navigate to Elastic IPs and click Allocate Elastic IP address:

Elastic IP

Then, click Allocate:

Elastic IP Allocate

Click on Associate this Elastic IP address:

Elastic IP Associate

Select your instance and click Associate:

Elastic IP Select Instance

IAM Role

We'll be using AWS ECR to pull images from AWS ECR to our EC2 instance during deployment. Since public access to ECR is not allowed, you'll need to create an IAM role with permissions to pull Docker images from ECR and attach it to your EC2 instance.

Navigate to the IAM console.

Click Roles in the left sidebar and then Create role:

IAM Roles

Select AWS Service and EC2, then click on Next: Permissions:

IAM Select Use Case

Enter container in the search box, select the AmazonEC2ContainerRegistryPowerUser policy, and click Next: Tags:

IAM Role Policy

Click Next: Review. Use django-ec2 for the name, and click on Create role:

IAM Role Review

Now you need to attach the new role to your EC2 instance.

Back in the EC2 console, click Instances, and then select your instance. Click the Actions dropdown -> Instance settings -> Attach/Replace IAM Role:

EC2 Attach IAM Role

Select the django-ec2 role, and then click Apply.

EC2 Select IAM Role

Click Close.

Add DNS Record

Add an A record to your DNS, for the domain that you are using, to point to your EC2 instance's public IP.

It's the Elastic IP you've associated to your instance.

AWS ECR

Amazon Elastic Container Registry (ECR) is a fully-managed Docker image registry that makes it easy for developers to store and manage images. Access is managed via IAM users and roles, so images are not publicly accessible.

Navigate to the ECR console. Click Repositories in the sidebar and then on Create repository:

ECR Repositories

Set the name to django-ec2 and click on Create repository:

ECR Create repository

AWS RDS

Now we can add configure a RDS Postgres database.

While you can run your own Postgres database in a container, since databases are critical services, adding additional layers, such us Docker, adds unnecessary risk in production. To simplify tasks such as minor version updates, regular backups, and scaling, it's recommended to use a managed service. So, we'll use RDS.

Navigate to the RDS console. Click on Create database:

RDS Home

Select the latest version of Postgres with the Free tier template:

RDS Create database

Under Settings, set:

  • DB Instance identifier: djangoec2
  • Master username: webapp
  • Select Auto generate a password

Stick with default settings for:

  • DB instance size
  • Storage
  • Availability & durability

Skip down to the Connectivity section and set the following:

  • Virtual private cloud (VPC): Default
  • Subnet group: default
  • Publicly accessible: No
  • VPC security group: django-ec2
  • Database port: 5432

RDS Create database connectivity

Leave Database authentication as it is.

Open Additional configuration and change Initial database name to djangoec2:

RDS Create database initial DB name

Leave the other settings as they are.

Finally, click Create database.

Click on View credentials details to see the generated password for the webapp user:

RDS View credentials

Store this password somewhere safe. You'll need to provide it to the Django application here shortly.

It will take a few of minutes for the instance to spin up. Once up, click on the DB Identifier of the newly created database to see its details. Take note of the database endpoint; you'll need to set it in your Django app.

RDS DB details

AWS Security Group

Within the EC2 console, click Security Groups in the sidebar. Find and click the ID of the django-ec2 group to edit its details.

Click on Edit inbound rules:

EC2 Security Group Details

Add an inbound rule that will allow Postgres connections to instances inside that Security Group. To do that:

  • click on Add rule
  • select PostgreSQL for the rule type
  • select Custom for the rule source
  • click into the search field and select the django-ec2 Security Group
  • click on Save rules

To limit access to your database, only connections from instances inside the same Security Group are allowed. Our application can connect because we set the same Security Group, django-ec2, for both the RDS and EC2 instances. Instances inside other Security Groups are therefore not allowed to connect.

EC2 Edit inbound rules

Project Config

With the AWS infrastructure set up, we now need to configure our Django project locally before deploying it.

First, clone down the contents from the GitHub project repo:

$ git clone https://github.com/testdrivenio/django-on-docker-letsencrypt django-on-docker-letsencrypt-aws
$ cd django-on-docker-letsencrypt-aws

This repository contains everything that you need to deploy a Dockerized Django with Let's Encrypt HTTPS certificates.

Docker Compose

When the app is deployed for the first time, you should follow these two steps to avoid issues with certificates:

  1. Start by issuing the certificates from Let's Encrypt's staging environment
  2. Then, when all is running as expected, switch to Let's Encrypt's production environment

You can read more about Let's Encrypt's limitations on production environments in the Let's Encrypt section from the previous post, Securing a Containerized Django Application with Let's Encrypt.

For testing, update the docker-compose.staging.yml. file like so:

version: '3.7'

services:
  web:
    build:
      context: ./app
      dockerfile: Dockerfile.prod
    image: <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:web
    command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
    expose:
      - 8000
    env_file:
      - .env.staging
  nginx-proxy:
    container_name: nginx-proxy
    build: nginx
    image: <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:nginx-proxy
    restart: always
    ports:
      - 443:443
      - 80:80
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
      - certs:/etc/nginx/certs
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
      - /var/run/docker.sock:/tmp/docker.sock:ro
    depends_on:
      - web
  nginx-proxy-letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    env_file:
      - .env.staging.proxy-companion
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - certs:/etc/nginx/certs
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
    depends_on:
      - nginx-proxy

volumes:
  static_volume:
  media_volume:
  certs:
  html:
  vhost:

For the web and nginx-proxy services, update the image properties to use images from ECR (which we'll add shortly).

Examples:

image: 123456789.dkr.ecr.us-east-1.amazonaws.com/django-ec2:web

image: 123456789.dkr.ecr.us-east-1.amazonaws.com/django-ec2:nginx-proxy

The values consist of the repository URL (123456789.dkr.ecr.us-east-1.amazonaws.com) along with the image name (django-ec2) and tags (web and nginx-proxy).

To keep things simple we're using a single registry to store both images. We used the web and nginx-proxy to differentiate between the two. Ideally, you should use two registries: one for web and one for nginx-proxy. Update this on your own if you'd like it.

Other than the image properties, we also removed the db service (and related volume) since we'll use RDS rather than managing Postgres in a container.

Environments

It's time to set up the environment files for the web and nginx-proxy-letsencrypt containers.

First, add an .env.staging file for the web container:

DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=<YOUR_DOMAIN.COM>
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=djangoec2
SQL_USER=webapp
SQL_PASSWORD=<PASSWORD-FROM-AWS-RDS>
SQL_HOST=<DATABASE-ENDPOINT-FROM-AWS-RDS>
SQL_PORT=5432
DATABASE=postgres
VIRTUAL_HOST=<YOUR_DOMAIN.COM>
VIRTUAL_PORT=8000
LETSENCRYPT_HOST=<YOUR_DOMAIN.COM>

Notes:

  1. Change <YOUR_DOMAIN.COM> to your actual domain
  2. Change SQL_PASSWORD and SQL_HOST to match those created in the RDS section
  3. Change SECRET_KEY to some long random string
  4. The VIRTUAL_HOST and VIRTUAL_PORT are needed by nginx-proxy container to auto create the reverse proxy configuration
  5. LETSENCRYPT_HOST is there so the nginx-proxy-companion can issue Let's Encrypt certificate for your domain.

For testing/debugging purposes you may want to use a * for DJANGO_ALLOWED_HOSTS the first time you deploy to simplify things. Just don't forget to limit the allowed hosts once testing is complete.

Second, add an .env.staging.proxy-companion file, making sure to update the DEFAULT_EMAIL value:

[email protected]
ACME_CA_URI=https://acme-staging-v02.api.letsencrypt.org/directory
NGINX_PROXY_CONTAINER=nginx-proxy

Review the Let's Encrypt Nginx Proxy Companion Service section from the Securing a Containerized Django Application with Let's Encrypt post to learn more about the above mentioned environment variables.

Build and Push Docker Images

Now we're ready to build the Docker images:

$ docker-compose -f docker-compose.staging.yml build

It may take a few minutes to build. Once done, we're ready to push the images up to ECR.

First, assuming that you have the awscli installed and that you've set your AWS credentials, log in to the ECR Docker repository:

$ aws ecr get-login-password --region <aws-region> | docker login --username AWS --password-stdin <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com
# aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com

You should see:

Login Succeeded

Then push the images to ECR:

$ docker-compose -f docker-compose.staging.yml push

Open your django-ec2 ECR repository to see the pushed images:

ECR Images

Running the Containers

Everything is set up for deployment.

It's time to move to your EC2 instance.

Assuming you have a project directory created on your instance, like /home/ubuntu/django-on-docker, copy the files and folders over with SCP:

$ scp -i /path/to/your/djangoletsencrypt.pem \
      -r $(pwd)/{app,nginx,.env.staging,.env.staging.proxy-companion,docker-compose.staging.yml} \
      [email protected]:/path/to/django-on-docker

Next, connect to your instance via SSH and move to the project directory:

$ ssh -i /path/to/your/djangoletsencrypt.pem [email protected]
$ cd /path/to/django-on-docker

Login to ECR Docker repository.

$ aws ecr get-login-password --region <aws-region> | docker login --username AWS --password-stdin <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com

Pull the images:

$ docker pull <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:web
$ docker pull <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:nginx-proxy

With that, you're ready to spin up the containers:

$ docker-compose -f docker-compose.staging.yml up -d

Once the containers are up and running, navigate to your domain in your browser. You should see something like:

HTTPS certificate not secure

This is expected. This screen is shown because the certificate was issued from a staging environment.

How do you know if everything works?

Click on "Advanced" and then on "Proceed". You should now see your app. Upload an image, and then make sure you can view the image at https://yourdomain.com/mediafiles/IMAGE_FILE_NAME.

Issue the Production Certificate

Now, that everything works as expected, we can switch over to Let's Encrypt's production environment.

Bring down the existing containers and exit your instance:

$ docker-compose -f docker-compose.staging.yml down -v
$ exit

Back on your local machine, open docker-compose.prod.yml and make the same changes that you did for the staging version:

  1. Update the ìmage properties to match your AWS ECR URLs for the ẁeb and nginx-proxy services
  2. Remove the db service along with the related volume
version: '3.7'

services:
  web:
    build:
      context: ./app
      dockerfile: Dockerfile.prod
    image: <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:web
    command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
    expose:
      - 8000
    env_file:
      - .env.prod
  nginx-proxy:
    container_name: nginx-proxy
    build: nginx
    image: <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:nginx-proxy
    restart: always
    ports:
      - 443:443
      - 80:80
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
      - certs:/etc/nginx/certs
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
      - /var/run/docker.sock:/tmp/docker.sock:ro
    depends_on:
      - web
  nginx-proxy-letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    env_file:
      - .env.prod.proxy-companion
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - certs:/etc/nginx/certs
      - html:/usr/share/nginx/html
      - vhost:/etc/nginx/vhost.d
    depends_on:
      - nginx-proxy

volumes:
  static_volume:
  media_volume:
  certs:
  html:
  vhost:

Next create an .env.prod file by duplicating the .env.staging file. You don't need to make any changes to it.

Finally, add an .env.prod.proxy-companion file:

[email protected]
NGINX_PROXY_CONTAINER=nginx-proxy

Build and push images again:

$ docker-compose -f docker-compose.prod.yml build
$ aws ecr get-login-password --region <aws-region> | docker login --username AWS --password-stdin <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com
$ docker-compose -f docker-compose.prod.yml push

Copy the new files and folders to your instance with SCP:

$ scp -i /path/to/your/djangoletsencrypt.pem \
      $(pwd)/{.env.prod,.env.prod.proxy-companion,docker-compose.prod.yml} \
      [email protected]:/path/to/django-on-docker

Like before, connect to your instance via SSH and move to the project directory:

$ ssh -i /path/to/your/djangoletsencrypt.pem [email protected]
$ cd /path/to/django-on-docker

Log in to your ECR Docker repository again:

$ aws ecr get-login-password --region <aws-region> | docker login --username AWS --password-stdin <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com

Pull the images:

$ docker pull <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:web
$ docker pull <aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com/django-ec2:nginx-proxy

And finally spin up the containers:

$ docker-compose -f docker-compose.prod.yml up -d

Navigate to your domain again. You should no longer see a warning.

Congrats! You're now using a production Let's Encrypt certificate for your Django application running on AWS EC2.

Conclusion

In this tutorial you deployed a containerized Django application to EC2. The app is running behind an HTTPS Nginx proxy with Let's Encrypt SSL certificates. You also used an RDS Postgres instance, and stored Docker images on ECR.

What's next?

  1. Set up S3 and configure Django to store static and media files in a bucket, outside of the Docker volume
  2. Set up a CI/CD pipeline on GitLab
  3. Configure a containerized Django app running on an EC2 instance to send logs to Amazon CloudWatch

Happy deploying!

Django on Docker Series:

  1. Dockerizing Django with Postgres, Gunicorn, and Nginx
  2. Securing a Containerized Django Application with Let's Encrypt
  3. Deploying Django to AWS with Docker and Let's encrypt (this article!)

Jan Giacomelli

Jan Giacomelli

Jan is a software engineer who lives in Ljubljana, Slovenia, Europe. He is co-founder of typless where he is leading engineering efforts. He loves working with Python and Django. When he's not writing code or deploying to AWS, he's probably skiing, windsurfing, or playing guitar.

Share this tutorial

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.