Aluno(a): Elyton da Silva Moreira Turma: Noturno Data: 25/07/2026 Aplicação usada: docker/getting-started-app — To-Do em Node.js
Pré-requisitos: Docker Engine + Docker Compose v2 · porta 3000 livre no host.
git clone https://github.com/TomD4vs/project-docker.git
cd project-docker
cp .env.example .env
docker compose up -d --buildAcesse: http://localhost:3000
Para derrubar: docker compose down (mantém dados) ou docker compose down -v (apaga dados).
Pular o build local: para subir a stack usando a imagem que o CD já publicou (seção 7.1) em vez de compilar na sua máquina, troque no compose.yaml a linha build: . do serviço app por:
image: tomd4vs/project-docker:latestComo o pipeline de CD publica a imagem no Docker Hub (seção 7.1), dá para rodar a aplicação em qualquer máquina com Docker sem baixar o código-fonte:
docker run -d -p 3000:3000 --name todo-hub tomd4vs/project-docker:latestAcesse: http://localhost:3000 · Para remover: docker rm -f todo-hub
Sem a variável MYSQL_HOST, a aplicação usa SQLite em /etc/todos em vez do MySQL — por isso ela sobe sozinha, sem precisar de banco nem de rede. Para preservar as tarefas entre execuções, monte um volume:
docker run -d -p 3000:3000 -v todo-hub-data:/etc/todos --name todo-hub tomd4vs/project-docker:latestVersão específica: a tag latest acompanha o último commit da main. Para fixar uma versão exata, use a tag do commit — tomd4vs/project-docker:sha-<commit> — ou o digest, que é imutável. O digest de cada entrega fica no resumo da execução do CD, no GitHub Actions.
Mesma imagem no GHCR: o pipeline publica em dois registries (seção 7.2). Para consumir do GitHub Container Registry, troque o nome da imagem:
docker run -d -p 3000:3000 --name todo-ghcr ghcr.io/tomd4vs/project-docker:latest
docker pull ghcr.io/tomd4vs/project-docker@sha256:c1131ebc37eaaa72bf37b667709ff2c36f4e4d00525b659b95f94612f0a84a01Estágios utilizados: builder (instala dependências com npm ci) e estágio final (runtime enxuto, só node_modules + código-fonte)
Imagem base: node:20-alpine Usuário de execução: node, não-root Tamanho final da imagem: 238MB
Por que o multi-stage ajuda? Cada FROM inicia uma nova cadeia de layers, e a imagem final é composta apenas pelas layers do último estágio — tudo que foi produzido no builder (toolchain python3/make/g++ para compilar os bindings nativos do sqlite3 via node-gyp, cache do npm e artefatos intermediários) é descartado e nunca chega ao registry. O COPY --from=builder traz somente o node_modules já compilado, e o --omit=dev garante que nenhuma devDependency seja instalada, o que reduz o tamanho final e elimina compiladores da superfície de ataque em runtime. Como bônus, copiar package.json/package-lock.json antes do código-fonte mantém a layer do npm ci em cache, invalidada apenas quando as dependências mudam.
Print 1 — build + docker images
Print 2 — aplicação rodando com tarefas cadastradas
Volume usado: todo-db (containers avulsos) / todo-mysql-data (compose) → montado em /etc/todos (SQLite) / /var/lib/mysql (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:
Docker compose down e docker compose down -v: O down remove containers, rede e mantém os volumes nomeados (os dados sobrevivem ao próximo up), enquanto o -v também apaga esses volumes, destruindo permanentemente o banco.
Rede criada: todo-net Serviços conectados: app e db A porta do banco está exposta ao host?
Não. O serviço db no compose.yaml não declara a chave ports:, então a porta 3306 fica acessível apenas dentro da rede todo-net — só o app alcança o banco, e nada é publicado para o host.
Por que o app consegue chamar o host mysql / db sem saber o IP?
Porque o Docker tem um DNS interno na rede todo-net que resolve o nome do serviço para o IP do container.
Print 5 — docker network inspect
Print 6 — dados dentro do MySQL (select * from todo_items;)
Serviços: app, db Rede: todo-net · Volume: todo-mysql-data Healthcheck em: db · depends_on com: condition: service_healthy Variáveis sensíveis: carregadas via .env (não versionado). Modelo em .env.example.
Print 7 — docker compose ps
Arquivo do workflow: .github/workflows/ci.yml Gatilhos: push e pull_request O que o pipeline faz:
- Valida o
compose.yaml - Builda a imagem do serviço
app - Sobe a stack
- Aguarda a app responder e testa criar uma tarefa via API
- Derruba a stack
Print 8 — execução verde
O que eu quebrei: no .github/workflows/ci.yml, no step "Aguardar a aplicação responder", troquei a URL http://localhost:3000/items por http://localhost:3000/items08 — uma rota que não existe na aplicação.
Erro que apareceu no log: A aplicacao nao subiu a tempo, precedido pelas 30 linhas de tentativa 1... até tentativa 30.... No fim do step, o Actions encerrou com Error: Process completed with exit code 1.
Como o CI reagiu: falhou no step "Aguardar a aplicação responder". O curl -sf retorna erro para rota inexistente (HTTP 404), então o laço nunca teve sucesso, esgotou as 30 tentativas e encerrou com exit 1 — marcando a execução como vermelha. A execução levou 2m18s contra os ~50s de uma execução normal.
Como eu corrigi: revertí a URL para http://localhost:3000/items no mesmo step e fiz um novo commit na branch quebra-proposital. O Actions rodou novamente e ficou verde.
Link do Pull Request: #1
Print 9 — execução vermelha + log do erro
Print 9.1 — Corrigindo erro CI execução verde + log do sucess
Arquivo do workflow: .github/workflows/ci.yml, job publish-dockerhub Gatilho: push na branch main, e somente se o job de CI (build-and-test) tiver passado — needs: build-and-test Destino: Docker Hub (docker.io/tomd4vs/project-docker) Autenticação: Secrets DOCKERHUB_USERNAME e DOCKERHUB_TOKEN Tags publicadas: latest e sha-<commit> Pull Request: #5
O que o pipeline de CD faz:
- Confere se os dois Secrets existem e falha com mensagem clara caso algum esteja faltando
- Autentica no Docker Hub com o token de acesso — nunca com a senha da conta
- Monta as tags da imagem:
latestesha-<commit>, para rastrear qual commit gerou qual imagem - Builda e faz push da imagem para o Docker Hub, com cache de layers entre execuções
- Baixa de volta do registry a imagem recém-publicada, sobe ela e chama
/items— se não responder, a entrega falha - Publica um resumo com o digest e o comando de
docker pulljá prontos
Observação: o enunciado cita um arquivo
cd.yml. Neste projeto o CD não ficou num arquivo separado — ele é o jobpublish-dockerhubdentro do próprio.github/workflows/ci.yml, para que oneeds: build-and-testpossa usar o CI como portão. É a esse job que as respostas abaixo se referem.
1. O que é o Docker Hub, na sua visão?
O Docker Hub é um registro público na nuvem para armazenamento e compartilhamento de imagens Docker. Ele funciona de forma semelhante ao GitHub, mas em vez de armazenar código-fonte, armazena pacotes e aplicações prontas em formato de contêiner para que qualquer pessoa consiga baixá-las e rodá-las facilmente. Foi exatamente isso na evidência 5: um docker pull numa máquina sem uma linha do código-fonte do projeto, e a aplicação no ar.
2. Qual a diferença entre o CI (atividade anterior) e o CD (esta)?
O CI (Integração Contínua) foca na validação e qualidade do código, executando testes e verificações automatizadas a cada novo push. Já o CD (Entrega Contínua) entra em ação após a aprovação do código para construir o pacote final (a imagem Docker) e publicá-lo automaticamente no ambiente de destino (Docker Hub).
Dá para ver essa diferença nos dois jobs deste repositório: o CI sobe a stack, testa e joga tudo fora no final (docker compose down -v) — o que ele entrega é um veredito, não um artefato. O CD pega o mesmo código já aprovado e entrega uma imagem versionada e imutável no registry. E um depende do outro: o needs: build-and-test faz o job de publicação nem começar enquanto o CI não passa, então imagem quebrada não chega ao Docker Hub.
3. Por que usamos um token e Secrets em vez de escrever o usuário e a senha no arquivo cd.yml?
Por razões de segurança. O arquivo cd.yml fica visível no histórico do repositório, e expor credenciais nele permitiria o acesso não autorizado à conta do Docker Hub. O uso dos Secrets criptografa os dados sensíveis no GitHub, e o Token é uma chave revogável que limita o acesso apenas ao necessário, sem expor a senha principal.
Vale reforçar dois pontos: apagar a credencial depois não resolve, porque o commit antigo continua no histórico e em todo clone e fork já feitos; e o token que criei tem escopo Read & Write, o mínimo para publicar — se vazar, eu o revogo sozinho, sem trocar a senha da conta nem afetar mais nada. No log da execução (print 14) dá para ver o GitHub mascarando o valor do secret como ***.
4. O que significa a tag latest no endereço da imagem?
A tag latest é uma etiqueta padrão que sinaliza qual é a versão mais recente e atualizada da imagem Docker disponível naquele repositório.
O detalhe importante é que ela é um ponteiro móvel, não uma versão: a cada push na main ela passa a apontar para outra imagem, então o mesmo docker pull ...:latest em dias diferentes pode trazer conteúdos diferentes. Por isso o pipeline publica também a tag sha-<commit> e imprime o digest no resumo da entrega — quando se precisa de uma versão exata e reproduzível, usa-se a tag do commit ou o digest, que é imutável por construção.
Criado em Docker Hub → Account settings → Personal access tokens → Generate new token, com permissão Read & Write — o mínimo necessário para publicar uma imagem. O valor do token aparece uma única vez, no momento da criação; depois só resta revogá-lo e gerar outro.
Cadastrados em Settings → Secrets and variables → Actions → New repository secret: DOCKERHUB_USERNAME com o usuário da conta e DOCKERHUB_TOKEN com o token da evidência 1. O GitHub guarda os dois criptografados e nunca mais os exibe — a tela lista apenas o nome e a data de atualização.
Push na main: o CI roda primeiro e, com ele verde, o CD - publicar imagem no Docker Hub executa em seguida. No log do step Login no Docker Hub o valor do secret aparece como *** — o Actions mascara automaticamente qualquer secret impresso.
Repositório público tomd4vs/project-docker no Docker Hub, com as tags geradas pelo pipeline. A cada entrega o CD cria uma tag sha-<commit> nova e move a latest para ela — por isso as tags sha- se acumulam, uma por publicação, enquanto a latest sempre aponta para a mais recente. É a diferença entre ponteiro móvel e versão fixa, respondida na pergunta 4.
docker pull tomd4vs/project-docker:latest
docker run -d -p 3000:3000 --name todo-hub tomd4vs/project-docker:latest
docker psExecutado fora do CI, em máquina que não tem o código-fonte do projeto: o Docker baixa a imagem pronta do registry e a aplicação sobe direto, sem build. O docker ps confirma o container todo-hub publicando a porta 3000.
E a aplicação respondendo, servida por essa imagem:
O mesmo pipeline publica a imagem também no GitHub Container Registry, no job publish, sob as mesmas condições (needs: build-and-test + só na main). A diferença está na autenticação: o GHCR aceita o GITHUB_TOKEN que o Actions injeta em toda execução, então esse job não precisa de token criado à mão nem de Secret cadastrado — basta a permissão packages: write. Manter os dois registries deixa a comparação explícita: token manual + Secrets no Docker Hub, credencial automática de escopo limitado no GHCR.
Os prints 11 a 11.5 são da execução do commit a407808, quando o GHCR ainda era o único registry do pipeline — por isso aparecem dois jobs, e não três.
Print 11 — o portão em ação: no Pull Request o CI passa e o CD é pulado
Execução disparada por on: pull_request. O job de CI fica verde, mas o CD - publicar imagem no GHCR aparece como Skipped — nada é publicado fora da main.
Print 11.1 — depois do merge na main: pipeline completo e resumo da entrega
Agora o gatilho é on: push na main (commit a407808). Os dois jobs ficam verdes e o CD publica a imagem, gerando o resumo com o digest sha256:c1131ebc... e as tags latest e sha-a407808.
Print 11.2 — o CD baixa de volta a imagem publicada e testa
Step "Smoke test da imagem que acabou de ser publicada": o runner faz docker pull do digest recém-publicado, sobe o container e confirma a resposta em /items. Não é o build que passou — é o artefato final que funcionou.
Print 11.3 — imagem publicada no GHCR
Pacote público, vinculado ao repositório, com as duas tags e o contador de downloads gerado pelo próprio smoke test.
Print 11.4 — docker pull + docker run da imagem em uma máquina fora do CI
O digest baixado (sha256:c1131ebc...) é o mesmo publicado pelo CD e o mesmo testado no smoke test — nada foi rebuildado no caminho.
Print 11.5 — a aplicação servida pela imagem do registry
Minha principal dificuldade foi na primeira configuração do ambiente, pois não tinha trabalhado com a ferramenta. Repeti o processo em outros dois computadores, ganhei prática e, na terceira vez, consegui instalar as ferramentas de forma simples e prática. O aprendizado foi ótimo: já apliquei parte do que vi em projetos em que estou trabalhando e ainda aproveitei as dicas do professor sobre instalação e configurações para Ubuntu Server.
Sobre containers em si, três problemas me ensinaram mais do que o resto da atividade. O docker compose down -v simplesmente não apagava os dados, e a causa era que o volume tinha sido criado por docker run na Parte 3, sem os rótulos que o Compose usa para reconhecer o que é dele. Na mesma parte, o container do app subia e morria em seguida: ele tentava conectar num MySQL que ainda estava inicializando, e o wait-port desiste em 10 segundos — foi o que me levou ao healthcheck com depends_on: service_healthy na Parte 4. E na Parte 2 o container nem iniciava, porque o usuário node não tinha permissão de escrita na pasta do banco, o que só se resolveu com o chown no Dockerfile.
O que ficou claro é que container não é só empacotar código numa imagem. É acertar ordem de dependência entre serviços, permissão de arquivo para um processo que não é root, e o ciclo de vida dos dados — que é justamente a parte que não está dentro da imagem.
- 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
- Todos os 9 prints no README
- Token de acesso criado no Docker Hub
- Secrets
DOCKERHUB_USERNAMEeDOCKERHUB_TOKENcadastrados no GitHub - Workflow de CD verde publicando a imagem com a tag
latest - Imagem visível no Docker Hub e baixada com
docker pull - Perguntas do CD respondidas na seção 7.1
- CD publicando a mesma imagem também no GHCR (extra)






























