Files
adaptador-planilhas/Dockerfile
T
camilafagundesandClaude Opus 5 dea510c3b4 Imagem Docker e compose reproduzíveis para homologação
Não existia nenhum artefato de deploy: sem Dockerfile, sem compose, sem pin de
Node. O runbook pedia `npm run start` e `npm run worker` à mão, o que não é um
ambiente reproduzível, e o worker dependia de `tsx`, uma devDependency, que
`npm ci --omit=dev` removeria.

- Dockerfile multistage. Web e worker saem da mesma imagem: mesmo código,
  mesmas dependências, só o subcomando difere.
- `tsx` foi para `dependencies`. Conferido em diretório limpo: os 13 pacotes
  externos que src e scripts usam existem no install de produção.
- A sobreposição das duas árvores de node_modules é segura e foi medida: 318
  pacotes em produção contra 153 traçados, zero divergência de versão entre os
  compartilhados, e 3 que existem só na traçada, que o COPY mescla sem
  remover. O package.json do standalone não é sobrescrito.
- Entrypoint com subcomandos, que garante `node --import tsx` em todos os
  scripts. Usar `node` direto convidaria de volta o defeito do SIGTERM.
- Healthcheck de liveness na web e de heartbeat no worker.
- Compose local reproduz a topologia com Postgres e Redis de verdade. Compose
  de homologação não declara Postgres (é o banco gerenciado), usa Redis
  isolado com senha e volume próprios, publica porta só em 127.0.0.1 e tem
  nome de projeto, rede e volume exclusivos. Nenhum serviço de outro
  aplicativo do ecossistema é declarado, tocado ou reiniciado.
- `.env.staging.example` sem segredo, `.nvmrc`, `engines` e `packageManager`.
- `config/staging-seed.json` e `.env.staging` no gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 20:39:29 -03:00

87 lines
4.0 KiB
Docker

# Imagem única para os dois processos do módulo: web (Next standalone) e worker.
# Rodar os dois do mesmo artefato garante que web e worker compartilhem
# exatamente o mesmo código e as mesmas dependências em homologação.
# ---------------------------------------------------------------------------
# Base
# ---------------------------------------------------------------------------
FROM node:24-bookworm-slim AS base
ENV NEXT_TELEMETRY_DISABLED=1
WORKDIR /app
# ---------------------------------------------------------------------------
# Dependências de produção. Inclui `tsx`, que o worker e os scripts
# operacionais usam em runtime — por isso `tsx` está em `dependencies`.
# `--ignore-scripts` é seguro aqui: nenhuma dependência de produção depende de
# postinstall, e o binário do esbuild vem do pacote de plataforma.
# ---------------------------------------------------------------------------
FROM base AS deps-prod
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
# ---------------------------------------------------------------------------
# Dependências completas e build
# ---------------------------------------------------------------------------
FROM base AS builder
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
COPY tsconfig.json next.config.ts postcss.config.mjs eslint.config.mjs next-env.d.ts ./
COPY src ./src
COPY migrations ./migrations
COPY scripts ./scripts
# Release do Sentry: o SHA do commit entra no bundle do cliente e no servidor,
# para o stack trace do evento apontar o código certo. Não é segredo.
ARG SENTRY_RELEASE=""
ENV SENTRY_RELEASE=$SENTRY_RELEASE
ENV NEXT_PUBLIC_SENTRY_RELEASE=$SENTRY_RELEASE
# O build não recebe segredo: `collectConfigIssues` é ignorado na fase de build
# e a validação acontece no start do container.
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# ---------------------------------------------------------------------------
# Runtime
# ---------------------------------------------------------------------------
FROM base AS runner
ARG SENTRY_RELEASE=""
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
ENV SENTRY_RELEASE=$SENTRY_RELEASE
ENV ADAPTER_WORKER_HEARTBEAT=/tmp/adaptador-worker-heartbeat
RUN groupadd --system --gid 1001 adaptador \
&& useradd --system --uid 1001 --gid adaptador adaptador
# 1) node_modules de produção primeiro. Traz `tsx` para o worker e os scripts.
COPY --from=deps-prod --chown=adaptador:adaptador /app/node_modules ./node_modules
# 2) Saída autocontida do Next por cima. Onde um pacote existe nas duas árvores,
# a versão traçada do build é a que vale para o servidor. Conferido: as duas
# árvores vêm do mesmo lockfile e não divergem em nenhuma versão; a árvore
# traçada tem 3 pacotes que não ficam no topo da de produção
# (@emnapi/runtime, isarray, supports-color), e o COPY mescla sem removê-los.
# O package.json do standalone NÃO é sobrescrito: é o que o server.js espera.
COPY --from=builder --chown=adaptador:adaptador /app/.next/standalone ./
COPY --from=builder --chown=adaptador:adaptador /app/.next/static ./.next/static
# 3) Arquivos lidos em runtime: migrations pelo migrate, src e scripts pelo
# worker, tsconfig pelos aliases `@/` do tsx.
COPY --chown=adaptador:adaptador migrations ./migrations
COPY --chown=adaptador:adaptador scripts ./scripts
COPY --chown=adaptador:adaptador src ./src
COPY --chown=adaptador:adaptador tsconfig.json ./
COPY --chown=adaptador:adaptador docker ./docker
USER adaptador
EXPOSE 3000
# Liveness não toca dependência externa: distingue processo vivo de dependência
# fora do ar, que é o papel de /api/health/ready. O contêiner do worker troca
# este healthcheck por `worker-health` no compose.
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
ENTRYPOINT ["/app/docker/entrypoint.sh"]
CMD ["web"]