O Docker Compose é uma ferramenta que permite definir e gerenciar aplicações multi-container de forma declarativa. Em vez de executar vários comandos docker run manualmente, você descreve toda a arquitetura em um arquivo YAML chamado docker-compose.yml e controla o ciclo de vida com comandos simples como docker compose up e docker compose down. Isso simplifica drasticamente o desenvolvimento, testes e implantação de sistemas que dependem de múltiplos serviços, como um backend, um banco de dados e um cache.

Nesta aula, vamos mergulhar nos conceitos fundamentais do Compose, desde a estrutura do arquivo YAML até a gestão de dependências entre serviços. Você verá como construir uma aplicação completa com banco de dados e API, entenderá os casos de uso mais comuns e aprenderá boas práticas para escrever arquivos Compose eficientes e portáveis.

Multi-container

Em aplicações modernas, é raro ter apenas um container. Por exemplo, uma aplicação web pode precisar de um servidor web, um backend, um banco de dados e um serviço de cache. Cada um desses componentes roda em seu próprio container, com sua própria imagem e configuração. Gerenciar esses containers individualmente é trabalhoso e propenso a erros, especialmente quando há dependências entre eles.

O Docker Compose resolve esse problema permitindo que você defina todos os serviços em um único arquivo. Com um comando, você pode criar e iniciar todos os containers, configurar redes entre eles e montar volumes compartilhados. Além disso, o Compose cuida da ordem de inicialização quando você define dependências, garantindo que serviços essenciais, como o banco de dados, estejam prontos antes que o backend comece.

Vamos ilustrar com um exemplo prático. Suponha que você tenha uma aplicação Node.js que usa um banco de dados PostgreSQL. Sem o Compose, você precisaria executar:

docker network create app-network
docker run -d --name db --network app-network -e POSTGRES_PASSWORD=secret postgres
docker run -d --name app --network app-network -p 3000:3000 myapp

Com o Compose, você define tudo em um arquivo e executa apenas docker compose up -d. Isso não só economiza tempo, mas também torna a configuração versionável e reproduzível.

docker-compose.yml

O arquivo docker-compose.yml é o coração do Compose. Ele segue a especificação YAML e contém a definição de serviços, redes e volumes. A estrutura básica é:

version: "3.8"
services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
  db:
    image: postgres:13
    environment:
      POSTGRES_PASSWORD: example

No exemplo acima, definimos dois serviços: web e db. Cada serviço especifica uma imagem e outras configurações, como portas e variáveis de ambiente. A chave version é opcional, mas recomendada para compatibilidade; as versões mais recentes do Compose (2.x) não exigem essa chave, mas é comum incluí-la para documentar a especificação.

Outras configurações comuns incluem:

  • build: para construir a imagem a partir de um Dockerfile.
  • volumes: para montar volumes (bind mounts ou volumes nomeados).
  • networks: para conectar o serviço a redes específicas.
  • depends_on: para definir dependências entre serviços.
  • environment: para variáveis de ambiente.
  • ports: para mapear portas do host para o container.
  • restart: para definir políticas de reinicialização.

Vejamos um exemplo mais completo com build e volumes:

version: "3.8"
services:
  web:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
    environment:
      NODE_ENV: development
  db:
    image: mongo:5
    volumes:
      - db-data:/data/db

volumes:
  db-data:

Aqui, o serviço web é construído a partir do Dockerfile no diretório atual, e o código local é montado em /app para desenvolvimento. O banco de dados usa um volume nomeado db-data para persistir os dados.

Serviços e dependências

Os serviços são a unidade central do Compose. Cada serviço representa um container que será criado a partir de uma imagem ou de um build. Os serviços podem se comunicar entre si através da rede criada automaticamente pelo Compose, usando o nome do serviço como hostname. Por exemplo, se você tem um serviço api e um db, o código da API pode se conectar ao banco usando o hostname db e a porta interna do container.

As dependências entre serviços são definidas com depends_on. Isso garante que o Compose inicie os serviços na ordem correta. Por exemplo:

services:
  api:
    build: ./api
    depends_on:
      - db
  db:
    image: postgres:13

Nesse caso, o serviço db será iniciado antes de api. No entanto, depends_on não espera que o banco esteja pronto para aceitar conexões; ele apenas espera que o container seja iniciado. Para aplicações que exigem que o banco esteja acessível, é comum usar scripts de espera ou health checks. O Compose também suporta a condição service_healthy quando combinado com health checks:

services:
  api:
    build: ./api
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:13
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

Isso garante que o serviço api só seja iniciado depois que o health check do banco passar, tornando a inicialização mais robusta.

Casos de uso

O Docker Compose é amplamente utilizado em diversos cenários. O mais comum é em ambientes de desenvolvimento, onde você precisa de uma pilha completa (banco, cache, filas) sem instalar tudo localmente. Com o Compose, você sobe toda a infraestrutura com um comando e a derruba com outro, mantendo o ambiente limpo.

Outro caso de uso é para testes automatizados. Você pode definir uma pilha de teste com serviços efêmeros e executar seus testes contra ela. O Compose também é usado em pipelines de CI/CD para criar ambientes isolados para cada execução.

Além disso, o Compose é útil para aplicações em produção de pequeno e médio porte, embora para grandes escalas seja comum usar orquestradores como Kubernetes. Mesmo assim, o Compose é uma ferramenta valiosa para prototipar e para implantações simples.

Vejamos um exemplo de caso de uso completo: uma aplicação web com Node.js, banco PostgreSQL e nginx como proxy reverso.

version: "3.8"
services:
  app:
    build: ./app
    environment:
      DB_HOST: db
      DB_USER: postgres
      DB_PASSWORD: secret
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:13
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - app

volumes:
  db-data:

Nesse exemplo, o nginx atua como proxy reverso para o serviço app, que se conecta ao banco. O volume db-data garante a persistência dos dados do banco.

Boas práticas

Ao trabalhar com Docker Compose, algumas boas práticas podem melhorar a eficiência e a segurança:

  • Use nomes de serviços descritivos e em minúsculas.
  • Prefira volumes nomeados para dados persistentes, em vez de bind mounts, a menos que seja para desenvolvimento.
  • Especifique versões de imagens (tags) exatas, evitando latest em produção.
  • Utilize variáveis de ambiente com arquivos .env para configurar diferentes ambientes.
  • Separe os arquivos Compose por ambiente (ex.: docker-compose.dev.yml e docker-compose.prod.yml) ou use overrides.
  • Inclua health checks para serviços críticos e use depends_on com condições.
  • Mantenha os serviços o mais enxutos possível, usando imagens oficiais e otimizadas.

Referências

Exercícios

  1. Crie um arquivo docker-compose.yml que defina dois serviços: um serviço web usando a imagem nginx:alpine e um serviço redis usando a imagem redis:alpine. O serviço web deve mapear a porta 8080 do host para a porta 80 do container, e o serviço redis deve expor a porta 6379.

    ✓ Resposta:
    version: "3.8"
    services:
      web:
        image: nginx:alpine
        ports:
          - "8080:80"
      redis:
        image: redis:alpine
        ports:
          - "6379:6379"
  2. No arquivo Compose do exercício anterior, adicione uma dependência para que o serviço web dependa do serviço redis. Explique o que isso garante.

    ✓ Resposta: Adicione depends_on: [redis] ao serviço web. Isso garante que o container redis seja iniciado antes do web, mas não espera que o Redis esteja pronto para conexões. Para esperar a prontidão, seria necessário usar health checks.
  3. Defina um serviço api que seja construído a partir de um Dockerfile no diretório ./api. O serviço deve montar o diretório local ./api em /app (bind mount) e definir a variável de ambiente NODE_ENV=development.

    ✓ Resposta:
    version: "3.8"
    services:
      api:
        build: ./api
        volumes:
          - ./api:/app
        environment:
          NODE_ENV: development
  4. Crie um serviço db usando a imagem postgres:13 com um volume nomeado pgdata montado em /var/lib/postgresql/data. Defina a variável de ambiente POSTGRES_PASSWORD=secret. Declare o volume na seção de volumes do arquivo.

    ✓ Resposta:
    version: "3.8"
    services:
      db:
        image: postgres:13
        environment:
          POSTGRES_PASSWORD: secret
        volumes:
          - pgdata:/var/lib/postgresql/data
    
    volumes:
      pgdata:
  5. Escreva um comando para iniciar todos os serviços em segundo plano a partir de um arquivo Compose chamado docker-compose.yml e um comando para parar e remover os containers, redes e volumes definidos no arquivo.

    ✓ Resposta: Para iniciar em segundo plano: docker compose up -d. Para parar e remover tudo (incluindo volumes): docker compose down -v. O -v remove volumes nomeados; sem ele, os volumes são preservados.