Já tem um tempo que venho trabalhando com Docker em alguns projetos, e fui fazendo várias configurações até que cheguei num modelo que funciona bem para mim e minha equipe, por isso decidi compartilhá-lo, caso seja útil pra mais pessoas.

Tenha em mente que é tudo baseado na minha experiência pessoal, podendo não usar as melhores práticas ou se aplicar ao seu caso. Mas aceito sugestões de melhorias mesmo assim!

Vou te mostrar como configurar um projeto inteiro do zero e rodar ele totalmente isolado dentro de contêineres, para que o ambiente seja reproduzível e funcione da mesma forma (eu espero), não importa o seu SO.

Eu uso Ubuntu no dia-a-dia, então este guia é dessa perspectiva, mas você deve conseguir adaptar para seu SO de preferência sem muita dificuldade. Provavelmente deve funcionar bem no Windows 10 com WSL 2, mas ainda não testei para ter certeza.

Tabela de Tópicos

O Projeto

Para ilustrar todos os passos, e ficar o mais realista possível, vamos criar um aplicativo web básico usando a seguinte stack:

Ao final do guia você terá um ambiente de edição de código consistente com:

  • Code-completion
  • Linting
  • Debugging
  • Configurações bem definidas de edição de código para todos trabalhando no projeto.

O motivo de escolha dessa stack é por serem tecnologias que eu já estou familiarizado, mas a maioria dos conceitos deste guia devem se aplicar a qualquer outra que você escolher.

Requisitos

Vamos precisar de poucas coisas instaladas localmente:

E estas extensões para o VS Code:

Começando

Para ficar mais fácil de você seguir, subi um repositório no GitHub com todo o código em https://github.com/davidrios/example-docker-project. Vou ligar cada passo ao commit relevante conforme formos prosseguindo, assim você poderá ver toda a estrutura conforme ela evolui.

Estamos começando o projeto do zero, então a primeiro coisa que precisamos organizar é sua estrutura. Esta é uma das que eu gosto de usar:

  • Raíz do projeto
    • README e coisa do docker
    • conf
      • projeto 1
      • projeto 2
      • misc
    • code
      • projeto 1
      • projeto 2

A próxima coisa que precisamos é de alguns contêineres básicos rodando, para podermos inicializar os projetos. Pela descrição do projeto, já sabemos que precisamos de pelo menos três contêineres: backend, frontend e database. Para isso precisamos criar o arquivo docker-compose.yml base. Mas antes, uma das coisas mais úteis que podemos ter é um arquivo .env, para que possamos ter algum nível de personalização sobre os contêineres.

Criemos o arquivo .env.template inicial, que cada usuário fará uma cópia local própria para personalizar:

LOCAL_USER_ID=1000
POSTGRES_PASSWORD=password

A variável LOCAL_USER_ID é usada para lidar com um problema de permissões que pode ocorrer com o Docker no Linux (mais sobre isso mais tarde). Você define ela com o uid do seu usuário no seu computador local, que pode ser consultado executando id -u no terminal. Você pode ignorar esse passo se estiver no Mac.

Depois de criar nossa cópia local do .env, é hora de definir o arquivo docker-compose.yml básico:

https://github.com/davidrios/example-docker-project/blob/2aac6eb151104b205461d025ca07647c44bc5d36/docker-compose.yml

Pontos de atenção:

  • Eu importo o arquivo .env nas linhas 3 a 5, e passo todas as variáveis definidas nele como variáveis de ambiente para todos os outros contêineres. Você pode ver como isso é feito nas linhas 10 e 11, 19 e 20, etc.
  • Estou usando imagens Alpine com versões major e minor especifícadas, como mandam as boas práticas. Você pode querer ou ser obrigado a usar uma imagem baseada em Ubuntu/Debian/etc, mas deve sempre escolher versões major/minor específicas.
  • Foi definido um volume para os dados do PostgreSQL, para que sejam preservados entre cada execução/reconstrução.
  • Sempre que possível prefiro rodar todos os projetos usando um usuário comum (não administrador) dentro dos contêineres, e por gosto de criar volumes para a home desse usuário para cada contêiner que eu espero logar e executar códigos com frequência, assim posso ter algumas coisas bacanas como o histórico do shell e outras coisas preservadas entre os rebuilds. Isso é uma preferência pessoal e completamente opcional, você pode simplesmente remover os volumes. No entanto, apesar disso ser bem conveniente, tenha em mente que pode ter consequências sutis na reproducibilidade geral do ambiente.
  • Para o backend e o frontend, como eles ainda não têm uma aplicação configurada, mas preciso deles rodando de qualquer forma para poder inicializar as aplicações, inicialmente eu coloco eles para rodar somente um longo sleep.

Neste momento podemos navegar para a raíz do projeto e executar:

$ docker-compose up --build

Você deve ver algumas mensagens do PostgreSQL sobre a inicialização do banco de dados e os três contêineres devem estar rodando.

A aplicação backend

Eu gosto de executar a aplicação com um usuário comum, e para prevenir problemas de permissão no Linux, esse usuário precisa ter o mesmo uid que meu usuário local do computador. Esta é a razão para a variável LOCAL_USER_ID no arquivo .env. Para poder seguir com este plano, vamos precisar de alguns scripts auxiliares, que serão usados dentro dos contêineres:

  • base.sh
  • run.sh
  • enter.sh

Vamos adicionálos a um diretório chamado conf/backend/scripts, como convencionado anteriormente. Esses scripts serão executados somente no ambiente de desenvolvimento, então não farão parte da imagem Docker.

Também vamos criar um arquivo Dockerfile para o contêiner com algumas personalizações iniciais. O arquivo do docker-compose será atualizado com a nova imagem e pontos de montagem para o código e os scripts. Um diretório chamado code também será criado, e por fim o docker-compose será executado novamente:

Veja como o projeto mudou:

https://github.com/davidrios/example-docker-project/commit/04ae1e95ca8dfe752e76a67fce5b9882847f2f8e

## *** paramos o docker-compose ***
$ mkdir code
$ docker-compose up --build

Agora temos o contêiner rodando e pronto para criar o projeto Django. Usamos o script enter.sh para entrar no contêiner com o venv já ativado e logado com o usuário correto. Agora vamos criar o projeto:

## Em outro terminal, na raíz do projeto:

## Cria o banco de dados:
$ docker-compose exec -u postgres postgres-db createdb -T template0 -E utf8 backend

## Entra no contêiner do *backend*:
$ docker-compose exec backend /scripts/enter.sh

## Agora dentro do contêiner:
$ cd /code
$ pip install --no-binary psycopg2 django psycopg2
$ django-admin startproject backend
$ cd backend

## Salvamos o *requirements*:
$ pip freeze > requirements.txt

## *** Editamos o arquivo backend/settings.py para conectar à
       instância do PostgreSQL pegando a senha do ambiente e os
       dados de conexão já definidos ***

$ python manage.py migrate
$ python manage.py createsuperuser
## *** escolhe os dados do usuário para login no admin ***

## Testa que a aplicação roda:
$ python manage.py runserver

## *** Parece tudo certo, paramos o servidor e saímos do contêiner ***

Dê uma olhada nos arquivos adicionados, com atenção especial à configuração do banco de dados:

https://github.com/davidrios/example-docker-project/commit/8f47c701e6a3984a32021da360ae0c49a2d68a95#diff-192a1d9e9543969133c5449ace7b1169de815b39d539bc55fc1d168f32eedb7bR76-R85

Agora precisamos mandar a aplicação executar automaticamente junto com o contêiner e expor a porta para que possamos acessar em nosso navegador local. A instalação dos pacotes necessários também será adicionada na imagem.

Para podermos executar localmente em qualquer porta de nossa escolha e não entrar em conflito com nenhuma outra coisa rodando, vamos adicionar esta opção como uma nova linha no arquivo .env.template e a mesma na nossa cópia .env:

APP_PORT=8000

além de fazer as alterações relacionadas ao docker. Veja as mudanças:

https://github.com/davidrios/example-docker-project/commit/c9276155e56f1f8c0168bb81902e5e0f22ed0dad

Agora paramos o docker-compose e executamos novamente, sempre com o parâmetro --buid. A aplicação deve estar rodando e acessível no endereço http://localhost:8000 (ou outra porta caso você tenha modificado)

É isso por enquanto para o backend. A configuração do VS Code vem mais abaixo no guia.

A aplicação frontend

Este é bem similar ao backend. Vamos criar os mesmos três scripts auxiliares, com algumas mudanças específicas para este ambiente, e personalizar a imagem um pouco.

Vamos aproveitar este momento e adicionar o arquivo .dockerignore, para que o Docker não tenha que copiar um monte de arquivos desnecessários na hora que for fazer o build de qualquer imagem. Isso vai agilizar o processo consideravelmente, a depender do tamanho do seu projeto.

Veja as mudanças:

https://github.com/davidrios/example-docker-project/commit/bbb2c6180437c8e1ae5b3b0ca2121e78dc1250af

## *** Paramos o docker-compose e iniciamos novamente ***
$ docker-compose up --build

Vamos inicializar o projeto Quasar da mesma forma:

$ docker-compose exec frontend /scripts/enter.sh
$ yarn global add @quasar/cli
$ cd /code
$ ~/.yarn/bin/quasar create frontend
## *** escolhendo opções do quasar ***
## Escolha pelo menos o ESLint, pois será usado depois.
## Também vou escolher o TypeScript, só para demonstrar o suporte do VS Code
## E finalmente escolha um *preset* ESLint que você vai usar no seu projeto. Agora vou ficar com o "standard"
## Deixe o Quasar instalar os pacotes com o yarn e saia do contêiner

Agora vamos configurar o contêiner do frontend para iniciar o Quasar em modo de desenvolvimento automaticamente e exportar a porta para que possamos acessá-lo.

Eis o resultado das mudanças:

https://github.com/davidrios/example-docker-project/commit/73e3b55df164ea2e1477cb51a8d591299c5f4643

Note que só movemos a porta exportada do contêiner do backend para o do frontend, mas já vamos consertar isso.

## *** Paramos o docker-compose e iniciamos novamente ***
$ docker-compose up --build

Agora o aplicativo Quasar deve estar rodando e acessível no endereço http://localhost:8000 (ou a porta que você definiu).

Um proxy nginx simples

Para facilitar nossa vida, vamos criar um proxy simples mas flexível com o nginx na frente de tudo. Vamos precisar usar um arquivo personalizado, e para uma maior flexibilidade vamos processar esse arquivo e misturar ele com as variáveis de ambiente antes de iniciar o serviço.

Estas são as mudanças feitas:

https://github.com/davidrios/example-docker-project/commit/b07c83199fd8d4b1cd4dfce1e830a19e76b64543

Agora podemos acessar a aplicação frontend no endereço http://localhost:8000, o admin Django em http://localhost:8000/admin, além do nginx redirecionar tudo vindo de /api* para a aplicação backend.

VS Code para o alto e avante!

Agora que temos todo nosso projeto organizadinho e rodando isolado do nosso ambiente local, precisamos começar a codar. Não seria ótimo se pudéssemos ter a mesma experiência agradável e consistente dos contêineres no nosso editor de texto também? É aí que entra o VS Code!

Vamos usar a extensão “Remote - Containers” para executar o VS Code diretamente dentro dos contêineres das aplicações, então ele vai ter acesso ao ambiente exatamente como ele roda, e também podemos instalar extensões sem afetar nossa instalação local.

Vamos usar um diretório chamado code/vscode para colocar as coisas relacionadas, para que também já estejam disponíveis dentro dos contêineres graças ao ponto de montagem /code. Você poderia colocar em outra pasta, mas aí também teria que configurar um outro ponto de montagem para cada contêiner.

Configurando para o backend

Primeiro criamos o diretório code/vscode/backend e adicionamos os seguintes arquivos:

  • backend.code-workspace: Substitua “backend” pelo nome real do seu projeto, assim fica mais fácil navegar entre as janelas abertas do Code.
  • flake8.ini: Onde colocamos nossas configurações de linter para o Python. Vamos usar flake8 ao invés do pylint, porque ele é muito mais rápido e também porque eu gosto mais :)
  • .devcontainer.json: Diz ao Code como configurar sua instância no contêiner

Também será necessário editar o script base.sh do contêiner, assim ele instala os pacotes necessário mesmo entre os rebuilds, além de iniciar a aplicação Python em modo de depuração. Como um agradinho também vamos configurar o servidor para recarregar automaticamente cada vez que algum arquivo é mudado, para isso utilizando a ótima biblioteca Watchdog. Isso também é necessário pois a extensão de depuração do Code não suporta o recarregamento automático nativo do Django. De brinde o watchdog ainda usa bem menos CPU para monitorar as mudanças em arquivos que o sistema nativo do Django.

Dá uma olhada como ficou (já adicionei também uma API de teste):

https://github.com/davidrios/example-docker-project/commit/33cb5d775b88d8b953ba690c17e1359dd5fa34a5

Agora tudo que precisamos fazer é parar o compose e rodar novamente, então tudo estará rodando e pronto. Para trabalhar com o código nós abrimos uma nova janela do Code (Ctrl+Shift+N), vamos no menu File > Open Folder e abrimos a pasta <PROJECT>/code/vscode/backend. No mesmo momento já deve aparecer uma notificação, então é só clicar no botão Reopen in Container. Também é possível utilizar a paleta de comandos (Ctrl+Shift+P), começar a digitar reopen in container e escolher a opção Remote-Containers: Reopen in container. Logo mais ele vai abrir, mostrar uma notificação de que está instalando o Code server e extensões, aí após alguns instantes o código deve ser aberto. No final terá uma indicação na barra de status de que você entá dentro de um contêiner, desta forma:

Então você terá todas as funcionalidades do editor, assim:

E para depurar, é só clicar na margem ao lado do número da linha no editor para configurar um ponto de pausa, ou pressionar a tecla F9 com a linha desejada selecionada, assim:

Então clique no menu lateral para ir à janela Run e clique no botão verde com o símbolo de play. A barra de status deve mudar para a cor laranja, indicando que o depurador está conectado. Se você tiver um ponto de pausa, como eu tinha, e navegar para a API no navegador, você será contemplado com esta maravilha:

Cada vez que um arquivo mudar, o servidor fará o recarregamento automático, desta forma:

E se você precisar executar algum comando administrativo do Django, pode facilmente com o terminal integrado. É só abrir o menu Terminal > New Terminal (Ctrl+Shift+<BACKTICK>), desse jeito:

Se por acaso o terminal não já estiver com o “venv” ativado, é só clicar no botão de lixeira para matar o terminal e abrí-lo novamente.

Configurando para o frontend

Aqui a configuração é bem similar à feita acima. Primeiro criamos o diretório code/vscode/frontend e adicionamos o arquivo .devcontainer.json. Aqui não vou configurar um arquivo de projeto pois não tem mais nenhuma pasta além da principal do projeto, e também para aproveitar os arquivos de configuração que o Quasar já cria na inicialização do projeto. Caso desejasse, você poderia mover tudo para um arquivo de projeto também, como fizemos no caso do backend.

Aqui está o resultado das mudanças:

https://github.com/davidrios/example-docker-project/commit/644b7547322d903f7e4b33454b787e5d1d4c6f49

Desta vez nem precisa parar e rodar o docker-compose, só abrir uma nova janela do Code, abrir o diretório code/vscode/frontend e clicar no botão Reload in Container. Depois de abrir o projeto, clique no botão ESLint na barra de status e escolha a opção allow para permitir a execução dele. Agora você tem um editor configurado, desta maneira:

Você tem até sugestões de completação para coisas como os componentes do Quasar!

Se quiser usar o CLI do Quasar para ações como criar um componente por exemplo, é só abrir o terminal embutido clicando no menu em Terminal > New Terminal (Ctrl+Shift+<BACKTICK>), assim:

Agora é hora de depurar!

Para isso, vamos criar um arquivo launch.json com as tarefas de depuração do Code. Isto não é tão trivial por algumas razões:

  • O VS Code tem dois depuradores que funcionam com o Chrome. Um novo que já vem pré instalado e o antigo que você pode baixar do marketplace. O problema é o que o novo é melhor, mas por alguma razão, suspeito eu que tenha a ver com o fato de rodar remotamente, ele não consegue lançar instâncias do Chrome. O antigo consegue, mas ele não é tão bom. Para resolver esse problema usaremos ambos!
  • Como temos uma porta personalizável através do nosso arquivo .env, precisamos de algum jeito de passá-la para a tarefa de depuração. Infelizmente as extensões de depuração executam no editor local, não no remoto, então elas não têm acesso às variáveis de ambiente. Para resolver esse problema, adicionei uma caixa de diálogo que solicita a porta que a aplicação está rodando, com o mesmo valor padrão do arquivo template do .env.

O arquivo de tarefas de depuração:

https://github.com/davidrios/example-docker-project/blob/6e5195fd13d2aea79b1a31c265ddf60808d9a77e/code/frontend/.vscode/launch.json

Temos três tarefas que usaremos dependendo do nosso navegador:

  • Attach to Chrome: Esta tarefa utiliza o novo depurador para se apegar à uma instância do Chrome já em execução, desde que tenha depuração remota habilitada, e você não precisa instalar mais nada. Veja aqui como executar o Chrome com a opção de depuração remota habilitada
  • Launch Chrome: Esta lança uma instância do Chrome com a depuração remota já habilitada, e também utilizando um perfil novo, para que não afete o seu pessoal. Para usá-la você precisa instalar a extensão Debugger for Chrome.
  • Launch Firefox: E por fim está lança uma instância (ou se apega à uma que já está em execução) do Firefox com a depuração remota habilitada, e também com um perfil novo para não afetar sua instância pessoal. Para utilizar esta tarefa você precisa instalar a extensão Debugger for Firefox.

Tem mais uma coisa que podemos fazer para deixar nossa vida mais agradável enquanto depuramos o código. A configuração padrão do Webpack não gera sourcemaps detalhados, o que acaba deixando o trabalho desnecessariamente mais difícil, além de deixar basicamente impossível de configurar pontos de pausa em arquivos que têm um processamento prévio. Nós corrigimos isso adicionando a opção devtool: 'eval-source-map' nas configurações.

Mesmo assim, para alguns arquivos processados como os do VueJS, ele gera várias variantes do mesmo arquivo com nomes diferentes para cada resultado intermediário do processamento. Isso é meio irritante, então acabamos com esse problema personalizando a função output.devtoolModuleFilenameTemplate do Webpack.

Já que este projeto é em Quasar, vamos fazer do modo correto, como instruído na documentação dele, editando o arquivo quasar.conf.js.

No fim das contas ele fica assim:

https://github.com/davidrios/example-docker-project/commit/1208e30bd280179df0780add22430af3a54c6c30

Agora você pode executar o depurador indo na tela de Run, selecionando a tarefa e clicando no botão de “play”:

Tente adicionar alguns console.logs e/ou configurar algum ponto de pausa, rodar no modo de depuração e usar a aplicação. Eventualmente você deve ver algo parecido com isto:

Adendo

Não sou muito fã de fazer muita coisa dentro do editor de texto, especialmente coias que já podem ser feitas de forma muito mais flexível pelo terminal, por isso que eu não uso a extensão Docker do Code para gerenciar ou executar o docker-compose. Também acho que funciona melhor assim, nesses casos em que você tem um projeto com vários contêineres com aplicações distintas, como no caso deste guia.

Como estamos gerenciando o fluxo manualmente, algumas vezes o VS Code vai exibir uma notificação destas:

Recomendo apenas clicar em Ignore. Se você precisar fazer o build novamente, é só parar o compose executando no terminal e rodar um docker-compose up --build de novo.

Conclusão

Têm vários pontos que podem ser melhorados e várias escolhar diferente que você pode fazer. Estas são as que eu fiz e que funcionam bem para mim, não necessariamente são as “certas”.

Note que esta configuração toda é bem mais adequada a times pequenos ou indivíduos, já que times maiores têm outros requisitos, por vezes mutualmente exclusivos com algumas das decisões aqui tomadas.

Além disso, todo este guia é bem voltado ao desenvolvedor no trabalho diário, então não existem imagens reais que poderiam ser geradas para colocar a aplicação em produção, mas isso pode ser resolvido com um pouco mais de trabalho.

Obrigado por ler!