panels-origin/docs/coolify.md
Cursor Agent 4f20c98e5d
docs: host interno de Coolify, no el ID del contenedor Postgres
Liquibase falla con UnknownHostException si DATABASE_URL usa el
CONTAINER ID (p. ej. c72dde7d6b47) en lugar del hostname de Postgres URL (internal).

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

8.1 KiB

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 → docker-compose.yml (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). Suele ser un UUID o el nombre de servicio de Coolify, no los 12 caracteres del CONTAINER ID.

Además, el recurso Docker Compose de PANELS debe estar en la misma red que Postgres y Redis (Connect to Predefined Network, a menudo coolify). Si no, el hostname interno existe pero el contenedor migrate no lo ve.

Tras corregir las URLs, redesplegar y revisar docker logs del contenedor migrate-* (debe terminar con Liquibase Update has been successful o similar, exit 0). El docker stop … No such container del helper de Coolify es ruido de limpieza, no la causa.

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

cp .env.example .env
docker compose -f docker-compose.yml -f docker-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)