Criando um container Docker para um projeto Django Existente

Neste post, vou mostrar como criar um container Docker para um projeto Django já existente. Como exemplo, resolvi buscar por uma issue aberta no github que estivesse pedindo para ser dockerizada. Criei um PR para a Issue e usei como exemplo aqui.

Por quê você vai querer dockerizar uma aplicação web django que já existe? Bom, existem muitas razões, se você acha que não tem uma, faça pela diversão!

Eu decidi usar o docker em uma das minhas aplicações porque ela estava ficando muito difícil de instalar. Muitos requisitos do sistema, vários bancos de dados, celery, rabbitmq e por aí vai. Sem dockerizar cada vez que uma nova pessoa entra no time é um inferno pra setar tudo porque levava tempo demais.

O mundo ideal é que o programador tenha em seu ambiente de desenvolvimento o mais próximo que puder do ambiente de produção. Se você usa SQLite na sua máquina mas Postgres no servidor pode ser que tenha problemas de dados que são simplesmente truncados localmente mas que vão levantar erros na base de produção. Só para ter ideia de um exemplo.

Se você não sabe o que é o docker, imagine que é um imenso virtualenv que no lugar de ter apenas pacotes python tem o sistema operacional “inteiro”. Isso consegue isolar seu app de tudo que está no seu SO, bancos de dados, workers, etc.

Mão na massa

Ok, falar é fácil, vamos codar um pouco.

Primeiro de tudo, instale o Docker. Fiz isso no Ubuntu e no Mac sem nenhum problema. Já no Windows Home não consegui fazer funcionar.

Para dizer ao docker que sua aplicação é um container você precisa criar um arquivo Dockerfile:



FROM python:3.6
ENV PYTHONUNBUFFERED 1

RUN mkdir /webapps
WORKDIR /webapps

# Installing OS Dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
libsqlite3-dev

RUN pip install -U pip setuptools

COPY requirements.txt /webapps/
COPY requirements-opt.txt /webapps/

RUN pip install -r /webapps/requirements.txt
RUN pip install -r /webapps/requirements-opt.txt

ADD . /webapps/

# Django service
EXPOSE 8000

Vamos lá, linha a linha:

Docker Images

FROM python:3.6

Aqui estamos usando uma imagem do docker hub. Isto, é um container pré-formatado do docker que permite que você monte sua máquina a partir daquela configuração inicial. Nesse caso, Python 3.6 é um container de um Ubuntu que tem o Python 3.6 instalado nele. Você pode procurar por containers no docker hub.

Environment Variables (Variáveis de ambiente)

Você pode criar todas as variáveis de ambiente que quiser com o ENV.

ENV PYTHONUNBUFFERED 1  # Here we can create all Environment variables for our container

Por exemplo, se você usa variáveis de ambiente para guardar sua secret key do Django é só fazer assim:

ENV DJANGO_SECRET_KEY abcde0s&&$uyc)[email protected]!a95nasd22e-dxt^9k^7!f+$jxkk+$k-

E usar no seu settings, desse jeito:

import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']

Run Commands

Docker Run Commands tem um nome meio óbvio. Você pode rodar um comando “dentro” do seu container. Estou colocando dentro entre aspas porque o docker na verdade cria algo como sub containers para que não precise rodar os mesmos comandos novamente no caso de precisar dar um rebuild do container.

RUN mkdir /webapps
WORKDIR /webapps

# Installing OS Dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
libsqlite3-dev

RUN pip install -U pip setuptools

COPY requirements.txt /webapps/
COPY requirements-opt.txt /webapps/

RUN pip install -r /webapps/requirements.txt
RUN pip install -r /webapps/requirements-opt.txt

ADD . /webapps/

Aqui estamos criando o diretório que guardará os arquivos do projeto: webapps/.

Workdir é uma instrução para mostrar ao docker em qual diretório ele vai rodar os comandos a partir dali.

Depois disso estou instalando as dependências do sistema operacional. Quando estamos usando requirements.txt no projeto, estamos colocando apenas os requisitos do python e não os do SO. Mais um motivo para querer usar o docker para desenvolver. Quanto maior seu projeto, mais requisitos de sistema operacional vão aparecer.

COPY e ADD

Copy e ADD são basicamente a mesma cosia. Ambos copiam um arquivo do seu computador (o Host) dentro do container (o Guest). No meu exemplo, estou apenas copiando o requirements para dentro do docker, para que eu possa dar pip install nos pacotes.

EXPOSE

Expose é para mapear uma porta do Guest (o Container) para o Host (seu computador)

# Django service
EXPOSE 8000

Ok, e agora? Como podemos adicionar mais containers para rodá-los juntos? E se precisarmos colocar um postgresql para rodar em um container também? Não se preocupe, vamos usar o docker-compose.

Docker-Compose

O compose é uma ferramenta para rodar múltiplos containers do docker. Só precisa criar um arquivo yml na pasta do seu projeto com o nome docker-compose.yml:

version: '3.3'

services:
  # Postgres
  db:
    image: postgres
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=postgres

  web:
    build: .
    command: ["./run_web.sh"]
    volumes:
      - .:/webapps
    ports:
      - "8000:8000"
    links:
      - db
    depends_on:
      - db

Aqui estou usando uma imagem do Postgres que também peguei no Docker Hub.

Agora vamos mudar o settings.py para poder usar o Postgres do container como banco de dados.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'postgres',
        'USER': 'postgres',
        'PASSWORD': 'postgres',
        'HOST': 'db',
        'PORT': '5432',
    }
}

Quase lá, deixa eu falar um pouco sobre o arquivo docker-compose.yml,

VOLUMES

Lembra do vagrant?

Era uma vez o Vagrant. Ele era uma forma de rodar um projeto dentro de uma Máquina Virtual que permitia configurar e mapear portas fácilmente, provisionar requisitos do sistema e compartilhar volumes. Seu computador (o Host) podia compartilhar um volume com a máquina virtual (o Guest, ou convidado). No docker, o volume é exatamente a mesma coisa. Quando você escreve um arquivo em um volume que está compartilhado o arquivo também está sendo escrito dentro do container.

volumes:
  - .:/webapps

Nesse o caso, o diretório em que nos encontramos (.) é o que está sendo compartilhado com o container.

LINKS

links:
  - db

Você pode se referir a outro container que pertence ao seu arquivo docker-compose utilizando o nome dele. Como criamos um container com o nome db para o Postgres nós podemos criar um link para ele no nosso container chamado web. Pode ver que no settings.py nós colocamos ‘db‘ como host.

DEPENDS_ON

Para rodar sua aplicação, seu banco de dados precisa estar pronto antes do container web, senão vai dar algum pau!

depends_on:
  - db

Command

Command é o comando padrão que o docker vai rodar logo depois que você subir, ou seja, colocar os containeres para funcionar.

No nosso exemplo, eu criei um run_web.sh que vai rodar as migrações, coletar o static files e iniciar o servidor de desenvolvimento.

#!/usr/bin/env bash

cd django-boards/
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py runserver 0.0.0.0:8000

Alguém pode argumentar que migrar assim automaticamente toda vez que subir o container pode não ser uma boa prática. Eu concordo. Você pode (e deve) rodar o migrate direto na máquina web. Você pode acessar seu container para rodar comandos assim (como no bom e velho vagrant ssh):

docker-compose exec web bash

Se você quiser , você pode rodar o comando sem acessar o container mesmo, apenas mudando o último argumento do comando acima:

docker-compose exec web python manage.py migrate

O mesmo para outros comandos:

docker-compose exec web python manage.py test
docker-compose exec web python manage.py shell

Rodando o Docker

Com nosso Dockerfile, docker-compose.yml e o run_web.sh no lugar, vamos rodar tudo junto:

docker-compose up

Você pode ver esse projeto aqui no meu GitHub.

Escrevi esse texto originalmente em inglês.

**EDITADO**

Antes eu estava usando run no lugar de exec. Mas o Bruno FS me mostrou que o exec é melhor pois está acessando exatamente o container que já está rodando e que o run na verdade está criando um novo container.

References:

Dockerizing Django for Development

In this post, I’ll show how to containerize an existing project using Docker. I’ve picked a random project from GitHub that had an open issue saying Dockerize to contribute and use as an example here.

Why in the world do you want to Dockerize an existing Django web application? There are plenty of reasons, but if you don’t have one just do it for fun!

I decided to use docker because one of my applications was getting hard to install. Lots of system requirements, multiple databases, celery, and rabbitmq. So every time a new developer joined the team or had to work from a new computer, the system installation took a long time.

Difficult installations lead to time losses and time losses lead to laziness and laziness leads to bad habits and it goes on and on… For instance, one can decide to use SQLite instead of Postgres and not see truncate problems on a table until it hits the Test server.

If you don’t know what docker is, just picture it as a huge virtualenv that instead of containing just some python packages have Containers for isolating everything from the OS to your app, databases, workers, etc.

Getting Things Done

Ok, talk is cheap. Show me some code, dude.

First of all, install Docker. I did it using Ubuntu and Mac OS without any problem, but on Windows Home, I couldn’t have it working.

To tell Docker how to run your application as a container you’ll have to create a Dockerfile



FROM python:3.6
ENV PYTHONUNBUFFERED 1

RUN mkdir /webapps
WORKDIR /webapps

# Installing OS Dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
libsqlite3-dev

RUN pip install -U pip setuptools

COPY requirements.txt /webapps/
COPY requirements-opt.txt /webapps/

RUN pip install -r /webapps/requirements.txt
RUN pip install -r /webapps/requirements-opt.txt

ADD . /webapps/

# Django service
EXPOSE 8000

So, let’s go line by line:

Docker Images

FROM python:3.6

Here we’re using an Image from docker hub. e.q. One pre-formated container that helps build on top of it. In this case, Python 3.6 is an Ubuntu container that already has Python3.6 installed on it.

Environment Variables

You can create all sort of Environment Variables using Env.

ENV PYTHONUNBUFFERED 1  # Here we can create all Environment variables for our container

For instance, if you use them for storing your Django’s Secret Key, you could put it here:

ENV DJANGO_SECRET_KEY abcde0s&&$uyc)[email protected]!a95nasd22e-dxt^9k^7!f+$jxkk+$k-

And in your code use it like this:

import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']

Run Commands

Docker Run Commands are kinda obvious. You’re running a command “inside” your container. I’m quoting inside because docker creates something as sub-containers, so it doesn’t have to run the same command again in case of rebuilding a container.

RUN mkdir /webapps
WORKDIR /webapps

# Installing OS Dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
libsqlite3-dev

RUN pip install -U pip setuptools

COPY requirements.txt /webapps/
COPY requirements-opt.txt /webapps/

RUN pip install -r /webapps/requirements.txt
RUN pip install -r /webapps/requirements-opt.txt

ADD . /webapps/

In this case, We are creating the directory that will hold our files webapps/.

Workdir is also kind of self-evident. It just telling docker to run the commands in the indicated directory.

After that, I am including one OS dependency. When we’re just using requirements.txt  we are not including any OS requirement for the project and believe me, for large projects you’ll have lots and lots of OS requirements.

COPY and ADD

Copy and ADD are similar. Both copy a file from your computer (the Host) into the container (The Guest OS). In my example, I’m just coping python requirements to pip install them.

EXPOSE

Expose Instruction is for forwarding a port from Guest to the Host.

# Django service
EXPOSE 8000

Ok, so now what? How can we add more containers and make them work together? What if I need a Postgresql inside a container too? Don’t worry, here we go.

Docker-Compose

Compose is a tool for running multiple Docker containers. It’s a yml file, you just need to create a docker-compose.yml on your project folder.

version: '3.3'

services:
  # Postgres
  db:
    image: postgres
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=postgres

  web:
    build: .
    command: ["./run_web.sh"]
    volumes:
      - .:/webapps
    ports:
      - "8000:8000"
    links:
      - db
    depends_on:
      - db

In this case, I’m using an Image of Postgres from Docker Hub.

Now, let’s change the settings.py to use Postgres as Database.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'postgres',
        'USER': 'postgres',
        'PASSWORD': 'postgres',
        'HOST': 'db',
        'PORT': '5432',
    }
}

We’re almost done. Let me talk a little about the docker-compose file.

VOLUMES

Remember vagrant?

Once upon a time, there was Vagrant and it was a form to run a project inside a Virtual Machine but easily configuring it, forwarding ports, provisioning requirements and, sharing volumes. Your machine (Host) could share a volume with your Virtual Machine (Guest). In docker, it’s exactly the same. When you’re writing a file on a shared volume this file is being written on your container as well.

volumes:
  - .:/webapps

In this case, the current directory (.) is being shared as webapps on the container.

LINKS

links:
  - db

You can refer to another container that belongs to your compose using its name. Since we created a db container for our Postgres we can link it to our web container. You can see in our settings.py file that I’ve used ‘db‘ as host.

DEPENDS_ON

In order for your application to work, your database has to be ready for use before web container, otherwise, it will raise an exception.

depends_on:
  - db

Command

Command is the default command that your container will run right after it is up.

For our example, I’ve created a run_web.sh, that will run the migrations, collect the static files and start the development server.

#!/usr/bin/env bash

cd django-boards/
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py runserver 0.0.0.0:8000

One can argue that run the migrate at this point, automatically, every time the container is up is not a good practice. I agree. You can run it directly on the web machine. You can access your container (just like the good’ol vagrant ssh) :

docker-compose exec web bash

If you’d like you can run it without accessing the container itself, just change the last argument from the previous command.

docker-compose exec web python manage.py migrate

The same for other commands

docker-compose exec web python manage.py test
docker-compose exec web python manage.py shell

Running Docker

With our Dockerfile, docker-compose.yml and run_web.sh set in place, just run it all together:

docker-compose up

You can see this project here on my GitHub.

**EDIT**

At first, I was using run instead of exec. But Bruno FS convinced me that exec is better because you’re executing a command inside the container you’re already running, instead of creating a new one.

References: