Desplegada y verificada de punta a punta contra el dominio público, en Chromium y en WebKit a 375px: sitio, catálogo filtrado, acceso al panel, persistencia de sesión y captura de leads con atribución de campaña. No quedó como recurso de Coolify, y está documentado por qué: la API de esa instancia no expone creación de aplicaciones (las rutas /applications/* responden 404 con cuerpo vacío y /openapi.yaml devuelve el HTML del panel), y al crear un servicio por /services la tarea de despliegue quedó colgada sin escribir nada en disco. El servidor está sano; el problema es la instancia. El contenedor corre en el Docker del LXC 102, en la red coolify, con las etiquetas de Traefik que replican el patrón de las apps que ya sirven con TLS ahí, y con coolify.managed=false para que nadie lo confunda con un recurso del panel. La imagen se construye dentro del LXC desde el repo público de Gitea. El costo de esta ruta está escrito en la documentación: un push no redespliega. Se agregó el procedimiento de tres comandos para publicar una versión nueva sin tocar el volumen, y la vía para migrar a app git-based si se consigue el acceso al panel web. Se documenta también una carrera que provocó un diagnóstico equivocado: el acceso usa una acción de servidor, y pulsar el botón antes de que React hidrate no hace nada ni da error. La primera verificación fallaba solo por el dominio público y funcionaba contra Traefik directo, lo que parecía señalar a Cloudflare. No era: los chunks son idénticos por ambos caminos y la hidratación es igual. Era la espera de la prueba. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
234 lines
10 KiB
Markdown
234 lines
10 KiB
Markdown
# 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.
|
|
|
|
## Cómo está desplegado hoy
|
|
|
|
**En línea:** https://fspm-demo.urieljareth.org
|
|
|
|
No es un recurso de Coolify, y eso es deliberado. La API de esta instancia no
|
|
expone creación de aplicaciones —las tres rutas `/applications/*` responden 404
|
|
con cuerpo vacío, y `/openapi.yaml` devuelve el HTML del panel—, y al crear un
|
|
servicio por `POST /services` la tarea de despliegue quedó colgada sin escribir
|
|
nada en `/data/coolify/services/`. La interfaz web reportaba "No available
|
|
server" aunque el servidor está sano (verificado por API: alcanzable, usable,
|
|
Traefik corriendo, red `coolify` presente, dominio comodín configurado).
|
|
|
|
Así que el contenedor corre directamente en el Docker del LXC 102, en la red
|
|
`coolify`, con las etiquetas de Traefik que replican el patrón de las
|
|
aplicaciones que ya sirven con TLS en ese servidor. Lleva
|
|
`coolify.managed=false` para que nadie lo confunda con un recurso del panel.
|
|
|
|
- Imagen: `fspm-web:demo`, construida **dentro del LXC 102** desde el repo
|
|
público de Gitea. No hay registro de contenedores en juego.
|
|
- Contenedor: `fspm-web`, con `--restart unless-stopped`, así que sobrevive a
|
|
un reinicio del LXC.
|
|
- Volumen: `fspm-datos` montado en `/app/data`.
|
|
- Certificado: lo emite el Traefik que ya estaba, con `letsencrypt`.
|
|
|
|
**La consecuencia operativa importante:** un push a Gitea **no** redespliega.
|
|
Ver "Publicar una versión nueva" abajo.
|
|
|
|
### Publicar una versión nueva
|
|
|
|
Tres comandos desde el host Proxmox (`ssh -i keys/proxmox_ed25519 [email protected]`):
|
|
|
|
```bash
|
|
# 1. traer el código nuevo
|
|
pct exec 102 -- git -C /opt/fspm-src pull
|
|
|
|
# 2. reconstruir la imagen (los NEXT_PUBLIC_* se hornean aquí)
|
|
pct exec 102 -- docker build -t fspm-web:demo --build-arg NEXT_PUBLIC_SITE_URL=https://fspm-demo.urieljareth.org --build-arg NEXT_PUBLIC_WHATSAPP=525540348395 /opt/fspm-src
|
|
|
|
# 3. recrear el contenedor — el volumen NO se toca, los leads se conservan
|
|
pct exec 102 -- docker rm -f fspm-web
|
|
# y volver a correr `deploy/fspm-live.sh` (lleva el AUTH_SECRET sustituido)
|
|
```
|
|
|
|
El script `deploy/fspm-live.sh` del repo tiene el `docker run` completo con las
|
|
etiquetas de Traefik; el `AUTH_SECRET` va como marcador y se sustituye al
|
|
ejecutarlo.
|
|
|
|
### Si algún día se puede usar Coolify
|
|
|
|
Con el correo del panel web se puede crear la aplicación como **Public
|
|
Repository → Dockerfile** apuntando al repo de Gitea, y entonces sí recupera el
|
|
redespliegue automático por push. La especificación exacta de campos está en la
|
|
sección "Coolify" de abajo. En ese momento hay que **eliminar el contenedor
|
|
manual** para que no compitan dos backends por el mismo dominio en Traefik.
|
|
|
|
## 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 <contenedor> --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="<secreto generado>" \
|
|
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=<uuid de la aplicación>"
|
|
```
|
|
|
|
## Una carrera que confunde: hidratación y latencia
|
|
|
|
El acceso al panel usa una acción de servidor. Hasta que React no hidrata la
|
|
página, pulsar el botón **no hace nada y no da error**. Con poca latencia no se
|
|
nota; a través de Cloudflare, sí.
|
|
|
|
Esto costó un diagnóstico equivocado: la primera verificación automatizada
|
|
pulsaba el botón demasiado pronto, fallaba solo por el dominio público y
|
|
funcionaba pegándole a Traefik directo, lo que apuntaba a Cloudflare. No era
|
|
Cloudflare —los chunks de JavaScript son byte a byte idénticos por los dos
|
|
caminos y la hidratación es igual—, era la espera de la prueba. Si alguien
|
|
vuelve a ver este síntoma, antes de culpar al proxy conviene esperar `load` más
|
|
un margen y volver a medir.
|
|
|
|
## 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.
|