O problema que o Compose resolve

Uma aplicação real quase nunca é um container só. Uma API web precisa de um banco de dados, o banco precisa de um volume para os dados sobreviverem a um reinício, e depois você adiciona um cache Redis e um worker em segundo plano. São quatro comandos docker run, cada um com suas flags de portas, volumes, variáveis de ambiente e uma rede compartilhada. Na ordem certa. Toda vez que você senta para trabalhar.

O Docker Compose troca esses comandos por um arquivo e um comando. Você descreve os containers e como eles se conectam em um arquivo chamado compose.yaml, e docker compose up sobe todos. Se você ainda não mexeu com imagens e docker run, comece por o que é Docker e como os containers funcionam.

O que o arquivo compose descreve

Um arquivo compose tem um punhado de chaves de primeiro nível. A que você sempre usa é services: cada serviço é um container que o Compose vai executar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:18
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  db-data:

Dois containers. api é construído a partir do Dockerfile do diretório atual e publica a porta 8000. db executa a imagem oficial postgres:18 e guarda os dados em um volume nomeado, então os dados continuam lá quando o container é recriado.

Dois detalhes fazem o trabalho de verdade aqui:

  • api alcança db pelo hostname db. O Compose coloca cada serviço em uma rede compartilhada e registra o nome de cada serviço como nome DNS. Sem endereços IP, sem o antigo --link. DATABASE_URL aponta para db:5432 e o Compose resolve o nome.
  • depends_on com condition: service_healthy segura api até o Postgres responder de verdade. Um depends_on simples só espera o container iniciar, não o processo do banco lá dentro aceitar conexões: essa diferença é a causa mais comum de um “connection refused” no primeiro boot.

As três chaves de primeiro nível: services, volumes, networks

Três chaves de primeiro nível correspondem a conceitos de Docker que você já conhece:

ChaveO que defineEquivalente manual
servicesos containers a executardocker run
volumesvolumes nomeados para dados que precisam persistirdocker volume create
networksredes entre serviçosdocker network create

Você raramente declara networks. O Compose cria uma rede por projeto e conecta cada serviço a ela, e é por isso que o exemplo com Postgres não precisou de nenhuma configuração de rede. Declare redes explicitamente só quando quiser impedir que grupos de serviços se enxerguem:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
services:
  api:
    build: .
    networks: [frontend, backend]
  db:
    image: postgres:18
    networks: [backend]        # not reachable from frontend
  proxy:
    image: caddy:2
    networks: [frontend]

networks:
  frontend:
  backend:

Os comandos que você vai usar de verdade

Rode estes comandos a partir do diretório que contém o compose.yaml.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Sobe tudo, com os logs no terminal
docker compose up

# Sobe em segundo plano
docker compose up -d

# Reconstrói as imagens que têm seção build e depois sobe
docker compose up --build

# Para e remove containers e redes (os volumes nomeados ficam)
docker compose down

# Igual, e ainda apaga os volumes nomeados
docker compose down -v

# O que está rodando neste projeto
docker compose ps

# Acompanha os logs de um serviço
docker compose logs -f api

# Comando pontual em um container novo
docker compose run --rm api python manage.py migrate

# Shell em um container que já está rodando
docker compose exec api bash

up e down são os comandos que você mais digita. Dá para rodar up de novo quantas vezes quiser: depois de editar o arquivo, ele recria só os serviços cuja configuração mudou e não mexe no resto.

O que docker compose up imprime no primeiro boot

Execute o exemplo com Postgres acima e você recebe algo assim:

1
2
3
4
5
[+] Running 4/4
 ✔ Network app_default    Created
 ✔ Volume "app_db-data"   Created
 ✔ Container app-db-1      Healthy
 ✔ Container app-api-1     Started

O Compose põe o nome do projeto na frente de cada nome (por padrão, o nome do diretório) e um número no fim, porque ele já está preparado para rodar mais de uma réplica do mesmo serviço. app-db-1 fica Healthy antes de app-api-1 subir — é o condition: service_healthy fazendo o trabalho dele.

Como manter as senhas fora do arquivo compose

O Compose lê um arquivo chamado .env no diretório do projeto e substitui as referências ${VAR} no arquivo compose:

1
2
3
4
5
services:
  db:
    image: postgres:18
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
1
2
# .env  — add this file to .gitignore
DB_PASSWORD=secret

Mantenha o .env fora do controle de versão e, no lugar dele, versione um .env.example com valores vazios ou fictícios. Para algo realmente sensível num ambiente em produção, use Docker secrets em vez de variáveis de ambiente.

Serviços opcionais com profiles

Nem todo serviço precisa subir toda vez. Uma entrada profiles mantém um serviço inativo até você pedir:

1
2
3
4
5
6
7
8
9
services:
  api:
    build: .
  db:
    image: postgres:18
  seed:
    build: .
    command: python manage.py seed_demo_data
    profiles: [tools]

docker compose up sobe api e db e ignora seed por completo. docker compose --profile tools run --rm seed executa o seeder quando você pede. Use profiles para tudo que é pontual: seeders, migrações, um shell de depuração, um gerador de carga que você só quer durante um teste.

Compose v2 vs o antigo docker-compose

Se um guia manda você executar docker-compose com hífen, ele é anterior a 2023. Aquela era a v1, escrita em Python, hoje sem suporte. A ferramenta atual é docker compose, um subcomando da CLI do Docker; já vem no Docker Desktop e no pacote do plugin Compose para o Docker Engine.

Arquivos antigos costumam trazer esta linha no topo:

1
version: "3.8"   # delete it

A chave version não tem efeito nenhum no Compose v2. O Compose valida contra a Compose Specification atual e avisa quando a chave está presente. Remova e o aviso some.

O nome padrão do arquivo também mudou: compose.yaml é o atual, docker-compose.yml ainda funciona, e o Compose procura pelos dois.

Live reload durante o desenvolvimento

docker compose watch (Compose 2.22 em diante) atualiza os containers enquanto você edita. Adicione um bloco develop ao serviço:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  api:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: ./requirements.txt

sync copia os arquivos alterados direto para o container em execução, então uma mudança de código aparece sem rebuild. rebuild dispara um rebuild completo da imagem quando muda um arquivo que afeta o build, como um lockfile de dependências. Inicie com docker compose watch, ou adicione --watch ao up.

Quando o Compose é a ferramenta errada

O Compose executa containers em uma máquina só. Esse é o limite, e a maior parte do que falta no Compose está do outro lado dele.

Ele serve para desenvolvimento local, testes automatizados em CI e deploys pequenos em um único host. Ele não sabe distribuir containers por vários servidores, substituir um quando um nó cai, fazer rollouts graduais condicionados aos healthchecks, nem autoescalar.

Isso é trabalho de um orquestrador: Kubernetes, ou Docker Swarm se você quer algo mais leve. Os dois não competem. Um arquivo compose costuma ser o rascunho que depois vira um conjunto de manifests do Kubernetes, e muitos times seguem usando o Compose localmente bem depois de a produção ter migrado para um cluster.

Quem usa Podman também tem um caminho compatível: podman compose e podman-compose leem o mesmo formato de arquivo, com as ressalvas cobertas em Docker vs Podman.

Erros comuns

  • Segredos commitados no compose.yaml. O arquivo vai para o Git. Use .env (no gitignore) ou Docker secrets.
  • Confiar no depends_on para esperar um serviço ficar pronto. Sozinho, ele espera o container iniciar, não o serviço aceitar conexões. Combine com um healthcheck e condition: service_healthy.
  • Definir container_name. Isso impede você de rodar mais de uma cópia do projeto e quebra docker compose up --scale. Deixe o Compose nomear os containers.
  • Fazer bind mount por cima de um diretório de dependências instaladas. Montar a pasta do projeto num container Node ou Python pode mascarar o node_modules ou o virtualenv criado durante o build. Monte subpastas do código, ou coloque um volume anônimo no caminho das dependências.

Como passar uma app existente para o Compose

Pegue a app que você hoje inicia com um script cheio de linhas docker run e mova para um compose.yaml um serviço de cada vez. Suba, leia os logs, conserte o que quebrar, adicione o serviço seguinte. Dê um healthcheck a tudo de que outros serviços dependem: é o passo que todo mundo pula, e é o que corta o “connection refused” num boot a frio.

Você vai saber que deu certo quando docker compose down -v && docker compose up reconstruir o ambiente inteiro do zero e a app voltar igual todas as vezes.

Artigos relacionados