Docker - Dockerfile com multi-stage build


 Hoje vamos apresentar o recurso do multi-stage build para otimizar o arquivo Dockerfile.

Se você esta chegando agora e não sabe o que é Dockerfile sugiro que acompanhe o meu curso de introdução ao Docker nesta série de artigos: Docker - Uma introdução básica - Macoratti.net

 
 

Se quiser apenas recordar os conceitos sobre Dockerfile veja este artigo: Docker - Criando uma imagem com Dockerfile - Macoratti

O Dockerfile

O Docker cria imagens automaticamente lendo as instruções de um Dockerfile - um arquivo de texto que contém todos os comandos, em ordem, necessários para construir uma determinada imagem.

Um Dockerfile adere a um formato específico e a um conjunto de instruções que você pode encontrar na referência do Dockerfile.

Uma imagem do Docker consiste em camadas somente leitura, cada uma representando uma instrução Dockerfile. As camadas são empilhadas e cada uma delas é um delta das alterações da camada anterior.

Considere este Dockerfile:

FROM Ubuntu: 18.04
COPY . /app
RUN make /app
CMD phyton /app/app.py

Cada instrução acima cria uma camada na imagem:

FROM cria uma camada a partir da imagem do ubuntu: 18.04 Docker.
COPY adiciona arquivos do diretório atual do cliente do Docker.
RUN constrói sua aplicação com make.
CMD especifica qual comando deve ser executado no contêiner

Era assim que as instruções eram usadas no Dockerfile antes do recurso multi-stage build.

Usando multi-stage

As multi-stage build ou compilações de vários estágios são úteis para otimizar Dockerfiles, mantendo-os fáceis de ler e manter.

Com compilações de vários estágios, podemos usar várias instruções FROM no Dockerfile e cada instrução FROM pode usar uma base diferente, e cada uma delas inicia uma nova etapa da construção.  Você pode copiar artefatos seletivamente de um estágio para outro, deixando para trás tudo o que não deseja na imagem final.

Antes do recurso multi-stage era permitido apenas uma instrução FROM que era carregada inicialmente e estabelecia qual imagem seria utilizada, afetando todos os comandos subsequentes.  Com o novo recurso podemos utilizar quantos comandos FROM precisarmos.

Cada FROM é um novo estágio que substitui o anterior, é como uma nova imagem, totalmente independente e isolada. Desta forma, se no estágio inicial estivermos utilizando uma imagem com .NET Core e no último estágio estamos utilizando uma imagem sem suporte a .NET Core a imagem final gerada ficará sem este suporte.

Além disso o comando FROM agora permite nomear os estágios através da instrução AS em sua assinatura.

Considere o exemplo a seguir de um Dockerfile gerado para uma aplicação ASP .NET Core:

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["apidemo.csproj", "."]
RUN dotnet restore "./apidemo.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "apidemo.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "apidemo.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "apidemo.dll"]

Neste exemplo podemos notar que temos quatro estágios usando quatro comandos FROM com as instrução AS.

Assim cada etapa do build pode conter a imagem mais eficiente possível, seja uma etapa para executar um teste ou rodar a aplicação final.

Além disso estamos usando  informações das etapas anteriores, para isso, o comando COPY usa o argumento:

 --from=<nome do estágio>

que permite copiar os arquivos existentes em etapas anteriores.

As vantagens do uso de estágios múltiplos incluem:

  1. Redução do tamanho da imagem: Ao dividir o processo de construção em várias etapas, é possível remover arquivos e dependências desnecessários da imagem final. Isso resulta em imagens menores e mais eficientes.
  2. Maior segurança: Como apenas o que é necessário para executar o aplicativo é incluído na imagem final, o risco de vulnerabilidades de segurança é reduzido.
  3. Melhor desempenho: Ao excluir arquivos desnecessários da imagem final, o desempenho do aplicativo pode ser melhorado.
  4. Facilidade de manutenção: Ao dividir o processo de construção em várias etapas, é mais fácil manter e atualizar o aplicativo.

A seguir, um exemplo prático de como usar o recurso de estágios múltiplos no Docker:

Suponha que você tenha um aplicativo Node.js que precisa ser executado em um contêiner Docker. O Dockerfile abaixo mostra como usar vários estágios para construir a imagem:

# Estágio 1: compilação do código fonte
FROM node:14-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Estágio 2: imagem final
FROM node:14-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --only=production
COPY --from=builder /app/dist ./dist
CMD ["npm", "start"]

Neste exemplo, o Dockerfile contém dois estágios:

  1. O primeiro estágio usa uma imagem do Node.js para compilar o código fonte, executar os testes e gerar os arquivos intermediários. O resultado é uma imagem temporária que contém todos os arquivos necessários para construir o aplicativo.
     
  2. O segundo estágio usa outra imagem do Node.js para criar a imagem final. A imagem final contém apenas os arquivos necessários para executar o aplicativo, que são copiados do estágio anterior. A imagem final é muito menor do que a imagem temporária do primeiro estágio, pois contém apenas os arquivos necessários para executar o aplicativo.

Ao executar o comando "docker build -t meu-app ." no diretório que contém o Dockerfile acima, o Docker irá construir a imagem usando os dois estágios e gerar uma imagem Docker final otimizada e eficiente que contém apenas os arquivos necessários para executar o aplicativo.

Com isso teremos imagens otimizadas e com um tamanho menor.

E estamos conversados...

"Guia-me na tua verdade, e ensina-me, pois tu és o Deus da minha salvação; por ti estou esperando todo o dia."
Salmos 25:5

Referências:


José Carlos Macoratti