mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 14:53:18 +00:00
Cloudflare R2 suele devolver 403 en HeadBucket con token acotado al bucket, y eso tumba el fail-fast de arranque. El ping ahora lista o escribe una sonda y registra el error S3. Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
166 lines
10 KiB
Markdown
166 lines
10 KiB
Markdown
# 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:
|
||
|
||
```bash
|
||
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`](../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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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.
|
||
|
||
```bash
|
||
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:** endpoint `https://<ACCOUNT_ID>.r2.cloudflarestorage.com` (sin barra final, **sin** el nombre del bucket), `S3_REGION=auto`, token **Account API** con Object Read & Write al bucket. R2 a menudo responde 403 a HeadBucket; el ping de la API usa ListObjects.
|
||
- **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)
|
||
|
||
```bash
|
||
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)
|