Como configurar um ambiente de desenvolvimento com Docker + VS Code
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
- Requisitos
- Começando
- A aplicação backend
- A aplicação frontend
- Um proxy nginx simples
- VS Code para o alto e avante!
- Adendo
- Conclusão
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:
- Backend: Python (Django)
- Database: PostgreSQL
- Frontend: VueJS (Quasar)
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:
- Docker 19+ (uma versão recente)
- docker-compose v1.27+
- VS Code 1.5+
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:
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:
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:
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 habilitadaLaunch 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.log
s 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!