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 --depends-on cache node:20-alpine
🗣️ Pergunte à turma:
- "Como compartilhar isso com outro dev?"
- "E se eu precisar subir isso em CI/CD?"
- "Como garantir que
appsó inicie depois quecacheestiver 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 |
--depends-on cache |
depends_on: [cache] |
Ordem de criação |
📌 Regra de Ouro do Compose:
- Não use
version: "3.9"(legado desde Compose v2). - O Compose cria automaticamente uma rede chamada
<projeto>_default. Você só declaranetworks: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:
💡 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¶
🔍 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¶
📤 Output esperado (trecho relevante):
services:
web:
image: nginx:alpine
ports:
- published: 8080
target: 80
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
./htmlpara o caminho absoluto do seu SO. - Expandiu a sintaxe curta de
portsevolumespara a forma longa e explícita. - Adicionou automaticamente uma rede
default(não aparece aqui, mas existe).
3️⃣ Levante e teste¶
🌐 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:
Se tem dois-pontos no volume (
nome:/path), declare no bloco raizvolumes:. Se não tem (./pasta:/path), é bind mount e não precisa declarar.
2️⃣ Valide a configuração¶
📤 Output esperado (trecho relevante):
services:
cache:
image: redis:alpine
volumes:
- type: volume
source: cache_data
target: /data
volumes:
cache_data:
name: tutorialdocker_cache_data
driver: local
🧠 O que o Compose fez:
- Validou que
cache_dataexiste na seção raiz. - Resolveu o caminho interno para
/data(absoluto, como exige o daemon). - Prefixou o nome com o projeto (
tutorialdocker_) para evitar colisão global.
3️⃣ Levante incrementalmente e teste¶
📤 Output esperado:
[+] Running 3/3
✔ Network tutorialdocker_default Created
✔ Volume tutorialdocker_cache_data Created
✔ Container tutorialdocker-cache-1 Started
🔍 Note: O Compose não recriou o web. Só criou o volume, anexou à rede e subiu o cache.
✅ 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)¶
- 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. - Extensibilidade: No futuro, você pode adicionar
driver: nfs,labels:, ouexternal: truedireto nessa declaração. - 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 (tutorialdocker/), crie a pasta:
Agora, abra o VS Code e crie os 3 arquivos abaixo dentro da pasta app/.
📄 app/package.json¶
🔍 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);
});
// Mantém o processo vivo para o container não parar
setInterval(() => {}, 1000);
🔍 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 (~50MB).COPY package*.json ./+RUN npm install: Aproveita cache do Docker. Só reinstala sepackage.jsonmudar.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:
[+] Running 4/4
✔ Network tutorialdocker_default Created
✔ Volume tutorialdocker_cache_data Created
✔ Container tutorialdocker-cache-1 Started
✔ Container tutorialdocker-app-1 Started
✔ Container tutorialdocker-web-1 Started
✅ 4. Teste DNS Interno & Logs¶
📤 Output esperado:
# 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:
📌 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)