Atividade prática do Módulo 11 — DevOps e Cloud Computing (Capacitação em Desenvolvimento Full Stack, iTeam), sobre containerização, orquestração com Docker Compose e Integração Contínua. Enunciado completo, contexto do curso e anotações de aula em iteam-devops-modulo11/atividade-docker-ci — o repositório de anotações do módulo.
Aluno(a): Kelvin Araújo Ferreira Turma: Noturno Data: 24/07/2026 Aplicação usada: docker/getting-started-app — To-Do em Node.js
Pré-requisito: ter o Docker instalado e rodando na máquina.
git clone https://github.com/DilliKel/getting-started-docker.git
cd getting-started-docker
cp .env.example .env
docker compose up -d --buildIsso builda a imagem da aplicação e sobe dois containers: o app (a aplicação To-Do) e o db (MySQL). Pode levar alguns segundos até o banco ficar pronto — dá pra acompanhar com docker compose ps (o db precisa aparecer como healthy).
Depois de subir, abra o navegador e acesse:
http://localhost:3000
Você vai ver a interface do To-Do app — dá pra cadastrar, marcar como concluída e excluir tarefas. Elas ficam salvas no banco MySQL, dentro do volume todo-mysql-data.
Para derrubar a aplicação:
docker compose down— para e remove os containers, mantém os dados (o volume continua existindo)docker compose down -v— para tudo e apaga os dados (remove o volume também)
Estágios utilizados: builder (instala as dependências com npm ci --omit=dev) e o estágio final (copia apenas node_modules + src, sem ferramentas de build).
Imagem base: node:20-alpine
Usuário de execução: node (não-root, já vem pronto na imagem node:alpine)
Tamanho final da imagem: ~58MB
Por que o multi-stage ajuda? Porque o estágio final só recebe o node_modules já pronto e o código-fonte — nada das ferramentas de build, cache do npm ou arquivos temporários do estágio builder vai parar na imagem final. Isso deixa a imagem menor (menos superfície de ataque, menos coisa pra escanear em busca de vulnerabilidade) e mais rápida de baixar/subir em produção.
Print 1 — docker build + docker images

Print 2 — aplicação rodando com tarefas cadastradas

Volume usado: todo-db → montado em /etc/todos (container avulso, Parte 2) — no Compose, o volume equivalente é todo-mysql-data → /var/lib/mysql
Print 3 — SEM volume: dados perdidos ao recriar o container

Print 4 — COM volume: dados preservados

Diferença entre docker compose down e docker compose down -v: down para e remove os containers e a rede, mas mantém os volumes nomeados (os dados sobrevivem); down -v faz tudo isso e também apaga os volumes, perdendo os dados de vez.
Rede criada: todo-net Serviços conectados: app e mysql/db
A porta do banco está exposta ao host? Não — só o app precisa conversar com o banco, e essa comunicação já acontece dentro da rede Docker (todo-net); publicar a porta 3306 no host abriria o MySQL pra qualquer coisa rodando na máquina (ou na rede, dependendo do firewall), sem necessidade nenhuma.
Por que o app consegue chamar o host mysql/db sem saber o IP? Porque containers na mesma rede Docker (definida pelo usuário, seja via docker network create ou automaticamente pelo Compose) resolvem uns aos outros pelo nome — o Docker mantém um DNS interno que traduz o nome do serviço/container pro IP real, então o app só precisa saber o nome (mysql ou db), nunca o IP.
Print 5 — docker network inspect (em duas partes, para caber tudo)

Print 6 — dados dentro do MySQL (select * from todo_items;)

Serviços: app, db Rede: todo-net · Volume: todo-mysql-data
Healthcheck em: db (mysqladmin ping) · depends_on com: condition: service_healthy
Variáveis sensíveis: carregadas via .env (não versionado). Modelo em .env.example.
Arquivo do workflow: .github/workflows/ci.yml Gatilhos: push e pull_request
O que o pipeline faz:
- Valida o
compose.yaml(docker compose config) - Builda a imagem do serviço
app - Sobe a stack (
docker compose up -d) - Aguarda a app responder e testa criar uma tarefa via API (smoke test do CRUD)
- Derruba a stack (
docker compose down -v, sempre, mesmo se algo falhar)
O que eu quebrei: troquei CMD ["node", "src/index.js"] por CMD ["node", "src/indexx.js"] no Dockerfile (arquivo que não existe).
Erro que apareceu no log: Error: Cannot find module '/app/src/indexx.js'
Como o CI reagiu: o job passou pelo build normalmente (o Docker não valida se o arquivo do CMD existe na hora de buildar), mas falhou no step "Aguardar a aplicação responder" — o container do app subia e morria na hora (MODULE_NOT_FOUND), então o curl nunca conseguia conectar em http://localhost:3000/items, e depois das 30 tentativas o step retornava exit 1.
Como eu corrigi: voltei o CMD para o caminho certo (src/index.js), na mesma branch quebra-proposital, com um segundo commit.
Link do Pull Request: #1
Print 9 — execução vermelha ❌ + log do erro

- Permissão negada rodando como não-root: ao trocar pro usuário
nodeno Dockerfile, o app quebrou comEACCEStentando criar/etc/todos(onde fica o banco SQLite). Entendi que rodar sem root é ótimo pra segurança, mas qualquer pasta que a aplicação precise escrever tem que já existir com o dono certo antes doUSERtrocar — resolvi criando a pasta e dandochownpro usuárionodeainda como root, no próprio Dockerfile. - Confundi container avulso com Compose: no teste de volume, tentei
docker rm -f todoe deu "no such container" — só depois percebi que o que estava rodando era a stack do Compose (meu-projeto-docker-app-1), não o containertodoque eu tinha criado antes. Aprendi na prática que container avulso (docker run) e serviço do Compose são coisas diferentes, mesmo rodando a mesma imagem. - Dado "fantasma" no teste de volume: ao testar a persistência, apareceu uma tarefa que eu não tinha cadastrado — era sobra de um teste anterior no mesmo volume nomeado (
todo-db). Voltou a mostrar que volume nomeado realmente não é apagado sozinho — se reaproveitar o mesmo nome, o dado antigo continua lá, então pra um teste limpo é preciso remover o volume explicitamente. - CI quebrou sem eu mexer no código: depois de só adicionar os prints (imagens) e dar push, o pipeline falhou no build da imagem — parecia não ter relação nenhuma com o que eu tinha mudado. Fui olhar o log e o problema era o
sqlite3(dependência nativa) tentando compilar do zero por falta do Python no estágio de build, algo que só acontece quando o binário pré-compilado não baixa. Foi a maior lição da atividade: o CI pega até os erros que "não deveriam" acontecer, e o log é sempre o primeiro lugar pra olhar antes de suspeitar do que você mudou por último.
- Dockerfile multi-stage funcionando
-
.dockerignorepresente - Container não roda como root
- Volume nomeado + persistência demonstrada
- Rede nomeada + banco não exposto ao host
-
compose.yamlsobe tudo com um comando -
.envno.gitignoree.env.exampleversionado - CI verde
- PR com CI vermelho documentado (#1, merged)
- Todos os 9 prints no README

