A tecnologia de contêineres permite o empacotamento de uma aplicação e as dependências necessárias para a sua execução (incluindo o sistema operacional). No processo tradicional de implantação de uma aplicação, precisamos nos preocupar em instalar o sistema operacional, a plataforma de execução que a aplicação foi desenvolvida, os pacotes/bibliotecas externas e a aplicação propriamente dita. No mundo dos contêineres, tudo isso fica armazenado em uma única imagem.
Um dos cuidados que precisamos ter quando utilizamos contêineres é minimizar o tamanho das imagens geradas. Imagens menores permitem subir instâncias da aplicação quase instantaneamente, que é muito importante em cenários onde precisamos de velocidade para escalar o ambiente. Visando essa otimização de tamanho, a maioria dos sistemas operacionais tem uma versão projetada para rodar em contêineres. Por exemplo, a Microsoft lançou o Windows 2016 Nano Server especificamente para esse propósito.
Nesse artigo, veremos algumas formas de diminuir o tamanho de imagens de aplicações ASP.NET Core 6.
Instalação dos pré-requisitos
Para acompanhar os exemplos desse artigo, você vai precisar instalar os seguintes softwares/componentes:
Para habilitar o Windows Subsystem for Linux, ou WSL, basta rodar o seguinte comando no PowerShell como administrador em uma versão recente do Windows 10 (build 19041 ou superior) ou Windows 11:
wsl --install
Caso você tenha a ferramenta winget, você pode instalar o Docker Desktop, Visual Studio Code e .NET Core SDK 6.0 com os seguintes comandos:
winget install -e --id Docker.DockerDesktop
winget install -e --id Microsoft.VisualStudioCode
winget install -e --id Microsoft.dotnet
Imagem versão tutorial
Verifique se todas as ferramentas foram instaladas corretamente. Para isso, reinicie a sua sessão do PowerShell para garantir que a variável de ambiente PATH
esteja atualizada, abra o Docker Desktop através do menu Iniciar do Windows e execute os comandos:
dotnet --list-sdks
docker ps
Se os comandos executaram corretamente, crie uma pasta (por exemplo, C:\repos\aspnet-docker
) para armazenar o projeto de exemplo.
mkdir C:\repos\aspnet-docker
cd C:\repos\aspnet-docker
Criada a pasta de trabalho, podemos gerar o projeto exemplo com os seguintes comandos:
dotnet new webapi -n api -o src/
dotnet new gitignore
dotnet new editorconfig
Além do projeto Web API, geramos também o arquivo .gitignore
(utilizado para configurar o que será versionado pelo gerenciador de código-fonte Git) e o arquivo .editorconfig
(utilizado pela extensão EditorConfig do Visual Studio Code para padronizar a formatação de código C#).
Criado o projeto, podemos executá-lo localmente através do seguinte comando:
dotnet run --project .\src\api.csproj --urls http://localhost:5000
Rode o seguinte comando em uma segunda sessão do PowerShell para chamar a API:
curl http://localhost:5000/WeatherForecast
Se tudo estiver configurado corretamente, a API deve retornar um JSON com esse formato:
[
{"date":"2022-02-23T00:05:03.7969198-03:00","temperatureC":42,"temperatureF":107,"summary":"Bracing"},
{"date":"2022-02-24T00:05:03.796925-03:00","temperatureC":51,"temperatureF":123,"summary":"Balmy"},
{"date":"2022-02-25T00:05:03.7969252-03:00","temperatureC":34,"temperatureF":93,"summary":"Balmy"},
{"date":"2022-02-26T00:05:03.7969253-03:00","temperatureC":45,"temperatureF":112,"summary":"Sweltering"},
{"date":"2022-02-27T00:05:03.7969254-03:00","temperatureC":50,"temperatureF":121,"summary":"Cool"}
]
Agora que temos a aplicação funcionando, podemos criar a primeira versão da imagem de contêiner baseado no tutorial Docker images for ASP.NET Core. Para isso, devemos criar dois arquivos (Dockerfile
e .dockerignore
) na pasta raiz do projeto. Para abrir o Visual Studio Code e criar esses arquivos, execute o seguinte comando (não esqueça do ponto após o comando):
code .
- Dockerfile
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
# copy csproj and restore as distinct layers
COPY src/*.csproj ./
RUN dotnet restore
# copy everything else and build app
COPY src/. ./
RUN dotnet publish -c release -o /app --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "api.dll"]
- .dockerignore
# directories
**/bin/
**/obj/
**/out/
# files
Dockerfile*
**/*.trx
**/*.md
**/*.ps1
**/*.cmd
**/*.sh
A árvore de pastas deverá ter o seguinte formato:
PS C:\repos\aspnet-docker> tree /F
Folder PATH listing
Volume serial number is E69B-3FCF
C:.
│ .dockerignore
│ .editorconfig
│ .gitignore
│ Dockerfile
│
└───src
│ api.csproj
│ appsettings.Development.json
│ appsettings.json
│ Program.cs
│ WeatherForecast.cs
├───Controllers
│ WeatherForecastController.cs
Com isso, podemos criar a nossa primeira versão da imagem com o comando:
docker build -t api:1.0 .
Para iniciar a aplicação dessa imagem, execute a seguinte instrução:
docker run -p 127.0.0.1:5000:80 --rm --name api api:1.0
Execute o mesmo comando curl http://localhost:5000/WeatherForecast
em uma segunda sessão do PowerShell para testar a API. A execução do contêiner pode ser interrompida com o comando:
docker stop api
Para verificar o tamanho da imagem, use a instrução docker images
. No meu caso, a imagem ficou com 212MB.
PS C:\repos\aspnet-docker> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
api 1.0 7ababd71e402 8 minutes ago 212MB
Imagem versão Alpine
Para diminuirmos o tamanho da imagem, podemos utilizar uma distribuição mais enxuta do Linux. A Microsoft também disponibiliza imagens do .NET Core para uma distribuição chamada Alpine Linux, que é conhecida por ser mais leve.
Utilize o seguinte Dockerfile para usar essa distribuição:
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
WORKDIR /src
# copy csproj and restore as distinct layers
COPY src/*.csproj ./
RUN dotnet restore
# copy everything else and build app
COPY src/. ./
RUN dotnet publish -c release -o /app --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "api.dll"]
Construa a nova versão da imagem usando o comando:
docker build -t api:1.1 .
Execute os mesmos passos da seção anterior (sem esquecer de trocar trocar a tag da imagem para api:1.1
) para executar e testar essa nova imagem. Compare o tamanho das imagens com a instrução docker images
.
PS C:\repos\aspnet-docker> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
api 1.1 d1ee77c4109b 7 seconds ago 104MB
api 1.0 7ababd71e402 18 minutes ago 212MB
No meu caso, a versão Alpine ficou com 104MB, uma redução de 108MB somente trocando a distribuição Linux.
Imagem versão otimizada para tamanho
Será que é possível diminuir ainda mais o tamanho dessa imagem? Com algumas restrições, sim, é possível. Seguindo as recomendações do artigo https://github.com/dotnet/dotnet-docker/tree/main/samples/aspnetapp#optimizing-for-size, vamos utilizar o seguinte Dockerfile para criar a nova versão:
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
WORKDIR /src
# copy csproj and restore as distinct layers
COPY src/*.csproj ./
RUN dotnet restore -r linux-musl-x64
# copy everything else and build app
COPY src/. ./
RUN dotnet publish -r linux-musl-x64 -c release --self-contained -o /app \
--no-restore -p:PublishTrimmed=true -p:PublishSingleFile=true
# final stage/image
FROM alpine
# https://github.com/dotnet/core/blob/main/Documentation/linux-prereqs.md
RUN apk add --no-cache libstdc++
ENV \
# Configure web servers to bind to port 80 when present
ASPNETCORE_URLS=http://+:80 \
# Enable detection of running in a container
DOTNET_RUNNING_IN_CONTAINER=true \
# Set the invariant mode since icu-libs isn't included
# (see https://github.com/dotnet/announcements/issues/20)
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["./api"]
Construa e execute a nova imagem com os comandos:
docker build -t api:1.2 .
docker run -p 127.0.0.1:5000:80 --rm --name api api:1.2
Use o curl http://localhost:5000/WeatherForecast
para verificar se a API está respondendo.
Comparando o tamanho das imagens, no meu caso essa última versão ficou com 49.7MB, o que representa uma redução de 162MB da primeira imagem que criamos.
PS C:\repos\aspnet-docker> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
api 1.2 a14f8660e581 42 minutes ago 49.7MB
api 1.1 d1ee77c4109b About an hour ago 104MB
api 1.0 7ababd71e402 About an hour ago 212MB
Para chegar nesse tamanho, utilizamos os seguintes artifícios:
- Publicação com as opções PublishTrimmed (que faz uma análise de dependência em tempo de compilação e remove todas as bibliotecas não utilizadas pela aplicação) e PublishSingleFile (que produz um único arquivo executável com todas as dependências).
- Desabilitamos as funcionalidades de globalização do .NET Core.
- Usamos a imagem base do Alpine Linux e instalamos o único pacote necessário (no caso, o
libstdc++
) para rodar a aplicação do modelowebapi
gerado pelo .NET CLI.
Os seguintes artigos descrevem as restrições impostas por esses artifícios:
- Trim self-contained deployments and executables
- Single file deployment and executable
- .NET Core Globalization Invariant Mode
- Linux System Prerequisites for .NET Core
Conclusão
Nesse artigo mostramos algumas formas para diminuir o tamanho das imagens para contêineres de aplicações ASP.NET Core. Na grande maioria dos cenários, podemos reduzir em pelo menos 108MB no tamanho da imagem caso não haja restrição em utilizar a distribuição Alpine Linux no seu ambiente produtivo.
Em cenários mais específicos, com algumas limitações, podemos reduzir até 162MB no tamanho dessa imagem.