Docker se tornou uma das ferramentas mais importantes no desenvolvimento moderno de software. Seja trabalhando com APIs REST, aplicações ASP.NET Core, microsserviços ou sistemas distribuídos, entender como utilizar containers pode transformar a maneira como você desenvolve, testa e publica suas aplicações.
Para desenvolvedores .NET, essa tecnologia oferece uma série de vantagens: permite executar aplicações em ambientes isolados, padronizar configurações, simplificar integrações com bancos de dados e preparar sistemas para diferentes ambientes de hospedagem.
Mas será que você precisa conhecer todos os recursos do Docker para começar a utilizá-lo?
A resposta é não.
Neste artigo, vamos explorar os principais conceitos que um desenvolvedor .NET realmente precisa dominar para trabalhar com Docker, desde os primeiros comandos até a criação de imagens otimizadas para aplicações ASP.NET Core.
Docker é uma plataforma que permite empacotar aplicações e suas dependências em unidades isoladas chamadas containers.
Na prática, isso significa que você pode executar uma aplicação .NET com todas as dependências necessárias, independentemente das ferramentas instaladas diretamente no computador.
Imagine o seguinte cenário:
Você desenvolve uma API utilizando ASP.NET Core e SQL Server. No seu computador, tudo funciona perfeitamente.
Entretanto, quando outro desenvolvedor tenta executar o projeto, surgem problemas:
Uma versão diferente do .NET SDK.
Banco de dados configurado de maneira diferente.
Dependências que não estão instaladas.
Configurações específicas do sistema operacional.
Variáveis de ambiente ausentes.
Essas diferenças podem provocar erros difíceis de identificar.
Com Docker, você consegue padronizar boa parte desse ambiente.
Em vez de cada desenvolvedor configurar manualmente todas as dependências, é possível definir a infraestrutura necessária em arquivos de configuração e executar os serviços utilizando containers.
Isso proporciona benefícios importantes:
Padronização dos ambientes de desenvolvimento.
Facilidade para configurar bancos de dados.
Maior consistência entre desenvolvimento, testes e produção.
Facilidade para executar múltiplos serviços.
Integração com pipelines de CI/CD.
Simplificação do processo de implantação.
Um ponto importante: Docker não substitui o .NET, o Visual Studio ou o Visual Studio Code. Ele complementa essas ferramentas, oferecendo uma maneira mais consistente de executar aplicações e suas dependências.
Antes de começar a trabalhar com aplicações .NET, precisamos entender alguns conceitos fundamentais.
Não é necessário estudar profundamente toda a arquitetura do Docker. Entretanto, existem alguns termos que você encontrará constantemente.
Uma imagem Docker é um pacote que contém os arquivos, bibliotecas, dependências e configurações necessárias para executar determinado software.
Por exemplo, a Microsoft disponibiliza imagens oficiais para diferentes versões do .NET.
Algumas das mais importantes são:
mcr.microsoft.com/dotnet/sdk: contém o SDK necessário para compilar aplicações.
mcr.microsoft.com/dotnet/aspnet: contém o runtime do ASP.NET Core.
mcr.microsoft.com/dotnet/runtime: contém o runtime utilizado por aplicações .NET que não dependem do ASP.NET Core.
Uma imagem funciona como um modelo utilizado para criar containers.
Um container é uma instância executável de uma imagem.
Imagine que você possui uma imagem contendo uma API ASP.NET Core.
A partir dela, pode criar um ou vários containers executando a aplicação.
Os containers possuem isolamento de processos e recursos, permitindo executar diferentes serviços sem misturar suas dependências.
Uma característica importante é que containers são projetados para serem descartáveis.
Isso significa que você pode interromper, remover e criar novamente um container sem precisar reconstruir toda a aplicação.
Entretanto, dados que precisam sobreviver à remoção do container devem ser armazenados em volumes ou em serviços externos.
O Dockerfile é um arquivo de texto que descreve como uma imagem Docker deve ser construída.
Nele, definimos:
Qual imagem será utilizada como base.
Qual diretório será utilizado.
Quais arquivos serão copiados.
Quais comandos serão executados.
Qual aplicação será iniciada.
Para desenvolvedores .NET, o Dockerfile é um dos arquivos mais importantes para aprender.
Docker Compose permite definir e executar aplicações compostas por múltiplos containers.
Imagine uma solução que possui:
Uma API ASP.NET Core.
Um banco de dados SQL Server.
Um serviço Redis.
Um serviço responsável pelo processamento de mensagens.
Em vez de iniciar cada container individualmente, podemos descrever toda a infraestrutura em um único arquivo chamado compose.yaml.
Com um único comando, conseguimos iniciar os serviços necessários.
Esses quatro conceitos representam a base que você precisa compreender para começar a trabalhar com Docker.
Vamos começar com um exemplo prático.
Imagine que você possui uma aplicação ASP.NET Core chamada MinhaApi.
O primeiro passo é criar um arquivo chamado Dockerfile na raiz do projeto.
Vamos utilizar o .NET 10 como referência neste exemplo.
O Dockerfile ficará assim:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["MinhaApi.csproj", "./"]
RUN dotnet restore "MinhaApi.csproj"
COPY . .
RUN dotnet publish "MinhaApi.csproj" \
-c Release \
-o /app/publish \
--no-restore
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENV ASPNETCORE_HTTP_PORTS=8080
ENTRYPOINT ["dotnet", "MinhaApi.dll"]
Vamos entender o que está acontecendo.
Define a imagem utilizada como base.
Na primeira etapa, utilizamos:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
Essa imagem contém o SDK do .NET, necessário para restaurar dependências, compilar e publicar a aplicação.
Na segunda etapa, utilizamos:
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
Essa imagem contém os componentes necessários para executar uma aplicação ASP.NET Core.
Observe que não precisamos levar todo o SDK para a imagem final.
Define o diretório de trabalho dentro do container.
WORKDIR /src
Isso evita precisar informar caminhos absolutos em todos os comandos seguintes.
Copia arquivos do computador para dentro da imagem.
COPY ["MinhaApi.csproj", "./"]
Nesse momento, copiamos apenas o arquivo do projeto.
Essa estratégia permite aproveitar melhor o cache de build do Docker.
Se o arquivo .csproj não sofrer alterações, o Docker poderá reutilizar a camada responsável pelo restore, evitando repetir desnecessariamente essa operação.
Executa comandos durante a construção da imagem.
RUN dotnet restore "MinhaApi.csproj"
Esse comando restaura os pacotes NuGet necessários para o projeto.
Posteriormente, executamos o publish:
RUN dotnet publish "MinhaApi.csproj" -c Release -o /app/publish --no-restore
Isso gera os arquivos necessários para executar a aplicação.
Indica a porta que a aplicação utiliza dentro do container.
EXPOSE 8080
É importante entender que essa instrução não publica automaticamente a porta no computador hospedeiro. Para isso, utilizamos o parâmetro -p no comando docker run ou a propriedade ports no Docker Compose.
Define o comando principal executado quando o container é iniciado.
ENTRYPOINT ["dotnet", "MinhaApi.dll"]
Nesse caso, estamos iniciando nossa API ASP.NET Core.
Um dos conceitos mais importantes para desenvolvedores .NET é o Multi-stage Build.
Observe novamente nosso Dockerfile.
Utilizamos duas imagens diferentes:
SDK: utilizada para compilar e publicar a aplicação.
ASP.NET Core Runtime: utilizada para executar a aplicação.
Essa separação é chamada de Multi-stage Build.
Mas por que isso é importante?
Imagine que utilizássemos somente a imagem do SDK.
Nossa imagem final carregaria ferramentas que não são necessárias durante a execução da aplicação.
Isso poderia aumentar o tamanho da imagem e ampliar sua superfície de ataque.
Com Multi-stage Build, conseguimos separar o ambiente de compilação do ambiente de execução.
O resultado é uma imagem final mais enxuta, contendo somente os arquivos necessários para executar a aplicação.
Essa abordagem é especialmente importante quando estamos preparando aplicações para ambientes de produção.
Outro benefício é a possibilidade de utilizar diferentes etapas para executar testes automatizados, compilar a aplicação e gerar a imagem final.
Agora que temos nosso Dockerfile, podemos construir a imagem.
Abra o terminal na pasta do projeto e execute:
docker build -t minha-api:1.0 .
Vamos entender os parâmetros:
docker build: inicia a construção da imagem.
-t: define o nome e a identificação da imagem.
minha-api:1.0: nome e versão da imagem.
.: indica que o contexto de build será o diretório atual.
Depois da execução, podemos verificar as imagens disponíveis:
docker images
Para executar nossa aplicação, utilizamos:
docker run -d -p 8080:8080 --name minha-api minha-api:1.0
Nesse exemplo:
-d executa o container em segundo plano.
-p 8080:8080 publica a porta 8080 do container na porta 8080 do computador.
--name define um nome para o container.
minha-api:1.0 identifica a imagem utilizada.
Agora podemos acessar nossa API através de:
Para verificar os containers em execução:
docker ps
Para visualizar os logs:
docker logs minha-api
Para interromper o container:
docker stop minha-api
E para removê-lo:
docker rm minha-api
Esses comandos já permitem trabalhar com boa parte das operações básicas do Docker.
Até agora, trabalhamos com apenas um container.
Entretanto, aplicações reais geralmente possuem diferentes componentes.
Uma API pode depender de um banco de dados, de um cache ou de um serviço responsável pelo processamento de mensagens.
É justamente nesse cenário que Docker Compose se torna muito útil.
Imagine que você deseja executar sua API .NET e um banco SQL Server localmente.
Em vez de configurar tudo manualmente, podemos utilizar um arquivo compose.yaml.
Exemplo:
services:
api:
build:
context: .
dockerfile: Dockerfile
container_name: minha-api
ports:
- "8080:8080"
environment:
ASPNETCORE_ENVIRONMENT: Development
ConnectionStrings__DefaultConnection: "Server=db,1433;Database=MinhaBase;User Id=sa;Password=${DB_PASSWORD};TrustServerCertificate=True"
depends_on:
db:
condition: service_healthy
db:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: meu-sqlserver
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: "${DB_PASSWORD}"
ports:
- "1433:1433"
volumes:
- sqlserver_data:/var/opt/mssql
healthcheck:
test: ["CMD-SHELL", "bash -c '
Esse exemplo demonstra como organizar os serviços de uma aplicação.
Observe alguns detalhes importantes.
Cada serviço representa um componente da nossa infraestrutura.
Nesse exemplo, temos dois serviços:
api
db
Permite definir variáveis de ambiente utilizadas pelos containers.
Observe esta configuração:
ConnectionStrings__DefaultConnection
No ASP.NET Core, o uso de dois underscores permite representar a hierarquia de configuração.
Nesse caso, estamos configurando a connection string DefaultConnection.
Observe a connection string:
Server=db,1433
Um detalhe importante: dentro do Docker Compose, os serviços conseguem se comunicar utilizando seus nomes como endereços de rede.
Portanto, nossa API utiliza db como hostname para encontrar o SQL Server.
Não devemos utilizar localhost nesse cenário.
Dentro de um container, localhost representa o próprio container, e não o computador hospedeiro ou outro serviço.
Observe:
volumes:
- sqlserver_data:/var/opt/mssql
Volumes permitem persistir dados mesmo quando um container é removido e recriado.
Isso é especialmente importante para bancos de dados.
Sem persistência adequada, você corre o risco de perder os dados armazenados quando recriar o ambiente.
O healthcheck permite verificar se um serviço está respondendo conforme o critério configurado.
No exemplo, verificamos se a porta do SQL Server está aceitando conexões.
É importante observar que esse teste verifica disponibilidade da porta, não garante que o banco esteja completamente inicializado ou pronto para executar consultas.
Para iniciar os serviços, execute:
docker compose up -d
Para visualizar os containers:
docker compose ps
Para acompanhar os logs:
docker compose logs -f
Para interromper os serviços:
docker compose down
Atenção: o comando docker compose down não remove volumes nomeados por padrão. Entretanto, utilizar a opção -v remove também os volumes associados ao projeto e pode provocar perda dos dados persistidos.
Outro detalhe: o exemplo utiliza uma senha fornecida por variável de ambiente. Para produção, prefira mecanismos apropriados de gerenciamento de secrets, evitando armazenar credenciais diretamente em arquivos de configuração.
Outro conhecimento importante é entender como trabalhar com configurações dentro dos containers.
Aplicações ASP.NET Core normalmente utilizam arquivos como:
appsettings.json
appsettings.Development.json
appsettings.Production.json
Entretanto, quando trabalhamos com Docker, é interessante externalizar configurações que podem variar entre ambientes.
Por exemplo:
Connection strings.
Endpoints de serviços externos.
Chaves de integração.
Configurações de autenticação.
Identificação do ambiente de execução.
O ASP.NET Core consegue carregar essas configurações através de variáveis de ambiente.
Imagine que você possui a seguinte configuração:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=MinhaBase"
}
}
No Docker Compose, podemos sobrescrever esse valor:
environment:
ConnectionStrings__DefaultConnection: "Server=db;Database=MinhaBase"
Dessa maneira, não precisamos modificar o arquivo appsettings.json para cada ambiente.
Isso facilita bastante a implantação e manutenção das aplicações.
Uma recomendação importante: não coloque senhas, tokens ou outras informações sensíveis diretamente no Dockerfile ou no controle de versão. Utilize secrets ou mecanismos de gerenciamento de credenciais apropriados ao ambiente.
Existe outro arquivo que muitos desenvolvedores esquecem de criar: o .dockerignore.
Ele define quais arquivos e diretórios devem ser ignorados durante o envio do contexto de build para o Docker.
Para uma aplicação .NET, podemos começar com:
bin/
obj/
.vs/
.vscode/
.git/
.github/
TestResults/
*.user
*.suo
.env
Por que isso é importante?
Imagine que seu projeto possui milhares de arquivos temporários, resultados de compilação e configurações locais.
Enviar tudo isso durante o processo de build é desnecessário.
O .dockerignore ajuda a:
Reduzir o contexto de build.
Melhorar o desempenho da construção das imagens.
Evitar copiar arquivos desnecessários.
Reduzir o risco de incluir informações locais na imagem.
Esse pequeno arquivo pode fazer uma diferença significativa no processo de desenvolvimento.
Uma dúvida bastante comum é se devemos utilizar a mesma configuração Docker para desenvolvimento e produção.
A resposta é que os objetivos são diferentes.
Durante o desenvolvimento, queremos agilidade.
É interessante ter:
Hot Reload.
Depuração da aplicação.
Logs detalhados.
Banco de dados local.
Facilidade para recriar ambientes.
Integração com ferramentas de desenvolvimento.
Nesse cenário, podemos utilizar imagens com SDK, volumes para compartilhar arquivos e configurações específicas para desenvolvimento.
Em produção, nossas prioridades mudam.
Precisamos considerar:
Imagens menores.
Segurança.
Atualizações de dependências.
Execução com usuário não privilegiado.
Monitoramento.
Logs estruturados.
Gerenciamento de secrets.
Estratégias de escalabilidade.
Health checks.
Uma boa prática é utilizar imagens específicas para runtime, evitando carregar ferramentas de compilação desnecessárias.
Também é importante manter as imagens atualizadas e utilizar versões compatíveis com o runtime da aplicação.
Não basta simplesmente colocar uma API dentro de um container e considerar que ela está preparada para produção.
A configuração do ambiente, a segurança e a estratégia de implantação continuam sendo responsabilidades importantes.
Se você trabalha com arquitetura de microsserviços, Docker se torna ainda mais interessante.
Imagine uma aplicação dividida nos seguintes serviços:
API de clientes.
API de pedidos.
Serviço de pagamentos.
Serviço de notificações.
Serviço de autenticação.
Cada serviço pode ser empacotado em sua própria imagem e executado em um container independente.
Isso permite que os componentes sejam desenvolvidos, testados e implantados de maneira independente.
Além disso, facilita a integração com tecnologias utilizadas em arquiteturas distribuídas, como:
RabbitMQ.
Apache Kafka.
Redis.
PostgreSQL.
SQL Server.
Entretanto, é importante entender que Docker e microsserviços não são sinônimos.
Você pode utilizar Docker em uma aplicação monolítica, assim como pode desenvolver microsserviços sem necessariamente utilizar Docker.
Docker é uma tecnologia de empacotamento e execução de aplicações. Microsserviços são uma abordagem arquitetural.
Essa distinção é importante para evitar a adoção de complexidade desnecessária.
Outro cenário em que Docker faz bastante diferença é a integração contínua e entrega contínua.
Imagine que você possui um projeto .NET hospedado no GitHub.
Sempre que um desenvolvedor envia alterações para o repositório, um pipeline pode executar automaticamente algumas etapas:
Restaurar os pacotes NuGet.
Compilar a aplicação.
Executar os testes automatizados.
Construir a imagem Docker.
Publicar a imagem em um registry.
Disponibilizar a imagem para implantação.
Esse processo permite padronizar a construção e distribuição da aplicação.
Você pode utilizar ferramentas como:
GitHub Actions.
Azure DevOps.
GitLab CI/CD.
Jenkins.
As imagens podem ser armazenadas em registries como:
Docker Hub.
GitHub Container Registry.
Azure Container Registry.
Amazon Elastic Container Registry.
Dessa maneira, o mesmo artefato construído no pipeline pode ser promovido entre diferentes ambientes, respeitando as configurações específicas de cada um.
É importante também incluir verificações de segurança e vulnerabilidades nas imagens antes de disponibilizá-las em produção.
Você não precisa memorizar centenas de comandos para começar.
Entretanto, alguns comandos aparecem constantemente no dia a dia.
| Comando | Finalidade |
|---|---|
| docker images | Lista as imagens disponíveis |
| docker ps | Lista containers em execução |
| docker ps -a | Lista todos os containers |
| docker build | Constrói uma imagem |
| docker run | Cria e executa um container |
| docker stop | Interrompe um container |
| docker start | Inicia um container existente |
| docker rm | Remove um container |
| docker rmi | Remove uma imagem |
| docker logs | Exibe os logs de um container |
| docker exec | Executa um comando dentro do container |
| docker inspect | Exibe informações detalhadas |
| docker compose up -d | Inicia os serviços definidos no Compose |
| docker compose down | Interrompe e remove os recursos gerenciados pelo Compose |
| docker compose logs -f | Acompanha os logs dos serviços |
Uma dica prática: familiarize-se principalmente com docker ps, docker logs, docker exec, docker build e docker compose.
Esses comandos resolvem boa parte das necessidades cotidianas de um desenvolvedor.
Depois de conhecer os principais conceitos, fica mais fácil organizar seus estudos.
Se você é desenvolvedor .NET, recomendo seguir esta sequência:
Aprenda:
O que são imagens e containers.
Como instalar e executar Docker.
Como utilizar docker run.
Como visualizar logs.
Como iniciar e interromper containers.
Aprenda:
Criar Dockerfiles.
Utilizar imagens oficiais do .NET.
Trabalhar com Multi-stage Builds.
Construir imagens utilizando docker build.
Publicar portas.
Configurar variáveis de ambiente.
Aprenda:
Docker Compose.
Integração com SQL Server.
Integração com Redis.
Volumes.
Redes entre containers.
Health checks.
Aprenda:
Otimização de imagens.
Segurança de containers.
Execução com usuário não privilegiado.
Registries de imagens.
Integração com pipelines CI/CD.
Monitoramento e observabilidade.
Depois de dominar esses conceitos, você terá uma base bastante sólida para trabalhar com aplicações .NET containerizadas.
Docker é uma ferramenta extremamente útil para desenvolvedores .NET, mas você não precisa se tornar um especialista em infraestrutura para começar a utilizá-la.
O mais importante é compreender como criar imagens, executar containers, configurar aplicações, integrar serviços e organizar ambientes utilizando Docker Compose.
Com esses conhecimentos, você já consegue melhorar bastante seu fluxo de desenvolvimento e preparar suas aplicações para cenários modernos de implantação.
Minha recomendação é começar com uma API ASP.NET Core simples, criar seu primeiro Dockerfile, adicionar um banco SQL Server utilizando Docker Compose e experimentar diferentes configurações.
Depois, avance para conceitos como otimização de imagens, segurança e integração com pipelines CI/CD.
O objetivo não é aprender todos os recursos do Docker, mas saber utilizá-lo corretamente para resolver problemas reais do desenvolvimento de software.
E você, já utiliza Docker nos seus projetos .NET ou ainda executa suas aplicações e bancos de dados diretamente no computador?
Compartilhe sua experiência nos comentários!