Como configurar um ambiente de desenvolvimento com Docker + VS Code
Tags: vscode, docker, webdev, braziliandevs
Published: 2021-04-07 04:24:08

There's a version in english here:
{% link davidrios/how-to-configure-a-docker-vs-code-development-environment-3jko %}

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](#the-project)
- [Requisitos](#requisitos)
- [Começando](#comecando)
- [A aplicação *backend*](#app-backend)
- [A aplicação *frontend*](#app-frontend)
- [Um *proxy* nginx simples](#proxy-nginx)
- [VS Code para o alto e avante!](#vs-code)
    - [Configurando para o *backend*](#vs-code-backend)
    - [Configurando para o *frontend*](#vs-code-frontend)
- [Adendo](#adendo)
- [Conclusão](#conclusao)


## O Projeto <a name="o-projeto"></a>

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)](https://www.djangoproject.com/)
- Database: [PostgreSQL](https://www.postgresql.org/)
- Frontend: [VueJS (Quasar)](https://quasar.dev/)

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 <a name="requisitos"></a>

Vamos precisar de poucas coisas instaladas localmente:

- [Docker](https://docs.docker.com/get-docker/) 19+ (uma versão recente)
- [docker-compose](https://docs.docker.com/compose/install/) v1.27+
- [VS Code](https://code.visualstudio.com/) 1.5+

E estas extensões para o VS Code:

- [Docker](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker)
- [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)


## Começando <a name="comecando"></a>

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* <a name="app-backend"></a>

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>

```bash
## *** 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:

```bash
## 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* <a name="app-frontend"></a>

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>

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

Vamos inicializar o projeto Quasar da mesma forma:

```bash
$ 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.

```bash
## *** 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 <a name="proxy-nginx"></a>

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! <a name="vs-code"></a>

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* <a name="vs-code-backend"></a>

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](https://pypi.org/project/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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d8a9mwayw50e7f8leqo1.png "Project opened")

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

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnma2gluz7tsmkg6a6p9.png)

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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6iy71kxn27d8838r74h2.png)

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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fppt8xkui449cavm18xx.png)

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

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/66kmx40xrhsnkhiyyx18.png)

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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/apy8i292493id3w7x254.png)

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* <a name="vs-code-frontend"></a>

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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hu4cin7flkrlbmaimg74.png)

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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1r3aj6tl1zm31y8bddd7.png)

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](https://blog.chromium.org/2011/05/remote-debugging-with-chrome-developer.html)
- `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](https://marketplace.visualstudio.com/items?itemName=msjsdiag.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](https://marketplace.visualstudio.com/items?itemName=firefox-devtools.vscode-firefox-debug).

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](https://webpack.js.org/) 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":

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3n67820af4t7c7npmz0x.png)

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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sfbquglbieoj4m8bmdt3.png)


## Adendo <a name="adendo"></a>

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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l5wl8yn6nafoon3m7vo6.png)

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 <a name="conclusao"></a>

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!