[ASP.NET] Publicando com Docker
Publicar uma aplicação web com conteinerização é atualmente uma das maneiras mais simples de obter uma infraestrutura facilmente escalável.
O que é um container
Um container é um pacote que contém todas as configurações de ambiente e dependências necessárias para executar um software, incluindo pacotes do sistema operacional, binários, bibliotecas de linguagem, código-fonte, etc.
Como conceito e tecnologia, a conteinerização não é nova e tem se transformado ao longo dos anos, ganhando muito mais popularidade com o lançamento do Docker em 2013.
Docker
O Docker é tanto o nome de uma tecnologia quanto o nome de uma empresa. A maior parte deste artigo fala sobre a tecnologia, não sobre a empresa.
A Docker Inc é atualmente o principal mantenedor da tecnologia Docker e oferece um conjunto de produtos e serviços em torno da conteinerização, alguns dos quais são proprietários e não gratuitos.
O Docker (tecnologia) é uma tecnologia de conteinerização de código aberto. O código está disponível em um repositório no GitHub.
Containers do Docker não contêm um sistema operacional completo dentro deles. Eles compartilham o mesmo kernel Linux rodando no sistema operacional do host e executam dependências adicionais em isolamento.
Logo, os containers do Docker não são feitos para ser executados diretamente em Windows. Para rodar o Docker no Windows, algum tipo de comunicação entre Linux e Windows é necessário, como o WSL ou virtualização.
O Docker Desktop para Windows abstrai esse processo de comunicação entre Linux e Windows, permitindo que o Windows execute containers com a mesma facilidade que um sistema operacional Linux.
Note que o Docker Desktop para Windows tem um custo de licença para uso comercial. Sempre verifique a licença de cada produto para entender suas limitações.
Componentes da conteinerização com Docker
Docker Image: Um template que contém todos os dados e configurações necessárias para rodar um container em um computador. Ela pode ser hospedada ou compartilhada em repositórios especializados, chamados container registries.
Docker Container: Uma instância de container, criada com base em um Docker Image.
Docker Daemon: Um serviço instalado no sistema operacional e é responsável por manter os containers em execução. Ele também é responsável por executar pull e push de imagens em registries, criar e gerenciar instâncias de containers com base em imagens, e conectar as portas de rede do computador com as portas dos containers.
Por que é útil
A conteinerização permite que o software seja distribuído juntamente com a configuração do ambiente. Com containers, não há necessidade de instalar dependências ou reconfigurar o sistema operacional para cada aplicativo que irá rodar na máquina.
Isso facilita a escalabilidade, pois um único servidor com Docker pode instanciar múltiplos containers usando a mesma imagem. As plataformas de nuvem também oferecem opções PaaS, como o Azure Container Apps, para executar containers sem se preocupar com servidores, além de oferecer configurações fáceis de auto-scaling.
Isso também permite que os desenvolvedores executem instâncias de software sem a necessidade de instalar o software e suas dependências diretamente em seu computador, evitando conflitos entre dependências de diferentes softwares. Como as dependências estão contidas dentro do container, apagar o container também remove todos arquivos e dependências dele, simplificando o processo de desinstalação.
Setup simplificado
Na pasta raiz do projeto, adicione um arquivo contendo as instruções Docker. Um arquivo que contém instruções Docker é chamado de Dockerfile. O nome padrão pra este tipo de arquivo também deve ser Dockerfile (sem extensão), mas qualquer nome pode ser usado se você especificá-lo no comando de build.
Para criar imagens de container no seu próprio computador, é necessário instalar o Docker. Se a imagem for criada automaticamente como parte de um processo de CI/CD, não é necessário ter o Docker instalado no computador.
# Example of a Dockerfile for an ASP.NET using .NET 8.0:
# Base image of an operating system that can build the code
# For .NET, the SDK version mirrors the .NET version
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /source
# Copy solution and relevant project files, and restore dependencies before publishing
# Docker does not support globbing to copy and filter files recursively
# Each file must be copied individually
COPY MyProject.sln ./MyProject.sln
COPY src/MyProject.Domain/MyProject.Domain.csproj ./src/MyProject.Domain/MyProject.Domain.csproj
COPY src/MyProject.Infrastructure/MyProject.Infrastructure.csproj ./src/MyProject.Infrastructure/MyProject.Infrastructure.csproj
COPY src/MyProject.Web/MyProject.Web.csproj ./src/MyProject.Web/MyProject.Web.csproj
RUN dotnet restore
# After restore, copy all source files and build
COPY src/. ./src/
WORKDIR /source/src/MyProject.Web
RUN dotnet publish -c Release -o /app --no-restore
# Copy the files into a base image capable of running the app
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "MyProject.Web.dll"]
A pasta raiz também pode incluir um arquivo .dockerignore, que irá excluir certos arquivos, nomes de arquivos ou caminhos do processo de build. Note que o Docker não suporta arquivos .dockerignore parametrizados.
Quando criar um Docker image de um projeto .NET localmente, é recomendado ignorar as pastas bin e obj para evitar copiar arquivos que podem ter referência a algum caminho local da máquina.
# Ignore folders
**/bin
**/obj
bin/
obj/
Para gerar um Docker image no computador, execute o comando:
docker build --progress=plain --file=Dockerfile -t <image_name> .
--progress=plain exibe o output e erros para ajudar a depurar o processo de build do Docker.
--file é opcional quando o Dockerfile tem o nome de arquivo 'Dockerfile'. Se precisar criar diferentes Docker images a partir da mesma pasta raiz, pode ser necessário ter múltiplos Dockerfiles com nomes diferentes.
-t é usado para especificar o nome ou tag da imagem criada.
Por que restaurar antes de publicar
O Docker usa um sistema de cache baseado em checksums, que é gerado com base no comando e nos arquivos contidos na imagem no momento em que o comando é executado.
A partir do segundo build, se o build anterior já executou um comando com o mesmo checksum do comando a ser executado, o Docker reutiliza o output do comando do build anterior sem executar o comando novamente. Quando o checksum difere, todos os comandos a partir do checksum divergente serão re-executados.
Ao separar o processo de restauração de dependências baseado apenas nos arquivos do projeto, o processo de restauração só executará novamente se os arquivos do projeto mudarem, por exemplo, ao adicionar ou atualizar uma dependência.
Dessa forma, o Docker pode frequentemente reutilizar o resultado do restore em cache, acelerando consideravelmente o processo.
Erros comuns
O seguinte erro é causado porque as pastas bin e obj locais foram copiadas para a imagem Docker e contêm referências à máquina local. Para corrigir, crie um arquivo .dockerignore que ignore essas pastas locais e permita que o processo de publicação dentro do Docker crie suas próprias pastas.
Note que esse erro é mais provável de acontecer em um computador local. Se estiver acontecendo na nuvem, é possível que um desenvolvedor tenha commitado as pastas bin e obj no repositório.
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018: The "ResolvePackageAssets" task failed unexpectedly. [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018: NuGet.Packaging.Core.PackagingException: Unable to find fallback package folder 'C:Program Files (x86)Microsoft Visual StudioSharedNuGetPackages'. [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at NuGet.Packaging.FallbackPackagePathResolver..ctor(String userPackageFolder, IEnumerable`1 fallbackPackageFolders) [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.NuGetPackageResolver.CreateResolver(IEnumerable`1 packageFolders) [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.ResolvePackageAssets.CacheWriter..ctor(ResolvePackageAssets task) [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.ResolvePackageAssets.CacheReader.CreateReaderFromDisk(ResolvePackageAssets task, Byte[] settingsHash) [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.ResolvePackageAssets.CacheReader..ctor(ResolvePackageAssets task) [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.ResolvePackageAssets.ReadItemGroups() [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.ResolvePackageAssets.ExecuteCore() [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.NET.Build.Tasks.TaskBase.Execute() [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.Build.BackEnd.TaskExecutionHost.Execute() [/source/src/MyProject.Web/MyProject.Web.csproj]
#19 1.367 /usr/share/dotnet/sdk/8.0.404/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error MSB4018:
at Microsoft.Build.BackEnd.TaskBuilder.ExecuteInstantiatedTask(TaskExecutionHost taskExecutionHost, TaskLoggingContext taskLoggingContext, TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask) [/source/src/MyProject.Web/MyProject.Web.csproj]