O que são as construções de Docker Multi-Stage?

As compilações de Docker em vários estágios são uma funcionalidade introduzida no Docker 17.05 que lhe permite usar várias instruções dentro de um único arquivo Docker. Cada instrução começa uma nova fase, que pode ter sua própria imagem base, dependências e comandos. Os artefatos podem ser copiados seletivamente de um estágio para outro, enquanto a imagem final apenas mantém o que é estritamente necessário para executar a aplicação. Esta abordagem elimina a necessidade de colar manualmente os arquivos Docker ou confiar em scripts complexos de compilação, e reduz drasticamente o tamanho da imagem excluindo compiladores, bibliotecas de desenvolvimento e arquivos intermediários.

A ideia principal é separar o ambiente de compilação do ambiente de execução. Numa compilação típica de um único estágio, um desenvolvedor instala todas as ferramentas de compilação (por exemplo, compiladores, gerenciadores de pacotes, frameworks de teste) na mesma imagem que será usada para produção. Isso incha a imagem e aumenta a superfície de ataque. Compilações de múltiplos estágios resolvem isso usando uma imagem espessa e rica em recursos para compilação e copiando apenas os artefatos resultantes para uma imagem mínima de execução, como ou .

Benefícios de Compilações Multi-Stage

As vantagens de adotar construções multiestágios vão além da simples redução de tamanho. Elas têm um profundo impacto na segurança, manutenção e velocidade de implantação.

1. Tamanho reduzido da imagem

Ao descartar dependências de tempo de compilação, as construções em vários estágios geralmente encolhem imagens em 50% a 90%. Por exemplo, uma aplicação Node.js construída usando a imagem completa (mais de 300MB) pode ser reduzida para menos de 20MB copiando apenas a pasta construída em uma base . Esta economia de armazenamento traduz-se diretamente para tempos de tração mais rápidos, menos largura de banda de rede e menores custos de registro.

2. Segurança melhorada

Cada pacote ou ferramenta instalado numa imagem de contentor é uma vulnerabilidade potencial. As construções em vários estágios permitem excluir compiladores, depuradores e bibliotecas de desenvolvimento da imagem final, reduzindo significativamente a superfície de ataque. Você pode até usar imagens como ou para o estágio de execução, que contém apenas o mínimo para executar o binário da aplicação.

3. Processo de construção simplificado

Todas as etapas de compilação são definidas em um único arquivo Docker, tornando o processo auto-suficiente e fácil de ser versionado. Os pipelines CI/CD se beneficiam de um único ponto de entrada: o arquivo Docker. Não há necessidade de manter scripts de compilação separados ou etapas de limpeza manual.

4. Reprodutibilidade e consistência melhoradas

Como toda a compilação é capturada em um arquivo Docker, qualquer desenvolvedor ou sistema pode reproduzir exatamente as mesmas camadas. O uso de tags de versão específicas para imagens de base garante ainda mais construções consistentes em ambientes.

Construindo um arquivo Docker de vários estágios: Passo a passo

Esta caminhada abrange a criação de um ficheiro Docker de vários estágios pronto para uma aplicação Node.js e React. Os mesmos princípios aplicam-se a qualquer linguagem compilada.

1. Planeje seus estágios

Antes de escrever o código, mapeie as etapas que você precisa. Uma construção típica de vários estágios tem pelo menos duas etapas:

  • Estágio do construtor – instala todas as ferramentas de compilação, instala dependências e executa o comando de compilação.
  • Estágio de execução – usa uma imagem de base mínima, copia apenas os artefatos construídos do estágio construtor, e define o comportamento de execução.

Para projetos complexos, você pode adicionar estágios intermediários para testes, análise estática ou compressão de ativos.

2. Escreva o estágio do construtor

Comece com uma imagem base que inclua a cadeia de ferramentas necessária. Use ] stages com o nome com para relevá-los mais tarde. Para Node.js:

FROM node:14-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

Pontos-chave:

  • Use para instalação de dependência determinística e mais rápida.
  • Mantenha os comandos como camadas separadas quando possível para alavancar o cache.
  • Instale ferramentas de compilação (como TypeScript, webpack) apenas nesta fase.

3. Adicione um estágio de teste intermediário (Opcional)

Para fazer cumprir a qualidade do código, adicione uma etapa que executa testes. Esta etapa pode reutilizar a imagem do construtor ou instalar ferramentas adicionais. Como não é a fase final, as falhas de teste não estarão presentes na imagem final.

FROM builder AS test
RUN npm run test

Você pode executar esta fase em seu pipeline CI com para pegar falhas cedo sem construir toda a imagem final.

4. Defina o Estágio de Execução

Para uma aplicação React, a imagem em tempo de execução pode ser um servidor Nginx. Para uma API de infra- estrutura, pode ser uma imagem base sem distros ou um alpino mínimo com o tempo de execução Node.js. Copie apenas os artefatos essenciais usando .

FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Se você precisar do runtime Node.js, evite copiar do construtor; em vez disso, reinstale dependências de produção na fase de runtime:

FROM node:14-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
RUN npm ci --only=production
EXPOSE 3000
CMD ["node", "dist/server.js"]

5. Construir e testar a imagem

Compilar a imagem final usando o comando padrão:

docker build -t myapp:latest .

Para verificar o tamanho, execute e compare com uma construção de um único estágio. Execute o recipiente e confirme que o aplicativo responde corretamente:

docker run -d -p 8080:80 myapp:latest
curl http://localhost:8080

Melhores práticas para a construção de múltiplos estágios

  • Use etiquetas de imagem de base específicas – evite para evitar surpresas. Prefere ou .
  • Optimizar cache de camadas – cópia e antes do resto do código fonte para que a camada só seja invalidada quando as dependências mudam.
  • Leverage buildKit – habilitar BuildKit com para construções mais rápidas, caches em linha e paralelismo melhor.
  • Criar várias fases finais para diferentes ambientes – por exemplo, uma fase de desenvolvimento com ferramentas de depuração e uma fase de produção com uma imagem base endurecida.
  • Use para construções de desenvolvimento – em desenvolvimento você pode parar na fase para obter mapas de recarregamento e fonte ao vivo, em seguida, reconstrua com ] para produção.
  • Mantenha segredos fora das imagens – use a bandeira do Docker BuildKit se você precisar passar credenciais durante a construção; nunca inclua-as na imagem final.

Padrões comuns e casos de uso

Línguas Compiladas (Ir, Rust, C++)

Para binários estáticos, o estágio de execução pode usar (imagem base vazia). Apenas o binário e talvez um arquivo de configuração são copiados. Exemplo para Ir:

FROM golang:1.20-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .

FROM scratch
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]

Aplicações Python

Use um estágio de construtor com para instalar dependências e compilar quaisquer extensões C, em seguida, copie apenas os pacotes instalados para uma fase de execução:

FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
COPY . .

FROM python:3.11-slim
COPY --from=builder /root/.local /root/.local
COPY --from=builder /app /app
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

Interface com o servidor API

Compila tanto a interface quanto a infraestrutura em um arquivo Docker. Use estágios de construção separados para cada um, e copie ambos os artefatos em uma única imagem de execução:

FROM node:14-alpine AS frontend-builder
WORKDIR /app
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

FROM node:14-alpine AS api-builder
WORKDIR /app
COPY api/package*.json ./
RUN npm ci
COPY api/ .
RUN npm run build

FROM node:14-alpine
WORKDIR /app
COPY --from=frontend-builder /app/build ./public
COPY --from=api-builder /app/dist ./dist
RUN npm ci --only=production
EXPOSE 3000
CMD ["node", "dist/server.js"]

Resolução de Problemas com Compilações de Vários Estágios

  • Baixar o cache não está funcionando – garantir dependências de ordem de comandos antes do código fonte. Use para excluir arquivos desnecessários.
  • Artefacto não encontrado – verificar os caminhos na instrução . A fase de construção deve produzir saída na localização especificada. Use para depuração.
  • Vazamento de segredo – nunca copie diretórios inteiros que possam conter ou . Cópia explícita apenas arquivos necessários.
  • Imagens finais grandes apesar de multi-estágio – verifique se você está acidentalmente copiando ou toda a fonte. Use para ver tamanhos de camadas.

Conclusão

As construções de Docker em vários estágios são uma pedra angular da moderna contêinerização. Eles permitem que você envie imagens enxutas e seguras, mantendo o processo de compilação simples e documentado em um único arquivo Docker. Ao separar preocupações entre build e runtime, você pode reduzir drasticamente o tamanho da imagem, melhorar a segurança e simplificar pipelines CI/CD. As técnicas mostradas aqui se aplicam a quase qualquer pilha, seja você construindo aplicativos Node.js, Go, Python, Java ou frontend. Comece convertendo seus arquivos Docker existentes em builds em vários estágios, e você verá imediatamente melhorias na velocidade de construção, eficiência de implantação e custos de infraestrutura globais.

Para mais detalhes, consulte o documentação oficial de construção do Docker multi-estágio e o Guia de boas práticas do Dockerfile. Exemplos do mundo real também estão disponíveis na documentação Docker Library.