Docker Compose é uma ferramenta essencial para orquestrar aplicações multi-container. Em vez de gerenciar cada container individualmente com comandos docker run, o Compose permite definir toda a infraestrutura em um único arquivo YAML, chamado docker-compose.yml, e gerenciá-la com comandos simples como docker-compose up e docker-compose down. Esta aula aborda desde os conceitos fundamentais até casos de uso avançados, capacitando você a estruturar ambientes complexos com eficiência.

Dominar o Docker Compose é crucial para qualquer profissional de DevOps, pois simplifica o desenvolvimento local, padroniza ambientes e facilita a integração contínua. Vamos explorar como definir serviços, conectar containers, gerenciar dependências e aplicar boas práticas para criar soluções robustas e escaláveis.

Multi-container

No mundo real, aplicações raramente são compostas por um único container. Um sistema web típico pode incluir um servidor web, um backend, um banco de dados, um cache, um fila de mensagens, entre outros. Gerenciar cada um separadamente é trabalhoso e propenso a erros, especialmente quando precisamos configurar redes, volumes e variáveis de ambiente.

O Docker Compose resolve esse problema permitindo que você defina todos os serviços em um único arquivo. Ele cria uma rede padrão para os containers, permitindo que eles se comuniquem por nome do serviço, e gerencia o ciclo de vida de todos juntos. Isso facilita a replicação de ambientes, o versionamento da infraestrutura e a colaboração entre equipes.

Por exemplo, um simples stack com Nginx, Node.js e PostgreSQL pode ser definido e iniciado com apenas um comando. Sem o Compose, você precisaria executar três comandos docker run com várias flags, além de configurar manualmente a rede. Com o Compose, tudo fica declarativo e versionável.

# Sem Docker Compose
sudo docker run -d --name db -e POSTGRES_USER=user -e POSTGRES_PASSWORD=pass postgres:15
docker network create mynet
docker network connect mynet db
docker run -d --name backend --network mynet -e DB_HOST=db mybackend:latest
docker run -d --name frontend --network mynet -p 80:80 nginx:alpine

# Com Docker Compose
# docker-compose.yml define tudo, e então:
docker-compose up -d

docker-compose.yml

O arquivo docker-compose.yml é o coração do Docker Compose. Ele segue a especificação YAML e contém a definição dos serviços, redes, volumes e outras configurações. A estrutura básica começa com a chave version (embora em versões recentes seja opcional) e a chave services.

Dentro de services, cada serviço é identificado por um nome e recebe configurações como image, build, ports, environment, volumes, depends_on, entre outras. É importante conhecer as principais diretivas para criar arquivos eficientes.

Vejamos um exemplo simples de docker-compose.yml para uma aplicação web com banco de dados:

version: "3.9"

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html
    depends_on:
      - api

  api:
    build: ./api
    environment:
      - DB_HOST=db
      - DB_USER=user
      - DB_PASSWORD=pass
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

Neste arquivo, definimos três serviços: web (Nginx), api (construído a partir de um Dockerfile em ./api) e db (PostgreSQL). O serviço web publica a porta 8080 e monta um diretório local. O api recebe variáveis de ambiente e depende do db. O volume db_data é nomeado para persistir os dados do banco.

Comandos úteis: docker-compose up -d para iniciar em segundo plano, docker-compose down para parar e remover, docker-compose ps para listar serviços, docker-compose logs para ver logs.

Serviços e dependências

Cada serviço no Compose representa um container. A diretiva depends_on controla a ordem de inicialização e encerramento. Se um serviço depende de outro, o Compose inicia o dependido primeiro e para o dependente primeiro no shutdown. No entanto, depends_on não espera o serviço estar pronto para receber conexões; ele apenas espera o container iniciar. Para garantir que um serviço esteja saudável, é comum usar health checks ou scripts de espera.

Por exemplo, um backend que depende do banco de dados pode precisar aguardar o PostgreSQL aceitar conexões. Podemos usar a opção condition: service_healthy se definirmos um healthcheck no serviço do banco. Isso garante que o backend só inicie quando o banco estiver saudável.

services:
  db:
    image: postgres:15
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "user"]
      interval: 5s
      timeout: 5s
      retries: 5

  api:
    build: .
    depends_on:
      db:
        condition: service_healthy

Além de depends_on, você pode usar a diretiva links (legado), mas é recomendado usar redes personalizadas. Por padrão, o Compose cria uma rede para o projeto, e os serviços se comunicam pelo nome do serviço. Você também pode definir redes customizadas para isolar grupos de serviços.

Outros aspectos importantes: environment define variáveis de ambiente, env_file carrega de um arquivo, volumes para persistência e compartilhamento, networks para conectar a redes específicas, ports para expor portas, e build para construir a imagem a partir de um Dockerfile.

Casos de uso

O Docker Compose é amplamente utilizado em vários cenários. Um dos mais comuns é o desenvolvimento local: com um único arquivo, você sobe toda a stack necessária (banco, cache, fila, etc.) e replica o ambiente de produção. Isso garante consistência entre máquinas de desenvolvedores.

Outro caso é a integração contínua: pipelines podem usar Docker Compose para subir serviços de teste e executar a suíte de testes. Ferramentas como GitHub Actions, GitLab CI e Jenkins podem invocar docker-compose up para preparar o ambiente.

Também é útil para demonstrações e prototipagem rápida: você pode criar um ambiente completo com poucas linhas e compartilhar com colegas. Em produção, embora não seja recomendado para grandes orquestrações, pode ser usado em servidores únicos ou pequenos deploys.

Exemplo de um caso de uso com múltiplos serviços: uma aplicação que consiste em um frontend (React), um backend (Node.js), um banco (PostgreSQL), um cache (Redis) e um worker (Python). O Compose permite subir tudo com um comando e escalar serviços individuais com docker-compose up --scale worker=3.

services:
  frontend:
    build: ./frontend
    ports: ["3000:3000"]
  backend:
    build: ./backend
    ports: ["5000:5000"]
    environment:
      - DB_HOST=db
      - REDIS_HOST=redis
  db:
    image: postgres:15
  redis:
    image: redis:7
  worker:
    build: ./worker
    depends_on:
      - db
      - redis

Boas práticas

Ao trabalhar com Docker Compose, algumas práticas melhoram a robustez e a segurança. Use volumes nomeados para dados persistentes, evite montar o diretório inteiro do projeto em produção, e prefira imagens oficiais quando possível. Defina health checks para dependências críticas e use depends_on com condição para garantir a prontidão.

Separe configurações por ambiente usando múltiplos arquivos: docker-compose.yml como base, docker-compose.override.yml para desenvolvimento (já usado por padrão) e docker-compose.prod.yml para produção. Utilize variáveis de ambiente com ${VAR} e um arquivo .env para evitar segredos no código.

Mantenha os serviços com responsabilidade única e evite colocar múltiplos processos em um container. Documente os comandos principais no README do projeto. E lembre-se de versionar o arquivo docker-compose.yml para que toda a equipe use a mesma configuração.

Referências

Exercícios

  1. Exercício 1: Crie um arquivo docker-compose.yml para uma aplicação que tenha um serviço web usando a imagem nginx:alpine e um serviço db usando postgres:15. O serviço web deve publicar a porta 8080 para a porta 80 do container. O banco deve ter um volume nomeado pgdata para persistência.

✓ Resposta:
version: "3.9"

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"

  db:
    image: postgres:15
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
  1. Exercício 2: No arquivo do exercício anterior, adicione um serviço backend que construa a imagem a partir do diretório ./backend e que defina variáveis de ambiente para conectar ao banco (host, usuário, senha). Use depends_on para garantir que o banco inicie antes.

✓ Resposta:
version: "3.9"

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"

  backend:
    build: ./backend
    environment:
      - DB_HOST=db
      - DB_USER=user
      - DB_PASSWORD=pass
    depends_on:
      - db

  db:
    image: postgres:15
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
  1. Exercício 3: Explique a diferença entre depends_on simples e usar health check com condition: service_healthy. Dê um exemplo de quando é necessário.

✓ Resposta: O depends_on simples apenas aguarda o container ser iniciado, não que o serviço esteja pronto para aceitar conexões. Com health check, o Compose aguarda o serviço estar saudável, garantindo que dependências como banco de dados estejam prontas. Isso é necessário quando o serviço dependente tenta se conectar imediatamente e falha se o banco ainda não estiver pronto.
  1. Exercício 4: Escreva um comando para escalar um serviço chamado worker para 3 instâncias usando Docker Compose.

✓ Resposta: docker-compose up --scale worker=3
  1. Exercício 5: Crie um docker-compose.yml para uma aplicação que use Redis como cache e um serviço app que depende do Redis. Adicione um health check no Redis e use condition: service_healthy no depends_on do serviço app.

✓ Resposta:
version: "3.9"

services:
  redis:
    image: redis:7
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  app:
    build: ./app
    depends_on:
      redis:
        condition: service_healthy