Orquestre serviços com Docker Compose
Pare de malabarismo com containers na mão. Declare uma stack multi-serviço inteira — uma app web mais um banco PostgreSQL, com rede privada e volume nomeado — num único docker-compose.yml, e depois suba, opere e destrua tudo com os comandos modernos `docker compose` v2.
Problema
Uma aplicação real nunca é um container só. Você tem uma app web, um banco de dados, talvez um cache e um worker de fundo — e todos precisam subir na ordem certa, se encontrar numa rede e manter seus dados entre reinícios. Ligar isso na mão com `docker run` é doloroso e propenso a erro: você repete longas strings de flags, inventa uma rede com `docker network create`, lembra qual container tem que subir primeiro e reconstrói tudo de memória toda santa vez. O **Docker Compose** troca essa fragilidade por um único arquivo declarativo. Um `docker-compose.yml` descreve cada serviço, rede e volume da sua stack. Um comando — `docker compose up` — cria tudo na ordem correta; um comando — `docker compose down` — limpa tudo. A definição vive no seu repositório, versionada junto do código, então um colega reproduz seu ambiente inteiro com um único comando. Neste lab você vai escrever um `docker-compose.yml` real para uma stack de dois serviços (um servidor web e um banco PostgreSQL), subi-la, ver o Compose construir a rede e os volumes para você, operar o ciclo de vida completo e provar que os dois serviços conversam entre si pelo DNS interno do Compose — usando a sintaxe moderna `docker compose` (v2) o tempo todo.
Objetivos
- Explicar o problema que o Compose resolve e como um
docker-compose.ymldeclara serviços, redes e volumes num só lugar - Ler e escrever as chaves centrais do Compose:
services,image/build,ports,environment,volumes,depends_on,networks - Escrever uma stack real de dois serviços (um serviço
webe um serviçodbPostgreSQL) com um volume nomeado e uma rede privada - Subir a stack com
docker compose up -de entender o que o Compose cria e em que ordem - Operar o ciclo de vida com
docker compose ps,logs -f,exec,stop/startedown, e alcançar um serviço a partir de outro pelo nome do serviço (DNS do Compose) - Externalizar a configuração com variáveis de ambiente e um arquivo
.env, validar comdocker compose confige escalar um serviço com--scale
Pré-requisitos
- Docker instalado e rodando, com Compose v2. Rode
docker compose version— se imprimir uma versão v2.x, você está pronto. (O Compose v2 vem dentro do Docker Desktop e como odocker-compose-pluginno Linux; é o subcomandodocker compose, com espaço.) - Familiaridade com o básico de container único dos labs de Docker anteriores:
docker run,ps,logs,exec, imagens vs containers - Um terminal (PowerShell, bash ou zsh) e um navegador
- Git instalado, para entregar seu deliverable no fim
- Acesso à internet para baixar as imagens
nginxepostgresdo Docker Hub
O que você vai construir
Você vai escrever um arquivo — docker-compose.yml — que declara uma stack pequena mas completa:
um serviço web (nginx) e um serviço db (PostgreSQL), conectados numa rede privada, com um
volume nomeado para o banco manter seus dados entre reinícios. Depois vai dirigir tudo com um
punhado de comandos docker compose: subir em segundo plano, inspecionar, ler os logs, abrir um
shell psql no banco, provar que o container web consegue resolver db pelo nome e, por fim,
derrubar tudo de forma limpa.
Por que o Compose existe
Nos labs anteriores você rodou containers únicos com docker run. Ótimo para uma coisa só, mas
uma app real são várias coisas ao mesmo tempo. Compare as duas abordagens para exatamente a mesma
stack:
| Na mão | Com Compose |
|---|---|
docker network create appnet |
bloco networks: (criado automaticamente) |
docker volume create dbdata |
bloco volumes: (criado automaticamente) |
um longo docker run … por serviço, na ordem certa, de memória |
docker compose up -d |
| derrubar cada container, rede e volume um a um | docker compose down |
O Compose transforma uma sequência frágil de comandos imperativos num único arquivo
declarativo que você comita no repositório. Você descreve o estado final desejado; o Compose
descobre como chegar lá.
O comando docker compose (v2) — atenção ao espaço
O Compose moderno é um plugin embutido na CLI do Docker, invocado como docker compose (duas
palavras, com espaço). O antigo docker-compose autônomo (com hífen) é o v1 e está obsoleto.
Tudo neste lab usa a forma v2.
Mais uma coisa que você vê em tutoriais antigos: uma linha version: "3.8" no topo do arquivo. No
Compose v2 essa chave version de topo é obsoleta e ignorada — você pode (e deve) simplesmente
omiti-la. Se aparecer num exemplo, é um resquício da era v1, não algo de que você precise.
O arquivo Compose num relance
Todo arquivo Compose é um mapa com algumas chaves de topo:
-
services:— os containers que compõem sua app; cada serviço nomeia umaimage:ou umbuild: -
networks:— redes privadas que seus serviços compartilham (o Compose cria uma padrão mesmo se você não declarar) -
volumes:— volumes nomeados para dados que precisam sobreviver a um container
Dentro de um serviço você vai usar chaves como image, build, ports, environment,
env_file, volumes, depends_on e networks. Você vai conhecer cada uma escrevendo-as, não só
lendo sobre elas.
Como trabalhar neste lab
Faça os passos em ordem — cada um se apoia no arquivo do anterior. Rode cada comando você mesmo e
leia a saída real. Você vai coletar as saídas que precisa para a entrega no Passo 6.
Passos
-
O problema que o Compose resolve e a anatomia do arquivo
Antes de escrever qualquer coisa, deixe claro por que o Compose existe e do que um arquivo Compose é feito.
Confirme que você tem o Compose v2
docker compose versionVocê deve ver uma linha
Docker Compose version v2.x.y. Se em vez dissodocker composefor
"not a docker command", você está num setup antigo — instale/habilite o plugin Compose v2 (ele
vem junto do Docker Desktop atual). Repare no espaço emdocker compose: essa é a CLI moderna
v2. Odocker-composecom hífen é o v1 autônomo, obsoleto.Por que não fazer tudo só com
docker run?Imagine subir uma app web mais um banco na mão. Você teria que:
-
docker network create appnetpara eles conversarem, -
docker volume create dbdatapara o banco sobreviver a reinícios, -
docker runo banco com a rede, o volume e um monte de flags-ede ambiente, -
docker runa app web com a mesma rede — depois que o banco existir, - e depois parar e remover cada container, a rede e o volume, um por um.
São muitos passos imperativos para lembrar e redigitar, e nada registra a intenção. Esqueça
uma flag e você tem um ambiente sutilmente quebrado.O que o Compose te dá em vez disso
Um único
docker-compose.ymldeclara a stack inteira. Seu formato de topo é:services: # os containers da sua app web: image: nginx:1.27-alpine ports: - "8080:80" depends_on: - db db: image: postgres:16-alpine environment: POSTGRES_PASSWORD: secret networks: # redes privadas (opcional; uma padrão é criada) # ... volumes: # volumes nomeados para dados persistentes # ...As chaves que você mais vai usar dentro de um serviço:
Chave O que faz imagea imagem a rodar (do Docker Hub ou de um registry) buildconstrói uma imagem a partir de um Dockerfilelocal em vez de baixarportspublica portas do container no host, "HOST:CONTAINER"environmentdefine variáveis de ambiente dentro do container env_filecarrega variáveis de um arquivo externo (ex.: .env)volumesmonta um volume nomeado ou bind mount para dados persistentes/compartilhados depends_onsobe este serviço só depois dos que ele lista networksqual(is) rede(s) privada(s) o serviço entra Checkpoint
- Quais três coisas um arquivo Compose pode declarar no topo? (services, networks, volumes.)
- Qual comando substitui "criar rede + criar volume + vários docker run em ordem"? (
docker compose up.) - A chave
version:de topo é obrigatória no Compose v2? (Não — é obsoleta e ignorada; omita.) - Qual a diferença entre
docker composeedocker-compose? (O primeiro é v2, um plugin da CLI; o segundo é o v1 autônomo, obsoleto.)
-
-
Escreva um docker-compose.yml real: web + db
Agora escreva o arquivo. Crie uma pasta nova e um
docker-compose.ymldentro dela.mkdir compose-lab cd compose-labO arquivo
Coloque isto em
docker-compose.yml. Leia os comentários — cada bloco justifica sua presença.# Sem `version:` de topo — é obsoleto no Compose v2 e omitido sem problema. services: web: # primeiro serviço: um servidor web image: nginx:1.27-alpine # tag fixada, base Alpine pequena ports: - "8080:80" # HOST:CONTAINER — alcance o nginx em http://localhost:8080 depends_on: - db # sobe db antes de web networks: - appnet # entra na rede privada db: # segundo serviço: PostgreSQL image: postgres:16-alpine environment: POSTGRES_PASSWORD: secret # exigido pela imagem postgres POSTGRES_USER: appuser POSTGRES_DB: appdb volumes: - dbdata:/var/lib/postgresql/data # volume nomeado -> dados sobrevivem a reinícios networks: - appnet networks: appnet: # uma rede bridge privada para a stack driver: bridge volumes: dbdata: # volume nomeado, gerenciado pelo DockerLeia bloco a bloco
-
services:contém dois serviços,webedb. Cada nome vira o hostname do container na
rede — isso importa no Passo 4. -
webbaixanginx:1.27-alpine, publica8080:80edepends_on: [db]para o Compose
subirdbprimeiro. Entra naappnet. -
dbbaixapostgres:16-alpine. A imagem postgres exigePOSTGRES_PASSWORD;
POSTGRES_USERePOSTGRES_DBsão opcionais e criam um usuário e um banco no primeiro boot. -
dbdata:/var/lib/postgresql/datamonta um volume nomeado no diretório de dados do
Postgres, então os arquivos do banco ficam num volume gerenciado pelo Docker, não na camada
descartável do container. -
networks: appnetdefine uma rede bridge isolada. Os dois serviços entram nela, então
conseguem se alcançar — e nada fora da stack consegue. -
volumes: dbdatadeclara o volume nomeado que o Compose vai criar.
Nota sobre
depends_on: ele controla a ordem de início, não a prontidão. O Compose sobe
dbantes deweb, mas não espera o Postgres terminar de bootar. Para um verdadeiro "espere
até o banco aceitar conexões", você adicionaria umhealthchecknodbe
depends_on: { db: { condition: service_healthy } }. Isso está além deste lab, mas é bom
saber a distinção.Checkpoint
- Por que
dbprecisa de um volume nomeado ewebnão? (O banco tem estado a manter; o nginx servindo uma página padrão não.) - O que
depends_on: [db]garante — e o que não garante? (Ordem de início, não que o Postgres esteja pronto para aceitar conexões.)
-
-
Suba a stack com docker compose up -d
Com o arquivo escrito, dar vida à stack inteira é um único comando.
Up, em segundo plano
De dentro de
compose-lab(a pasta que contém odocker-compose.yml):docker compose up -d-
uplê odocker-compose.ymle cria tudo que ele declara. -
-d(detached) roda em segundo plano e devolve seu prompt.
Observe a saída. O Compose faz muita coisa por você, em ordem:
-
Cria a rede
compose-lab_appnet(o Compose prefixa os nomes com o projeto, que por
padrão é o nome da pasta). -
Cria o volume
compose-lab_dbdata. -
Baixa
postgres:16-alpineenginx:1.27-alpinese você ainda não os tiver. -
Sobe
dbprimeiro, depoisweb, respeitando odepends_on.
Essa ordenação — rede e volumes antes dos containers, dependências antes dos dependentes — é
exatamente a frágil sequência manual do Passo 1, agora feita por você toda vez.Veja o que está rodando
docker compose psVocê verá os dois serviços —
webedb— com seu estado (running) e o mapeamento de porta
doweb(0.0.0.0:8080->80/tcp). Diferente dodocker pspuro, odocker compose psé
escopado neste projeto, então você vê só a sua stack.Abra http://localhost:8080 — a página "Welcome to nginx!" confirma que
webestá de pé.Confirme os recursos que o Compose criou
docker network ls | grep appnet # compose-lab_appnet existe docker volume ls | grep dbdata # compose-lab_dbdata existe(No PowerShell, use
docker network ls | Select-String appnet.)Essas são a rede e o volume que você nunca precisou criar na mão — o Compose os fez a partir do
seu arquivo, e odocker compose downvai saber como removê-los.Checkpoint
- Em que ordem o Compose subiu os dois containers, e por quê? (
dbantes deweb, por causa dodepends_on.) - De onde veio a rede
compose-lab_appnet? (O Compose a criou a partir do bloconetworks:; o prefixo é o nome do projeto.)
Entregável deste passo: a saída de
docker compose psmostrando os dois serviços de pé. -
-
Opere o ciclo de vida e prove que web fala com db
Uma stack em execução não é uma caixa-preta. Estes comandos são como você a observa, entra
dentro dela e prova que os serviços de fato se alcançam.Acompanhe os logs
O
docker compose logsagrega a saída de todos os serviços, cada linha prefixada com o
nome do serviço:docker compose logsVocê verá linhas de
db(o Postgres reportando "database system is ready to accept
connections") e linhas dewebjuntas. Para acompanhar ao vivo, adicione-f:docker compose logs -fCtrl+Cpara de acompanhar — não para os containers. Para ver só um serviço:docker compose logs -f dbAbra um shell psql no banco
O
docker compose execroda um comando dentro de um serviço em execução (pelo nome do
serviço, não pelo id do container):docker compose exec db psql -U appuser -d appdbIsso abre o cliente PostgreSQL dentro do container
db, conectado ao bancoappdbcomo
appuser. Tente uma query e depois saia:SELECT version(); \qVocê acabou de conversar com o banco sem nenhum cliente Postgres instalado no seu host — está
tudo dentro do container.Prove que
webalcançadbpelo nome do serviçoEste é o coração da rede do Compose. Na rede
appnet, cada serviço é alcançável pelo seu
nome de serviço como um hostname de DNS. Então, de dentro deweb, o banco é simplesmente
db.Entre (exec) no
webe resolvadb:docker compose exec web sh # agora dentro do container web: getent hosts db # imprime o IP interno de db — o DNS resolveu o nome do serviço nc -z -v db 5432 # (se nc existir) conexão TCP com o Postgres na rede interna exitSó o
getent hosts dbretornando um IP já prova: owebresolveu o nomedbpara o container
do banco pelo DNS interno do Compose, sem nenhum endereço IP fixado em lugar nenhum. Numa app
real, aDATABASE_URLdo seu serviço web usariadbcomo hostname — ex.:
postgres://appuser:secret@db:5432/appdb.Pare e inicie sem perder dados
docker compose stop # para os dois serviços, mantém containers, rede, volume docker compose ps # os dois mostram 'exited' docker compose start # traga de voltaComo os dados do banco vivem no volume
dbdata, parar e iniciar — ou atédowne depoisup
— mantém cada linha. O volume sobrevive ao container.Checkpoint
- Dentro do
web, qual hostname alcança o banco, e o que faz isso funcionar? (db— o DNS interno do Compose resolve nomes de serviço na rede compartilhada.) - O
docker compose stopapaga os dados do seu banco? (Não — o volume nomeado persiste; sódown -vo removeria.)
Entregável deste passo: a saída de
docker compose logsmostrando os dois serviços, e a
saída degetent hosts db(oupsql) provando quewebalcançadb. - Dentro do
-
Configuração: .env, validar, escalar, boas práticas
Fixar
POSTGRES_PASSWORD: secretno arquivo serve para um demo, mas stacks reais externalizam
a configuração. O Compose tem suporte de primeira classe para isso.Mova variáveis para um arquivo
.envO Compose lê automaticamente um arquivo chamado
.envna pasta do projeto e substitui as
referências${VARIAVEL}nodocker-compose.yml. Crie o.env:# .env — NÃO comite este arquivo POSTGRES_USER=appuser POSTGRES_PASSWORD=secret POSTGRES_DB=appdb WEB_PORT=8080Depois referencie as variáveis no
docker-compose.yml:services: web: image: nginx:1.27-alpine ports: - "${WEB_PORT}:80" depends_on: - db networks: - appnet db: image: postgres:16-alpine environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - dbdata:/var/lib/postgresql/data networks: - appnet networks: appnet: driver: bridge volumes: dbdata:Agora o mesmo arquivo funciona em dev, staging e prod — só o
.envmuda. Existe também uma
chaveenv_file:por serviço se você quiser apontar um serviço para um arquivo específico.Valide antes de rodar
O
docker compose configfaz o parse do seu arquivo, aplica a substituição de variáveis e
imprime a configuração final, totalmente resolvida — ou um erro claro se algo estiver errado:docker compose configLeia a saída:
${WEB_PORT}agora é8080, as variáveis de ambiente estão preenchidas. Esta é
a forma mais rápida de pegar um erro de digitação ou uma variável faltando antes doup.
(Repare também que oconfignão emite uma chaveversion:— confirmando que é obsoleta no
v2.)Escale um serviço
O Compose pode rodar múltiplas réplicas de um serviço sem estado. Como uma porta fixa de
host não pode ser compartilhada por dois containers, remova o mapeamento fixo deports(ou
use uma faixa) antes de escalar, então:docker compose up -d --scale web=3 docker compose ps # três réplicas de web, um dbVocê agora tem três containers
websob o mesmo projeto — a base do escalonamento horizontal.
(Não escale odbassim: bancos têm estado e precisam de replicação de verdade, não de cópias
ingênuas.)Boas práticas do capítulo
-
Use variáveis de ambiente e
.envpara tudo que muda entre ambientes — mantenha o próprio arquivo compose estável. -
Nunca comite secrets. Adicione
.envao.gitignore; comite um.env.examplecom valores em branco ou fictícios para os colegas saberem o que é preciso. - Use volumes nomeados para dados com estado (bancos especialmente) para os dados sobreviverem à rotatividade de containers.
-
Isole com redes privadas — exponha ao host só as portas que realmente precisam ser publicadas; serviços internos conversam pela rede do Compose, não pelo
localhost. -
Dê nomes claros aos serviços — o nome do serviço é o hostname de DNS que os outros serviços usam, então nomeie pelo papel (
db,web,cache), não pela implementação. -
Mantenha o arquivo legível — comente blocos não óbvios; valide com
docker compose configno CI.
Checkpoint
- Como o Compose sabe substituir
${WEB_PORT}? (Ele lê automaticamente o arquivo.envdo projeto.) - O que o
docker compose configfaz, e por que rodá-lo? (Imprime a config totalmente resolvida; pega erros/digitação antes doup.) - Por que
.envnão deve ser comitado? (Contém secrets; comite um template.env.exampleno lugar.)
-
Use variáveis de ambiente e
-
Entregue: documente sua stack orquestrada
Prove que você subiu uma stack multi-serviço de verdade, operou ela e mostrou os serviços
conversando — capturando a evidência num pequeno repositório.Derrube tudo de forma limpa primeiro (para suas saídas serem honestas)
Quando terminar de experimentar:
docker compose down # remove containers + a rede, mantém o volume nomeado # docker compose down -v # adicione -v SÓ se também quiser apagar o volume dbdataO
downremove os containers e a redeappnet. Ele mantém volumes nomeados a menos que
você passe-v— essa é a segurança que evita você apagar um banco por acidente.Monte sua entrega
Crie um repositório git contendo seu
docker-compose.yml, seu.env.example(valores
fictícios, seguro para comitar), um.gitignoreque ignora.env, e umSUBMISSION.mdcom
suas saídas reais. Cole a saída real dos comandos — não texto inventado.# Docker Lab 3 — Orquestre serviços com Docker Compose ## 1. docker compose version <cole — deve mostrar v2.x> ## 2. Meu docker-compose.yml <cole o arquivo final: web + db, rede, volume nomeado, sem chave `version:`> ## 3. docker compose up -d <cole a saída mostrando o Compose criar a rede, o volume, e subir db e depois web> ## 4. docker compose ps <cole — web e db rodando, web mapeando HOST:80> ## 5. Logs dos dois serviços <cole linhas de docker compose logs mostrando os prefixos `db` E `web`> ## 6. Prova de que web alcança db Saída de `getent hosts db` (ou um psql bem-sucedido) rodado de dentro do container web: <cole — um IP interno de `db` prova que o DNS por nome de serviço funciona> ## 7. Configuração Saída de `docker compose config` com os valores do meu `.env` substituídos (nota: sem chave `version:`): <cole> ## Reflexão (2-3 frases) Nas minhas palavras: o que o Compose declara que o `docker run` me faz fazer na mão, e como o `web` encontra o `db` sem nenhum IP fixado.Entregue
git init echo ".env" > .gitignore git add docker-compose.yml .env.example .gitignore SUBMISSION.md git commit -m "Docker lab 3 — orquestre serviços com Compose" # faça push para um repo público ou crie um gist, depois entregue essa URLEntregue a URL do repositório ou gist como seu deliverable do lab.
Critério de submissão (autoverificação) — sem stubs
- O
docker-compose.ymltem dois serviços (web+db), um volume nomeado e uma rede privada - O arquivo não tem chave
version:de topo (você está no Compose v2) - A saída de
docker compose up -dmostra o Compose criando a rede e o volume e subindodbantes deweb - O
docker compose psmostra os dois serviços rodando, comwebpublicado numa porta de host - A saída de
docker compose logsinclui linhas dos dois,dbeweb - Você provou que
webalcançadbpelo nome (um IP real degetent hosts dbou uma conexãopsqlbem-sucedida) — não só afirmou - O
.envestá no git-ignore e um.env.examplecom valores fictícios é comitado no lugar — sem secrets no repositório - Sua reflexão explica corretamente, nas suas palavras, o que o Compose declara e como funciona o DNS por nome de serviço
Qualquer coisa colada precisa ser saída real da sua máquina. Texto de placeholder ou
inventado reprova a verificação — o ponto inteiro é que você de fato rodou a stack.O que vem a seguir
Você orquestrou serviços e os viu se encontrar pelo nome numa rede privada — mas essa parte de
rede foi quase toda automática. No próximo lab de Docker você vai mais fundo em redes no
Docker: bridge vs host vs redes customizadas, como o DNS de container realmente funciona e
como controlar exatamente quais serviços podem falar com quais. - O