| 1 | [S] | GHCR auth: sudo vs user context | ~/.docker/config.json is per-user |
| 2 | [S] | nginx-proxy network name varies by installation | Check with docker network ls |
| 3 | [S] | Secret URLs must include https:// | Zod z.string().url() rejects without protocol |
| 4 | [S] | Port mapping unnecessary with nginx-proxy | No ports: in staging/prod compose |
| 5 | [S] | DNS must point to the server IP | Let's Encrypt needs HTTP-01 challenge |
| 6 | [S] | Concurrency groups block deploys | cancel-in-progress: false queues |
| 7 | [S] | Lint locally before pushing to develop | Push triggers CD; errors waste cycles |
| 8 | [S] | Re-trigger without workflow_dispatch | gh run rerun or git commit --allow-empty |
| 9 | [B] | bitnami/postgresql image discontinued | Use postgres:17 with POSTGRES_USER |
| 10 | [B] | --skipLibCheck required in build | Prisma client generates conflicting types |
| 11 | [B] | Prettier not installed as dependency | Must be an explicit devDependency |
| 12 | [B] | Zod validation fails in CI | All vars from src/env.ts in the test step |
| 13 | [B] | DATABASE_URL with wrong prefix | Project's Zod requires postgres:// |
| 14 | [B] | Zod vars in Generate .env of CD | Update CI and CD when adding a var in Zod |
| 15 | [B] | VIRTUAL_PORT required for port ≠ 80 | nginx-proxy default is 80 |
| 16 | [B] | continue-on-error is a workaround | Use only temporarily |
| 17 | [B] | server.ts guard for NODE_ENV=test | Prevents EADDRINUSE in tests |
| 18 | [F] | VITE_* are build-time, not runtime | Env vars in the nginx container have no effect |
| 19 | [F] | Docker image is environment-specific | Staging and prod are different images |
| 20 | [F] | build-and-push needs environment: | To access VITE_* secrets as build-args |
| 21 | [F] | No VIRTUAL_PORT for nginx | nginx listens on port 80 (default) |
| 22 | [F] | Healthcheck Alpine: 127.0.0.1 | localhost may resolve to ::1 (IPv6) |
| 23 | [F] | vite.config.ts must be versioned | Without it, bundle without React plugin → blank page |
| 24 | [F] | Vitest collecting Playwright E2E tests | vitest.config.ts with exclude: ['e2e/**'] |
| 25 | [F] | treeshake.moduleSideEffects + circular chunks | Remove custom treeshake and manualChunks |
| 26 | [S] | GHCR login required in deploy job | docker/login-action@v3 before pull (both projects) |
| 27 | [B] | Biome checks all files by default | Use files.includes in biome.jsonc to limit scope to src/ or fix config files |
| 28 | [S] | First deploy requires workflows on develop branch | CD Staging triggers on push to develop — workflows must be on that branch before the first push |
| 29 | [B] | docker run does not auto-pull if the tag exists locally on self-hosted runners | Always docker pull <image> before docker run <image> in migration steps — stale cache causes "no pending migrations" while the app expects new schema |
| 30 | [S] | npm run -w <ws> exec -- é sintaxe inválida em monorepo npm | exec não é script de package.json; usar npm exec -w <ws> -- <cmd>. Falha cedo (Missing script: "exec") e mascara steps subsequentes |
| 31 | [S] | ESLint v9 flat config é per-workspace, não herda | Cada workspace que rode eslint precisa do próprio eslint.config.{js,mjs,cjs} — bump pra v9 num workspace não dá config aos siblings |
| 32 | [S] | devDep com subtree em versões antigas não hoista em monorepo npm | npm aninha o subtree em packages/<ws>/node_modules/X, fora do alcance da resolução Node ESM partindo de outra dep hoisted. Diagnóstico: comparar node_modules/X (raiz) vs packages/<ws>/node_modules/X no lock |
| 33 | [F] | vitest 3 + msw v2 + jsdom esconde 2 bugs latentes | Hoisting (jsdom@20 não hoista) + AbortSignal mismatch (jsdom injeta primitivas próprias incompatíveis com undici nativo). happy-dom resolve ambos: subtree leve hoista limpo + AbortController nativo do Node |
| 34 | [S] | compose run --rm orphan + nginx-proxy = upstream pool poisoning | One-off compose run herda VIRTUAL_HOST do serviço; se --rm falha (CI cancel / OOM / daemon restart), órfão fica registrado pelo docker-gen no upstream pool e recebe round-robin com config stale. up -d --remove-orphans NÃO cobre (mesmo serviço, suffix-hash). Fix: -e VIRTUAL_HOST= -e LETSENCRYPT_HOST= no compose run + step pre-rolling docker rm -f em *-run-*. Diagnóstico: 20 hits paralelos = split de status codes |
| 35 | [S] | secrets.RUNNER_REGISTRATION_TOKEN estática é equilíbrio frágil — chicken-and-egg quando quebra | Registration tokens vencem em 1h; design só funciona porque compose up detecta no-diff entre deploys e pula recriação do runner service. Qualquer evento que force re-registro (host restart, OOM, ephemeral ciclando) → config.sh com token vencido → 404 → crashloop. Deploy fica queued sem runner, runner não sobe sem deploy. Recovery: rotacionar GH secret + apagar registro fantasma + subir via compose -p <project> up -d --no-deps runner (mesmo token e labels match). Fix permanente: token a quente no workflow OU PAT no compose centralizado |
| 36 | [S] | GHCR TLS handshake timeout vs unauthorized — não são o mesmo bug | unauthorized = TLS completou, credencial rejeitada (rotacionar PAT). TLS handshake timeout = TCP conectou mas handshake não completou — credencial é irrelevante. Isolation key: se build-and-push em ubuntu-latest passa mas deploy em self-hosted falha, GHCR está saudável → problema é rede do host runner (MTU em VPN/overlay drops Certificate frames; ou proxy corporativo de TLS inspection). Fix imediato: bash retry wrapper no step de login (3x, backoff 10s/20s) — absorve flake transiente. Fix root: mtu: 1400 em /etc/docker/daemon.json + restart docker. docker/login-action@v3 não tem retry nativo |
| 37 | [B] | Monorepo cujos workspaces shared exportam TS source → imagem roda via tsx, não node dist/ | Pacotes @scope/shared-* com main: ./src/index.ts (TS cru, convenção de import com extensão .js): o dist/ compilado morre em runtime com ERR_MODULE_NOT_FOUND/ERR_UNKNOWN_FILE_EXTENSION ao resolver o .ts do sibling — tsc não inlina workspace deps e node puro não carrega .ts. Fix: o estágio runtime roda tsx src/index.ts (esbuild), igual ao dev. NÃO repontar o exports do shared p/ dist — quebra o Vite/bundler do frontend que consome o source. Ver troubleshooting-backend.md |
| 38 | [F] | tsc --noEmit é VAZIO em tsconfig com project references (files: []) — gate de CI falso | O tsconfig.json raiz padrão de Vite/Lovable tem "files": [] + references p/ tsconfig.app.json. tsc --noEmit então checa ZERO arquivos e sai 0 — typecheck verde-fake. Usar tsc -b --noEmit p/ checar de fato os projetos referenciados. Corolário: introduzir esse gate num projeto que só rodava vite build (esbuild, sem typecheck) revela um backlog de erros de tipo latentes. Ver troubleshooting-frontend.md |
| 39 | [B] | Workspace importa sibling NÃO declarado (só hoist resolve) → scoped npm ci -w quebra no Docker | Ex.: o frontend importa @scope/shared-api-types sem declará-lo no package.json; funciona local pelo hoist do workspace, mas npm ci -w @scope/frontend no build Docker não cria o symlink → Vite build falha em resolver. Fix: npm ci cheio no estágio builder (descartado — só dist/ vai à imagem final, tamanho irrelevante). Distinto da lesson 32 (subtree não-hoistável) |
| 40 | [S] | USER node + named volume novo = write falha sem mkdir+chown na imagem ANTES do USER | Docker inicializa um named volume novo a partir do conteúdo e da ownership do path na imagem. Se o dir não existe (ou é root-owned), o volume monta root-owned e o user não-root não grava (PDFs/storage → EACCES). Fix: RUN mkdir -p /app/storage && chown -R node:node /app/storage antes do USER node. (Só vale p/ named volumes — bind mounts não copiam ownership.) |
[S] | ~50% das requests autenticadas retornam 401 mesmo com JWT comprovadamente válido (200 quando replay direto via curl) | Container órfão de compose run --rm antigo (ex.: prisma migrate deploy que não disparou --rm por CI cancelado / OOM) ainda Up, herdou VIRTUAL_HOST do serviço, registrado pelo docker-gen no upstream pool do nginx-proxy. Round-robin envia ~50% pra config stale. Confirmação: 20 hits paralelos com mesmo token → split de status codes. Ver cd-pipeline-pitfalls.md §4 | |
| 41 | [S] | Wrapper como PID 1 no container engole SIGTERM → sem shutdown gracioso | CMD npx tsx …/npm start/npm run … deixa o npx/npm como PID 1; ele forka o processo real e NÃO repassa SIGTERM. No docker stop/redeploy o filho nunca recebe o sinal → SIGKILL após o grace period (sem drain de conexões, sem $disconnect() do Prisma). Fix: init: true no service do compose (um init tipo tini reapeia zumbis e repassa sinais), ou ENTRYPOINT ["tini","--"] na imagem. Ver cd-pipeline-pitfalls.md §8 |
| 42 | [B] | Corolário da 37: tsx/prisma em dependencies deixam a imagem de runtime usar --omit=dev | Se as ferramentas que o runtime/migrate precisam (tsx, Prisma CLI) ficam em devDependencies, não dá p/ enxugar a imagem — npm ci --omit=dev as removeria e quebraria o boot/migrate. Movendo-as p/ dependencies, o estágio runtime roda npm ci --omit=dev e o test tooling pesado (vitest, testcontainers, supertest, typescript) sai da imagem. O builder segue com npm ci cheio (lesson 39) p/ generate/typecheck. Verificar com docker run … ls node_modules. Ver troubleshooting-backend.md |
| 43 | [S] | CI gate duplicado entre ci.yml e re-gate de cd-staging.yml → extrair composite action | Os mesmos passos (setup-node + install + lint/typecheck/test) copiados nos dois workflows driftam. Extrair .github/actions/<gate>/action.yml (composite) como fonte única. Pegadinhas: uses: ./.github/actions/… exige actions/checkout ANTES no job chamador (o composite NÃO faz checkout); os nomes de job permanecem contratuais p/ required checks; validar com actionlint. Ver troubleshooting-shared.md §10 |
| 44 | [S] | CI só com trigger pull_request deixa push direto a branch protegido escapar do gate | Se o ci.yml dispara só em pull_request, um push direto a develop/main (admin, ou branch protection sem "require status checks") NÃO roda lint/typecheck/test. Fix: trigger push: nos branches protegidos (gate roda no merge) E/OU impor branch protection com required checks. staging fica de fora se já houver um cd-staging.yml com CI gate próprio. Ver checklist-shared.md §5 |
| 45 | [S] | Pinar imagem base por digest: descobrir o @sha256: sem pull cheio | Aplique às imagens do app (node, nginx, postgres). Tag flutuante (node:22-alpine, postgres:17) re-resolve no rebuild → não reprodutível. Descobrir o digest sem baixar a imagem: docker buildx imagetools inspect <img> | grep Digest (lê só o manifest); depois FROM img:tag@sha256:… (compose: image: img:tag@sha256:…). Exceção: a imagem do RUNNER — pinar por digest sem cadência de bump é contraproducente; o GitHub força currency e a versão congela até deprecar (lição 49 / §8a). Ver self-hosted-runner-docker.md |
| 46 | [S] | §7 (registration token chicken-and-egg) RECORRE porque o recovery não é cura — o fix durável é migrar p/ ACCESS_TOKEN (PAT) in-place | O recovery (rotacionar token + recriar) compra só ~1h, e EPHEMERAL:false NÃO previne: o entrypoint custom limpa .runner e re-registra a cada restart, então qualquer restart bate no token vencido (RestartCount em milhares). Fix: manter o runner no compose do produto e trocar RUNNER_TOKEN: ${...} → ACCESS_TOKEN: ${RUNNER_ACCESS_TOKEN:-} + RUNNER_SCOPE: repo; entrypoint passa a aceitar ACCESS_TOKEN OU RUNNER_TOKEN; PAT vive SÓ no .env persistente do host (nunca GH secret). Pegadinhas: gh NÃO cunha PAT (só web UI; gh auth token com escopo repo serve de stopgap mas acopla ao login); valide o PAT (GH_TOKEN=… gh api .../registration-token) ANTES de recriar; prove a cura com docker restart (re-registra sem 404). ACCESS_TOKEN cura SÓ o §7 — não imuniza contra §8 (binário deprecado) nem §9 (config stale), que são ortogonais ao modelo de credencial. Ver self-hosted-runner-docker.md §7 → "Migração ACCESS_TOKEN in-place" |
| 47 | [S] | Runner crashloopa com Runner version vX is deprecated and cannot receive messages mesmo conectando OK | Distinto do §7 (token) e do §9 ("registration deleted"): o runner registra, conecta e lista jobs, então o GitHub recusa entregar trabalho porque o binário foi deprecado. Causa: imagem :latest baixada uma vez e nunca re-puxada + auto-update desligado → binário apodrece. Tell: Up <segundos> mas RestartCount milhares; status pisca online/offline. Fix imediato: docker compose pull + up -d --force-recreate; durável: ligar auto-update. O job queued é pego AUTOMATICAMENTE quando o runner volta online (sem gh run rerun). Ver self-hosted-runner-docker.md §8 |
| 48 | [S] | DISABLE_AUTO_UPDATE é footgun — qualquer valor não-vazio (até "0"/"false") DESLIGA o auto-update | O entrypoint do myoung34 faz [ -n "${DISABLE_AUTO_UPDATE}" ] → presença de QUALQUER string ativa --disableupdate. Para LIGAR o auto-update (e evitar a lição 47), REMOVER a variável do compose, não setá-la "0". Aplicar exige recriar o container (up -d --force-recreate); confirmar com docker exec <runner> printenv DISABLE_AUTO_UPDATE (não deve retornar nada). Ver self-hosted-runner-docker.md §8a |
| 49 | [S] | Pinar a imagem do RUNNER por digest é contraproducente sem cadência de bump (exceção à lição 45) | A lição 45 (pin por digest) vale p/ node/nginx/postgres, mas o GitHub força currency de versão do runner: um digest congelado deprecia em ~1–2 meses e cai na lição 47 (§8). Escolha consciente: (a) :latest + auto-update ligado, OU (b) pin por digest + cron/rotina mensal de docker compose pull. Reuso de config (CONFIGURED_ACTIONS_RUNNER_FILES_DIR + named volume) ressuscita credencial morta após o GitHub apagar registro de runner offline demais → §9; fix é docker volume rm <config-volume> (limpa estado LOCAL, distinto do §6). Ver self-hosted-runner-docker.md §8a / §9 |
| 50 | [S] | §7/§8/§9 podem EMPILHAR no mesmo runner — cada fix desmascara o próximo (um único deploy queued exigiu os três em sequência) | O modelo "case o log → aplique o único fix" é insuficiente quando as falhas se sobrepõem. docker volume rm (§9, config morta) desmascara um ACCESS_TOKEN expirado — invisível enquanto o reuso de config nunca exercia o PAT — e o PAT novo desmascara o binário deprecado (§8). Descascar de baixo p/ cima (config → token → binário) e re-ler os logs após cada fix (a assinatura muda). Triagem: gh api …/actions/runners lista os offline também → runner ausente (não "offline") = registro apagado/§9. O PAT é host-wide (um expira → derruba todos os runners do host; um PAT novo + up -d --force-recreate conserta todos). Ver self-hosted-runner-docker.md §10 |
| 51 | [S] | Deploy self-hosted queued é SILENCIOSO — timeout-minutes não limita tempo em fila; §7–§10 ensinam a consertar, não a SER ALERTADO | O timeout-minutes de um job self-hosted só começa após um runner pegar o job — sem runner, fica queued sem ❌/timeout/e-mail (num incidente passou ~5 semanas despercebido; o site seguia no ar com a imagem antiga). Detecção em 2 camadas, ambas em ubuntu-latest: (a) preflight gate antes do deploy que lista /actions/runners e falha rápido se não há runner online com o label — gotcha: GITHUB_TOKEN NÃO lista runners (exige admin), precisa de PAT Administration: Read; degrade p/ no-op sem o secret e fail-open em erro de PAT (com set -e + ONLINE=$(gh api …) o gate bloqueia TODO deploy se o PAT expira/rotaciona, com msg enganosa "no runner" — use if ! ONLINE=$(…) e exit 0 no erro de API); (b) watchdog agendado (cron) que detecta deploy preso — gotchas: cheque status de JOB (o run fica in_progress com o job queued; ?status=queued no run erra) e schedule só roda do branch default. É detecção, não cura (root-cause segue host-side). Ver self-hosted-runner-docker.md §11 |
| 52 | [B] | (Django) ALLOWED_HOSTS sem 127.0.0.1/localhost → healthcheck interno 400 → container nunca fica healthy | O HEALTHCHECK bate em http://127.0.0.1:8000/healthz/ com Host 127.0.0.1; o Django responde 400 a Host fora de ALLOWED_HOSTS, então o wait-healthy do CD estoura mesmo com login/pull/migrate OK. Isolation key: log do container mostra GET /healthz/ 400. Fix: ALLOWED_HOSTS=<dominio>,localhost,127.0.0.1. Ver django-backend.md |
| 53 | [B] | (Django) admin/sessão dá 403 CSRF sob HTTPS atrás de nginx-proxy (a API JWT funciona) | O TLS termina no proxy → o Django não enxerga HTTPS e rejeita o POST do admin/login por CSRF. Setar SECURE_PROXY_SSL_HEADER=('HTTP_X_FORWARDED_PROTO','https') + CSRF_TRUSTED_ORIGINS=https://<dominio>. A SPA via JWT não precisa — só admin/sessão. Ver django-backend.md |
| 54 | [B] | (Django) imagem prod: gunicorn + collectstatic em build + WhiteNoise; healthcheck sem curl; migrate one-off | collectstatic --noinput em build-time exige SECRET_KEY (DEBUG=False) mas NÃO acessa DB → passe um SECRET_KEY dummy só no RUN. WhiteNoise CompressedStaticFilesStorage (NÃO Manifest…, que quebra dev DEBUG=True sem collectstatic) serve o estático do admin sob gunicorn. python:slim não tem curl/wget → HEALTHCHECK via python -c "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8000/healthz/')". Migração é one-off (compose run --rm backend python manage.py migrate --noinput), análogo ao prisma migrate deploy. Ver django-backend.md |
| 55 | [S] | GHCR push falha em org com Maiúscula (ChewieSoft) | github.repository_owner preserva a caixa, mas paths de imagem GHCR têm de ser lowercase. Lowercale antes de montar a tag: owner=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') → ghcr.io/$owner/<img> |
| 56 | [S] | docker compose run manual no host dá unauthorized no GHCR, mas o deploy via runner funciona | Pacotes GHCR nascem privados mesmo em repo público; o deploy puxa porque o runner faz docker login, mas um one-off manual no host não está logado. Para seed/manutenção use docker exec no container já rodando (sem re-pull do GHCR), ou docker login ghcr.io antes do compose run/pull |
| 57 | [S] | Commit só de documentação redeploya à toa | on.push.paths-ignore: ['**.md','docs/**'] pula o CD em mudanças só de doc. Nuance: paths-ignore só pula quando TODOS os arquivos do push batem — um commit que toque em código/infra junto com docs ainda roda o deploy |
| 58 | [S] | O "fix durável" §7 (migrar p/ PAT dedicado) TEM PRAZO — um PAT com expiração recai no §10 PAT-401 na cadência de vencimento (visto RECORRER ~1 mês depois) | Migrar RUNNER_TOKEN→ACCESS_TOKEN cura o crashloop de registration token (404), mas o PAT gravado no .env é ele próprio expirável: quando vence, mesmo crashloop com log DIFERENTE — curl (22) 401 / Invalid configuration provided for token ao Obtaining the token (RestartCount milhares) + deploy queued silencioso (§11). Standalone (não só empilhado no §10) e, no modelo per-produto (cada app com seu runner+.env em infra/*/), isolado a um runner — os outros do host seguem UP (o "host-wide" do §10 vale só p/ compose CENTRALIZADO). Recovery-higiene: o log do runner já é prova definitiva — NÃO re-extraia+curle o secret (classifier de agente bloqueia como exfiltração); valide o PAT CANDIDATO via gh api …/registration-token (sessão do gh, sem tocar no secret cru) e grave via stdin. Fix de fato durável: PAT sem expiração OU lembrete/cron de rotação. Ver self-hosted-runner-docker.md §10a |