Deploying Django Applications to AWS EC2 with Docker

Introduction

In the fast-paced field of web applications, containerization has become not only common but the preferred mode of packaging and delivering web applications. Containers allow us to package our applications and deploy them anywhere without having to reconfigure or adapt our applications to the deployment platform.

At the forefront of containerization is Docker, which is a tool that is used to package and run applications in containers that are platform agnostic. Serverless technology is also flourishing in this era of containerization and is proving to be the go-to option for developers when deploying their applications with more and more providers allowing users to deploy containerized software.

While building an application is important, making it available to the end-users is also a crucial part of the product. In this post, we will package a Django application using Docker and deploy it to Amazon's EC2.

What is EC2?

Amazon's Elastic Compute Cloud (EC2) is an offering that allows developers to provision and run their applications by creating instances of virtual machines in the cloud. EC2 also offers automatic scaling where resources are allocated based on the amount of traffic received.

Just like any other AWS offerings, EC2 can be easily integrated with the other Amazon services such as the Simple Queue Service (SQS), or Simple Storage Service (S3), among others.

EC2 has the following features:

  • Instances: Virtual computing environments or servers that allow developers to run their applications. These instances can be configured in terms of memory, storage, computing power, and network resources to suit the current need or scenario.
  • Amazon Machine Images (AMIs): Preconfigured templates that are used to create instances. They come with operating systems and preloaded software as required and are customizable.
  • Instance Store Volumes: Used to store data temporarily. This data is deleted when the instance is terminated.
  • Elastic Block Store (EBS) Volumes: Highly available and reliable storage volumes that are attached to instances for the purpose of persistently storing data. The data stored in EBS Volumes outlives the instances and multiple volumes can be mounted on an instance.
  • Security Groups: Virtual firewalls that govern access to instances by specifying protocols, IP address ranges, and ports. This enables us to control and restrict traffic to our instances.

These are but a few of the features of Amazon's Elastic Compute Cloud and more can be found in the documentation.

Prerequisites

In this tutorial, we'll build a web application and deploy it to Amazon's EC2 service. To achieve that we need:

  • An Amazon Web Services (AWS) account which will give us access to EC2. Through this link, you can sign up for the free tier which is sufficient for the work in this post.
  • Python 3.6+, Pip, and Virtualenv installed in order to build our Django application.
  • Docker will also be needed to package our application and easily run it in a container that is not only portable but can run anywhere Docker is installed.

Dockerizing a Django Application

We are going to start by building our simple Django application and containerizing it to allow us to easily deploy it. Let's start off with creating a folder for our project:

$ mkdir django_ec2 && cd $_

Then a virtual environment:

$ virtualev --python=python3 env --no-site-packages

Then, let's activate it and install Django:

$ source env/bin/activate
$ pip install Django

A simple placeholder Django app will suffice. All we have to do to bootstrap the project is run django-admin's startproject command, which starts a basic project for the given directory name:

$ django-admin startproject django_ec2_project

Then, let's enter the project directory:

$ cd django_ec2_project

And start a lightweight development server:

$ python manage.py runserver

If all goes well, we should be able to view the following landing page when we access our application at localhost:8000:

django_setup

Before packaging our Django application, we need to allow traffic to all sources, which we can achieve by modifying the ALLOWED_HOSTS setting in django_ec2_project/django_ec2_project/settings.py:

# Add the asterisk in the empty list
ALLOWED_HOSTS = ['*']

Note: It's not advisable to leave a wildcard in a production environment. Please use the domain of your own production site instead of it.

Since this is enough to be deployed, let us go ahead and containerize our application by adding a Dockerfile in the root of our project containing the following:

FROM python:3.6-alpine

MAINTAINER Robley Gori <ro6ley.github.io>

EXPOSE 8000

RUN apk add --no-cache gcc python3-dev musl-dev

ADD . /django_ec2

WORKDIR /django_ec2

RUN pip install -r requirements.txt

RUN python django_ec2_project/manage.py makemigrations

RUN python django_ec2_project/manage.py migrate

CMD [ "python", "django_ec2_project/manage.py", "runserver", "0.0.0.0:8000" ]

This Dockerfile describes how our application will be containerized and run. At the top, we use a base image that comes with Python 3.6 installed. We also expose the port 8000, which means that traffic into the container should be directed to that port, which is also where our Django application will be running from. We install a few packages to our image and then add our Django application to the django_ec2 directory.

Since our Django project is containerized, we will not need to create a virtual environment since it is already isolated from the machine that will be running it. Therefore, we install the requirements directly and run the migrations.

At the very end, we add the command that will be executed when the container is started, which in our case will also start our Django application and run it on port 8000.

If you'd like a more in-depth explanation on this topic, check out our article Dockerizing Python Applications.

The next step will be to build our Docker image using the Dockerfile above. Before that, we will save the dependencies installed in the environment to a file:

$ pip freeze > requirements.txt

And only then, let's build the docker image:

$ docker build . -t django_ec2

Through this command, Docker will look for our Dockerfile in the current folder and use it to build an image, which will be tagged as django_ec2. Once our image is built, we can run it using the command:

$ docker run -d -p 8000:8000 django_ec2

This command will start our container that has our running Django application and map the port 8000 on our machine to the container's port 8000, as specified by the -p flag and will run headlessly (even after we close the terminal) as specified by the -d flag.

We should be welcomed by the same Django landing page when we navigate to localhost:8000 once again, only that this time we will be accessing the application in the Docker container as opposed to the one in our local machine.

With our image ready, we need to publish it to Dockerhub to ease the deployment process on EC2.

Dockerhub is a registry for ready images that enables users to create and share customized Docker images for all purposes. It also allows us to publish our images to be accessed on other platforms such as AWS. In our case, we will publish our image to Dockerhub, then pull it into EC2 for deployment.

To publish our image, we need to create an account on Dockerhub first, and log in to it on our terminal:

$ docker login

Once logged in, we will need to tag our image with our username and then push it to Dockerhub:

$ docker tag django_ec2 <DOCKERHUB_USERNAME>/django_ec2
$ docker push <DOCKERHUB_USERNAME>/django_ec2

With this, we are ready for the next step, which is to deploy our application to Amazon's Elastic Compute Cloud.

Deploying to EC2

With our Docker image ready and published to Dockerhub, we can now log in to our AWS account console and in the EC2 dashboard, we can spin up a new instance - which is achieved through a series of steps.

Choose the AMI

The first step involves selecting an Amazon Machine Image (AMI) that will be used to create our instance. We are presented with options including Red Hat, Ubuntu Server, and Windows Server.

For this demonstration we will need an image that is customized to run containers and ships with Docker. To find it, type ECS in the search bar:

aws_ami_selection

The Amazon ECS-Optimized Amazon Linux 2 is ideal for our scenario and it is the one we will choose.

Choose Instance Type

ec2_instance_type

After choosing the AMI for our instance, we now have to choose an instance type. Our choice here will dictate the number of resources our instance will have in terms of CPU, memory, storage, and network performance capacity.

Since we are on the AWS free tier, we will go ahead and use the t2.micro instance, which is meant for general-purpose instances and comes with 1 virtual CPU and 1 GiB of memory.

The list contains more powerful instance types with others being optimized for compute power, memory, or storage.

Configure Instance

ec2_instance_details

Now that we have chosen the instance type, the next step allows us to specify some more details about our instance, including the number of instances to be launched at any given time, networking options, and file systems, among other details. We will not make any changes to the default options in this step.

Add Storage

ec2_adding_storage

The fourth step involves adding and specifying the storage details for our instance. This section allows us to add additional volumes, specify volume size and types, and whether our storage will be encrypted or not.

8GB is the default option and is more than enough for our simple Django application.

Add Tags

ec2_add_tags

AWS allows us to assign labels to our resources through which we can categorize them in terms of purpose, access, or environment. Tags are not mandatory but highly recommended to help identify resources as they increase in number.

Configure Security Group

ec2_configure_security_group

We defined Security Groups earlier in the post, and in this step of the process, we configure them by either creating a new security group or using an existing one.

We are going to create a new Security Group that will define what traffic will be accepted to our server. The first rule is the SSH rule that will allow SSH traffic into our instance via port 22.

We will modify the source to Anywhere and also add a new rule for Custom TCP and set the source to Anywhere and the port range to 8000. This will allow us to access our Django web application via the port 8000.

Review and Launch

ec2_review_and_launch

This is the final step where we are presented with the configuration details of our instance for verification. We can also edit the configuration at this point before launching our instance.

If everything is correct, we can finally click on "Launch" to finally start our instance:

aws_create_key_pair

Before our instance is launched, we need to create a key pair that will enable us to access our running instance. In order to run our Django application, we need to sign in to the instance and deploy it there.

The private key will be used to authenticate us and give us access to the instance for us to proceed with our deployment. The confirmation of the launch of the instance is then displayed on the next page:

ec2_instance_launched

Accessing the EC2 Instance

With the private key downloaded and the instance running, let us now log in and deploy our application.

For this, we need the .pem file downloaded earlier and a terminal window. We will also need a user name for the chosen AMI - for the Amazon Linux AMI, the default username is ec2-user.

The instance's public DNS is also required to connect to it and this can be found on the instance's details section on the EC2 console dashboard.

Let us open a terminal in the folder that contains our private key file. We'll first need to change the key permissions to avoid seeing an "unprotected key file" warning:

$ chmod 400 <PRIVATE_KEY_FILE_NAME>

Then we can use the ssh utility, along with our key file, to connect to the instance:

$ ssh -i <PRIVATE_KEY_FILE_NAME> [email protected]<PUBLIC_DNS>

   __|  __|  __|

   _|  (   \__ \   Amazon Linux 2 (ECS Optimized)

 ____|\___|____/

For documentation, visit http://aws.amazon.com/documentation/ecs

12 package(s) needed for security, out of 25 available

Run "sudo yum update" to apply all updates.

-bash: warning: setlocale: LC_CTYPE: cannot change locale (UTF-8): No such file or directory

[[email protected]###-##-##-## ~]$

The response above means that we have successfully signed in to our instance, we will start by pulling our application image from Dockerhub and running it using the docker run command:

$ docker run -d -p 8000:8000 <DOCKERHUB_USERNAME>/django_ec2

Once our Docker image is pulled into our instance and successfully running, we can now access our Django application on the web through the same address we used to SSH into it.

When we do, we are welcomed with:

ec2_instance_deployed

Our Django application is now live on AWS Elastic Compute Cloud!

Conclusion

In this post, we have containerized a Django application using Docker and successfully deployed it to Amazon's EC2 service. We have also learned what EC2 is and what it offers to us as developers and how we can leverage it to make our web applications available for the end-users.

The source code for the script in this project can be found here on GitHub.