O que são Dev Containers?

editar

Dev Containers são uma tecnologia que se usa de contêineres para a criação de ambientes de desenvolvimento reprodutíveis. Através deles é possível realizar o setup de um projeto para desenvolvimento de forma ágil e sem conflitos com softwares já instalados na máquina alvo.

Existem diferentes implementações de Dev Containers, aqui vamos abordar mais especificamente a implementação usada pelo Visual Studio Code que usa Docker como motor de conteinerização através da extensão Dev Containers.

Como criar Dev Containers?

editar

Dev Containers são definidos por um arquivo .devcontainer.json ou .devcontainer/devcontainer.json. Dentro desse arquivo existem muitas propriedades que podem ser definidas para customizar o ambiente de desenvolvimento. A documentação completa pode ser vista em Dev Containers metadata reference, mas aqui vamos apresentar as propriedades principais para uma configuração básica.

Configurando o devcontainer.json

editar

"name"

editar

Uma string> que define o nome do ambiente, útil para manter a organização.

{
    "name": "My Node Project"
}

"containerUser"

editar

Especifica qual usuário ser usado dentro do contêiner. Por padrão o usuário root será usado e isso não é uma boa prática.

{
    "containerUser": "dev"
}

Note que essa propriedade define qual usuário vai ser usado, ela não vai criar o usuário por você, mas sim usar um usuário já criado pela imagem ou Dockerfile.

"image"

editar

Define qual a imagem deve ser usada para o contêiner do ambiente. A Microsoft disponibiliza dezenas de imagens prontas para determinados ambientes através do Microsoft Container Registry. Mas é possível usar imagens de qualquer registro de contêiner (Dockerhub, por exemplo).

A título de contexto, uma imagem define um modelo para criação dos contêineres. Dois contêineres criados a partir de uma mesma imagem devem ter comportamento idêntico. Assim, dois dev containers criados a partir de uma mesma imagem definirão ambientes de desenvolvimento idêntico para os desenvolvedores.

{
    "image": "mcr.microsoft.com/devcontainers/typescript-node"
}

"build"

editar

Alternativamente, ao invés de usar uma imagem pronta, pode-se definir a imagem usando um Dockerfile. Essa propriedade é definida com um objeto, abaixo vamos indicar os atributos mais importantes desse objeto.

Apenas vale ressaltar, que toda vez que citarmos um caminho, ele é relativo ao local onde se encontra o .json.

"build"."dockerfile"
editar

Define o caminho do Dockerfile a ser usado para construir a imagem do ambiente.

"build"."context"
editar

Contexto do build da imagem (similar ao context usado com o comando docker build [CONTEXT]).

Esse contexto indica a que os caminhos são relativos em instruções como COPY no Dockerfile.

"build"."args"
editar

Uma lista com os argumentos definidos no Dockerfile com a instrução ARG.

{
    "build": {
        "dockerfile": "Dockerfile",
        "context": "..",
        "args": {
            "USER": "dev",
            "NODE_VERSION": "22"
        }
    }
}

"dockerComposeFile"

editar

É possível usar uma composição de contêineres como ambiente de desenvolvimento, ao invés de um contêiner único definido com "image" ou "build". Aqui, se define onde está o arquivo docker-compose.yaml.

{
    "dockerComposeFile": "docker-compose.yaml"
}

"service"

editar

Quando usando "dockerComposeFile" é necessário indicar qual dos serviços definidos é o serviço principal, onde o VS Code irá operar.

{
    "dockerComposeFile": "docker-compose.yaml",
    "service": "app"
}

"forwardPorts"

editar

Quando criando projetos que vão expor aplicações à rede, pode-se realizar port forwarding das portas de dentro do contêiner para o host, para que a aplicação possa ser acessada de fora do ambiente de desenvolvimento.

{
    "forwardPorts": [3000]
}

Em um cenário com Docker Compose, você pode especificar de qual serviço é a porta com a sintaxe "service:port".

"mounts"

editar

Permite montar diretórios do hospedeiro dentro do contêiner do ambiente de desenvolvimento.

{
    "mounts": [
        { "type": "volume", "source": "some-volume", "target": "/folder/in/container" },
        { "type": "bind", "source": "/folder/in/host", "target": "/folder/in/container" }
    ]
}

"workspaceMount" e "workspaceFolder"

editar

Usados em conjunto para definir como o projeto será montado dentro do ambiente conteinerizado.

Por exemplo:

{   
    "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
    "workspaceFolder": "/workspace"
}

Com isso, a pasta do projeto (pasta aberta no VS Code) será montada em /workspace dentro do contêiner assim que o ambiente for iniciado.

"customizations"."vscode"

editar

Permite que definam configurações para o VS Code que irá executar dentro do ambiente. Dentre as customizações disponíveis estão, por exemplo: extensões.

{
    "customizations": {
        "vscode": {
            "extensions": [
                "dbaeumer.vscode-eslint",
                "esbenp.prettier-vscode"
            ]
        }
    }
}

"postCreateCommand"

editar

Define um comando a ser executado imediatamente após o ambiente ser criado. Isso é muito útil para instalar as dependências do projeto que precisam que a pasta do projeto seja montada dentro do contêiner. Por exemplo:

{
    "postCreateCommand": "bash -i -c 'npm install'"
}

"postStartCommand"

editar

Define um comando a ser executado toda vez que o ambiente é iniciado, diferentemente do "postCreateCommand" que é executado apenas uma vez.

Variáveis de Ambiente

editar

Em algumas das strings acima é possível usar variáveis de ambiente para definir valores.

  • ${localEnv:VAR}: usa uma variável de ambiente do host
  • ${containerEnv:VAR}: usa uma variável de ambiente do contêiner
  • ${localWorkspaceFolder}: diretório onde se encontra o projeto no host
  • ${containerWorkspaceFolder}: diretório onde se encontra o projeto no contêiner

Como Usar Dev Containers?

editar

O VS Code disponibiliza a extensão Dev Containers que é usada para gerenciar espaços de desenvolvimento. Normalmente, assim que um diretório for aberto e ele tiver um .devcontainer.json ou .devcontainer/devcontainer.json um pop-up será exibido sugerindo Reopen in Container. Além disso, a command palette do VS Code (pode ser aberta com Ctrl+Shift+P) fornece alguns comandos, alguns dos mais importantes são:

  • Reopen in Container: abre o VS Code dentro do contêirner do ambiente de desenvolvimento
  • Rebuild and Reopen in Container: similar ao anterior, mas realiza o rebuild do(s) contêiner(es) previamente, isso faz com que o postCreateCommand seja executado novamente.
  • Rebuild without Cache and Reopen in Container: similar ao anterior, mas descarta o cache antes do rebuild.
  • New Dev Container: cria um novo DevContainer, a partir de uma diversa seleção de imagens preparadas.

O reopen vai fazer o VS Code reabrir conectado ao ambiente de desenvolvimento do contêiner pronto para uso.

Resolvendo Problemas

editar

Muitas vezes, sua definição de Dev Container não estará correta e o ambiente não poderá ser iniciado. Caso isso ocorra, o VS Code irá sugerir Open devcontainer.json locally, isso irá fazer o VS Code voltar ao seu ambiente local (fora do contêiner) com um log de erros do ambiente para que você possa corrigir sua configuração e depois realizar o rebuild.

Talvez sua configuração não esteja correta, mas não esteja errada o suficiente para que o ambiente não possa ser iniciado. Assim, você pode editar o arquivo de definição do Dev Container e imediatamente o VS Code irá sugerir um rebuild do ambiente.

Aplicações

editar

Testagem de software

editar

Para facilitar a testagem, é necessário ter um ambiente controlado para que cada rodada de testes seja o mais parecida uma com a outra possível, em relação a uso de processamento, memória e tempo de execução. Isso facilita a análise dos possíveis problemas e possíveis pontos de melhoria, além de observar os impactos dessas melhorias e/ou mudanças.de melhoria, além de observar os impactos dessas melhorias e/ou mudanças.

Isso inclui, entre outras coisas, a automação dessa testagem, que pode ser feita utilizando postCreateCommand ou postStartCommand.

Isolamento de ambientes

editar

Para desenvolvedores, que possuem uma grande quantidade de projetos em trabalho, isolar cada um desses projetos é importante para que as dependências e configurações não possam interferir no funcionamento de uns dos outros. Isso é importante principalmente em projetos que utilizam dependências e linguagens muito parecidas, o que pode confundir tanto o desenvolvedor, quanto o sistema, caso os Dev Containers não sejam usados.

Portabilidade

editar

Caso o desenvolvedor possua mais de uma máquina ou faça alterações nessa máquina, os Dev Containers serão responsáveis por proporcionar o mesmo ambiente, independente do ambiente atual da máquina usada. Claro que, é necessário que o Container deve ser configurado de forma que seja um "piso" de capacidade de processamento, em relação aos ambientes de desenvolvimento disponíveis.

Reprodutibilidade

editar

Para garantir que a execução, desenvolvimento e testes feitos em ambiente sejam reproduzidos da forma mais fiel possível, garantindo ambiente imutáveis. Isso evita problemas do tipo: "ah, mas no meu funciona!".

Exemplo

editar
{
   "name": "node-app",
   "image": "docker.io/node:22",
   "containerUser": "node",
   "forwardPorts": [ 3000 ],   
   "workspaceFolder": "/app",
   "workspaceMount": "source=${localWorkspaceFolder},target=/app,type=bind,consistency=cached",
   "customizations": {
       "vscode": {
           "extensions": [
               "dbaeumer.vscode-eslint",
               "esbenp.prettier-vscode"
           ]
       }
   },
   "postCreateCommand": "bash -i -c 'cd /app && npm install'"
}

O exemplo acima cria um simples ambiente para desenvolvimento de aplicações usando Node.js. Nesse ambiente temos um contêiner criado com Node 22, que expõe a porta 3000 para o sistema e vai montar o diretório do projeto no diretório /app do contêiner. Sempre que dispararmos um rebuild o iremos executar um npm install para instalar as dependências do projeto.

Observe que tudo isso ocorre dentro de um contêiner e portanto, as dependências de projeto são completamente independentes do que você tiver instalado no seu computador, evitando assim conflitos durante o setup.