Publicando aplicações ASP.NET com Docker e imagens Alpine

Gustavo Bellini Bigardi
7 min readJul 25, 2022

--

Objetivo

Neste artigo quero trazer um exemplo prático e real, sobre como utilizar as imagens do .NET baseadas no Alpine Linux para otimização de espaço no Docker Registry, mas com os devidos ajustes necessários para atender cenários como uso de diferentes Formatos / Culturas.

Introdução

E aí, pessoal, tudo bem? Estava um pouco sumido, mas estou retomando o conteúdo aqui no blog aos poucos.

Nesta semana estive trabalhando em uma solução ASP.NET, onde a mesma precisava ser executada em ambiente Docker, mas com uma imagem bem otimizada, ocupando pouco espaço, para que o upload para o Registry e implantações fossem rápidos e consumindo menos tráfego de rede, além de custos de armazenamento no Docker Registry.

Neste artigo quero mostrar o passo a passo que segui para otimizar a imagem e os ajustes que foram necessários. Antes de mais nada, vamos ao primeiro cenário.

Observação importante: A aplicação encontra-se disponível no github apresentado no cenário 1, com várias branches, uma para cada cenário. Assim você pode clonar e observar cada cenário separadamente.

Observação 2: Neste artigo estou utilizando o último preview do .NET 7, mas caso queira utilizar no .NET 6, basta seguir os comandos do passo a passo trocando a versão para 6. Como o ambiente é todo executado no Docker, não há necessidade de instalar as versões preview localmente.

Cenário 1 — Imagem padrão, aplicação de demonstração

Vamos criar uma aplicação ASP.NET do tipo WebApi, através do comando a seguir:

Tela do terminal com o comando dotnet new webapi -minimal —name DotnetAlpine

Este comando irá criar uma aplicação de Web APIs, já utilizando o formato de Minimal APIs, um bom ponto de partida para nós. Vamos abrir a aplicação com o Visual Studio Code ou editor de sua prefeência e alterar o arquivo Program.cs como na imagem a seguir, para criar um endpoint que retorna a data atual convertida a partir de uma string, utilizando uma cultura específica, informada por parâmetro. Iremos adicionar neste código um tratamento de erro, caso conversão falhe por um formato inválido ou outro problema.

Iremos remover o código de exemplo já existente, deixando mais limpo e focado no objetivo do artigo, como na imagem. Lembrando que o código está disponível no link do GitHub informado na introdução, na branch cenario_1:

Imagem com o código de exemplo, que recebe duas strings, data e cultura e faz o parse da data.

Vamos agora habilitar o Docker para nossa aplicação. Para isto, vamos criar, na raiz do projeto, um arquivo chamado Dockerfile, com o conteúdo a seguir:

Arquivo Dockerfile com conteúdo de build da aplicação.

Vamos agora construir nossa imagem, executando comando a seguir no diretório do projeto. O build pode demorar de alguns segundos até alguns minutos, dependendo da velocidade de sua conexão de internet ou cache de imagens do docker para .NET que você tenha em sua máquina.

Tela do terminal com o comando docker build . -t demo-docker

Após o build, vamos executar a aplicação no docker, com o comando a seguir:

Tela do terminal com o comando docker run -p 8080:5000 demo-docker — name aspnet-docker-1

No comando acima estamos informando ao Docker para executar, fazendo um “proxy” da porta 8080 para a porta 5000 do container, nossa imagem demo-docker, nomeando o container como aspnet-docker-1. Após a execução você poderá acessar seu navegador no endereço https://localhost:8080/swagger e ver a tela doSwagger com o endpoint criado.

Navegador aberto com a tela do swagger de nossa aplicação em execução.

Vamos fazer alguns testes:

  1. Informar a data e hora “22/03/2022 14:56” com a cultura “pt-br” e executar uma requisição. Podemos notar que teremos a resposta onde o parse da data foi realizado e a mesma devolvida com sucesso.
  2. Informar a data e hora “22/03/2022 14:56” com a cultura “en-US” e executar uma requisição. Podemos notar que teremos a resposta com uma exceção, informado que o formato da data é inválido para a cultura informada.
  3. Informar a data e hora “03/22/2022 14:56” com a cultura “en-US” e executar uma requisição. Podemos notar que teremos a resposta onde o parse da data foi realizado e a mesma devolvida com sucesso.

Até aqui, temos nossa aplicação funcionando conforme o esperado. Mas, vamos executar o comando “docker images” para ver nossa imagem Docker, e podemos notar que seu tamanho é de 214Mb, algo não tão grande, mas que podemos otimizar mais.

Resultado do comando docker images, mostrando nossa imagem com 214Mb.

Cenário 2— Utilizando as imagens do Linux Alpine

Neste cenário, vamos alterar nossa imagem para utilizar como base as imagens do .NET com base no Linux Alpine, uma distribuição Linux extremamente enxuta e com tamanho reduzido.

Caso queira saber mais sobre a distribuição Alpine e imagens da mesma para Docker, acesse o site https://www.alpinelinux.org/ para informações de releases e documentação.

Vamos alterar o conteúdo do arquivo Dockerfile, modificando a versão das imagens com o sufixo “-alpine”, para utilizar a imagem do Alpine, resultando no arquivo final como na imagem seguir:

Arquivo Dockerfile alterado para utilizar a imagem do .NET baseada no Linux Alpine

Agora vamos compilar novamente nossa imagem com o comando a seguir, mas desta vez alterando o nome da tag para demo-docker-alpine:

Terminal com o comando docker build . -t demo-docker-alpine para build da nova imagem

Depois do build completo, podemos executar novamente o comando “docker images” e comparar o tamanho das duas imagens. Podemos notar que na imagem do Alpine, que possui o tamanho 106Mb, onde tivemos uma economia de espaço de mais de 50%.

Imagem do comando docker images, comparando a imagem padrão e a do Linux Alpine.

Vamos executar novamente nossa aplicação, expondo ela na porta 8080, mas agora executando a imagem demo-docker-alpine e nomeando o container como aspnet-docker-2:

Imagem da nova imagem em execução com o docker run -p 8080:5000 demo-docker-alpine — name aspnet-docker-2

Após a aplicação iniciar, vamos repetir os mesmos testes de antes, mas podemos notar que no primeiro teste já teremos um retorno de erro com a mensagem como na imagem a seguir:

Imagem do navegador na página do Swagger com a mensagem de erro retornada.
Only the invariant culture is supported in globalization-invariant mode. See https://aka.ms/GlobalizationInvariantMode for more information. (Parameter 'name')\npt-br is an invalid culture identifier.

Na mensagem acima, podemos ver que nossa aplicação está sendo executada em um modo onde a cultura é fixa e não pode ser alterada, devido a imagem do linux Alpine ser reduzida, já que não possui todas as bibliotecas de cultura instaladas. Vamos ao Cenário 3 onde iremos corrigir este problema.

Cenário 3 — Utilizando as imagens do Linux Alpine permitindo múltiplas culturas

Depois de pesquisar um pouco, motivo inclusive de eu estar escrevendo este artigo, para compartilhar a informação e facilitar a busca de outras pessoas que estejam com esse problema, encontrei que o linux Alpine por padrão vem com o mínimo de componentes instalados.

Existe um componente chamado icu-libs (International Components for Unicode libraries), disponível no repositório de componentes do linux Alpine que habilita o SO a trabalhar com múltiplas culturas.

Link do componente, para maiores informações e documentação: https://pkgs.alpinelinux.org/package/edge/main/x86/icu-libs

Para instalar este componente, vamos editar o arquivo Dockerfile na linha 3, após a instrução “WORKDIR /app” da imagem base, adicionando a instrução “RUN apk add — no-cache icu-libs icu-data-full”. Isto vai fazer com que ao realizarmos o build da imagem, ele instale o componente ao chegar nesse passo.

Notem que outro componente está sendo instalado, o icu-data-full. O componente icu-libs vem apenas com as culturas en-US e en-GB. Este outro pacote adiciona as demais culturas ao sistema do linux Alpine.

Outro ponto que precisamos modificar no arquivo, e adicionar após a linha 5, o comando “ENV DOTNET_SYSTEM_GLOBALIZTION_INVARIANT=false”, isto irá configurar o .NET para suportar várias culturas, o que por padrão é desabilitado nas imagens baseadas no linux Alpine.

O arquivo final do Dockerfile ficará desta maneira:

Imagem com a versão final do Dockerfile, com a linha que adicionamos o componente icu-libs

Vamos agora realizar um novo build da imagem, como fizemos no cenário 2:

Tela do terminal com o comando docker build . -t demo-docker-alpine para reconstruir a imagem

Agora vamos executar novamente a aplicação:

Tela do terminal com a aplicação em execução no Docker, utilizando o mesmo comando exibido anteriormente

Ao testar novamente nossa aplicação, podemos ver que ela funciona corretamente, reconhecendo a cultura informada e os 3 testes que realizamos com as imagens originais do .NET agora funcionam com a versão do linux Alpine:

Tela do navegador na página do Swagger, agora com sucesso na execução dos testes.

Update: Podemos executar o comando “docker images” novamente e verificar o tamanho final da imagem, que ficou em 141Mb, dado o acréscimo dos componentes para diferentes culturas instalados no linux Alpine, que não contemplam apenas conversões de datas, mas também outras informações relacionadas a globalização da aplicação. Mesmo assim ficamos com uma redução final de aproximadamente 35% em relação a imagem padrão do ASP.NET.

Encerrando

Neste artigo, quis mostrar não apenas a importância de otimizar uso de armazenamento e tráfego de rede quando trabalhamos com containers, mas também que precisamos ter atenção ao tipo de imagem que estamos utilizando, sendo que muitas delas tem suas peculiaridades e precisam de alguns parâmetros extras para funcionar.

Espero que tenham gostado e que este artigo ajude outras pessoas que possam encontrar o mesmo problema que tive durante o desenvolvimento de uma feature que trabalhava com conversão de data utilizando o Docker e imagem do ASP.NET baseada no linux Alpine.

Até a próxima!

--

--

Gustavo Bellini Bigardi

Architect Manager @ CI&T | Microsoft MVP