panels-origin/docs/coolify.md
Cursor Agent ccf86c5b2e
api: etiquetar fallos de arranque (postgres, redis, s3)
Tras Liquibase, Coolify marca api unhealthy si el fail-fast tira
antes de Deno.serve. Los logs ahora dicen qué ping falló.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-03 05:32:06 +00:00

10 KiB
Raw Blame History

Deploy PANELS en Coolify (Docker Compose + Postgres + Redis)

Resumen

Qué Cuánto
Postgres gestionado en Coolify 1 instancia por ambiente, con 2 bases: panels_platform, panels_product (esquemas iam/core)
Redis gestionado en Coolify 1 instancia por ambiente, con 2 usuarios ACL (panels_iam_redis, panels_core_redis)
Object storage Cloudflare R2 (S3-compatible) para expedientes/PDFs/logos cifrados
Contenedores de la app 4 (migrate un shot por deploy + api + web-panel + web-saas)
Volumen persistente 1 → /app/data en api (solo fallback local si no hay Contabo configurado -- no usar así en producción)

Los fronts (Alpine + nginx) hacen proxy de /v1 al servicio api, así las cookies de sesión van same-origin.

Ver el plan de migración para el detalle de arquitectura: por qué panels_platform es una base separada, por qué iam/core son esquemas distintos dentro de panels_product, y las reglas del monolito modular.

Datos sensibles (qué NO va al git)

Ítem Estado
.env gitignored — no commitear
data/ (fallback local de archivos, si no hay Contabo) gitignored
SMTP password en SaaS vive en panels_platform.smtp_settings — proteger backups
Defaults de desarrollo en código SESSION_SECRET/DOCS_KEY solo tienen fallback si DENO_ENV no es production -- fuera de eso, la app falla al arrancar si faltan

Variables de entorno (servicio api)

Obligatorias

Variable Formato Uso
SESSION_SECRET string largo aleatorio Firma interna (no ya la cookie -- la sesión vive en Redis, ver Fase 4)
DOCS_KEY 64 caracteres hex (32 bytes) Cifrado AES-GCM de documentos
SEED_PASSWORD string Password inicial usado por api/scripts/bootstrap-admin.ts
PANEL_LOGIN_URL URL absoluta Link en correos de acceso
DATABASE_URL_PLATFORM postgresql://panels_platform_app:...@host:5432/panels_platform Runtime, rol _app
DATABASE_URL_PLATFORM_OWNER igual, rol _owner Job migrate (Liquibase SaaS)
DATABASE_URL_IAM postgresql://panels_iam_app:...@host:5432/panels_product Runtime, rol _app, esquema iam
DATABASE_URL_CORE postgresql://panels_core_app:...@host:5432/panels_product Runtime, rol _app, esquema core
DATABASE_URL_IAM_OWNER igual, rol _owner Solo para el lookup de login por username (bypassa RLS a propósito, ver api/iam_db.ts)
DATABASE_URL_CORE_OWNER igual, rol _owner Solo para resoluciones administrativas puntuales (ver api/db.ts#getCoreDb)
REDIS_URL_IAM redis://panels_iam_redis:...@host:6379 Sesiones
REDIS_URL_CORE redis://panels_core_redis:...@host:6379 Cache

Generar secretos:

openssl rand -hex 32   # SESSION_SECRET o DOCS_KEY
openssl rand -hex 24   # passwords de roles Postgres/Redis

Recomendadas (HTTPS / Coolify)

Variable Default compose Uso
COOKIE_SECURE true Cookie solo por HTTPS
PORT 8000 Puerto interno API
CORS_ORIGINS vacío Lista https://a,https://b si la API se llama cross-origin; con proxy /v1 en nginx no hace falta

Opcionales

Variable Uso
API_KEY Auth alternativa por header X-API-Key + X-Tenant-Id obligatorio (ya no ve todos los tenants, ver revisión de seguridad)
VCARD_BASE Prefijo QR/vCard gafetes
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM Correo por env (alternativa al panel /smtp)
S3_ENDPOINT / S3_BUCKET / S3_REGION / S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY Cloudflare R2. Endpoint https://<ACCOUNT_ID>.r2.cloudflarestorage.com, S3_REGION=auto. Sin esto, cae a disco local -- no recomendado en producción

web-panel y web-saas no necesitan variables de entorno en runtime (estáticos + proxy nginx).

Imágenes

  • api: Deno 2.9, sin FFI ni JRE (Fase 2/7 del plan de migración).
  • migrate: JRE 21 + Liquibase -- imagen aparte, de un solo uso, NO sirve tráfico (ver Dockerfile.migrate).
  • web-panel / web-saas: build Node Alpine → nginx Alpine.

Archivos: Dockerfile.api, Dockerfile.migrate, Dockerfile.provision, Dockerfile.web-panel, Dockerfile.web-saas, docker-compose.yml, web-panel/nginx.conf, web-saas/nginx.conf.

En Coolify el servicio de PANELS se llama web-panel (FQDN ej. panels.mrdev.mx). La carpeta web/ queda libre para una website futura.

Paso a paso en Coolify

  1. Provisionar Postgres, Redis y bucket — 1 de cada por ambiente (no por módulo; no compartir prod con staging).
  2. Crear accesos y validar: ./db/provision/create-accesses.sh --apply --verify --out .env.<env>.local contra esos hosts (ver db/provision/README.md). Coolify no crea los roles panels_* ni los ACL de Redis solo.
  3. Coolify → Docker Compose → solo docker-compose.yml (Compose Location). No añadas overlays/compose.local.yml ni ningún docker-compose.local.yml. El overlay local pide PLATFORM_*_PASSWORD etc.; en Coolify esas 8 variables no se rellenan: los accesos van en las DATABASE_URL_* / REDIS_URL_* ya expandidas. Este archivo no levanta Postgres/Redis; usa los recursos que ya creaste. Conecta el stack a la misma red que Postgres y Redis (Connect to Predefined Network).
  4. Cargar en el recurso compose todas las variables obligatorias (incluidas las 3 DATABASE_URL_*_OWNER: las usa el job migrate). DENO_ENV=production.
  5. Deploy. En cada deploy corre migrate (--context-filter=!dev, sin demo) y después arranca api. No hace falta entrar al VPS a correr Liquibase.
  6. Bootstrap del primer admin (solo la primera vez): bootstrap-admin.ts (ver db/README.md).
  7. Persistent storage: volumen panel-data → /app/data en api (solo fallback si no hay R2).
  8. Dominios: web-panel → panels; web-saas → saas; api sin FQDN (proxy /v1).
  9. Verificar https://app…/v1/health → ok, core, platform, redis.iam, redis.core y storage (en prod backend:"s3"). Si algo falla, la API no arranca.
  10. Login SaaS admin / tu SEED_PASSWORD (tras el bootstrap).
  11. SMTP en /smtp o por SMTP_*.

Troubleshooting: UnknownHostException / Liquibase no conecta

El host de las 6 DATABASE_URL_* y de las 2 REDIS_URL_* no es el ID corto del contenedor (docker exec -it c72dde7d6b47 …). Docker DNS en Coolify no resuelve ese ID; Liquibase falla con UnknownHostException.

Usa el hostname de Postgres URL (internal) / Redis URL (internal) en Coolify: el segmento entre @ y :5432 (o :6379). Un UUID tipo qzegekm3sr2bevgxl4wh4th2 es el host correcto; un CONTAINER ID de 12 caracteres (c72dde7d6b47) no.

Ese UUID solo resuelve en la red Docker coolify. docker-compose.yml une migrate y api a esa red (networks.data). En Coolify deja también Connect to Predefined Network. Redis tiene otro UUID (el de Redis URL internal), no el de Postgres.

Si tras unir la red sigue UnknownHostException, en el VPS:

sudo docker network inspect coolify | grep -E 'Name|Aliases|qzegekm3'

Usa el alias que aparezca junto al contenedor de Postgres (a veces postgres-<uuid>).

El log de deploy de Coolify no incluye stdout de Liquibase. Si service "migrate" didn't complete successfully: exit 1, el error real está en:

sudo docker logs migrate-<uuid-del-recurso>-<timestamp>

Ejemplo: sudo docker logs migrate-pdyrt8ccp804tfmutefau0k6-052025710094. UnknownHostException con un UUID largo = migrate fuera de coolify, no un host mal copiado. Liquibase OK + exit 0 = bien.

El docker stop … No such container del helper de Coolify es ruido de limpieza, no la causa. El build de imágenes puede ser OK y el deploy igual falla en migrate.

dependency api failed to start / api is unhealthy

Liquibase ya corrió si ves migrate … Exited y acto seguido api … Starting. La API no llega a escuchar si Redis, S3 o los roles _app fallan (fail-fast antes de Deno.serve). Coolify entonces marca unhealthy en 1–2 s.

sudo docker logs api-<uuid>-<timestamp>

Busca [startup] FAIL …. Causas típicas:

  • Redis: REDIS_URL_IAM / REDIS_URL_CORE con el UUID de Postgres. Redis tiene el suyo (Redis URL internal).
  • S3/R2: faltan S3_* o HeadBucket falla (token Account API, Object Read & Write, bucket correcto, S3_REGION=auto).
  • Postgres _app: Liquibase usa *_OWNER; el runtime usa DATABASE_URL_PLATFORM, DATABASE_URL_IAM, DATABASE_URL_CORE (passwords distintos).

Coolify pide PLATFORM_OWNER_PASSWORD, IAM_APP_PASSWORD, *_REDIS_PASSWORD…

Eso sale de Reload Compose mezclando el overlay de desarrollo. En producción no las rellenes. En el recurso: Compose file = docker-compose.yml únicamente. Borra esas 8 variables si Coolify las marcó Required. Siguen haciendo falta las URLs completas (DATABASE_URL_*, REDIS_URL_*).

Local (todo en docker-compose, incluyendo Postgres/Redis propios)

cp .env.example .env
docker compose -f docker-compose.yml -f overlays/compose.local.yml up --build

El overlay local añade Postgres/Redis + provision y corre Liquibase con context=dev (demo). En Coolify solo se usa docker-compose.yml: migrate con !dev.

Sin las variables obligatorias, compose no arranca (:? en docker-compose.yml).

Checklist ops

  • .env y data/ fuera del git
  • Secrets distintos a los de desarrollo (incluye los 6 roles Postgres + 2 Redis)
  • Backup de panels_platform Y panels_product (ver db/backups/) -- no solo una
  • Backup del bucket de Contabo (documentos cifrados)
  • COOKIE_SECURE=true en HTTPS
  • Healthcheck API OK (/v1/health con las 5 conexiones en true)
  • --context-filter explícito en cada corrida de Liquibase contra staging/producción

Qué no hace falta

  • Contenedor monolítico Node+Deno+Java sirviendo tráfico (Liquibase vive en su propia imagen, de un solo uso)
  • FFI de SQLite (ya no hay SQLite en ningún ambiente)