Deploying containers to production with Terraform and AWS Fargate – Containerization
Tags: docker, python, django, aws
Published: 2019-07-15 03:09:30

In the [previous post](https://dev.to/ianrodrigues/deploying-containers-to-production-with-terraform-and-aws-fargate-introduction-28l8), I've covered some of the services available to [run containers on AWS](https://aws.amazon.com/ecs/). Also, I've presented [Terraform](https://terraform.io), a tool to declare and provision those services using codes. This time, I'm gonna set up the project to run on [Docker](https://docker.io) either locally and in production, using almost the same configuration.

Though I have more experience with [PHP](https://php.net), I'm gonna provision a [REST API](https://en.wikipedia.org/wiki/Representational_state_transfer) written in Python ([Django](https://www.djangoproject.com/)), since I've been working with it lately a lot. Please, don't focus on the application itself; it's written only for the sake of this post. Also, I'm sure you can apply the same principles I will use here for any language or framework. _So, let's get the hands dirty!_

## Setting up the local environment

First, let's set up the local environment to run with Docker. This way, we will end up with an environment to work with identical to the production one. For this, we'll use [Docker Compose](https://docs.docker.com/compose/). Create a new file called `docker-compose.yml` on the root directory:

```yml
version: '3.7'

volumes:
  postgres-data:
    driver: local

services:
  django:
    build: .
    volumes:
      - .:/usr/local/app
    ports:
      - 8000:8000
    depends_on:
      - database

  database:
    image: postgres:11-alpine
    volumes:
      - postgres-data:/var/lib/postgresql
    environment:
      POSTGRES_USER: wannajob
      POSTGRES_PASSWORD: wannajob
      POSTGRES_DB: wannajob
```

I won't get into details of the `docker-compose.yml` file. If you wanna learn more about it, [check this out](https://docs.docker.com/compose/compose-file/). Let's now create a `Dockerfile`. Docker Compose will use it to build the Django's container: 

```Dockerfile
FROM python:3.6-slim-stretch

ENV PYTHONUNBUFFERED 1

WORKDIR /usr/local/app

RUN pip install --upgrade pip safety bandit pylint flake8 pytest coverage

COPY requirements.txt /usr/local/app/requirements.txt
RUN pip install -r requirements.txt

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

Again, I won't get into details of the `Dockerfile`. If you wanna learn more about it, [check this out](https://docs.docker.com/engine/reference/builder/). Everything's set up, it's time to get the project up and running:

```bash
$ docker-compose up -d
```

Finally, run the migrations:

```bash
$ docker-compose run --rm --no-deps django python manage.py migrate
```

See? The project is ready for local development using Docker. Now, it's easy to start up the project anywhere. You can share it with your entire team, and everyone will have the same environment. No more, "But it works on my machine!".

## Preparing for production

Now that the project is running locally, we can spend some time and make a few adjustments to prepare it for production. While locally we are using the `runserver` command – a lightweight development webserver – to serve the application, in production we'll need a "real" webserver. Here is where [Nginx](https://www.nginx.com/) comes in. Yet, we can't serve the app directly through Nginx as it cannot process Python. We need something (a [WSGI](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface)) to where Nginx will forward the request, process the Python code, send back to Nginx, and then return the response to the client. For this, we will use [Gunicorn](https://gunicorn.org/).

The first thing to do is to update the `Dockerfile`. We will make use of the [Docker Multi-Stage Build](https://docs.docker.com/develop/develop-images/multistage-build/), see:

```Dockerfile
##############
# Base Stage
##############
FROM python:3.6-slim-stretch AS base

ENV PYTHONUNBUFFERED 1

WORKDIR /usr/local/app

RUN pip install --upgrade pip gunicorn

COPY requirements.txt /usr/local/app/requirements.txt
RUN pip install -r requirements.txt

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--log-level", "warning", "--reload", "wannajob.wsgi:application"]


#####################
# Development Stage
#####################
FROM base AS development

RUN pip install --upgrade safety bandit pylint flake8 pytest coverage


####################
# Production Stage
####################
FROM base AS production

ENV DJANGO_SETTINGS_MODULE 'wannajob.settings.production'

COPY . /usr/local/app


#################
# Statics Stage
#################
FROM production AS statics

RUN python manage.py collectstatic --no-input --clear


############################
# Production (Nginx) Stage
############################
FROM nginx:1.17-alpine AS nginx

COPY ./docker/nginx/django.conf /etc/nginx/conf.d/default.conf

WORKDIR /usr/local/app

RUN mkdir statics
COPY --chown=nginx:nginx --from=statics /usr/local/app/static /usr/local/app/static
```

Then, create the `docker/nginx/django.conf` file:

```conf
server {
  listen 80;

  location = /favicon.ico {
    access_log    off;
    log_not_found off;
  }

  location /static/ {
    alias /usr/local/app/static/;
  }
  
  location / {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://django:8000;
  }
}
```

And finally, update the `docker-compose.yml`, to use Nginx:

```yml
version: '3.7'

volumes:
  postgres-data:
    driver: local

services:
  django:
    build:
      context: .
      target: development
    volumes:
      - .:/usr/local/app
    depends_on:
      - database

  nginx:
    image: nginx:1.17-alpine
    volumes:
      - ./docker/nginx:/etc/nginx/conf.d
      - ./static:/usr/local/app/static
    ports:
      - 8000:80
    depends_on:
      - django

  database:
    image: postgres:11-alpine
    volumes:
      - postgres-data:/var/lib/postgresql
    environment:
      POSTGRES_USER: wannajob
      POSTGRES_PASSWORD: wannajob
      POSTGRES_DB: wannajob
```

Let's rebuild the application:

```bash
$ docker-compose up -d --build
```

Now, the project is being served through Nginx locally, there's no difference from what we will be doing in production in the future. Also, thanks to the Docker Multi-Stage Build, we have all the necessary development tools locally, but we can get rid of them when running in production, which will result in a more lightweight image.

Have a look at the size of the development and production images:

```bash
$ docker images --filter reference=ianrodrigues/wannawork-app 
REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
ianrodrigues/wannawork-app   production          29a4de730436        3 minutes ago       196MB
ianrodrigues/wannawork-app   development         cf3a9bf4ca10        2 hours ago         223MB
```

Pretty cool, huh?!

As you could see, it's simple to set up Docker for a project to run locally, and with a few modifications, you can use the same setup ready for production. Thanks to Docker Multi-Stage Build, you can create flexible Dockerfiles that will result in more lightweight images, which is pretty suitable for production.

The [source code](https://github.com/ianrodrigues/wannajob-api) used in this post is available on Github. If you have questions or suggestions, please let me know in the comments. In the next post, I'll set up the environment on the AWS using Terraform, stay tuned!