Nos posts
anteriores
arranhamos um pouco o conceito por trás do Docker,
bem como introduzimos o utilitário de linha de comando docker
. Nesse último artigo da série, vamos
explorar como usar o Dockerfile no empacotamento de uma aplicação Django.
Se você ainda tem dúvidas em como "encaixar" o Docker no seu fluxo de desenvolvimento, o artigo "Why I Switched from a Traditional Deployment to Using Docker" pode ser uma boa leitura.
Vamos configurar uma aplicação de exemplo para sermos capazes de dar os passos necessários
na criação do Dockerfile
. Primeiramente, vamos criar
virtualenv Python com pyenv:
$ pyenv virtualenv 3.6.1 helloworld-django-docker
Criamos o diretório para o projeto:
$ mkdir ~/Workspace/helloworld
$ cd ~/Workspace/helloworld/
Ativamos o virtualenv:
$ pyenv activate helloworld-django-docker
Instalamos as dependências Django e iniciamos o projeto:
$ pip install django gunicorn
(...)
$ pip freeze > requirements.txt
$ django-admin startproject helloworld ~/Workspace/helloworld
Rodamos o python manage.py runserver
só para termos certeza que está tudo ok:
Além disso, garantimos que o gunicorn
rodará sem maiores problemas:
$ gunicorn helloworld.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 3
Para saber mais sobre o Gunicorn, leia a documentação oficial do projeto.
Com o Docker instalado,
vamos começar a escrever o Dockerfile
. O Mundo Docker define o arquivo como:
(...) um arquivo de definição onde é possível realizar ou preparar todo ambiente a partir de um script de execução. Em resumo, o Dockerfile é um arquivo texto com instruções, comandos e passos que você executaria manualmente, basicamente o Docker executa uma receita de bolo.
É nesse arquivo que escreveremos todos os procedimentos que o Docker executará para criar a imagem da nossa aplicação.
Começamos especificando qual será a "imagem base" da nossa imagem/container. Faremos isso
através da expressão FROM
. Poderíamos dizer aqui que queremos estender a imagem _/debian
e realizar toda a instalação do Python.
Mas felizmente já existem imagens prontas com a linguagem no Docker Hub.
Vamos com a imagem oficial como base:
# Dockerfile
FROM python:3
Quando "buildarmos", o Docker baixará a imagem do Python do repositório (se ela já não existir localmente
em sua máquina), e dará sequência às instruções encontradas no Dockerfile
.
Uma boa prática é "fecharmos o escopo" de trabalho ao setar explicitamente qual será o
diretório no qual iremos realizar demais operações como cópia de arquivos ou execução de binários. Para isso
temos o comando WORKDIR
:
FROM python:3
WORKDIR /usr/src/app
Agora podemos instalar o Django e o Gunicorn, assim como fizemos no início desse artigo:
FROM python:3
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install -r requirements.txt
Você já deve ter reparado um padrão na escrita do Dockerfile
, certo? Os comandos (em maiúsculo)
determinam qual operação será realizada. Os parâmetros (em minúsculo) determinam como isso irá ocorrer.
No caso do comando COPY
, passamos o arquivo requirements.txt
da nossa "máquina local" para
o container, assim o Docker será capaz de encontrar o arquivo e realizar a instalação das dependências
através do comando pip
.
Para finalizar, podemos copiar o resto do projeto e executar o servidor WSGI:
FROM python:3
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "helloworld.wsgi:application", "--bind=0.0.0.0:8000", "--workers=3"]
Comandos RUN
serão executados em tempo de build. São essenciais para a construção do ambiente,
e são responsáveis pelos "layers" discutidos nos posts anteriores. Portanto é bem comum
vermos comandos como RUN apt-get dist-upgrade -y
. Já o CMD
será executado no momento que a sua
imagem for executada.
Diferença entre RUN
, CMD
e ENTRYPONT
.
E se você está se perguntando o motivo de termos dois COPY
, essa prática é útil para o mecanismo de caching
do Docker e invalidação do mesmo, como pode ser visto no guia de boas práticas.
Hora de "buildar"! Execute o seguinte comando:
$ docker build -t <docker-hub-user>/helloworld-django-docker .
Onde:
build
: É o comando para criar uma imagem Docker a partir de um Dockerfile;-t
: O parâmetro -t
é utilizado para nomear e tagear a imagem em questão.
É interessante usar o padrão <usuario-ou-repositorio>/nome-da-imagem
;.
: O caminho onde encontra-se o Dockerfile.Se tudo der certo, será possível listar a imagem recém criada:
$ docker images
REPOSITORY TAG IMAGE ID
kplaube/helloworld-django-docker latest b8d3b0c234a9
Agora sim seremos capazes de executar a aplicação Django através de um container:
$ docker run -it -p 8000:8000 <docker-hub-user>/helloworld-django-docker
E mais uma vez seremos capazes de ver o "It worked!" em http://localhost:8000
.
Belezura! Uma vez que a imagem esteja ok, podemos publicá-la no Docker Hub (afinal, propaganda é a alma do negócio):
$ docker push <docker-hub-user>/helloworld-django-docker
E... voilá! A imagem foi publicada no Docker Hub.
Nos posts anteriores demos uma pincelada numa das características dos containers que é a sua "efemeridade". Isso significa que, a cada nova versão da sua app Django você terá que "buildar" uma nova versão da imagem Docker. A boa notícia é que com o mecanismo de caching do Docker essa operação deverá ser ligeiramente mais rápida do que da primeira vez.
Além disso, mesmo que você tenha uma leve camada de escrita ao executar um container, a informação será perdida ao parar/reiniciar o mesmo. Portanto, o uso de Docker Volumes é essencial quando se faz necessária a persistência de dados.
Com esse post fechamos essa pequena ode ao Docker. Sem dúvida, uma ferramenta que vem mudando paradigmas (o mais recente deles com o lançamento da versão 1.0 do Container Standards).
Pretendo abordar mais sobre Docker aqui no blog, principalmente detalhando melhor o uso do mesmo no deploy de aplicações Django. Com a adoção de conteinerização por serviços PaaS como Heroku e Openshift, há muito assunto a ser explorado nessa seara.
Até a próxima!