# check=skip=SecretsUsedInArgOrEnv # La directiva de arriba tiene que ser la primera línea del archivo. Silencia la # advertencia de BuildKit por el AUTH_SECRET de la etapa de construcción, que es # un relleno para que `next build` no falle: no llega a la imagen final ni se # usa en ejecución. Sin esto, cada construcción sale con una advertencia y se # acaba aprendiendo a ignorar también las advertencias de verdad. # ============================================================================= # FSPM — imagen de producción # # Tres etapas sobre node:22-alpine, aprovechando output: "standalone" de # next.config.ts. La idea de cada etapa: # # deps instala las dependencias en su propia capa, así un cambio de código # no vuelve a bajar node_modules. # build genera el cliente de Prisma, hornea una base de demostración y # compila Next. # runner imagen mínima que solo sabe arrancar el servidor, con usuario no # root y la base de datos en un volumen. # # OJO CON LAS VARIABLES NEXT_PUBLIC_* # Next las sustituye literalmente durante `next build`, no las lee al arrancar. # Por eso NEXT_PUBLIC_SITE_URL y NEXT_PUBLIC_WHATSAPP van como ARG de # construcción: cambiar el dominio exige reconstruir la imagen, no basta con # editar el entorno del contenedor. Está explicado en docs/DESPLIEGUE.md. # ============================================================================= # --- etapa 1: dependencias ----------------------------------------------------- FROM node:22-alpine AS deps # libc6-compat y openssl: Prisma los necesita en Alpine para sus motores. RUN apk add --no-cache libc6-compat openssl WORKDIR /app # El esquema entra antes de npm ci porque el postinstall de @prisma/client # intenta generar el cliente y sin esquema deja una advertencia. COPY package.json package-lock.json ./ COPY prisma/schema.prisma ./prisma/schema.prisma # Aquí NO se define NODE_ENV=production: hacen falta las dependencias de # desarrollo (prisma, tsx, typescript, tailwind) para construir y para sembrar. RUN npm ci # CLI de Prisma aislada, con sus dependencias propias resueltas en su propio # árbol. El punto de entrada del contenedor la necesita para aplicar el esquema # al arrancar, y copiar paquetes suelto por suelto no funciona: la CLI arrastra # dependencias transitivas (effect, entre otras) que no son evidentes. # La versión se lee de package.json, así que no puede desincronizarse. # # Después de instalarla se le quitan dos cosas que no va a usar nunca: los # mapas de código fuente y el motor de consultas. Esta CLI solo ejecuta # `db push`, que usa el motor de esquema; el motor de consultas que la # aplicación necesita es el que viene dentro de la salida standalone. Son unos # 40 MB menos en la imagen final. RUN npm install --no-save --no-audit --no-fund --prefix /prisma-cli \ "prisma@$(node -p "require('/app/package.json').devDependencies.prisma")" \ && find /prisma-cli/node_modules -type f -name "*.map" -delete \ && rm -f /prisma-cli/node_modules/@prisma/engines/libquery_engine-*.so.node # --- etapa 2: construcción ----------------------------------------------------- FROM node:22-alpine AS build RUN apk add --no-cache libc6-compat openssl WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . # Valores por omisión iguales a .env.example: si nadie pasa --build-arg, la # imagen queda coherente en vez de quedar con la cadena vacía, que rompería # `new URL(SITIO_URL)` en el layout raíz. ARG NEXT_PUBLIC_SITE_URL="https://fspm-demo.urieljareth.org" ARG NEXT_PUBLIC_WHATSAPP="525540348395" ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL} ENV NEXT_PUBLIC_WHATSAPP=${NEXT_PUBLIC_WHATSAPP} ENV NEXT_TELEMETRY_DISABLED=1 ENV NODE_ENV=production # `next build` y `prisma generate` exigen que DATABASE_URL exista, aunque no se # conecten a la base real. Este valor es solo de compilación: apunta a la base # de demostración que se hornea aquí mismo y nunca se usa en ejecución. ENV DATABASE_URL="file:/semilla/fspm.db" # Igual con el secreto de sesión: existe para que el build no falle. El # contenedor recibe el suyo por entorno. ENV AUTH_SECRET="secreto-de-compilacion-sin-valor-en-ejecucion" RUN npx prisma generate # --- base de demostración horneada en la imagen --- # # El seed es DESTRUCTIVO: `prisma/seed.ts` hace deleteMany() de todas las tablas # antes de reconstruir. Ejecutarlo dentro del contenedor de producción borraría # los leads capturados en cada reinicio. Por eso se ejecuta aquí, una sola vez, # contra una base que queda dentro de la imagen; el contenedor solo la copia al # volumen cuando el volumen está vacío. Ese es el motivo real por el que `tsx`, # que es dependencia de desarrollo, nunca se instala en la imagen final. RUN mkdir -p /semilla \ && npx prisma db push --skip-generate \ && if [ -f prisma/seed.ts ]; then \ npx tsx prisma/seed.ts; \ else \ echo "AVISO: no existe prisma/seed.ts. La imagen queda con el esquema pero sin datos de demostración."; \ fi RUN npm run build # --- etapa 3: ejecución -------------------------------------------------------- FROM node:22-alpine AS runner # openssl y libc6-compat también aquí: el cliente de Prisma los carga en # ejecución, no solo al construir. RUN apk add --no-cache libc6-compat openssl WORKDIR /app ENV NODE_ENV=production ENV HOSTNAME=0.0.0.0 ENV PORT=3000 ENV NEXT_TELEMETRY_DISABLED=1 # El aviso de "hay una versión nueva de Prisma" sale en cada arranque y solo # entrena a nadie a leer los registros. La versión se actualiza cuando el # proyecto lo decida, no cuando el contenedor lo sugiera. ENV PRISMA_HIDE_UPDATE_MESSAGE=true # Ruta absoluta a propósito. Una relativa como "file:../data/fspm.db" se # resuelve contra la carpeta del esquema y es justo el tipo de detalle que # funciona en local y falla en el contenedor. ENV DATABASE_URL="file:/app/data/fspm.db" RUN addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 --ingroup nodejs nextjs # CLI de Prisma, en su propio árbol para que el punto de entrada pueda aplicar # el esquema al arrancar. Va en /app/prisma-cli, aparte del node_modules de la # aplicación: así no puede pisar el @prisma/client generado que trae la salida # standalone, que es el que la aplicación usa en ejecución. COPY --from=deps /prisma-cli/node_modules ./prisma-cli/node_modules # El esquema tiene que estar presente en ejecución: `prisma db push` lo lee. COPY --from=build /app/prisma/schema.prisma ./prisma/schema.prisma # Base de demostración horneada. Solo se copia al volumen si el volumen no # tiene una base todavía. COPY --from=build /semilla/fspm.db ./semilla/fspm.db # Salida de Next. El standalone va al final para que su @prisma/client, que es # el que la aplicación usa, gane sobre cualquier otro. COPY --from=build /app/public ./public COPY --from=build /app/.next/standalone ./ COPY --from=build /app/.next/static ./.next/static COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh # El directorio de datos nace con dueño nextjs: si el volumen se crea vacío, # Docker hereda estos permisos y el proceso no root puede escribir la base. # /app/prisma-cli entra en el chown a propósito: al arrancar, `prisma db push` # verifica y a veces reescribe sus motores dentro de su propio node_modules. Sin # permiso de escritura el contenedor muere con # "Can't write to /app/prisma-cli/node_modules/@prisma/engines" y el arranque # nunca llega a Next. Se detectó con el contenedor ya construido, no en el build. RUN mkdir -p /app/data \ && chown -R nextjs:nodejs /app/data /app/semilla /app/.next /app/prisma-cli USER nextjs # Aquí vive el SQLite y TIENE que sobrevivir a los redespliegues. Si no se # monta, cada nueva versión de la imagen empieza con la base de demostración y # los leads capturados se pierden. Está explicado en docs/DESPLIEGUE.md. VOLUME ["/app/data"] EXPOSE 3000 # La sonda toca /api/salud, que a su vez consulta la base: así un volumen no # escribible se reporta como contenedor enfermo en vez de pasar por sano. # --start-period cubre el `prisma db push` del arranque más el boot de Next. HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ CMD wget --quiet --tries=1 --spider "http://127.0.0.1:${PORT}/api/salud" || exit 1 ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] CMD ["node", "server.js"]