Docker · 45 min

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

Pré-requisitos

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:

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

  1. 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 version
    

    Você deve ver uma linha Docker Compose version v2.x.y. Se em vez disso docker compose for
    "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 em docker compose: essa é a CLI moderna
    v2. O docker-compose com 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:

    1. docker network create appnet para eles conversarem,
    2. docker volume create dbdata para o banco sobreviver a reinícios,
    3. docker run o banco com a rede, o volume e um monte de flags -e de ambiente,
    4. docker run a app web com a mesma rede — depois que o banco existir,
    5. 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.yml declara 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
    image a imagem a rodar (do Docker Hub ou de um registry)
    build constrói uma imagem a partir de um Dockerfile local em vez de baixar
    ports publica portas do container no host, "HOST:CONTAINER"
    environment define variáveis de ambiente dentro do container
    env_file carrega variáveis de um arquivo externo (ex.: .env)
    volumes monta um volume nomeado ou bind mount para dados persistentes/compartilhados
    depends_on sobe este serviço só depois dos que ele lista
    networks qual(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 compose e docker-compose? (O primeiro é v2, um plugin da CLI; o segundo é o v1 autônomo, obsoleto.)
  2. Escreva um docker-compose.yml real: web + db

    Agora escreva o arquivo. Crie uma pasta nova e um docker-compose.yml dentro dela.

    mkdir compose-lab
    cd compose-lab
    

    O 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 Docker
    

    Leia bloco a bloco

    • services: contém dois serviços, web e db. Cada nome vira o hostname do container na
      rede — isso importa no Passo 4.
    • web baixa nginx:1.27-alpine, publica 8080:80 e depends_on: [db] para o Compose
      subir db primeiro. Entra na appnet.
    • db baixa postgres:16-alpine. A imagem postgres exige POSTGRES_PASSWORD;
      POSTGRES_USER e POSTGRES_DB são opcionais e criam um usuário e um banco no primeiro boot.
    • dbdata:/var/lib/postgresql/data monta 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: appnet define uma rede bridge isolada. Os dois serviços entram nela, então
      conseguem se alcançar — e nada fora da stack consegue.
    • volumes: dbdata declara 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
    db antes de web, mas não espera o Postgres terminar de bootar. Para um verdadeiro "espere
    até o banco aceitar conexões", você adicionaria um healthcheck no db e
    depends_on: { db: { condition: service_healthy } }. Isso está além deste lab, mas é bom
    saber a distinção.

    Checkpoint

    • Por que db precisa de um volume nomeado e web nã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.)
  3. 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 o docker-compose.yml):

    docker compose up -d
    
    • up lê o docker-compose.yml e 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:

    1. Cria a rede compose-lab_appnet (o Compose prefixa os nomes com o projeto, que por
      padrão é o nome da pasta).
    2. Cria o volume compose-lab_dbdata.
    3. Baixa postgres:16-alpine e nginx:1.27-alpine se você ainda não os tiver.
    4. Sobe db primeiro, depois web, respeitando o depends_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 ps
    

    Você verá os dois serviços — web e db — com seu estado (running) e o mapeamento de porta
    do web (0.0.0.0:8080->80/tcp). Diferente do docker ps puro, o docker 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 web está 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 o docker compose down vai saber como removê-los.

    Checkpoint

    • Em que ordem o Compose subiu os dois containers, e por quê? (db antes de web, por causa do depends_on.)
    • De onde veio a rede compose-lab_appnet? (O Compose a criou a partir do bloco networks:; o prefixo é o nome do projeto.)

    Entregável deste passo: a saída de docker compose ps mostrando os dois serviços de pé.

  4. 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 logs agrega a saída de todos os serviços, cada linha prefixada com o
    nome do serviço:

    docker compose logs
    

    Você verá linhas de db (o Postgres reportando "database system is ready to accept
    connections") e linhas de web juntas. Para acompanhar ao vivo, adicione -f:

    docker compose logs -f
    

    Ctrl+C para de acompanhar — não para os containers. Para ver só um serviço:

    docker compose logs -f db
    

    Abra um shell psql no banco

    O docker compose exec roda 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 appdb
    

    Isso abre o cliente PostgreSQL dentro do container db, conectado ao banco appdb como
    appuser. Tente uma query e depois saia:

    SELECT version();
    \q
    

    Você acabou de conversar com o banco sem nenhum cliente Postgres instalado no seu host — está
    tudo dentro do container.

    Prove que web alcança db pelo nome do serviço

    Este é 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 de web, o banco é simplesmente
    db.

    Entre (exec) no web e resolva db:

    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
    exit
    

    Só o getent hosts db retornando um IP já prova: o web resolveu o nome db para o container
    do banco pelo DNS interno do Compose, sem nenhum endereço IP fixado em lugar nenhum. Numa app
    real, a DATABASE_URL do seu serviço web usaria db como 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 volta
    

    Como os dados do banco vivem no volume dbdata, parar e iniciar — ou até down e depois up
    — 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 stop apaga os dados do seu banco? (Não — o volume nomeado persiste; só down -v o removeria.)

    Entregável deste passo: a saída de docker compose logs mostrando os dois serviços, e a
    saída de getent hosts db (ou psql) provando que web alcança db.

  5. Configuração: .env, validar, escalar, boas práticas

    Fixar POSTGRES_PASSWORD: secret no 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 .env

    O Compose lê automaticamente um arquivo chamado .env na pasta do projeto e substitui as
    referências ${VARIAVEL} no docker-compose.yml. Crie o .env:

    # .env  — NÃO comite este arquivo
    POSTGRES_USER=appuser
    POSTGRES_PASSWORD=secret
    POSTGRES_DB=appdb
    WEB_PORT=8080
    

    Depois 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 .env muda. Existe também uma
    chave env_file: por serviço se você quiser apontar um serviço para um arquivo específico.

    Valide antes de rodar

    O docker compose config faz 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 config
    

    Leia 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 do up.
    (Repare também que o config não emite uma chave version: — 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 de ports (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 db
    

    Você agora tem três containers web sob o mesmo projeto — a base do escalonamento horizontal.
    (Não escale o db assim: 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 .env para tudo que muda entre ambientes — mantenha o próprio arquivo compose estável.
    • Nunca comite secrets. Adicione .env ao .gitignore; comite um .env.example com 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 config no CI.

    Checkpoint

    • Como o Compose sabe substituir ${WEB_PORT}? (Ele lê automaticamente o arquivo .env do projeto.)
    • O que o docker compose config faz, e por que rodá-lo? (Imprime a config totalmente resolvida; pega erros/digitação antes do up.)
    • Por que .env não deve ser comitado? (Contém secrets; comite um template .env.example no lugar.)
  6. 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 dbdata
    

    O down remove os containers e a rede appnet. 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 .gitignore que ignora .env, e um SUBMISSION.md com
    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 URL
    

    Entregue a URL do repositório ou gist como seu deliverable do lab.

    Critério de submissão (autoverificação) — sem stubs

    • O docker-compose.yml tem 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 -d mostra o Compose criando a rede e o volume e subindo db antes de web
    • O docker compose ps mostra os dois serviços rodando, com web publicado numa porta de host
    • A saída de docker compose logs inclui linhas dos dois, db e web
    • Você provou que web alcança db pelo nome (um IP real de getent hosts db ou uma conexão psql bem-sucedida) — não só afirmou
    • O .env está no git-ignore e um .env.example com 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.