Otimizando o tamanho de imagens de contêineres ASP.NET Core
Published Feb 23 2022 12:37 PM 2,921 Views
Microsoft

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 modelo webapi gerado pelo .NET CLI.

Os seguintes artigos descrevem as restrições impostas por esses artifícios:

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.

3 Comments
Co-Authors
Version history
Last update:
‎Jun 27 2022 11:43 AM
Updated by: