DevGuideTK
← Trilha Backend

Containers e Docker

O que é um container, como ele difere de uma máquina virtual e como se constrói uma imagem pequena, rápida e segura.

As decisões de uma imagem são cinco: qual imagem base usar, como construir sem levar ferramentas para produção, como aproveitar o cache, como rodar com segurança e como versionar.

A imagem base

BaseExemploTamanhoVantagensCuidados
Completanode:24, python:3.13Centenas de MB a mais de 1 GBTem tudo: compiladores, ferramentas, shellGrande e com muito mais pacotes do que a produção usa
Slim (Debian reduzido)node:24-slim, python:3.13-slimDezenas a poucas centenas de MBCompatível com quase tudo, menorPode faltar alguma biblioteca de sistema
Alpinenode:24-alpineA menor entre as que têm shellMuito pequenaUsa outra biblioteca C (musl), que quebra alguns pacotes com código nativo
Distrolessgcr.io/distroless/static-debian12, base-debian12MínimaSó o necessário para rodar; sem shell e sem gerenciador de pacotesDepurar exige outras técnicas, já que não dá para entrar no container
ScratchscratchVaziaSó o seu binárioSó para binários estáticos, como Go sem CGO

Para a maioria das aplicações, slim é o ponto de partida mais seguro. Alpine quando o tamanho importa e as dependências funcionam com musl. Distroless quando a segurança pesa mais que a conveniência de depurar.

Em Go, Rust e outras linguagens que geram binários estáticos, a imagem final pode ser distroless ou scratch. No teste, uma API Go ficou com 14 MB numa base distroless, sem shell dentro: uma tentativa de executar sh no container falhou porque ele não existe.

Construção em estágios

A imagem final não precisa das ferramentas que construíram a aplicação. A construção em estágios (multi-stage build) usa uma imagem para compilar e outra, enxuta, para rodar:

# Estágio 1: tem compilador e ferramentas
FROM golang:1.24-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /api .

# Estágio 2: só o binário
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /api /api
ENTRYPOINT ["/api"]

Só o último estágio vira a imagem publicada. O compilador, o código-fonte e os arquivos intermediários ficam para trás.

LinguagemO que fica no estágio de construçãoO que vai para a imagem final
Node/TypeScriptDependências de desenvolvimento, compilador TypeScriptO JavaScript gerado e as dependências de produção
PythonCompiladores para pacotes com código nativoO ambiente virtual já instalado
GoO compilador GoUm binário
JavaMaven ou Gradle, o JDKO JAR e um JRE
C#O SDK do .NETA aplicação publicada e o runtime do ASP.NET

Aproveitar o cache

PráticaEfeito
Copiar os arquivos de dependências e instalar antes de copiar o códigoMudanças no código não refazem a instalação
Usar o arquivo de trava (package-lock.json, poetry.lock, go.sum) e o comando de instalação exata (npm ci)A mesma versão de cada dependência em toda construção
Um .dockerignore bem feitoArquivos irrelevantes não invalidam o cache nem entram na imagem
Instruções que mudam pouco no topo, as que mudam muito no fimMais camadas reaproveitadas
Cache de construção montado (RUN --mount=type=cache)O cache do gerenciador de pacotes sobrevive entre construções

O .dockerignore merece atenção especial. Sem ele, COPY . . leva para dentro da imagem a pasta .git, o node_modules local, arquivos de teste e, no pior caso, um .env com senhas.

.git
node_modules
.env*
coverage
tests
*.log
Dockerfile*

Executar com segurança

PráticaPor quê
Usuário sem privilégios (USER)Uma falha explorada não dá poder de root dentro do container
Forma de lista no CMDO processo recebe o SIGTERM direto, sem um shell no meio
Sistema de arquivos somente leitura, quando possívelImpede que um invasor altere ou acrescente arquivos
Limites de CPU e memóriaUm container com vazamento de memória não derruba os vizinhos
Nenhum segredo na imagemQualquer um com acesso à imagem lê o que estiver nas camadas, mesmo se apagado numa camada posterior
Varredura de vulnerabilidadesFerramentas como Trivy e Grype apontam pacotes com falhas conhecidas

O processo principal e o sinal de encerramento

O primeiro processo do container (PID 1) é quem recebe o SIGTERM quando o container é parado. Se esse processo for um shell, o sinal fica nele e não chega à aplicação.

No teste, com CMD npm start na forma de texto, o PID 1 era /bin/sh -c npm start. O docker stop esperou os 10 segundos do limite padrão e matou o container à força, sem que a aplicação recebesse o sinal. Com CMD ["node", "server.js"], o PID 1 era o próprio Node, que recebeu o sinal, encerrou e o docker stop levou 0,1 segundo.

FormaExemploPID 1
Texto (shell form)CMD npm startUm shell, que não repassa o sinal
Lista (exec form)CMD ["node", "server.js"]A própria aplicação
Script de entradaENTRYPOINT ["./entrada.sh"]O script, a menos que ele termine com exec

Se um script de entrada for necessário, ele deve terminar com exec node server.js, que troca o shell pela aplicação. Aplicações que criam processos filhos se beneficiam de um init mínimo como tini ou da opção --init do Docker.

Versionar as imagens

PráticaPor quê
Não usar latest em produçãoNinguém sabe qual versão está rodando, e um rollback não tem para onde voltar
Etiquetar com a versão ou o commit: loja/api:1.4.2, loja/api:3f9c2e1Cada deploy aponta para uma imagem exata e imutável
Fixar a versão da imagem base: node:24.11-alpineUma nova versão da base não muda a imagem sem ninguém saber
Reconstruir periodicamenteRecebe as correções de segurança da imagem base

Fixar a base e reconstruir periodicamente parecem contraditórios. Não são: fixar garante que cada construção seja reproduzível, e a atualização da base vira uma mudança explícita, revisada e testada.

Resumo da decisão

  1. Base: slim por padrão; alpine ou distroless quando tamanho e segurança pesam mais.
  2. Construção em estágios: ferramentas no estágio de construção, só o necessário na imagem final.
  3. Cache: dependências antes do código, arquivo de trava, .dockerignore.
  4. Execução: usuário sem privilégios, CMD em forma de lista, sem segredos, com limites.
  5. Versões: etiquetas imutáveis por versão ou commit, base fixada e atualizada de propósito.

O próximo nível mostra Dockerfiles completos para Node e Go, a aplicação preparada para container nas cinco linguagens e um ambiente local com Docker Compose.

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Escolher a imagem base

    Quero escolher a imagem base certa para a minha aplicação.
    
    Pergunte qual linguagem e versão eu uso, se há dependências com código nativo e quanta importância eu dou a tamanho, segurança e facilidade de depurar.
    
    Compare para o meu caso as imagens completa, slim, alpine, distroless e scratch: tamanho aproximado, compatibilidade, superfície de ataque e o que muda na hora de investigar um problema em produção.
    
    Explique em detalhe os riscos da alpine com bibliotecas nativas, por causa da musl, e como depurar um container distroless sem shell.
    
    Termine com uma recomendação e com a forma de fixar a versão da base e mantê-la atualizada.
  • Revisar um Dockerfile

    Vou colar abaixo o Dockerfile e o .dockerignore da minha aplicação. Revise com foco em tamanho, cache, segurança e comportamento em execução.
    
    Verifique, nesta ordem, e para cada item diga se está correto, incorreto ou ausente, citando a linha:
    1. A imagem base é adequada e tem versão fixada?
    2. Há construção em estágios, sem ferramentas de construção na imagem final?
    3. As dependências são instaladas antes da cópia do código, usando o arquivo de trava?
    4. O .dockerignore exclui .git, dependências locais, testes e arquivos .env?
    5. Há algum segredo em qualquer camada?
    6. O processo roda com usuário sem privilégios?
    7. O CMD ou ENTRYPOINT está em forma de lista, ou o script termina com exec?
    8. A aplicação escuta em 0.0.0.0?
    
    Para cada problema, explique a consequência e mostre a correção. No final, mostre o Dockerfile completo corrigido e estime a diferença de tamanho.
    
    Dockerfile e .dockerignore:
    [cole aqui]
  • Segurança de containers

    Quero entender os riscos de segurança de containers e as práticas que os reduzem.
    
    Explique:
    1. Por que rodar como root dentro do container é perigoso, mesmo isolado.
    2. Como segredos acabam vazando em imagens, e as formas corretas de entregá-los ao container.
    3. O que é varredura de vulnerabilidades, quais ferramentas existem e como interpretar o resultado.
    4. Sistema de arquivos somente leitura, remoção de capacidades do Linux e limites de recursos.
    5. Por que o isolamento de um container é menor que o de uma máquina virtual, e quando isso importa.
    
    Pergunte onde os meus containers rodam e termine com uma lista priorizada do que aplicar primeiro.