18 pontos por xguru 2024-11-17 | 6 comentários | Compartilhar no WhatsApp
  • Ao criar imagens de contêiner Docker, se o Dockerfile não tiver uma estrutura Multi-Stage, há grande chance de incluir arquivos desnecessários
  • Isso leva ao aumento do tamanho da imagem e também ao aumento de vulnerabilidades de segurança
  • Analisa as principais causas dos “arquivos desnecessários” que podem surgir em imagens de contêiner e explica como resolvê-las com Multi-Stage Build

Causas do aumento do tamanho da imagem

  • Aplicações têm dependências de build time e de runtime.
  • As dependências de build time são mais numerosas do que as de runtime e têm mais vulnerabilidades de segurança (CVEs).
  • Se a mesma imagem for usada para build e execução, dependências de build time desnecessárias (compiladores, linters etc.) acabam incluídas.
  • As imagens de build e de runtime devem ser separadas, mas isso muitas vezes é ignorado.

Exemplo de estrutura incorreta de Dockerfile

Exemplo incorreto para aplicação Go

FROM golang:1.23  
WORKDIR /app  
COPY . .  
RUN go build -o binary  
CMD ["/app/binary"]  
  • A imagem golang:1.23 é voltada para compilação, mas usá-la diretamente em produção faz com que todo o compilador Go e suas dependências também sejam incluídos.
  • Tamanho da imagem: mais de 800MB, com mais de 800 vulnerabilidades de segurança.

Exemplo incorreto para aplicação Node.js

FROM node:lts-slim  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
ENV NODE_ENV=production  
EXPOSE 3000  
CMD ["node", "/app/.output/index.mjs"]  
  • A pasta node_modules passa a incluir até dependências de desenvolvimento que não são necessárias em runtime.
  • Não dá para resolver isso apenas com npm ci --omit=dev, porque dependências de desenvolvimento necessárias no processo de build podem ser removidas.

Como criar imagens Lean antes do Multi-Stage Build

Padrão Builder

  1. Fazer o build da aplicação em Dockerfile.build:
FROM node:lts-slim  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  1. Copiar o artefato gerado para o host:
docker cp $(docker create build:v1):/app/.output .  
  1. Criar a imagem de runtime em Dockerfile.run:
FROM node:lts-slim  
WORKDIR /app  
COPY .output .  
CMD ["node", "/app/.output/index.mjs"]  
•	Problemas: necessidade de escrever vários Dockerfiles, gerenciar a ordem do build e usar scripts adicionais.  

Entendendo o Multi-Stage Build

  • Multi-Stage Build é um recurso que implementa o padrão Builder dentro do próprio Docker.
    • É possível definir estágios de build e runtime em um único Dockerfile usando vários comandos FROM.
    • Com o comando COPY --from=<stage>, é possível trazer os arquivos gerados em um estágio anterior.

Exemplo de Dockerfile Multi-Stage (Node.js)

# Build stage  
FROM node:lts-slim AS build  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  
# Runtime stage  
FROM node:lts-slim AS runtime  
WORKDIR /app  
COPY --from=build /app/.output .  
ENV NODE_ENV=production  
CMD ["node", "/app/.output/index.mjs"]  
  • Ao copiar diretamente os artefatos gerados com COPY --from=build, é possível mover os arquivos sem passar pelo host.

Exemplos práticos de Multi-Stage Build

Aplicação React

# Build stage  
FROM node:lts-slim AS build  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  
# Runtime stage  
FROM nginx:alpine  
COPY --from=build /app/build /usr/share/nginx/html  
ENTRYPOINT ["nginx", "-g", "daemon off;"]  
  • Após o build, uma aplicação React se torna um conjunto de arquivos estáticos e pode ser servida com Nginx.

Aplicação Go

# Build stage  
FROM golang:1.23 AS build  
WORKDIR /app  
COPY . .  
RUN go build -o binary  
  
# Runtime stage  
FROM gcr.io/distroless/static-debian12:nonroot  
COPY --from=build /app/binary /app/binary  
ENTRYPOINT ["/app/binary"]  
  • O uso de uma imagem distroless fornece um ambiente de runtime minimizado.

Aplicação Java

# Build stage  
FROM eclipse-temurin:21-jdk-jammy AS build  
WORKDIR /build  
COPY . .  
RUN ./mvnw package -DskipTests  
  
# Runtime stage  
FROM eclipse-temurin:21-jre-jammy  
COPY --from=build /build/target/app.jar /app.jar  
CMD ["java", "-jar", "/app.jar"]  
  • No build usa-se o JDK, e no runtime um JRE mais leve.

Conclusão

  • Multi-Stage Build separa os ambientes de build e runtime, evitando o aumento do tamanho da imagem causado por dependências de desenvolvimento desnecessárias
  • Com isso, é possível reduzir o tamanho da imagem, reforçar a segurança e simplificar o processo de build
  • Multi-Stage Build é a abordagem padrão para criar imagens de contêiner eficientes e também oferece suporte a recursos avançados, como condições de ramificação e testes unitários durante o build

6 comentários

 
savvykang 2024-11-18

No caso do Java, o jlink foi introduzido a partir da versão 9, mas a usabilidade não é boa, já que é preciso encontrar e especificar os módulos dependentes com o jdeps. Quando vejo que as pessoas não conhecem esse tipo de método ou ficam procurando um JRE, parece que falta divulgação das ferramentas do Java, e também que seria preciso melhorar isso para que um JRE pudesse ser gerado com um único comando.

 
brainer 2024-11-17

Eu uso desse jeito, mas acho que a desvantagem é que o tempo de build fica longo.

 
kandk 2024-11-18

O tempo de build não deve ter diferença. Se houver diferença, é porque foi configurado errado!

 
brainer 2024-11-18

Ah, entendi!

 
qurare 2024-11-18

Dependendo da estratégia, às vezes dá até para colocar um estágio inteiro em cache, então no meu caso o tempo de build acabou até diminuindo!

 
brainer 2024-11-18

Acho que vou precisar aprender um pouco mais sobre Docker!