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>
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
- 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 →
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). - 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). 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
.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)