El host qzegekm3sr2bevgxl4wh4th2 es el UUID interno de Postgres; solo resuelve en esa red. Sin ella Liquibase lanza UnknownHostException. Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
9.3 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
- Provisionar Postgres, Redis y bucket — 1 de cada por ambiente (no por módulo; no compartir prod con staging).
- Crear accesos y validar:
./db/provision/create-accesses.sh --apply --verify --out .env.<env>.localcontra esos hosts (verdb/provision/README.md). Coolify no crea los rolespanels_*ni los ACL de Redis solo. - Coolify → Docker Compose → solo
docker-compose.yml(Compose Location). No añadasoverlays/compose.local.ymlni ningúndocker-compose.local.yml. El overlay local pidePLATFORM_*_PASSWORDetc.; en Coolify esas 8 variables no se rellenan: los accesos van en lasDATABASE_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). - Cargar en el recurso compose todas las variables obligatorias (incluidas las 3
DATABASE_URL_*_OWNER: las usa el jobmigrate).DENO_ENV=production. - Deploy. En cada deploy corre
migrate(--context-filter=!dev, sin demo) y después arrancaapi. No hace falta entrar al VPS a correr Liquibase. - Bootstrap del primer admin (solo la primera vez):
bootstrap-admin.ts(verdb/README.md). - Persistent storage: volumen
panel-data→/app/dataenapi(solo fallback si no hay R2). - Dominios:
web-panel→ panels;web-saas→ saas;apisin FQDN (proxy/v1). - Verificar
https://app…/v1/health→ok,core,platform,redis.iam,redis.coreystorage(en prodbackend:"s3"). Si algo falla, la API no arranca. - Login SaaS
admin/ tuSEED_PASSWORD(tras el bootstrap). - SMTP en
/smtpo porSMTP_*.
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.
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
.envydata/fuera del git- Secrets distintos a los de desarrollo (incluye los 6 roles Postgres + 2 Redis)
- Backup de
panels_platformYpanels_product(verdb/backups/) -- no solo una - Backup del bucket de Contabo (documentos cifrados)
COOKIE_SECURE=trueen HTTPS- Healthcheck API OK (
/v1/healthcon las 5 conexiones entrue) --context-filterexplí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)