Aprenda / Infraestrutura — Docker

IntermediárioAula 16 de 1624 min0% da trilha

Projeto guiado: preparando o compose pra produção

O compose do projeto de fundamentos sobe local igualzinho a produção — mas produção de verdade pede rede isolada, healthcheck, limites e um override próprio.

No projeto guiado de fundamentos, a API de Pedidos (Rails), a tela de Tarefas (Next.js) e o Postgres subiram juntos com um docker-compose.yml funcional. Funcional não é o mesmo que pronto pra produção. Aqui, esse mesmo compose evolui incorporando tudo que o intermediário cobriu: rede customizada isolando o ambiente, healthcheck em cada serviço, limites de recursos e um arquivo de override específico pra produção.

1. Rede customizada isolando o ambiente

Em vez de depender só da rede padrão que o Compose cria automaticamente pro projeto, declaramos uma rede nomeada — deixa explícito que esses três serviços formam um ambiente isolado, e facilita conectar outro projeto a ela no futuro, se for preciso (lição de redes customizadas).

docker-compose.yml (rede)
networks:
  pedidos_net:
    driver: bridge

2. Healthcheck em cada serviço, e depends_on esperando de verdade

O compose original usava depends_on só pra ordem de inicialização. Agora, api só sobe depois que db está de fato aceitando conexões, e web só sobe depois que api está respondendo — não só "iniciado".

docker-compose.yml (healthcheck + depends_on)
services:
  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5

  api:
    build: ./api
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      retries: 3

  web:
    build: ./web
    depends_on:
      api:
        condition: service_healthy

3. Limites de recursos

Cada serviço ganha um teto de memória e CPU — se algum deles vazar memória, só ele é derrubado pelo OOM killer, o resto do ambiente continua de pé.

docker-compose.yml (limites)
services:
  api:
    mem_limit: 512m
    cpus: "0.5"

  web:
    mem_limit: 512m
    cpus: "0.5"

  db:
    mem_limit: 1g
    cpus: "1.0"

4. docker-compose.prod.yml — o override de produção

Ajustes que só fazem sentido em produção (restart policy, variáveis de produção, sem bind mount de código) ficam num arquivo separado, combinado na hora do deploy — sem misturar com o que é usado localmente.

docker-compose.prod.yml
services:
  api:
    restart: unless-stopped
    env_file: .env.production

  web:
    restart: unless-stopped
    env_file: .env.production
    environment:
      NEXT_PUBLIC_API_URL: https://api.exemplo.com

  db:
    restart: unless-stopped
    env_file: .env.production
terminal — deploy
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build

O compose base, completo, juntando tudo

docker-compose.yml (final)
services:
  web:
    build: ./web
    ports:
      - "3000:3000"
    environment:
      NEXT_PUBLIC_API_URL: http://localhost:3001
    depends_on:
      api:
        condition: service_healthy
    networks:
      - pedidos_net
    mem_limit: 512m
    cpus: "0.5"

  api:
    build: ./api
    ports:
      - "3001:3000"
    environment:
      DATABASE_URL: postgres://postgres:postgres@db:5432/app_production
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      retries: 3
    networks:
      - pedidos_net
    mem_limit: 512m
    cpus: "0.5"

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - pedidos_net
    mem_limit: 1g
    cpus: "1.0"

volumes:
  postgres_data:

networks:
  pedidos_net:
    driver: bridge

Note o que não mudou em relação ao projeto de fundamentos: db continua sem ports: (só a rede interna precisa alcançá-lo), NEXT_PUBLIC_API_URL continua usando localhost porque quem chama é o navegador. O que evoluiu foi tudo que protege o ambiente de um problema real em produção: rede isolada, espera de verdade por dependência saudável, e um teto pra cada serviço.

Exercício

O time quer adicionar um Redis pra cache, só usado pela api, seguindo os mesmos padrões deste compose: rede pedidos_net, healthcheck, limite de memória de 256MB e sem porta publicada (nada de fora precisa acessá-lo diretamente). Escreva o serviço.

Quiz rápido

Por que colocar api, web e db na mesma rede customizada (pedidos_net) no compose de produção?
Nesse projeto, por que api depende de db com condition: service_healthy em vez de um depends_on simples?