Primeiros passos com Docker: criando uma aplicação .NET

Compreender a importância de uma nova tecnologia como o Docker é fundamental ao iniciar seu aprendizado e o primeiro passo é identificar os problemas que ela resolve. Se uma tecnologia não oferece soluções significativas, sua adoção pode não ser necessária. Isso é especialmente verdadeiro no dinâmico campo do desenvolvimento de software, onde as necessidades e as ferramentas estão em constante evolução.

Quando se trata do Docker, a compreensão de suas vantagens é bastante direta. Vamos explorar os principais benefícios de usar o Docker, especialmente para desenvolvedores que estão começando a sua jornada na área.

Uma das principais razões para aprender Docker é a sua relevância no mundo corporativo. O Docker tornou-se um padrão na indústria para o desenvolvimento, entrega e operação de aplicações, tornando-se uma habilidade quase indispensável para os desenvolvedores modernos. Integrar o Docker em projetos pessoais não só amplia a experiência prática, mas também prepara o desenvolvedor para as exigências e desafios encontrados em ambientes de produção profissionais.

Por que aprender Docker?

Antes de mergulharmos nas especificidades do Docker e de como ele revoluciona o desenvolvimento de software, é importante entender os desafios frequentemente enfrentados por desenvolvedores. Estes problemas, embora possam parecer menores à primeira vista, podem se tornar obstáculos significativos à eficiência e ao sucesso de um projeto. Para contextualizar melhor o impacto do Docker, vamos explorar alguns desses problemas comuns e as soluções tradicionais que são comumente aplicadas.

Problema 1: conflito de portas e compatibilidade de versões

Considere um cenário comum em desenvolvimento: você está trabalhando em duas APIs em .NET, uma API de produtos e outra de clientes. Ambas estão configuradas, no arquivo de inicialização (Startup), para usar a mesma porta, digamos a porta padrão 5000. Ao tentar executar as duas simultaneamente, você se depara com um problema de conflito de portas. Para resolver isso sem Docker, seria necessário alterar manualmente a configuração de uma das APIs para usar uma porta diferente, permitindo a execução simultânea.

Problema 2: compatibilidade de versões

Além do conflito de portas, um desafio frequente em desenvolvimento de software é a compatibilidade de versões de ferramentas e bibliotecas. Por exemplo, diferentes projetos podem depender de versões específicas do .NET ou de outras dependências, levando a conflitos quando executados no mesmo ambiente.

Problema 3: compartilhamento e disponibilização do projeto

Imagine que você está desenvolvendo o backend de uma aplicação em .NET, enquanto um colega assume o desenvolvimento do frontend. Para que ele possa integrar e utilizar os endpoints da sua API, é necessário tornar o projeto acessível. Idealmente, isso envolveria o deploy da aplicação, mas no estágio inicial, o foco é no desenvolvimento local.

Para compartilhar o progresso, você pode subir o código para o GitHub e fornecer acesso ao repositório. No entanto, isso implica que seu colega terá que configurar o ambiente de desenvolvimento: instalar a IDE apropriada, como o Visual Studio, e garantir que a versão correta do runtime seja baixada – um processo que pode ser especialmente complicado se o projeto utilizar uma versão do .NET que não seja padrão na IDE, como por exemplo o .NET 5.

O colaborador também precisará instalar e configurar o banco de dados necessário, seja MySQL, SQL Server ou outro. Isso pode ser um processo trabalhoso, exigindo orientação adicional caso ele não esteja familiarizado com a IDE ou com o processo de configuração das dependências do projeto.

Agora, eis onde o Docker entra como uma solução elegante para esse cenário. Com ele, seu colega simplesmente precisa tê-lo instalado em sua máquina. Então, ele pode baixar e executar a imagem do seu projeto Dockerizado. Essa imagem contém tudo o que é necessário para o projeto funcionar – o serviço backend, a versão correta do runtime do .NET e o banco de dados. Tudo isso sem a necessidade de configurações adicionais de IDE ou instalação de banco de dados, simplificando significativamente o processo de disponibilização e compartilhamento do ambiente de desenvolvimento.

Possíveis soluções sem Docker

As soluções convencionais para os problemas de conflito de portas e compatibilidade de versões incluem:

  • Uso de múltiplas máquinas: designar uma máquina física dedicada para cada aplicação.
  • Máquinas virtuais: configurar várias máquinas virtuais, cada uma com seu próprio ambiente operacional isolado.

Ambas as abordagens, embora funcionais, têm suas desvantagens. Elas requerem mais recursos, tempo para configuração e manutenção, e não são tão eficientes em termos de uso de hardware e energia.

Conforme a imagem, comparamos uma máquina virtual com um contêiner Docker. Máquinas virtuais replicam ambientes operacionais completos para cada aplicação, enquanto contêineres Docker, sendo mais leves, compartilham o mesmo sistema operacional do host, mas isolam as aplicações em ambientes separados. Isso resulta em maior eficiência de recursos, rapidez na inicialização e facilidade de gerenciamento.

No esquema apresentado, vemos diferentes contêineres, cada um hospedando um serviço específico – APIs e bancos de dados. O Docker permite que cada contêiner tenha sua própria porta interna, que é mapeada para uma porta única no host. Isso evita o conflito direto de portas no host, já que o Docker gerencia o encaminhamento das requisições para as portas corretas internamente nos contêineres. O resultado é uma coexistência harmoniosa onde múltiplos serviços operam independentemente, sem a necessidade de reconfiguração manual ou o uso de múltiplas máquinas. Isso exemplifica a eficiência e a simplicidade do Docker na resolução de conflitos de portas, facilitando a gestão de aplicações em ambientes de desenvolvimento complexos.

Exemplo Prático: rodando uma aplicação .NET com docker no Windows

Após entendermos os benefícios do Docker e como ele pode simplificar nosso trabalho, é hora de colocar a mão na massa com um exemplo prático. Vamos criar um ambiente Docker que executa uma aplicação .NET simples no Windows.

Instale o Docker Desktop no Windows

Acesse o site oficial e baixe a versão mais recente do Docker Desktop para Windows. Execute o instalador e siga as instruções para instalar o Docker no seu sistema. Após isso, abra o PowerShell ou o Prompt de Comando e digite o seguinte comando para verificar se o Docker foi instalado corretamente:

Você deve ver a versão instalada como resultado.

Crie um Projeto .NET Simples

Nesse exemplo estamos utilizando o Visual Studio 2022. Abra-o na versão que preferir e crie um novo projeto. Buscando por “Api”, selecione o projeto “API Web do ASP.NET Core” e siga para a próxima página.

Após dar um nome para o projeto, na próxima página vamos selecionar a versão do .NET que utilizaremos. Nesse exemplo, estarei usando o .NET 6.0.

Há a opção de habilitar o Docker nesse momento, mas não iremos marcar para fazer esse processo nos próximos passos.

Após criar o projeto, para o nosso exemplo, iremos excluir essas duas classes iniciais “WeatherForecastController” e “WeatherForecast“. Vamos criar uma nova controller simples para exemplo.

Com o botão direito em “Controllers”, vamos criar uma nova classe chamada “HomeController“.

Nessa classe, use o seguinte código:

using Microsoft.AspNetCore.Mvc;

namespace DockerApi.Controllers

{

    [ApiController]

    [Route("[controller]")]

    public class HomeController : ControllerBase

    {

        [HttpGet]

        public async Task<IActionResult> ReturnHello()

        {

            var response = "Hello, Docker!";

            return Ok(response);

        }

    }

}

Criamos um endpoint simples que retorna um “Hello, Docker!”.

Para testar, vamos rodar a aplicação no botão verde de início, ou então com o atalho “F5”. Abrirá a tela do Swagger com o nosso endpoint GET /Home criado. Clicando em “Try it out” no canto direito e depois em “Execute“, temos sucesso na resposta (código 200) e o nosso “Hello”, conforme esperado.

Detalhes do projeto

Para que tudo corra bem com o Docker, vamos fazer algumas modificações simples no projeto.

Primeiro, no arquivo Program (ou Startup caso esteja usando uma versão anterior ao .NET 6), temos o seguinte código:

Nesse trecho, caso a aplicação esteja rodando no ambiente de desenvolvimento, então o Swagger é ativado, ou seja, irá exibir a página que vimos anteriormente para testar nossos endpoints. Em outros ambientes, por motivos de segurança, não fica disponível. Como o foco aqui é apenas teste e não estamos nos preocupando com a segurança da API, iremos modificar para o seguinte código:

if (app.Environment.IsDevelopment())

{

    app.UseDeveloperExceptionPage();

}

app.UseSwagger();

app.UseSwaggerUI();

Dessa forma ativamos o Swagger em todos os ambientes, permitindo visualizarmos essa página de documentação quando estivermos utilizando o container. Para fazer isso de forma mais segura, uma das soluções seria definir o Docker como ambiente de desenvolvimento incluindo uma variável de ambiente no Dockerfile (veremos mais detalhes sobre ele adiante). Isso possibilita configurar o comportamento da aplicação com base no ambiente em que está sendo executada.

Conteinerizando o projeto

Agora com o projeto feito, podemos criar o Dockerfile. Há duas formas de fazer, a primeira é manualmente:

Na raiz do projeto, vamos criar um novo arquivo documento de texto e chamá-lo de Dockerfile. Vamos excluir a extensão txt, o nome deverá ficar apenas “Dockerfile”. Aparecerá uma caixa de diálogo para confirmar a ação. Atenção ao local de criação, deverá ser na pasta onde encontramos o arquivo Program.cs conforme mostrado:

Após isso, no Visual Studio, o arquivo aparecerá para que possamos editá-lo. Incluímos o seguinte:

# Usa a imagem oficial do SDK do .NET 6.0 como base

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build

# Especifica a porta que a aplicação irá expor

EXPOSE 80

# Define a pasta de trabalho dentro do contêiner

WORKDIR /app

# Copia os arquivos do projeto para o contêiner

COPY . .

# Restaura as dependências e faz o build da aplicação

RUN dotnet restore

RUN dotnet publish -c Release -o out

# Cria uma imagem menor usando a imagem oficial do ASP.NET

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime

WORKDIR /app

# Copia os arquivos publicados da fase de build

COPY --from=build /app/out ./

# Especifica o comando a ser executado quando o contêiner for iniciado

CMD ["dotnet", "NomeDaSuaApi.dll"]

A segunda forma de fazer é ainda mais simples, podemos gerar esse Dockerfile automaticamente no Visual Studio: clicando com o botão direito no projeto, em seguida Adicionar e depois Suporte Docker. O SO de destino deverá ser Linux.


Igualmente, teremos um arquivo Dockerfile com os passos semelhantes ao que criamos manualmente.

O Dockerfile é o arquivo de configuração usado para construir imagens de contêineres. É como uma “receita” ou um conjunto de instruções para criar a imagem Docker, onde cada comando adiciona uma camada à imagem final. Ele contém comandos específicos para copiar os arquivos, instalar pacotes, definir variáveis de ambientes, expor portas, definir comandos de inicialização, etc. Fizemos manualmente o básico do básico para criação da imagem, mas poderíamos ter adicionado outros comandos conforme nossa necessidade.

Construção da imagem e execução do container

Agora com o Dockerfile em mãos, vamos gerar a imagem do nosso projeto.

Abrindo o CMD você pode navegar até a pasta do projeto ou, dentro da pasta onde está o projeto, pode escrever CMD na barra de navegação, dessa forma:

Com o CMD aberto, usaremos o comando:

docker build -t dockerapi .

Para entender o comando:

  • docker build : comando de build para construir a imagem com base nos comandos do Dockerfile
  • -t dockerapi : o -t ou --tag é usada para atribuir um nome ou tag à imagem que estamos construindo, que no caso será “dockerapi”.
  • . (ponto) : é o diretório onde está o Dockerfile. O ponto, no caso, indica que é a pasta raiz (onde você abriu o CMD).

Agora vamos rodar a imagem. Utilizamos o seguinte comando:

docker run -d -p 8000:80 --name DockerApiContainer dockerapi

  • docker run : comando usado para criar e executar um container a partir de uma imagem docker.
  • -d : é opcional, usamos para não travar o terminal. Essa opção indica que o contêiner deve ser executado em segundo plano, em modo detached, o que significa que a linha de comando estará disponível para ser utilizada para outros comandos.
  • -p 8000:80 : o “p” ou “–publish” é usado para mapear as portas. No Dockerfile nós falamos que a aplicação será exposta na porta 80. Nesse comando estamos mapeando essa porta 80 do container para a 8000 do host (nosso computador). O 8000 poderia ser qualquer outra porta livre, como 5000, 6000, etc.
  • -- name DockerApiContainer : usado para atribuir um nome ao container. Você não precisa utilizar essa opção, então o container será criado com um nome aleatório.
  • dockerapi : é o nome da imagem a partir da qual o container será criado.

Repare que após o comando de run, no terminal aparecem alguns números/código. Aquele é o ID do nosso container, podemos usar ele ou então o nome para executar outros comandos, como o log, ou verificar as configurações deste container.

Para confirmar que tudo está funcionando, vamos abrir o Docker Desktop:

Como vemos, o container está em execução (running) com o nome que demos e utilizando nossa imagem criada. Agora para abrir a aplicação, basta clicar no link da porta 8000:80.

Um detalhe importante: a página que abrir terá um erro “Não foi possível encontrar a página deste localhost” ou algo parecido, pois precisamos utilizar a URL do Swagger para exibir os endpoints:

http://localhost:8000/swagger/index.html

A porta será a que escolheu, mas precisa do caminho /swagger/index.html. Testando o endpoint, temos o mesmo retorno de sucesso.

Não abordaremos nesse momento essa etapa, mas caso queira compartilhar o projeto com outras pessoas, basta subir essa imagem para o repositório do Dockerhub, que é semelhante ao Github. Assim, apenas com o nome é possível fazer o “pull” (baixar) a imagem e rodá-la facilmente.

Conclusão

Por fim, neste guia exploramos passo a passo a “conteinerização” de uma aplicação .NET, aproveitando as vantagens que o Docker oferece para facilitar o desenvolvimento e compartilhamento de projetos.

Destacamos a importância do Docker, não apenas como uma ferramenta poderosa para o desenvolvedor individual, mas também como um padrão na indústria para o desenvolvimento, entrega e operação de aplicações.

É legal notar que esse guia cobre aspectos básicos dessa tecnologia, sendo apenas uma introdução para quem está começando a explora-la. O Docker oferece recursos avançados e flexibilidade, que serão explorados em futuros posts, abordando temas como orquestração de contêineres, gerenciamento de volumes, redes, entre outros. Ao dominar esses conceitos, os desenvolvedores poderão maximizar o potencial do Docker em seus projetos, garantindo eficiência, escalabilidade e consistência em ambientes de produção.

Postar um comentário

0 Comentários