Reduzindo o tamanho de imagens de contêiner com Docker Multi-Stage Build
(labs.iximiuz.com)- 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_modulespassa 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
- Fazer o build da aplicação em
Dockerfile.build:
FROM node:lts-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
- Copiar o artefato gerado para o host:
docker cp $(docker create build:v1):/app/.output .
- 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.
- É possível definir estágios de build e runtime em um único Dockerfile usando vários comandos
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
No caso do Java, o
jlinkfoi introduzido a partir da versão 9, mas a usabilidade não é boa, já que é preciso encontrar e especificar os módulos dependentes com ojdeps. 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.Eu uso desse jeito, mas acho que a desvantagem é que o tempo de build fica longo.
O tempo de build não deve ter diferença. Se houver diferença, é porque foi configurado errado!
Ah, entendi!
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!
Acho que vou precisar aprender um pouco mais sobre Docker!