Pular para conteúdo

Tradução CLI → Docker Compose (Construção Passo a Passo)

Filosofia: Docker Compose não é magia. É CLI declarativa. Cada flag vira um campo YAML. Vamos construir o arquivo juntos, serviço por serviço.

🔹 1. O Problema da CLI (Motivação)

Mostre na tela:

docker run -d --name web -p 8080:80 -v $(pwd)/html:/usr/share/nginx/html:ro nginx:alpine
docker run -d --name cache -v cache_data:/data redis:alpine
docker run -d --name app -p 3000:3000 -e DB_HOST=cache --network app-net node:20-alpine
# ...e esta terceira linha só pode rodar DEPOIS das outras duas:
# o `docker run` não tem nenhuma opção para declarar dependências.

🗣️ Pergunte à turma:

  • "Como compartilhar isso com outro dev?"
  • "E se eu precisar subir isso em CI/CD?"
  • "Como garantir que app só inicie depois que cache estiver pronto?"
  • "E se eu esquecer uma flag na 3ª linha?"

✅ Resposta: Precisamos de um manifesto declarativo, versionável no Git, que o Docker Engine execute de forma idempotente. Esse manifesto é o docker-compose.yml.


🔹 2. Mapeamento Direto (CLI ↔ YAML)

Antes de escrever, mostre a tradução exata. Cole isso no slide/README:

Flag docker run Campo docker-compose.yml O que faz
--name web services: web: Define o identificador do serviço
nginx:alpine image: nginx:alpine Imagem base
-p 8080:80 ports: ["8080:80"] Mapeamento Host → Container
-e DB_HOST=cache environment: DB_HOST: cache Injeção de variáveis
-v ./html:/path volumes: ["./html:/path"] Bind Mount
-v cache_data:/path volumes: ["cache_data:/path"] Named Volume
--network app-net networks: ["app-net"] Anexa à rede
(não existe no CLI: você roda os comandos na ordem certa, à mão) depends_on: [cache] Ordem de criação

📌 Regra de Ouro do Compose:

  • Não use version: "3.9": o campo é obsoleto, e o Compose v2 o ignora (e avisa).
  • O Compose cria automaticamente uma rede chamada <projeto>_default. Você só declara networks: se quiser isolamento extra ou nomes customizados.
  • Todos os serviços na mesma rede resolvem DNS por nome do serviço.

📁 Passo 0: Preparação do Projeto

Crie a estrutura inicial na sua máquina:

mkdir compose-step-by-step && cd compose-step-by-step
mkdir html
echo "<h1>Compose Funcionando!</h1>" > html/index.html
touch docker-compose.yml

Tip

Pode criar esses arquivos e diretorio usando o vscode

📝 Abra docker-compose.yml no editor e comece com o esqueleto vazio:

services:

💡 Conceito: services: é o nível raiz. Cada chave abaixo dele será um container gerenciado pelo Compose.


🟢 Passo 1: Serviço Web (Nginx)

1️⃣ Adicione o serviço web

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro

🔍 Explicação de cada chave:

Chave O que faz
image: nginx:alpine Baixa e usa a imagem oficial. Se não existir localmente, o Docker puxa do Docker Hub.
ports: ["8080:80"] Mapeia a porta 8080 da sua máquina para a porta 80 dentro do container. Formato: host:container.
volumes: ["./html:/usr/share/nginx/html:ro"] Bind mount. Espelha a pasta html do host no container. :ro = read-only (segurança + performance).

2️⃣ Valide a configuração

docker compose config

📤 Output esperado (trecho relevante):

services:
  web:
    image: nginx:alpine
    ports:
      - mode: ingress
        target: 80
        published: "8080"
        protocol: tcp
    volumes:
      - type: bind
        source: /caminho/absoluto/para/compose-step-by-step/html
        target: /usr/share/nginx/html
        read_only: true

🧠 O que o Compose fez:

  • Resolveu ./html para o caminho absoluto do seu SO.
  • Expandiu a sintaxe curta de ports e volumes para a forma longa e explícita.
  • Adicionou automaticamente uma rede default (no fim da saída, em networks:, com o nome compose-step-by-step_default).

3️⃣ Levante e teste

docker compose up -d
docker compose ps

🌐 Abra http://localhost:8080 → Deve aparecer "Compose Funcionando!".

✅ Checkpoint: O primeiro serviço está no ar. O Compose criou a rede interna e o bind mount. Vamos adicionar o próximo.


🔴 Passo 2: Serviço Cache (Redis) + Declaração Obrigatória de Volume

1️⃣ Adicione o serviço cache E declare o volume raiz

O Compose v2 valida rigidamente a estrutura. Para evitar o erro undefined volume, você deve declarar o volume no final do arquivo antes ou junto com o uso.

Atualize seu docker-compose.yml para:

services:
  web:
    image: nginx:alpine
    ports: ["8080:80"]
    volumes: ["./html:/usr/share/nginx/html:ro"]

  cache:
    image: redis:alpine
    environment:
      REDIS_MAXMEMORY: 256mb
    volumes:
      - cache_data:/data

volumes:
  cache_data:  # ← OBRIGATÓRIO para named volumes

🔍 Explicação de cada chave:

Chave O que faz
image: redis:alpine Imagem oficial leve.
environment: Injeta variáveis no container. ⚠️ Redis ignora REDIS_MAXMEMORY nativamente. É um exemplo didático de injeção. Em prod, use redis.conf.
volumes: ["cache_data:/data"] Named Volume. O Docker cria e gerencia o armazenamento. O container acessa via /data.
volumes: (bloco raiz) Declaração raiz obrigatória. Informa ao Compose que cache_data é um recurso gerenciado, não um caminho do host.

📌 Regra de Ouro do Compose:

Olhe o que vem antes dos dois-pontos. Se é um nome (cache_data:/data), é um named volume: declare-o no bloco raiz volumes:. Se é um caminho (./html:/path, começando com . ou /), é bind mount e não precisa declarar.

2️⃣ Valide a configuração

docker compose config

📤 Output esperado (trecho relevante):

services:
  cache:
    image: redis:alpine
    volumes:
      - type: volume
        source: cache_data
        target: /data
volumes:
  cache_data:
    name: compose-step-by-step_cache_data

🧠 O que o Compose fez:

  • Validou que cache_data existe na seção raiz.
  • Resolveu o caminho interno para /data (absoluto, como exige o daemon).
  • Prefixou o nome com o projeto (compose-step-by-step_, o nome da pasta) para evitar colisão global.

3️⃣ Levante incrementalmente e teste

docker compose up -d

📤 Output esperado (o formato exato varia com a versão do Compose):

 ✔ Volume "compose-step-by-step_cache_data"  Created
 ✔ Container compose-step-by-step-web-1      Running
 ✔ Container compose-step-by-step-cache-1    Started

🔍 Note: O Compose não recriou o web (ele aparece como Running), e a rede compose-step-by-step_default já existia desde o Passo 1. Só criou o volume e subiu o cache na mesma rede.

✅ Teste rápido:

docker compose exec cache redis-cli ping
# → PONG
docker compose exec cache env | grep REDIS
# → REDIS_MAXMEMORY=256mb

💡 Por que o Compose exige isso? (Para explicar na aula)

  1. Segurança de parsing: Sem a declaração raiz, o Compose não saberia se cache_data é um volume gerenciado ou se você esqueceu o : de um bind mount.
  2. Extensibilidade: No futuro, você pode adicionar driver: nfs, labels:, ou external: true direto nessa declaração.
  3. Previsibilidade: Garante que todos os recursos (redes, volumes, configs) são explicitamente declarados antes do deploy.

✅ Checkpoint: Dois serviços rodando na mesma rede automática. Dados do Redis serão persistidos no volume nomeado.


🟡 Passo 3: Serviço App (Node)

1️⃣ Prepare a estrutura

Na raiz do projeto (compose-step-by-step/), crie a pasta:

mkdir app

Agora, abra o VS Code e crie os 3 arquivos abaixo dentro da pasta app/.


📄 app/package.json

{
  "name": "app",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  }
}

🔍 O que faz: Define o ponto de entrada (index.js) e o comando que será executado no Dockerfile.


📄 app/index.js

const http = require('http');

const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'online',
db_host: process.env.DB_HOST || 'não configurado',
timestamp: new Date().toISOString()
}));
});

// 0.0.0.0 é OBRIGATÓRIO para o Docker mapear a porta corretamente
server.listen(3000, '0.0.0.0', () => {
console.log('✅ App ouvindo em http://0.0.0.0:3000');
console.log('🔗 DB_HOST:', process.env.DB_HOST);
});

(Não é preciso nenhum truque para "manter o processo vivo": enquanto o servidor estiver escutando a porta, o Node não termina.)

🔍 Por que '0.0.0.0'?Se o Node ouvir apenas 127.0.0.1 (padrão de alguns frameworks), a requisição fica presa na interface de loopback interna do container. 0.0.0.0 instrui o processo a escutar em todas as interfaces de rede, permitindo que o Docker Engine redirecione a porta do host.

📦 Nota: Este código usa apenas o módulo nativo http. Não precisa instalar express. O package.json anterior já funciona perfeitamente.


📄 app/Dockerfile

FROM node:20-alpine
WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
EXPOSE 3000

CMD ["npm", "start"]

🔍 O que faz:

  • FROM node:20-alpine: Imagem leve (~45 MB para baixar, bem menor que a node:20 completa, que passa de 1 GB descompactada).
  • COPY package*.json ./ + RUN npm install: Aproveita cache do Docker. Só reinstala se package.json mudar.
  • CMD ["npm", "start"]: Processo principal (PID 1) que mantém o container vivo.

🔄 2. Atualize o docker-compose.yml

Adicione o bloco app abaixo do cache. O arquivo completo deve ficar assim:

services:
  web:
    image: nginx:alpine
    ports: ["8080:80"]
    volumes: ["./html:/usr/share/nginx/html:ro"]

  cache:
    image: redis:alpine
    environment:
      REDIS_MAXMEMORY: 256mb
    volumes:
      - cache_data:/data

  app:
    build: ./app
    ports: ["3000:3000"]
    environment:
      DB_HOST: cache
      APP_ENV: development
    depends_on:
      - cache

volumes:
  cache_data:

🔍 Explicação das novas chaves:

Chave O que faz
build: ./app O Compose executa docker build ./app automaticamente antes de subir.
environment: DB_HOST: cache Injeta a variável. cache é o nome do serviço, não um IP. O DNS interno do Compose resolve.
depends_on: [cache] Garante que o container cache seja criado antes do app. ⚠️ Só garante criação, não prontidão.

🛠️ 3. Valide e Levante

# 1. Verifique se o YAML está sintaticamente correto e resolveu os caminhos
docker compose config

# 2. Suba tudo (o Compose vai fazer build do app automaticamente)
docker compose up -d --build

📤 Output esperado (trecho):

 ✔ Container compose-step-by-step-cache-1  Running
 ✔ Container compose-step-by-step-web-1    Running
 ✔ Container compose-step-by-step-app-1    Started

(Antes disso, o Compose mostra o build da imagem do app. Rede e volume já existiam.)

✅ 4. Teste DNS Interno & Logs

# Veja o que o app imprimiu ao iniciar
docker compose logs app

📤 Output esperado:

app-1  | ✅ App ouvindo em http://0.0.0.0:3000
app-1  | 🔗 DB_HOST: cache
# Prove que o DNS interno está funcionando
docker compose exec app sh -c "nslookup cache"
# → Name: cache
# → Address 1: 172.18.0.3 (IP dinâmico, pode variar)

🧠 Conceito fixado:

O Compose criou uma rede default, anexou os 3 serviços nela, e o DNS interno resolveu cache → IP real do container Redis. O app não precisa saber IPs. Só precisa do nome do serviço.


🛠️ Como validar que o app está rodando

# 1. Rebuild e suba apenas o app (incremental)
docker compose up -d --build app

# 2. Teste via terminal
curl http://localhost:3000
# → {"status":"online","db_host":"cache","timestamp":"2026-04-30T..."}

# 3. Ou abra no navegador: http://localhost:3000

📤 Output esperado nos logs:

docker compose logs app
# → ✅ App ouvindo em http://0.0.0.0:3000
# → 🔗 DB_HOST: cache

📌 Conceito-chave para fixar:

Item O que significa
EXPOSE 3000 no Dockerfile Apenas documentação. Não abre porta.
ports: ["3000:3000"] no Compose Cria a regra de NAT no Docker Engine.
server.listen(3000, '0.0.0.0') no código Ativa o listener no container. Sem isso, o mapeamento de porta não tem para onde redirecionar.

✅ Checklist de Validação Final do Stack

docker compose ps
# → web, cache, app devem estar "Up"

curl http://localhost:8080   # ✅ Nginx (web)
curl http://localhost:3000   # ✅ Node (app)
docker compose exec cache redis-cli ping  # ✅ Redis (cache)