# syntax=docker/dockerfile:1.7 ARG NODE_IMAGE=node:24.18.0-bookworm-slim # ============================================================ # 1. Dependencias # ============================================================ FROM ${NODE_IMAGE} AS deps WORKDIR /app ARG DEPS_CACHE_REV=20260724-3 ENV NODE_ENV=development \ NEXT_TELEMETRY_DISABLED=1 \ NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ \ NPM_CONFIG_REPLACE_REGISTRY_HOST=always \ NPM_CONFIG_PACKAGE_LOCK=true \ NPM_CONFIG_PACKAGE_LOCK_ONLY=false \ NPM_CONFIG_DRY_RUN=false \ NPM_CONFIG_GLOBAL=false \ NPM_CONFIG_AUDIT=false \ NPM_CONFIG_FUND=false \ NPM_CONFIG_FETCH_RETRIES=2 \ NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=5000 \ NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=30000 \ NPM_CONFIG_FETCH_TIMEOUT=120000 \ NPM_CONFIG_MAXSOCKETS=8 \ NPM_CONFIG_LOGLEVEL=info COPY package.json package-lock.json .npmrc ./ # ============================================================ # Normalizar registries grabados en package-lock.json # ============================================================ RUN node <<'NODE' const fs = require("node:fs"); const filename = "package-lock.json"; const publicRegistry = "https://registry.npmjs.org/"; let content = fs.readFileSync(filename, "utf8"); let replacements = 0; const internalRegistryPatterns = [ /https?:\/\/packages\.applied-caas-gateway1\.internal\.api\.openai\.org\/artifactory\/api\/npm\/npm-public\//g, ]; for (const pattern of internalRegistryPatterns) { content = content.replace(pattern, () => { replacements += 1; return publicRegistry; }); } fs.writeFileSync( filename, `${content.trimEnd()}\n`, "utf8" ); const normalizedContent = fs.readFileSync( filename, "utf8" ); const forbiddenHosts = [ "internal.api.openai.org", "packages.applied-caas-gateway1", ]; const detectedHosts = forbiddenHosts.filter( (host) => normalizedContent.includes(host) ); if (detectedHosts.length > 0) { console.error( `ERROR: registries no permitidos: ${detectedHosts.join(", ")}` ); process.exit(1); } const packageJson = JSON.parse( fs.readFileSync("package.json", "utf8") ); const lockfile = JSON.parse(normalizedContent); const productionDependencies = Object.keys( packageJson.dependencies || {} ); const developmentDependencies = Object.keys( packageJson.devDependencies || {} ); const declaredDependencies = productionDependencies.length + developmentDependencies.length; const lockedPackages = Object.keys( lockfile.packages || {} ).length; console.log( `Proyecto: ${packageJson.name}` ); console.log( `Dependencias production: ${productionDependencies.length}` ); console.log( `Dependencias development: ${developmentDependencies.length}` ); console.log( `Dependencias declaradas: ${declaredDependencies}` ); console.log( `Entradas package-lock: ${lockedPackages}` ); console.log( `Referencias normalizadas: ${replacements}` ); if (declaredDependencies === 0) { console.error( "ERROR: package.json no contiene dependencias." ); process.exit(1); } if (lockedPackages <= 1) { console.error( "ERROR: package-lock.json no contiene el árbol de dependencias." ); process.exit(1); } NODE # ============================================================ # Validar configuración efectiva de npm # ============================================================ RUN echo "DEPS_CACHE_REV=${DEPS_CACHE_REV}" \ && node --version \ && npm --version \ && echo "registry=$(npm config get registry)" \ && echo "package-lock=$(npm config get package-lock)" \ && echo "package-lock-only=$(npm config get package-lock-only)" \ && echo "dry-run=$(npm config get dry-run)" \ && echo "global=$(npm config get global)" \ && echo "omit=$(npm config get omit)" \ && npm ping # ============================================================ # Instalar dependencias # ============================================================ RUN --mount=type=cache,target=/root/.npm,sharing=private <<'SHELL' set -eu echo "==================================================" echo "Instalando dependencias de ARI Shopping" echo "==================================================" rm -rf /app/node_modules timeout \ --signal=TERM \ --kill-after=30s \ 900s \ npm ci \ --include=prod \ --include=dev \ --include=optional \ --package-lock=true \ --package-lock-only=false \ --dry-run=false \ --global=false \ --no-audit \ --no-fund \ --prefer-online \ --foreground-scripts \ --loglevel=info \ --timing echo echo "==================================================" echo "Validando node_modules" echo "==================================================" test -d /app/node_modules || { echo "ERROR: npm ci no creó /app/node_modules." exit 1 } test -f /app/node_modules/next/package.json || { echo "ERROR: Next.js no fue instalado." exit 1 } test -f /app/node_modules/react/package.json || { echo "ERROR: React no fue instalado." exit 1 } test -f /app/node_modules/typescript/package.json || { echo "ERROR: TypeScript no fue instalado." exit 1 } node -e " console.log( 'Next.js:', require('./node_modules/next/package.json').version ) " node -e " console.log( 'React:', require('./node_modules/react/package.json').version ) " node -e " console.log( 'TypeScript:', require('./node_modules/typescript/package.json').version ) " du -sh /app/node_modules SHELL # ============================================================ # 2. Compilación Next.js # ============================================================ FROM ${NODE_IMAGE} AS builder WORKDIR /app ENV NODE_ENV=production \ NEXT_TELEMETRY_DISABLED=1 \ NODE_OPTIONS=--max-old-space-size=2048 COPY --from=deps \ /app/node_modules \ ./node_modules COPY package.json package-lock.json ./ COPY . . RUN test -d /app/node_modules \ && test -f /app/node_modules/next/package.json \ && test -f /app/node_modules/react/package.json RUN --mount=type=cache,target=/app/.next/cache,sharing=private \ npm run build RUN test -f /app/.next/standalone/server.js \ && test -d /app/.next/static # ============================================================ # 3. Runtime de producción # ============================================================ FROM ${NODE_IMAGE} AS runner WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive \ NODE_ENV=production \ NEXT_TELEMETRY_DISABLED=1 \ HOSTNAME=0.0.0.0 \ PORT=80 RUN apt-get update \ && apt-get install -y \ --no-install-recommends \ ca-certificates \ libcap2-bin \ && rm -rf /var/lib/apt/lists/* \ && groupadd \ --gid 1001 \ nodejs \ && useradd \ --uid 1001 \ --gid 1001 \ --create-home \ --shell /usr/sbin/nologin \ nextjs \ && setcap \ 'cap_net_bind_service=+ep' \ /usr/local/bin/node # El runtime solo ejecuta "node server.js" (standalone output de Next.js); # nunca invoca npm/npx/corepack. Sacarlos del stage final reduce el árbol # de dependencias escaneado por Trivy a lo que realmente corre en # producción, en vez de arrastrar el npm completo de la imagen base # (con sus propias deps y CVEs, ej. CVE-2026-59873 en node-tar). RUN rm -rf \ /usr/local/lib/node_modules/npm \ /usr/local/lib/node_modules/corepack \ /usr/local/bin/npm \ /usr/local/bin/npx \ /usr/local/bin/corepack COPY --from=builder \ --chown=nextjs:nodejs \ /app/public \ ./public COPY --from=builder \ --chown=nextjs:nodejs \ /app/.next/standalone \ ./ COPY --from=builder \ --chown=nextjs:nodejs \ /app/.next/static \ ./.next/static USER nextjs EXPOSE 80 HEALTHCHECK --interval=30s --timeout=8s --start-period=30s --retries=3 CMD ["node","-e","fetch('http://127.0.0.1/api/health').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"] CMD ["node", "server.js"]