DEV Community

Cover image for Docker Compose - orquestrando múltiplos containers
Rafael Dutra for apsis-cc

Posted on

Docker Compose - orquestrando múltiplos containers

1. Retomando: do docker run repetido a um arquivo único

No artigo anterior, subir uma API e um Postgres conectados exigiu dois comandos docker run longos, com flags de rede, volume e variáveis de ambiente para lembrar (e digitar) toda vez. Em um projeto real, com mais serviços — cache, fila, worker em background — isso rapidamente vira inviável de manter na cabeça ou em um script solto. O Docker Compose resolve isso descrevendo toda a aplicação multi-container em um único arquivo declarativo, versionado junto com o código.

2. O arquivo compose.yaml

Compose lê um arquivo YAML (por convenção compose.yaml, ou o nome legado docker-compose.yml, ainda amplamente usado) descrevendo serviços (cada um vira um ou mais containers), redes e volumes:

# compose.yaml
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://postgres:segredo@banco:5432/postgres
    depends_on:
      - banco

  banco:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: segredo
    volumes:
      - pg-dados:/var/lib/postgresql/data

volumes:
  pg-dados:
Enter fullscreen mode Exit fullscreen mode

Isso substitui inteiramente os dois docker run do artigo anterior. Uma diferença importante já aparece aqui: por padrão, Compose cria uma rede própria para o projeto e conecta todos os serviços a ela automaticamente — não é preciso um docker network create manual, nem declarar --network em cada serviço. Cada serviço já é acessível pelos demais pelo nome declarado em services: (aqui, banco resolve para o container do Postgres), exatamente como as redes definidas pelo usuário do artigo anterior.

3. Comandos essenciais do Compose

docker compose up -d          # sobe todos os serviços em segundo plano
docker compose ps              # lista os containers do projeto e seu status
docker compose logs -f api     # segue os logs de um serviço específico
docker compose logs -f         # segue os logs de todos os serviços, intercalados
docker compose exec api bash   # abre um shell dentro do container de um serviço
docker compose stop            # para os containers sem removê-los
docker compose down            # para e remove containers e rede (volumes nomeados sobrevivem)
docker compose down -v         # remove também os volumes — cuidado, apaga dados
Enter fullscreen mode Exit fullscreen mode

O padrão docker compose <comando> <serviço> se repete: a maioria dos comandos aceita opcionalmente o nome de um serviço específico (como em logs -f api) ou afeta o projeto inteiro se omitido.

4. depends_on: ordem de início, não prontidão

depends_on controla a ordem em que os containers são criados e iniciados — no exemplo acima, banco inicia antes de api. O que ele não garante é que o Postgres já esteja pronto para aceitar conexões nesse momento: o processo do Postgres pode levar alguns segundos para inicializar depois que o container começa a rodar, e uma API que tenta conectar imediatamente pode falhar nessa janela.

A forma correta de esperar prontidão de verdade é um healthcheck:

services:
  api:
    build: .
    depends_on:
      banco:
        condition: service_healthy

  banco:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: segredo
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
Enter fullscreen mode Exit fullscreen mode

Com condition: service_healthy, o Compose só considera banco "pronto" (e só então inicia api) depois que o healthcheck passar — muito mais confiável do que assumir um tempo fixo de espera ou implementar retry manual na aplicação (embora ter retry na aplicação continue sendo uma boa prática defensiva, mesmo com healthcheck).

5. Variáveis de ambiente e o arquivo .env

Segredos e configuração que variam por ambiente (senha de banco, chaves de API) não deveriam ficar hardcoded no compose.yaml. Compose lê automaticamente um arquivo .env no mesmo diretório e substitui variáveis referenciadas com ${NOME}:

# .env
POSTGRES_PASSWORD=segredo-local
API_PORT=8000
Enter fullscreen mode Exit fullscreen mode
services:
  api:
    build: .
    ports:
      - "${API_PORT}:8000"
  banco:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
Enter fullscreen mode Exit fullscreen mode

Isso permite versionar o compose.yaml normalmente enquanto o .env (que contém segredos reais) fica de fora do controle de versão via .gitignore — o mesmo princípio de nunca commitar credenciais, aplicado à configuração do Compose.

6. Um ambiente completo: app + banco + cache

Estendendo o exemplo para um cenário mais realista, com Redis como cache além da API e do Postgres:

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@banco:5432/postgres
      REDIS_URL: redis://cache:6379
    depends_on:
      banco:
        condition: service_healthy
      cache:
        condition: service_started

  banco:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pg-dados:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    volumes:
      - redis-dados:/data

volumes:
  pg-dados:
  redis-dados:
Enter fullscreen mode Exit fullscreen mode

Um docker compose up -d sobe os três serviços, na ordem certa, já conectados entre si e com dados persistentes — o equivalente a uma infraestrutura local completa de desenvolvimento, reproduzível em qualquer máquina que tenha Docker instalado, sem instalar Postgres ou Redis diretamente no sistema.

7. Escalando um serviço

Para rodar múltiplas instâncias do mesmo serviço (por exemplo, vários workers processando uma fila em paralelo):

docker compose up -d --scale worker=3
Enter fullscreen mode Exit fullscreen mode

Isso só funciona sem conflito para serviços que não publicam uma porta fixa do host (ports:) — três containers não podem, todos, mapear a porta 8000 do host simultaneamente. Serviços pensados para escalar dessa forma tipicamente não expõem porta ao host diretamente, sendo acessados por outro serviço (como um proxy reverso) através da rede interna do Compose.

8. Conclusão e próximos passos

Docker Compose transforma uma coleção de comandos docker run frágeis e difíceis de lembrar em um arquivo único, declarativo e versionável, que descreve toda a aplicação — serviços, rede, volumes, ordem de inicialização e variáveis de ambiente. No último artigo desta série, o assunto vai para práticas de nível de produção: multi-stage builds para reduzir drasticamente o tamanho final da imagem, segurança (rodar sem root, scan de vulnerabilidades) e como tudo isso se encaixa em um pipeline de CI/CD.


Imagem de capa: Logo oficial do Docker — Wikimedia Commons, fonte: docker.com/company/newsroom/media-resources

Referências:

  1. Docker Documentation — Compose
  2. Compose File Reference

Top comments (0)