# 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 # 4) Ponto de montagem do armazenamento local já com o dono certo. Um volume # nomeado VAZIO herda dono e modo deste diretório na primeira montagem. Sem # ele, o Docker cria /app/.data como root:root 755 e o uid 1001 não escreve # (STORAGE_NOT_WRITABLE). Volumes que já existem não herdam nada: para eles # há o serviço storage-init do compose. RUN mkdir -p /app/.data/storage \ && chown adaptador:adaptador /app/.data /app/.data/storage \ && chmod 750 /app/.data /app/.data/storage 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"]