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).
networks:
pedidos_net:
driver: bridge2. 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".
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_healthy3. 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é.
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.
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.productiondocker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --buildO compose base, completo, juntando tudo
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: bridgeNote 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.