Reestructuración pedida por el cliente tras revisar la primera versión. Conversión: - Formulario de 14 campos a 4: nombre, correo, teléfono y mensaje. La clasificación de riesgo ya no se pregunta, se deduce del contexto de la página y viaja oculta. Menos fricción, misma riqueza para el análisis. - El inicio deja de bifurcar y empuja al catálogo: 36 llamados desembocan ahí, y las rutas por industria y escenario pasan a ser la forma de entrar al catálogo ya filtrado, en lugar de competir como destino. - Catálogo rehecho como tienda: barra lateral con filtros, búsqueda que acepta expresiones regulares, títulos que abren la ficha, un solo botón primario por tarjeta y el comparador detrás de un desplegable. - Ficha de producto orientada al cierre: bloque en lenguaje llano sobre si el equipo corresponde al caso, y "Cotizar este equipo" con el formulario precargado. Se retiraron los enlaces que sacaban del embudo a leer normas. - Todos los llamados se unifican en "Recibir cotización". - Widget de WhatsApp: nombre, teléfono, correo y mensaje libre. El texto que escribe la persona es el que viaja a la conversación. Doble audiencia: - 13 escenarios con la iconografía original de FSPM traducen un lugar reconocible a clases de fuego y productos, para quien no domina la terminología. La capa técnica se conserva para búsqueda. - Héroe con fotografía en todas las landings. Correcciones: - El contenedor moría al arrancar porque el CLI de Prisma no podía escribir sus motores; el chown ahora incluye /app/prisma-cli. - Iconos repetidos entre escenarios. El set original no tiene icono de cocina, así que ese escenario usa un marcador tipográfico explícito. - FIREMIKS no aparecía en ningún escenario. - Los ids de campo del formulario colisionaban con dos instancias por página. - Se documentó el despliegue, que faltaba por completo. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
189 lines
8.3 KiB
Docker
189 lines
8.3 KiB
Docker
# 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"]
|