Docker Compose para ambientes de desenvolvimento
Um arquivo descreve a stack inteira do seu app e todo dev sobe o mesmo ambiente com um comando. Veja a anatomia de um compose, healthchecks e onde não usar.
Toda vez que alguém novo entrava no time, o primeiro dia era o mesmo ritual triste: instalar Postgres na mão, descobrir que a versão era diferente da produção, subir um Redis, achar a porta ocupada e perder a tarde inteira só para conseguir rodar a aplicação localmente. Multiplique isso por cinco devs e você tem uma semana de produtividade jogada fora em "funciona na minha máquina". O Docker Compose existe exatamente para matar esse ritual. Em vez de um documento de onboarding com vinte passos manuais, você tem um arquivo que descreve a stack inteira: o app, o banco, o cache, tudo. O novo dev clona o repo, roda um comando e em dois minutos tem o mesmo ambiente que todo mundo.
Este artigo cobre o que o Compose resolve no dia a dia de desenvolvimento, a anatomia de um arquivo de compose e onde ele brilha (e onde você não deveria usá-lo). Se containers ainda são novidade para você, vale ler primeiro o guia de Docker para iniciantes, que cobre o básico de images e containers antes de empilharmos vários deles.
O problema que o Compose resolve
Rodar um container isolado com docker run é simples até o momento em que sua aplicação precisa de companhia. Um app web real raramente vive sozinho: ele fala com um banco, talvez um cache, talvez uma fila. Subir isso na mão significa decorar uma sequência de comandos docker run gigantes, com flags de porta, volume, rede e variável de ambiente espalhadas por toda parte. Você documenta no README, alguém esquece de atualizar, e o README mente em uma semana.
O Compose troca tudo isso por um único arquivo declarativo, normalmente docker-compose.yml, versionado junto com o código. Ele descreve cada service, como ele é construído ou de qual image vem, quais portas expõe, quais volumes monta e como conversa com os outros. O comando docker compose up lê esse arquivo e levanta a stack inteira de uma vez. O arquivo vira a fonte da verdade: não tem README mentindo, porque o ambiente É o arquivo.
Anatomia de um arquivo de compose
Vamos olhar um exemplo realista de um app que precisa de um banco Postgres:
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules
environment:
DATABASE_URL: postgres://dev:dev@db:5432/appdev
depends_on:
- db
db:
image: postgres:16.3
ports:
- "5432:5432"
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
POSTGRES_DB: appdev
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Cada chave dentro de services é um container que vai subir. Repare nas decisões:
buildvsimage: o serviceappusabuild: ., ou seja, o Compose constrói a image a partir doDockerfileda pasta atual. Já odbusaimage: postgres:16.3, uma image pronta puxada do registry. Você usabuildpara o seu código eimagepara dependências de prateleira.ports:"3000:3000"mapeia a porta do host para a porta do container (host:container). É só isso que você acessa do navegador.volumespara hot-reload:.:/usr/src/appmonta seu código dentro do container. Você edita um arquivo no editor e o container já vê a mudança, sem rebuild. O segundo volume,/usr/src/app/node_modules, é um truque comum para impedir que onode_modulesda sua máquina sobrescreva o do container.volumespara persistência do banco:pgdata:/var/lib/postgresql/dataé um named volume. Sem ele, você perde todos os dados do banco toda vez que rodadocker compose down. Com ele, os dados sobrevivem.environment: variáveis injetadas no container. Aqui o app sabe se conectar ao banco pelaDATABASE_URL.depends_on: define ordem de inicialização. Odbsobe antes doapp. Mas atenção a uma armadilha que vamos detalhar a seguir.
depends_on não espera o banco ficar "pronto"
Esse é o erro que pega quase todo mundo na primeira semana. depends_on garante a ordem de inicialização, mas não garante que o service dependente está pronto para aceitar conexões. O Compose sobe o container do Postgres e, no instante em que o container existe, considera a dependência satisfeita e sobe o app. Só que o Postgres leva alguns segundos para inicializar de verdade. Resultado: o app tenta conectar, o banco ainda está engatinhando, a conexão falha e o app morre.
A solução correta é um healthcheck no service do banco, combinado com depends_on na forma estendida:
db:
image: postgres:16.3
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev"]
interval: 5s
timeout: 3s
retries: 5
E no app:
app:
depends_on:
db:
condition: service_healthy
Agora o Compose espera o pg_isready retornar sucesso antes de subir o app. Dito isso, a abordagem mais robusta a longo prazo é fazer sua aplicação tolerar a indisponibilidade do banco e tentar reconectar, porque healthcheck resolve o startup mas não resolve um banco que cai no meio do dia.
Comandos do dia a dia
Na prática você vive com três ou quatro comandos. Para subir a stack inteira em background:
docker compose up -d
Para acompanhar o que está acontecendo, especialmente quando algo não subiu:
docker compose logs -f app
E para derrubar tudo no fim do expediente, liberando as portas:
docker compose down
Vale lembrar: down por padrão preserva os named volumes. Se você quer um banco limpo de verdade, docker compose down -v apaga também os volumes. Use com cuidado, porque aí os dados vão embora mesmo.
Networking: services se acham pelo nome
Repare que no exemplo a DATABASE_URL aponta para db:5432, e não para localhost. Isso não é mágica. O Compose cria automaticamente uma rede para o projeto e cada service vira um hostname dentro dela, usando o próprio nome do service. O app resolve db para o IP do container do Postgres sem você configurar nada. Por isso o nome do service importa: trocar db por database no arquivo significa trocar a DATABASE_URL também.
Para entender a estrutura de um arquivo de compose ou conferir uma indentação que você não tem certeza se está no nível certo, converter o YAML para JSON com um conversor de YAML para JSON ajuda a enxergar de forma explícita o que está aninhado dentro do quê.
Dev vs prod, profiles e override files
O Compose é excelente para desenvolvimento e bom o suficiente para CI. Mas cuidado em tratá-lo como orquestrador de produção. Ele não tem rolling deploy decente, auto-healing distribuído, escalonamento entre máquinas nem os recursos que um Kubernetes oferece. Para produção séria, o Compose é a ferramenta errada; para o seu laptop, é perfeita.
Dois recursos ajudam a manter um único repo servindo cenários diferentes. Profiles permitem marcar services como opcionais: um service de seed de dados ou um painel de admin só sobe quando você roda docker compose --profile tools up. Assim o up padrão fica leve. Override files são o docker-compose.override.yml, que o Compose lê automaticamente em cima do arquivo base, permitindo cada dev ajustar portas ou volumes localmente sem mexer no arquivo versionado pelo time.
Duas opiniões que eu defendo sem pestanejar. Primeira: fixe a versão das images, nunca use :latest. postgres:latest significa que dois devs podem estar rodando versões diferentes do banco sem perceber, e que um build seu de hoje é diferente do de amanhã. postgres:16.3 é reproduzível. Segunda: use named volumes para dados de banco, nunca bind mounts. Named volume é gerenciado pelo Docker, é portátil entre sistemas operacionais e não depende de um caminho que existe só na sua máquina.
Perguntas frequentes
Qual a diferença entre docker run e docker compose?
docker run sobe um container por vez, com toda a configuração passada na linha de comando. docker compose lê um arquivo declarativo que descreve vários services de uma vez, com suas redes, volumes e dependências, e sobe tudo junto. Para qualquer coisa além de um container solitário, o Compose poupa muito comando decorado.
Preciso de healthcheck mesmo usando depends_on?
Sim, se a ordem de prontidão importa. depends_on só garante que um container iniciou antes do outro, não que ele está pronto para receber conexões. Para esperar um banco aceitar queries, você precisa de um healthcheck no service e da condição service_healthy no depends_on.
Posso usar o mesmo docker-compose.yml em produção?
Dá para usar em cenários simples, mas eu não recomendaria. O Compose não foi feito para orquestração distribuída: faltam escalonamento entre máquinas, deploy sem downtime e auto-healing sérios. Para dev e CI ele é ótimo; para produção com requisitos de disponibilidade, prefira um orquestrador de verdade.
O que acontece com os dados do banco quando eu rodo docker compose down?
Por padrão, down remove os containers e a rede, mas preserva os named volumes, então os dados do banco continuam lá na próxima vez que você subir. Se você rodar docker compose down -v, os volumes também são apagados e o banco volta zerado.
O que levar daqui
O Compose transforma um onboarding de um dia em um docker compose up -d, porque um único arquivo passa a descrever a stack inteira e vira a fonte da verdade do ambiente. Fixe as versões das images, use named volumes para dados que precisam sobreviver, e não confie no depends_on para esperar um banco ficar pronto: use healthcheck. Trate o Compose como a ferramenta de desenvolvimento brilhante que ele é, e deixe a produção séria para um orquestrador feito para isso. Se os conceitos de container e image ainda parecem vagos, o guia de Docker para iniciantes cobre os fundamentos antes de você empilhar vários deles num arquivo só.
- 01 O que é software open source Open source não significa gratuito. Entenda o que a licença realmente define, as diferenças entre MIT, GPL e Apache, e como projetos open source se sustentam financeiramente.
- 02 PostgreSQL vs MySQL: diferenças práticas para quem precisa escolher Comparativo honesto entre PostgreSQL e MySQL: tipos nativos, JSONB vs JSON_EXTRACT, concorrência MVCC e extensões. Por que PostgreSQL como default faz sentido.