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.

Os números desta página vêm de construções reais com Docker, numa aplicação Node pequena com uma dependência de produção e duas de desenvolvimento, e numa API Go mínima. Os tamanhos variam com as versões das imagens base; as proporções são o que importa.

Um Dockerfile para Node: do ingênuo ao cuidadoso

A versão que costuma aparecer primeiro:

FROM node:24
WORKDIR /app
COPY . .
RUN npm install
CMD npm start

A versão cuidadosa:

# Estágio 1: instala só as dependências de produção
FROM node:24-alpine AS dependencias
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Estágio 2: a imagem que vai para produção
FROM node:24-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=dependencias /app/node_modules ./node_modules
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]

O que medimos com as duas:

IngênuoCuidadoso
Tamanho da imagem1,73 GB236 MB
Reconstrução depois de mudar uma linha de código5,4 s (reinstala as dependências)0,4 s (dependências do cache)
Processo principal (PID 1)/bin/sh -c npm startnode server.js
docker stop10,2 s: o sinal não chegou, e o container foi morto à força0,1 s: a aplicação recebeu o SIGTERM e encerrou
Usuáriorootnode

Cada linha da tabela vem de uma decisão do Dockerfile:

DecisãoEfeito
node:24-alpine em vez de node:24A maior parte da redução de tamanho
npm ci --omit=devSem TypeScript, Vitest e outras dependências de desenvolvimento na imagem
Copiar package*.json e instalar antes de COPY . .A instalação vem do cache quando só o código muda
CMD em forma de listaO Node é o PID 1 e recebe os sinais
USER nodeO processo roda sem privilégios. A imagem oficial do Node já traz esse usuário

Para TypeScript, acrescente um estágio que instala todas as dependências e roda a compilação, e copie para a imagem final só a pasta gerada.

Um Dockerfile para Go: binário estático

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 -ldflags="-s -w" -o /api .

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /api /api
EXPOSE 3000
ENTRYPOINT ["/api"]

CGO_ENABLED=0 gera um binário que não depende de nenhuma biblioteca do sistema, e -ldflags="-s -w" remove informações de depuração. A imagem final ficou com 14 MB e roda como um usuário sem privilégios, já definido pela variante nonroot da base. Não há shell: uma tentativa de executar sh dentro do container falhou.

Para as outras linguagens, a estrutura é a mesma, com estas peças:

LinguagemEstágio de construçãoImagem final típica
Pythonpython:3.13-slim, criando um ambiente virtual com pip installpython:3.13-slim, copiando o ambiente virtual
Javaeclipse-temurin:21-jdk com Maven ou Gradleeclipse-temurin:21-jre, só com o JAR
C#mcr.microsoft.com/dotnet/sdk:9.0 com dotnet publishmcr.microsoft.com/dotnet/aspnet:9.0

A aplicação preparada para container

Um container bem construído também depende de como a aplicação se comporta dentro dele. Três regras resolvem a maior parte:

  • Configuração pelo ambiente: a mesma imagem roda em testes e em produção, com variáveis diferentes.
  • Falhar logo se faltar configuração: melhor o container não subir do que subir e falhar no primeiro pedido.
  • Logs na saída padrão: o Docker e o orquestrador coletam o que sai em stdout. Gravar em arquivos dentro do container perde os logs a cada substituição.

O encerramento gracioso, a quarta regra, está no tópico de escalabilidade.

function obrigatoria(nome: string): string {
  const valor = process.env[nome];
  if (!valor) {
    log("erro", `variável de ambiente ${nome} ausente`);
    process.exit(1);
  }
  return valor;
}

function log(nivel: string, mensagem: string, extra: object = {}) {
  console.log(JSON.stringify({
    hora: new Date().toISOString(),
    nivel,
    mensagem,
    ...extra,
  }));
}

export const config = {
  porta: Number(process.env.PORT ?? 3000),
  bancoUrl: obrigatoria("DATABASE_URL"),
};

log("info", "iniciando", { porta: config.porta });

O que muda de uma linguagem para outra

Logs em JSON na saída padrãoCuidado no container
Nodeconsole.log(JSON.stringify(...)), ou PinoNada especial; o Node já escreve na saída padrão sem atraso
PythonUm Formatter próprio, ou python-json-loggerDefina PYTHONUNBUFFERED=1, ou os logs podem demorar a aparecer
Goslog com NewJSONHandler, da biblioteca padrãoNenhum
JavaLogback ou Log4j2 com codificador JSONA JVM respeita os limites de memória do container nas versões atuais; confira -XX:MaxRAMPercentage
C#AddJsonConsole(), já incluído no ASP.NETNenhum

Um detalhe que pega muita gente: a aplicação dentro do container precisa escutar em 0.0.0.0, e não em localhost. Escutando em localhost, ela só aceita conexões vindas de dentro do próprio container, e o mapeamento de porta do Docker não funciona.

Ambiente local com Docker Compose

Para desenvolver, o Docker Compose sobe a aplicação e as dependências com um comando:

# compose.yaml
services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://loja:loja@banco:5432/loja
      REDIS_URL: redis://cache:6379
    depends_on:
      banco:
        condition: service_healthy

  banco:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: loja
      POSTGRES_PASSWORD: loja
    volumes:
      - dados:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "loja"]
      interval: 2s
      retries: 15

  cache:
    image: redis:7-alpine

volumes:
  dados:
docker compose up --build

Três detalhes do arquivo:

  • Os serviços se encontram pelo nome: a aplicação conecta em banco:5432, e não em localhost. O Compose cria uma rede com DNS interno.
  • O healthcheck com condition: service_healthy faz a aplicação esperar o banco aceitar conexões, e não apenas o container do banco iniciar.
  • O volume dados guarda os dados do PostgreSQL fora do container, e eles sobrevivem a um docker compose down. Com docker compose down -v, o volume é apagado.

As senhas do exemplo servem só para desenvolvimento local. Em produção, segredos vêm do orquestrador ou de um cofre de segredos, nunca de arquivos versionados.

Lista de revisão

Construção

  • A imagem usa construção em estágios, sem ferramentas de construção na imagem final?
  • As dependências são instaladas antes da cópia do código, com o arquivo de trava?
  • Existe um .dockerignore que exclui .git, dependências locais, testes e .env?
  • A imagem base tem versão fixada?
  • Nenhum segredo está em nenhuma camada da imagem?

Execução

  • O processo roda com um usuário sem privilégios?
  • O CMD ou ENTRYPOINT está em forma de lista, ou o script termina com exec?
  • A aplicação escuta em 0.0.0.0?
  • A configuração vem de variáveis de ambiente, e a aplicação falha logo se faltar alguma?
  • Os logs vão para a saída padrão?
  • Dados que precisam sobreviver ficam fora do container?

Publicação

  • Cada imagem tem uma etiqueta imutável, por versão ou commit?
  • As imagens passam por varredura de vulnerabilidades?
  • Existe uma rotina para reconstruir com a base atualizada?

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.

  • Do Dockerfile ingênuo ao de produção

    Quero transformar um Dockerfile simples num Dockerfile de produção, medindo cada melhoria.
    
    Pergunte qual linguagem e framework eu uso.
    
    Vamos em etapas. Em cada uma, me passe os comandos para medir e explique o resultado:
    1. Escreva comigo a versão mais simples, que funciona.
    2. Meça o tamanho da imagem, o tempo de reconstrução depois de mudar uma linha e o tempo do docker stop.
    3. Reordene as instruções para aproveitar o cache e meça de novo.
    4. Troque a imagem base e acrescente construção em estágios, e meça.
    5. Acrescente o .dockerignore e o usuário sem privilégios.
    6. Corrija o CMD para a forma de lista e meça o docker stop de novo.
    7. Rode uma varredura de vulnerabilidades nas duas versões e compare.
    
    No final, monte comigo uma tabela com a evolução de cada número.
  • Ambiente local com Docker Compose

    Quero montar o ambiente de desenvolvimento da minha aplicação com Docker Compose.
    
    Pergunte qual linguagem eu uso e de quais serviços a aplicação depende (banco, cache, fila).
    
    Monte comigo o compose.yaml, explicando cada parte:
    1. O serviço da aplicação, construído a partir do Dockerfile.
    2. Os serviços de dependência, com versões fixadas.
    3. A rede interna e por que a aplicação conecta pelo nome do serviço.
    4. Verificações de saúde e a espera pela dependência pronta, e não só iniciada.
    5. Volumes para os dados do banco.
    6. Recarregamento automático do código durante o desenvolvimento, montando a pasta do projeto.
    
    Explique também a diferença entre usar o Compose para desenvolver e para produção.
  • Preparar a aplicação para rodar em container

    Quero adaptar a minha aplicação para se comportar bem dentro de um container.
    
    Pergunte qual linguagem e framework eu uso.
    
    Explique, com código para o meu caso:
    1. Como ler toda a configuração de variáveis de ambiente e falhar logo se faltar alguma obrigatória.
    2. Como escrever logs estruturados em JSON na saída padrão.
    3. Por que escutar em 0.0.0.0, e não em localhost.
    4. Como respeitar os limites de memória e CPU do container no meu runtime.
    5. Como tratar o SIGTERM para encerrar sem derrubar pedidos.
    6. Onde guardar arquivos que precisam sobreviver ao container.
    
    Termine com uma lista de verificação para eu conferir a aplicação antes do primeiro deploy em container.