# Despliegue Sitio de FSPM con micro CRM. Un solo contenedor: Next.js en modo `standalone` y SQLite en un volumen. No hay base de datos externa. ## Variables de entorno | Variable | Requerida | Qué pasa si falta | |---|---|---| | `DATABASE_URL` | Sí | Prisma no arranca. En el contenedor ya viene fijada a `file:/app/data/fspm.db`; **no la cambies** salvo que muevas el volumen. | | `AUTH_SECRET` | Sí | El arranque falla con un error explícito. Firma las cookies de sesión del panel. | | `NEXT_PUBLIC_SITE_URL` | Sí, **en construcción** | Las URL canónicas, el sitemap y Open Graph apuntan al dominio equivocado. Ver la advertencia de abajo. | | `NEXT_PUBLIC_WHATSAPP` | Sí, **en construcción** | El widget de WhatsApp abre una conversación con un número vacío. | | `DEMO_ACCESO_RAPIDO` | No | Con cualquier valor distinto de `false`, el acceso de un clic por perfil está activo en `/admin/login`. | | `AUTH_COOKIE_INSECURE` | No | Solo para pruebas locales sin TLS. Ver la advertencia de HTTPS abajo. **Nunca en producción.** | | `NODE_ENV` | No | Ya viene en `production` dentro de la imagen. | ### Advertencia sobre las variables `NEXT_PUBLIC_*` Next las sustituye **literalmente durante `next build`**, no las lee al arrancar. Cambiar `NEXT_PUBLIC_SITE_URL` o `NEXT_PUBLIC_WHATSAPP` en el entorno del contenedor **no tiene ningún efecto**: hay que reconstruir la imagen pasándolas como `--build-arg`. Es el error más fácil de cometer aquí, y se manifiesta como un sitemap con el dominio incorrecto y un widget de WhatsApp que no marca a nadie. ```bash docker build \ --build-arg NEXT_PUBLIC_SITE_URL=https://fspm-demo.urieljareth.org \ --build-arg NEXT_PUBLIC_WHATSAPP=525540348395 \ -t fspm-web:demo . ``` ### Generar un `AUTH_SECRET` ```bash node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))" ``` Uno por entorno. No reutilices el de desarrollo: cualquiera que lo tenga puede firmar una cookie de sesión válida y entrar al panel como administrador. ### El panel exige HTTPS La cookie de sesión va marcada como `Secure`, que es lo correcto. La consecuencia es que **si el panel se sirve por HTTP plano, en Safari y en iOS nadie puede entrar**: el navegador se niega a guardar la cookie, el acceso devuelve a la pantalla de inicio de sesión y no dice por qué. Chromium hace una excepción con `localhost`, así que el problema se ve solo en WebKit y es fácil de atribuir a otra cosa. Detrás de Coolify con Cloudflare esto no ocurre, porque el tráfico llega por HTTPS. Importa en dos casos: si alguien expone el contenedor por IP y puerto sin TLS, y al probar en local. Para lo segundo existe `AUTH_COOKIE_INSECURE=true`, que **no debe usarse en un entorno accesible desde internet**. ## El volumen — la parte frágil **Si no montas `/app/data`, cada redespliegue borra los leads capturados.** La base de demostración se hornea dentro de la imagen durante la construcción. Al arrancar, el punto de entrada la copia al volumen **solo si el volumen está vacío**; si ya hay una base, no la toca. Ese es el mecanismo que permite que los leads que llegan por el formulario y por el widget sobrevivan a una nueva versión de la imagen. Sin volumen, el contenedor sigue funcionando —arranca con los datos sembrados—, así que la falla es silenciosa: nadie se entera hasta que un lead real desaparece. En Coolify: **Storages → Add → Volume Mount**, con destino `/app/data`. Verifícalo después del primer despliegue con: ```bash docker inspect --format '{{json .Mounts}}' ``` ## Local con Docker ```bash docker build -t fspm-web:demo . docker run -d --name fspm-demo -p 3100:3000 \ -v fspm-datos:/app/data \ -e AUTH_SECRET="" \ fspm-web:demo curl -s http://127.0.0.1:3100/api/salud ``` `/api/salud` consulta la base, así que un volumen no escribible se reporta como contenedor enfermo en lugar de pasar por sano. ## Coolify Los endpoints `POST /applications/*` de la API devuelven 404 en esta instancia (verificado en 4.1.2 y de nuevo en 4.3.12), así que la aplicación **se crea desde la interfaz web** y solo el despliegue se dispara por API. 1. Crea la aplicación apuntando al repositorio de Gitea, rama `main`, tipo **Dockerfile**. 2. Fija el dominio, para que Coolify no asigne uno aleatorio y devuelva 503. 3. Carga las variables de entorno. Las `NEXT_PUBLIC_*` van también como argumentos de construcción. 4. Monta el volumen en `/app/data`. 5. Despliega y vigila el registro. Despliegues posteriores: ```bash curl -H "Authorization: Bearer $COOLIFY_TOKEN" \ "$COOLIFY_API_URL/deploy?uuid=" ``` ## Verificación posterior al despliegue Hazla en este orden; cada punto descarta una clase distinta de falla. ```bash BASE=https://fspm-demo.urieljareth.org curl -s $BASE/api/salud # {"ok":true,...} curl -s $BASE/robots.txt | grep admin # debe prohibir /admin curl -s $BASE/sitemap.xml | grep -c admin # debe dar 0 curl -sI $BASE/icon | grep -i content-type # image/* curl -sI $BASE/opengraph-image | grep -i content-type curl -s $BASE/sitemap.xml | grep -o 'https://[^<]*' | head -3 # ¿el dominio correcto? ``` Después, a mano: 1. Envía el formulario del inicio y confirma que devuelve folio. 2. Abre el widget de WhatsApp, envíalo y revisa que el enlace lleve el prefijo `[DEMO]`, el folio y el mensaje que escribiste. 3. Entra a `/admin/login` —**no está enlazado desde el sitio**, es intencional— y usa el acceso rápido para ver el panel desde los dos roles. 4. Confirma que la oportunidad de los pasos 1 y 2 aparece en el panel con su campaña y su producto de interés. 5. Redespliega y vuelve a entrar al panel: los leads de los pasos 1 y 2 **deben seguir ahí**. Si desaparecieron, el volumen no está montado. ## Antes de enseñarlo a un cliente - `AUTH_SECRET` distinto del de desarrollo. - `DEMO_ACCESO_RAPIDO=false` si la demo queda publicada sin acompañamiento: con el acceso rápido activo, cualquiera que llegue a `/admin/login` entra como administrador. - Los datos del panel son ficticios y los precios de los pedidos son inventados. Conviene decirlo en voz alta antes de abrir la pantalla de pedidos. ## Notas del entorno de desarrollo - **El repositorio vive en una carpeta sincronizada a la nube.** El cliente de sincronización mueve archivos de `.next` a media compilación y `npm run build` falla con `Cannot find module './NNNN.js'` aunque el código esté correcto. Está reproducido. La construcción dentro de Docker no se ve afectada, porque copia el contexto y compila en el contenedor. Para verificar en local, usa la imagen, no `next build`. - `next.config.ts` acepta `NEXT_DIST_DIR` para mover el directorio de salida, pero Next lo resuelve relativo a la raíz del proyecto y no acepta rutas absolutas, así que no resuelve el problema anterior por sí solo. - El `chown` del Dockerfile incluye `/app/prisma-cli` a propósito: al arrancar, `prisma db push` reescribe sus motores dentro de su propio `node_modules`, y sin permiso el contenedor muere con `Can't write to /app/prisma-cli/node_modules/@prisma/engines`. Se detectó con el contenedor ya construido, nunca durante el build.