How to dockerize your Python Django application for AWS Fargate?
Tags: python, docker, aws
Published: 2019-11-19 16:06:34

The biggest game-changer for Docker on AWS was the announcement of AWS Fargate. Operating Docker containers could not be easier. With AWS Fargate, you launch Docker containers in the cloud without any need to manage virtual machines. [Django](https://www.djangoproject.com/) is a popular [Python](https://www.python.org/) web framework that encourages rapid development and clean, pragmatic design.

> This blog post is an excerpt from our book [Rapid Docker on AWS](https://cloudonaut.io/rapid-docker-on-aws/) and was first published on [cloudonaut.io](https://cloudonaut.io/how-to-dockerize-your-python-django-application-for-aws-fargate/). 

The following post describes how you can dockerize your Python Django application and run it on AWS Fargate.

### Building the Docker images

Two [Docker images](https://docs.docker.com/v17.09/engine/userguide/storagedriver/imagesandcontainers/) are needed:

* NGINX to serve static files and proxy to Django
* Python Django application

First, you will learn how to build NGINX image. The `Dockerfile` makes use of [multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/). You can use more than one `FROM` statement in your Dockerfile, as shown in the following example.

1. Static assets are generated in a Python stage
    1. Based on the official [python](https://hub.docker.com/_/python) image
    2. Using `pip3` to install the Python dependencies
    3. Copying the app
    4. Generating the static assets with `python3 manage.py collectstatic` (output goes to the `assets` folder)
2. The static assets are copied (using `COPY --from=build`) into the NGINX stage which produces the final Docker image
    1. Based on the official [nginx](https://hub.docker.com/_/nginx) image
    2. Copying the `static/` folder from the previous stage

> **Customization** Most likely, your folder structure is different. Therefore, adapt the *Copy Python files* section in the following Dockerfile to your needs.

```
# Static Assets
FROM python:3.7.4 AS build

WORKDIR /usr/src/app

# Install Python dependencies
COPY requirements.txt /usr/src/app/
RUN pip3 install -r requirements.txt

# Copy Python files
COPY example /usr/src/app/example # MODIFY THIS LINE: YOUR FOLDERS ARE DIFFERENT
COPY rapid /usr/src/app/rapid # MODIFY OR REMOVE THIS LINE: YOUR FOLDERS ARE DIFFERENT
COPY manage.py /usr/src/app/

# Build static assets
RUN SECRET_KEY=secret python3 manage.py collectstatic


# NGINX
FROM nginx:1.14

# Configure NGINX
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf

# Copy static files
COPY --from=build /usr/src/app/build/ /var/www/html/static
RUN chown -R nginx:nginx /var/www/html
```

The NGINX configuration file forwards requests to the Python container if the path does not start with `/static/`.

```
server {
    listen       80;
    server_name  localhost;
    root         /var/www/html;

    location ~ ^/static/ {
      # serve from NGINX
    }

    location / {
      # pass to Python gunicorn based on
      # http://docs.gunicorn.org/en/stable/deploy.html
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Host $http_host;
      # we don't want nginx trying to do something clever with
      # redirects, we set the Host: header above already.
      proxy_redirect off;
      proxy_pass http://127.0.0.1:8000;
    }
}
```

Next, you will learn how to create the Django imge. The `Dockerfile` is based on the official [python](https://hub.docker.com/_/python) image with the following additions:

* [wait-for-it](https://github.com/vishnubob/wait-for-it) is installed to wait for the MySQL database container if you test locally.
* Python dependencies are installed with `pip3 install`.
* A [custom etrypoint](https://docs.docker.com/engine/reference/run/#entrypoint-default-command-to-execute-at-runtime) is defined to run commands before the Django app starts (read on to learn more).
*  [gunicorn](https://gunicorn.org/) runs the app.

> **Customization** Most likely, your folder structure is different. Therefore, adapt the *Copy Python files* section in the following Dockerfile to your needs.

```
FROM python:3.7.4

WORKDIR /usr/src/app

# Install wait-for-it
COPY docker/wait-for-it.sh /usr/local/bin/
RUN chmod u+x /usr/local/bin/wait-for-it.sh

# Install Python dependencies
COPY requirements.txt /usr/src/app/
RUN pip3 install -r requirements.txt

# Copy Python files
COPY example /usr/src/app/example # MODIFY THIS LINE: YOUR FOLDERS ARE DIFFERENT
COPY rapid /usr/src/app/rapid # MODIFY THIS LINE: YOUR FOLDERS ARE DIFFERENT
COPY manage.py /usr/src/app/

# Configure custom entrypoint to run migrations
COPY docker/python/custom-entrypoint /usr/local/bin/
RUN chmod u+x /usr/local/bin/custom-entrypoint
ENTRYPOINT ["custom-entrypoint"]

# Expose port 8000 and start Python server
EXPOSE 8000
CMD ["gunicorn", "-b", "0.0.0.0", "-w", "2", "rapid.wsgi"]
```

> **Customization** The `-w` parameter of gunicorn defines the number of [workers](http://docs.gunicorn.org/en/stable/settings.html#workers) and should be in the range of `2-4 x $(NUM_CORES)`.

The custom entrypoint is used to:

* Wait for the MySQL container if the `WAIT_FOR_IT` environment variable is set (used for testing locally only).
* Run the database migrations before the Django app is started.

```
#!/bin/bash
set -e

if [ -n "${WAIT_FOR_IT}" ]; then
  wait-for-it.sh mysql:3306
fi

echo "running migrations"
python3 manage.py migrate

echo "starting $@"
exec "$@"
```

That's it. You have everything you need to build both, the NGINX as well as the Django image. Next, you will learn how to test your containers and application locally.

### Testing locally

Use [Docker Compose](https://docs.docker.com/compose/) to run your application locally. The following `docker-compose.yml` file configures Docker Compose and starts three containers: NGINX, Django as well as a MySQL database.

```
version: '3'
services:
  nginx:
    build:
      context: '..'
      dockerfile: 'docker/nginx/Dockerfile'
    depends_on:
    - python
    network_mode: 'service:python' # use network interface of python container to simulate awsvpc network mode
  python:
    build:
      context: '..'
      dockerfile: 'docker/python/Dockerfile'
    ports:
    - '8080:80' # forwards port of nginx container
    depends_on:
    - mysql
    environment:
      DATABASE_HOST: mysql
      DATABASE_NAME: app
      DATABASE_USER: app
      DATABASE_PASSWORD: secret
      SECRET_KEY: secret
      WAIT_FOR_IT: 'true'
  mysql:
    image: 'mysql:5.6'
    command: '--default-authentication-plugin=mysql_native_password'
    ports:
    - '3306:3306'
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: app
      MYSQL_USER: app
      MYSQL_PASSWORD: secret
```

The following command starts the application:

```bash
docker-compose -f docker/docker-compose.yml up --build
```

Magically, Docker Compose will spin up three containers: NGINX, Django, and MySQL. Point your browser to [http://localhost:8080](http://localhost:8080) to check that your web application is up and running. The log files of all containers will show up in your terminal, which simplifies debugging a lot.

After you have verified that your application is working correctly, cancel the running `docker-compose` process by pressing `CTRL + C`, and tear down the containers:

```bash
docker-compose -f docker/docker-compose.yml down
```

### Deploying on AWS

You are now ready to deploy your application on AWS.

(1) Build Docker images:
```bash
docker build -t python-django-nginx:latest \
  -f docker/nginx/Dockerfile .
docker build -t python-django-python:latest \
  -f docker/python/Dockerfile .
```
(2) Create ECR repositories:
```bash
aws ecr create-repository --repository-name python-django-nginx \
  --query 'repository.repositoryUri' --output text
aws ecr create-repository --repository-name python-django-python \
  --query 'repository.repositoryUri' --output text
```
(3) Login to Docker registry (ECR):
```bash
$(aws ecr get-login --no-include-email)
```
(4) Tag Docker images:
```bash
docker tag python-django-nginx:latest \
111111111111.dkr.ecr.eu-west-1.amazonaws.com/\
python-django-nginx:latest
docker tag python-django-python:latest \
111111111111.dkr.ecr.eu-west-1.amazonaws.com/\
python-django-python:latest
```
(5) Push Docker images:
```bash
docker push \
111111111111.dkr.ecr.eu-west-1.amazonaws.com/\
python-django-nginx:latest
docker push \
111111111111.dkr.ecr.eu-west-1.amazonaws.com/\
python-django-python:latest
```

There is only one step missing: you need to spin up the cloud infrastructure.

1. Use our [Free Templates for AWS CloudFormation](https://templates.cloudonaut.io/en/stable/fargate/#using-the-clusters-load-balancer-and-path-andor-host-based-routing).
2. Use our [cfn-modules](https://github.com/cfn-modules/docs/tree/master/examples/fargate-alb-proxy-pattern).
3. Use the blueprint from our book [Rapid Docker on AWS](https://cloudonaut.io/rapid-docker-on-aws/).