Conteúdo
Tutorial de builds em múltiplos estágios no Docker: reduza suas imagens
Muitos programas precisam de compiladores, pacotes de desenvolvimento e ferramentas de build durante a compilação, mas só precisam do resultado compilado durante a execução. Se você colocar tudo na mesma imagem, a imagem final levará um monte de coisas desnecessárias.
Um build em múltiplos estágios permite usar vários FROM em um Dockerfile, e cada FROM inicia um estágio. Somente o conteúdo do último estágio é mantido; os estágios anteriores servem apenas para gerar arquivos.
Experimento: um estágio vs. múltiplos estágios
Vamos comparar usando um programa web simples em Go (main.go):
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from a distroless container!")
})
fmt.Println("Listening on :8080")
http.ListenAndServe(":8080", nil)
}
Um estágio (Dockerfile.single): compile na imagem oficial do Go e use a mesma imagem para executar:
FROM golang:1.27
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /server .
CMD ["/server"]
Múltiplos estágios (Dockerfile):
# Estágio um: compilação
FROM golang:1.27 AS builder
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server .
# Estágio dois: execução
FROM gcr.io/distroless/static-debian13
COPY --from=builder /out/server /server
USER nonroot
ENTRYPOINT ["/server"]
AS builderdá um nome ao primeiro estágio.COPY --from=buildercopia arquivos daquele estágio.gcr.io/distroless/static-debian13é uma imagem mínima mantida pelo Google: não tem shell nem gerenciador de pacotes, apenas os arquivos essenciais para executar programas estáticos.
Faça o build da versão em múltiplos estágios:
$ docker build -t go-multi .
[+] Building 51.7s (14/14) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 326B 0.0s
=> [internal] load metadata for gcr.io/distroless/static-debian13:latest 2.1s
=> [internal] load metadata for docker.io/library/golang:1.27 0.4s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [builder 1/6] FROM docker.io/library/golang:1.27@sha256:e432b43af23a9328d56a7c499be047681 0.0s
=> => resolve docker.io/library/golang:1.27@sha256:e432b43af23a9328d56a7c499be0476810aa344ac 0.0s
=> [stage-1 1/2] FROM gcr.io/distroless/static-debian13:latest@sha256:58133991db06659feaabe0 3.4s
=> => resolve gcr.io/distroless/static-debian13:latest@sha256:58133991db06659feaabe0f4e97a35 0.0s
=> => … (download and extraction of layers omitted)
=> [internal] load build context 0.0s
=> => transferring context: 120B 0.0s
=> CACHED [builder 2/6] WORKDIR /src 0.0s
=> [builder 3/6] COPY go.mod ./ 0.0s
=> [builder 4/6] RUN go mod download 0.5s
=> [builder 5/6] COPY . . 0.0s
=> [builder 6/6] RUN CGO_ENABLED=0 go build -o /out/server . 47.6s
=> [stage-1 2/2] COPY --from=builder /out/server /server 0.0s
=> exporting to image 1.1s
=> => exporting layers 0.6s
=> => exporting manifest sha256:8b506df2e51c2cbf7df7b452669cde4652ac2d83d924e8e5b67ed8f37b6a 0.0s
=> => exporting config sha256:f1efccd3fdd70ffd1054b9ed0ff4154e3be0d93324c196bd46438e12a4b372 0.0s
=> => exporting attestation manifest sha256:4cf5049d1a21697ca953ccd67da3874da40c466c9db43ac9 0.0s
=> => exporting manifest list sha256:463874676a48cc064e399fd1968bcd5e301d77f20909360c40fff1c 0.0s
=> => naming to docker.io/library/go-multi:latest 0.0s
=> => unpacking to docker.io/library/go-multi:latest 0.4s
Você pode ver as etapas dos dois estágios identificadas como [builder …] e [stage-1 …]. O BuildKit baixa ao mesmo tempo as imagens base necessárias para os dois estágios. Compare os tamanhos das duas imagens:
$ docker images --filter reference=go-single --filter reference=go-multi
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
go-multi:latest 463874676a48 20.2MB 5.77MB
go-single:latest c6c844f9076e 1.43GB 343MB
É o mesmo programa: 1.43 GB em um estágio e 20.2 MB em múltiplos estágios, uma diferença de 70 vezes. A imagem de um estágio leva todo o compilador Go e o sistema Debian; a imagem final em múltiplos estágios contém apenas o executável compilado e os arquivos básicos do distroless.
Quanto menor a imagem, mais rápido o download e a implantação, e menos pacotes potencialmente vulneráveis. Vamos executar:
$ docker run -d --name goapp -p 8080:8080 go-multi
d474e2de30d65e21a1f6395aac76c185fcfc8d4d6caa1240ab6ef3ebd7406e25
$ curl -s http://localhost:8080
Hello from a distroless container!
$ docker exec goapp sh
OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH
$ docker rm -f goapp
goapp
A imagem distroless nem sequer tem sh (executable file not found). Mesmo que um invasor comprometa o programa, será difícil fazer algo dentro do contêiner. A desvantagem é que a depuração fica menos prática; quando precisar, você pode usar a versão :debug, que inclui ferramentas de depuração.
Exemplos em múltiplos estágios para outras linguagens
Frontend Node.js: use Nginx depois do build
Depois do build, projetos frontend como React, Vue e Astro têm apenas arquivos estáticos e não precisam do ambiente de execução do Node.js:
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.30-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
As ferramentas de build e todo o node_modules ficam fora da imagem final.
Backend Node.js: mantenha apenas os pacotes de produção
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json .
USER node
CMD ["node", "dist/index.js"]
Os estágios deps e build são independentes, então o BuildKit os compila em paralelo.
Java: compile com o JDK e execute com o JRE
FROM maven:3.9-eclipse-temurin-25 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn -q dependency:go-offline
COPY src ./src
RUN mvn -q package -DskipTests
FROM eclipse-temurin:25-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
Faça o build de um estágio específico: --target
Com os estágios, você pode criar versões de desenvolvimento e de produção no mesmo Dockerfile:
FROM node:24-alpine AS base
WORKDIR /app
COPY package.json ./
FROM base AS dev
ENV NODE_ENV=development
COPY . .
CMD ["npm", "run", "dev"]
FROM base AS prod
ENV NODE_ENV=production
COPY . .
CMD ["node", "server.js"]
$ docker build -q --target dev -t target-demo:dev .
sha256:3d0fdd2a77233426c90dfbc575aafaef652a1393cb2730d066e627362860ec09
$ docker build -q --target prod -t target-demo:prod .
sha256:9e14e3363d26d46ed628e49b4cd806a29e9699a5be2e6e4078976bd380632568
$ docker inspect --format '{{.Config.Env}} {{.Config.Cmd}}' target-demo:dev
[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NODE_VERSION=24.21.0 YARN_VERSION=1.22.22 NODE_ENV=development] [npm run dev]
$ docker inspect --format '{{.Config.Env}} {{.Config.Cmd}}' target-demo:prod
[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NODE_VERSION=24.21.0 YARN_VERSION=1.22.22 NODE_ENV=production] [node server.js]
--target dev faz o build somente até o estágio dev; --target prod ignora dev. As variáveis de ambiente e os comandos de inicialização das duas imagens são diferentes. -q faz com que docker build imprima apenas o ID da imagem.
Copie de uma imagem externa
Com COPY --from, você também pode especificar diretamente qualquer imagem, sem precisar usar um estágio deste arquivo:
COPY --from=nginx:1.30-alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
A segunda linha é um uso comum: copie o executável diretamente da imagem oficial do uv sem instalá-lo separadamente. Veja aplicação Python.
Resumo
- Cada
FROMinicia um novo estágio, que você nomeia comAS nome. - O último estágio é a imagem de saída e contém apenas o que foi copiado com
COPY --from. - Deixe ferramentas de compilação, código-fonte e pacotes de desenvolvimento nos estágios anteriores para reduzir a imagem em dezenas de vezes.
--targetfaz o build somente até o estágio indicado, como um estágio de testes.
Exercícios
Exercício 1: adicione um estágio FROM base AS test ao Dockerfile em múltiplos estágios para executar os testes. Como executar apenas os testes no CI sem fazer o build da imagem final?
Exercício 2: use docker history para comparar as imagens Go de um estágio e de múltiplos estágios deste capítulo. A camada do compilador Go está na imagem final?
Dica
A imagem final em múltiplos estágios contém apenas a camada base do distroless e a camada COPY --from=build; a camada do conjunto de ferramentas do Go não aparece.
Perguntas frequentes
Um build em múltiplos estágios deixa o build mais lento?
Não. O BuildKit compila em paralelo os estágios que não dependem uns dos outros e só compila os estágios necessários para o estágio final. Veja na prática o exemplo de TypeScript em [aplicação Node.js](/pt/docker/nodejs/).
As imagens dos estágios intermediários ficam no computador?
Os estágios intermediários não geram imagens com nome, mas suas camadas ficam no cache de build e podem ser removidas com docker builder prune.
Posso usar outra imagem com COPY --from?
Sim. Por exemplo, COPY --from=nginx:1.30-alpine /etc/nginx/nginx.conf . copia arquivos diretamente de uma imagem externa. Há um exemplo neste capítulo.
Atualizado 14 de outubro de 2026