Dia 22: Docker Compose — Multi-Service Stacks e Orquestração Básica
No Dia 21 cobrimos o Docker em isolado: construir imagens, gerir volumes e ligar containers com redes. Mas um stack real não é um container — é uma app web que precisa de uma base de dados, um cache e um reverse proxy. Ligar quatro containers com docker run na mão produz comandos de 200 caracteres que ninguém consegue reproduzir.
O Docker Compose resolve isso: descreve toda a stack num ficheiro YAML declarativo e sobe os serviços com um único comando. Este guia cobre a versão V2 do plugin docker compose (subcomando nativo do Docker CLI), a sintaxe do docker-compose.yml, redes e volumes declarativos, profiles para ambientes selectivos e os erros que aparecem quando se migra do docker run manual.
Neste artigo
- Introdução: docker compose vs docker run
- Instalar Docker Compose
- docker-compose.yml: serviços, redes, volumes e environment
- Comandos: up, down, logs, exec e profiles
- Erros Comuns
Introdução: docker compose vs docker run
O docker run trata de um container de cada vez. Para ligar três containers — app, base de dados e cache — é preciso criar a rede manualmente, dar nomes aos containers, passar variáveis de ambiente em flags longas e garantir a ordem de arranque. O comando fica assim:
docker network create app-net
docker run -d --name db --network app-net \
-e POSTGRES_PASSWORD=secret \
-v db-data:/var/lib/postgresql/data \
postgres:16
docker run -d --name redis --network app-net \
redis:7-alpine
docker run -d --name app --network app-net \
-e DATABASE_URL=postgres://user:secret@db:5432/myapp \
-e REDIS_URL=redis://redis:6379 \
-p 8080:3000 \
myapp:latest
Se o servidor reiniciar, ou um colega precisar de reproduzir o ambiente, esses comandos desaparecem — só existem no histórico do terminal. O Docker Compose resolve o problema ao descrever toda a stack num ficheiro docker-compose.yml versionado no Git. Um comando sobe tudo, na ordem certa, com redes e volumes declarados.
A diferença fundamental: docker run é imperativo (diz o que fazer); docker compose é declarativo (diz o estado desejado). O ficheiro YAML é a fonte de verdade: se um container crasha, o up recria-o com a configuração definida no ficheiro, não com a que estava em memória (Docker Compose documentation).
docker compose (sem hífen) integrado no Docker CLI. O Python docker-compose V1 (com hífen) está deprecated desde Junho de 2023 e removido em versões recentes. A partir daqui, todos os comandos usam docker compose (sem hífen).
Instalar Docker Compose
O Docker Compose V2 vem como plugin no pacote docker-ce na maioria das distribuições. Se o Docker já está instalado (como vimos no Dia 21), o Compose já deve estar disponível. Verificar:
docker compose version
Se devolver Docker Compose version v2.x, está pronto. Se o comando não existe, instalar o plugin:
Ubuntu/Debian (repositório oficial Docker):
apt install docker-compose-plugin
Rocky/Alma/RHEL/Fedora:
dnf install docker-compose-plugin
Instalação manual (binário único, qualquer distro):
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m) \
-o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
A instalação manual coloca o binário em ~/.docker/cli-plugins/ para o utilizador corrente. Para sistema, usar /usr/libexec/docker/cli-plugins/ em RHEL ou /usr/lib/docker/cli-plugins/ em Debian (Install Docker Compose).
docker compose (espaço, sem hífen). Se ainda tens scripts com docker-compose (hífen), criar um alias: alias docker-compose='docker compose' em ~/.bashrc.
docker-compose.yml: serviços, redes, volumes e environment
O ficheiro docker-compose.yml é a espinha dorsal do Compose. Define serviços (containers), redes (isolamento L2), volumes (persistência) e environment (configuração). A versão da sintaxe deixou de ser obrigatória — o campo version: foi removido na especificação actual do Compose e é ignorado se presente.
Exemplo completo — stack web com PostgreSQL, Redis e app Node.js:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: myapp
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
networks:
- backend
app:
build: .
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
environment:
DATABASE_URL: postgres://myapp:${DB_PASSWORD}@db:5432/myapp
REDIS_URL: redis://redis:6379
ports:
- "8080:3000"
volumes:
- ./src:/app/src
networks:
- backend
- frontend
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
networks:
- frontend
profiles:
- production
volumes:
db-data:
networks:
backend:
driver: bridge
frontend:
driver: bridge
Serviços
Cada chave sob services: é o nome do serviço — esse nome torna-se o hostname DNS dentro das redes do Compose. O serviço db é reachável como db a partir do app. Não há IP para decorar — o DNS interno do Docker resolve os nomes dos serviços automaticamente.
Os campos mais usados: image para imagens prontas do registry, build para construir a partir de um Dockerfile, ports para mapear portas host:container, depends_on para ordem de arranque e restart para a política de restart automático.
depends_on e healthchecks
O depends_on sozinho só garante a ordem de arranque — não espera que o serviço esteja pronto para receber ligações. Para esperar de verdade, combinar com healthcheck e usar condition: service_healthy. No exemplo acima, o app só arranca depois do pg_isready do PostgreSQL devolver sucesso. Sem isto, a app tenta ligar à BD antes de ela estar pronta e crasha em loop (Compose service reference).
Redes
As redes definidas no topo criam bridge networks isoladas. No exemplo, db e redis estão na rede backend, o nginx na frontend e o app em ambas — funciona como bridge entre as duas. O nginx não consegue chegar ao db directamente, o que é exactamente o pretendido: o reverse proxy só fala com a app, nunca com a base de dados.
Se nenhuma rede for declarada, o Compose cria uma rede predefinida com o nome do projecto (nome do directório em minúsculas) e liga todos os serviços a ela. Declarar redes explícitas é boa prática para micro-segmentação.
Volumes
Há dois tipos: named volumes (declarados no topo, geridos pelo Docker, persistem entre down/up) e bind mounts (caminhos do hospedeiro montados directamente, úteis em desenvolvimento para hot-reload). No exemplo, db-data é um named volume para os dados do PostgreSQL; ./src:/app/src é um bind mount que monta o código-fonte do host no container para desenvolvimento. O sufixo :ro no volume do nginx torna a montagem read-only (Compose volumes reference).
Environment
Há três formas de passar variáveis de ambiente: environment: inline no YAML, env_file: para carregar de um ficheiro .env e a interpolação ${VAR} que lê do ambiente do shell ou de um ficheiro .env no mesmo directório. O .env é automaticamente lido pelo Compose — não precisa de ser referenciado:
# .env (na mesma pasta do docker-compose.yml)
DB_PASSWORD=minha-senha-secreta
NODE_ENV=production
PORT=3000
A interpolação ${DB_PASSWORD} no YAML é substituída pelo valor do .env. Isto evita credenciais no ficheiro versionado — adicionar .env ao .gitignore e commitar apenas um .env.example com valores de placeholder (Environment variables in Compose).
.env com credenciais reais. O Compose lê-o automaticamente, mas o Git não deve vê-lo. Usar .env.example versionado e .env no .gitignore.
Comandos: up, down, logs, exec e profiles
Todos os comandos correm a partir do directório onde está o docker-compose.yml. O nome do directório torna-se o prefixo do projecto — todos os containers, redes e volumes recebem esse prefixo. Para usar um nome diferente, usar -p nome ou definir COMPOSE_PROJECT_NAME.
up — subir a stack
# Sobe todos os serviços em foreground (ver logs em tempo real)
docker compose up
# Modo detached (background) — devolve o terminal
docker compose up -d
# Forçar rebuild das imagens locais
docker compose up -d --build
# Só um serviço específico (+ dependências)
docker compose up -d db
# Recriar containers mesmo sem mudanças no YAML
docker compose up -d --force-recreate
O up é idempotente: se já está a correr, aplica apenas as diferenças. Se o YAML mudou, recria apenas os serviços afetados. Se nada mudou, não faz nada.
down — parar e remover
# Para e remove containers, redes (NÃO remove volumes)
docker compose down
# Remove também named volumes (PERIGOSO — apaga dados!)
docker compose down -v
# Remove também imagens usadas
docker compose down --rmi local
# Remove imagens, volumes e orphans (containers não definidos no YAML)
docker compose down -v --rmi all --remove-orphans
down -v elimina os named volumes permanentemente. A base de dados, o estado do Redis, tudo o que está em volumes declarados é apagado. Usar apenas em ambientes de desenvolvimento descartáveis.
logs — seguir output
# Logs de todos os serviços (live)
docker compose logs -f
# Logs de um serviço específico
docker compose logs -f app
# Últimas 50 linhas e segue
docker compose logs -f --tail 50 app
# Com timestamps
docker compose logs -t app
exec — comandos dentro do container
# Abrir shell no container
docker compose exec app sh
# Executar comando directo
docker compose exec db psql -U myapp -d myapp
# Como root (override do user)
docker compose exec --user root app sh
O exec requer que o container esteja a correr. Para arrancar um serviço parado sem subir toda a stack, usar docker compose start app primeiro.
profiles — ambientes selectivos
Os profiles permitem ter serviços opcionais no mesmo docker-compose.yml que só sobem quando explicitamente pedidos. No exemplo acima, o nginx tem profiles: [production] — não sobe com docker compose up normal. Só quando se ativa o profile:
# Subir só os serviços do profile production
docker compose --profile production up -d
# Combinar via variável de ambiente
COMPOSE_PROFILES=production docker compose up -d
# Ver que serviços estão definidos
docker compose config --profiles
Cenário típico: usar profiles para separar debug (com ferramentas de profiling), production (com nginx e certificados) e test (com mock services) — tudo num único ficheiro, sem manter múltiplas cópias (Compose profiles).
Outros comandos essenciais
# Estado dos serviços
docker compose ps
# Validar o YAML e mostrar config resolvida
docker compose config
# Ver consumo de recursos
docker compose stats
# Parar sem remover (preserva containers)
docker compose stop
# Arrancar serviços parados
docker compose start
# Reiniciar um serviço
docker compose restart app
# Rebuild de imagens
docker compose build
docker compose build --no-cache app
Erros Comuns
| Erro | Causa | Solução |
|---|---|---|
Bind address already in use |
A porta do host já está ocupada por outro processo ou container anterior | docker compose down para limpar; se persistir, ss -tlnp | grep PORTA para identificar o processo; mudar o mapeamento de porta no YAML |
no configuration file provided: not found |
O comando foi executado fora do directório com o docker-compose.yml |
Usar -f caminho/para/docker-compose.yml ou cd para o directório correcto |
service "db" has neither an image nor a build context |
O serviço não tem image: nem build: definidos |
Adicionar image: imagem:tag ou build: . ao serviço |
dependency failed to start: container db is unhealthy |
O healthcheck falha e o depends_on: condition: service_healthy bloqueia o arranque |
Verificar docker compose logs db; ajustar o comando test: do healthcheck ou aumentar retries: e interval: |
Variable is not set. Defaulting to a blank string |
A interpolação ${VAR} não encontra a variável no ambiente nem no .env |
Criar/editar o ficheiro .env no mesmo directório; usar ${VAR:-default} para fallback inline |
network not found ao fazer up |
Um container órfão de uma execução anterior ainda referencia a rede antiga | docker compose down --remove-orphans para limpar containers órfãos; se persistir, docker network prune |
Cannot connect to the Docker daemon |
O serviço Docker não está a correr ou o utilizador não está no grupo docker |
sudo systemctl start docker; adicionar utilizador: usermod -aG docker $USER e fazer re-login |
| YAML inválido mas sem mensagem clara | Indentação inconsistente (tabs misturados com espaços) ou chave sem valor | docker compose config valida o YAML e mostra o erro exacto; usar sempre espaços (2 por nível), nunca tabs |
Dados perdidos após down |
Foi usado down -v ou os dados estavam num bind mount que foi removido |
Usar down sem -v para preservar named volumes; para bind mounts, garantir que o caminho do hospedeiro não é temporário |
| Containers não se veem (DNS não resolve) | Serviços em redes diferentes sem bridge comum | Verificar networks: de cada serviço no YAML; colocar ambos na mesma rede ou numa rede partilhada |
Artigos Relacionados
- Dia 21: Docker — Containers, Imagens, Volumes e Networks — Fundamentos do Docker que este artigo expande com Compose
- Dia 15: Apache e Nginx — Web Server e Virtual Hosts — O reverse proxy nginx do exemplo pode ser substituído por esta configuração manual
- Dia 7: Systemd — Services, Timers e journalctl — Integração do Docker Compose com systemd para auto-start no boot
- Dia 9: Firewalls — iptables, nftables, UFW e firewalld — Abrir portas 80 e 8080 no firewall para os containers
- Dia 14: Backups e Recuperação — Estratégia de backup para volumes Docker e dados de bases de dados containerizadas