Actualiza toolkit operativo y documentación
This commit is contained in:
@@ -94,7 +94,7 @@ Each rule lists **what**, **why**, and **how to verify**.
|
||||
- **Why:** TLS is issued by Traefik via **DNS challenge** (Cloudflare API token), so it
|
||||
works regardless of Cloudflare's "Always Use HTTPS". Do **not** assume HTTP-01 challenge
|
||||
— that path is intentionally not used here
|
||||
(see [`issue-coolify-static-app-deploy.md`](issue-coolify-static-app-deploy.md)).
|
||||
(see [`2026-04-11-coolify-static-app-deploy.md`](incidentes/2026-04-11-coolify-static-app-deploy.md)).
|
||||
- **Verify:**
|
||||
```powershell
|
||||
curl.exe -k -sSI https://<name>.urieljareth.org/
|
||||
@@ -285,13 +285,13 @@ curl.exe -k -sSI https://<name>.urieljareth.org/
|
||||
|
||||
| Symptom | Root cause | Fix | Source |
|
||||
|---------|-----------|-----|--------|
|
||||
| App's domain returns **502 Bad Gateway** | `ports_exposes` doesn't match the real listening port (often `3000` vs `80`) | Set `ports_exposes` to the actual port before deploy | [issue-coolify-static-app-deploy.md](issue-coolify-static-app-deploy.md) |
|
||||
| 502 / no certificate on the app domain | Traefik couldn't get a cert via HTTP challenge | Environment uses **DNS challenge**; ensure FQDN is set and don't depend on HTTP-01 | [issue-coolify-static-app-deploy.md](issue-coolify-static-app-deploy.md) |
|
||||
| App reachable only at an ugly **UUID subdomain** | FQDN not set, Coolify auto-generated it | Set `fqdn` to `https://<name>.urieljareth.org` before first deploy | [issue-coolify-static-app-deploy.md](issue-coolify-static-app-deploy.md) |
|
||||
| App's domain returns **502 Bad Gateway** | `ports_exposes` doesn't match the real listening port (often `3000` vs `80`) | Set `ports_exposes` to the actual port before deploy | [2026-04-11-coolify-static-app-deploy.md](incidentes/2026-04-11-coolify-static-app-deploy.md) |
|
||||
| 502 / no certificate on the app domain | Traefik couldn't get a cert via HTTP challenge | Environment uses **DNS challenge**; ensure FQDN is set and don't depend on HTTP-01 | [2026-04-11-coolify-static-app-deploy.md](incidentes/2026-04-11-coolify-static-app-deploy.md) |
|
||||
| App reachable only at an ugly **UUID subdomain** | FQDN not set, Coolify auto-generated it | Set `fqdn` to `https://<name>.urieljareth.org` before first deploy | [2026-04-11-coolify-static-app-deploy.md](incidentes/2026-04-11-coolify-static-app-deploy.md) |
|
||||
| App **won't start**, port conflict | Compose publishes `80`/`443` (owned by `coolify-proxy`) | Remove host port publishing; let Traefik route | [runbooks/baserow.md](runbooks/baserow.md) |
|
||||
| App **can't reach its DB** | Used `localhost`, or DB is on a different network | Use the DB **service name**; for shared services join the `coolify` network | [runbooks/nextcloud.md](runbooks/nextcloud.md), [runbooks/baserow.md](runbooks/baserow.md) |
|
||||
| Coolify **UI blank** when opening the app page | Cloudflare tunnel route order — `/app/*` captured `/application/...` | Operator fix: `/project/*` route must precede `/app/*` in the dashboard | [issue-coolify-static-app-deploy.md](issue-coolify-static-app-deploy.md) |
|
||||
| **WebSocket / terminal** drops, `tls: first record does not look like a TLS handshake` | Tunnel routes for ports `6001`/`6002` set to `https://` | Operator fix: those routes must be `http://` | [ISSUE_cloudflare-tunnel_routing_websocket-tls-handshake.md](ISSUE_cloudflare-tunnel_routing_websocket-tls-handshake.md) |
|
||||
| Coolify **UI blank** when opening the app page | Cloudflare tunnel route order — `/app/*` captured `/application/...` | Operator fix: `/project/*` route must precede `/app/*` in the dashboard | [2026-04-11-coolify-static-app-deploy.md](incidentes/2026-04-11-coolify-static-app-deploy.md) |
|
||||
| **WebSocket / terminal** drops, `tls: first record does not look like a TLS handshake` | Tunnel routes for ports `6001`/`6002` set to `https://` | Operator fix: those routes must be `http://` | [2026-04-11-cloudflare-tunnel-websocket-tls.md](incidentes/2026-04-11-cloudflare-tunnel-websocket-tls.md) |
|
||||
|
||||
> The last two are *operator/infrastructure* fixes (Cloudflare dashboard), not things the
|
||||
> app developer changes — listed here so an agent recognizes the symptom and points the
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
# Tool index — catálogo canónico de herramientas
|
||||
|
||||
**Este es el índice único de todo lo ejecutable del repo.** Si buscas "qué script
|
||||
uso para X", empieza aquí y no en los `TOOLS.md` de cada skill (esos son guías de
|
||||
uso; este es el catálogo).
|
||||
|
||||
Verificado contra el host real el **2026-08-07**. Las firmas de parámetros se
|
||||
extrajeron del AST de PowerShell, no a mano.
|
||||
|
||||
---
|
||||
|
||||
## 0. Cómo leer este índice
|
||||
|
||||
- **R/W** — `RO` = solo lectura, se puede ejecutar sin preguntar. `W` = muta
|
||||
estado, **exige confirmación explícita del usuario antes de ejecutar**.
|
||||
`RO/W` = depende de los parámetros (columna "notas" lo aclara).
|
||||
- **Env** — variables que deben estar cargadas (`. .\.env.local.ps1`). Si faltan,
|
||||
el script *lanza excepción*, no falla silenciosamente.
|
||||
- Todo se ejecuta desde la raíz del repo, en PowerShell.
|
||||
|
||||
---
|
||||
|
||||
## 1. Los 6 gotchas que producen resultados silenciosamente incorrectos
|
||||
|
||||
Léelos antes de invocar nada. No son teóricos: los cuatro primeros se
|
||||
verificaron el 2026-08-07 y el quinto el 2026-08-23. Cada uno rompe una tarea de
|
||||
forma que *parece* haber funcionado.
|
||||
|
||||
### 1.1 `-Raw` significa lo OPUESTO en dos familias de wrappers
|
||||
|
||||
Hay dos convenciones incompatibles. Elegir mal no da error: da un resultado vacío.
|
||||
|
||||
| Wrapper | Sin `-Raw` (default) | Con `-Raw` |
|
||||
|---|---|---|
|
||||
| `coolify_skill\scripts\Invoke-CoolifyApi.ps1` | **string JSON** | **objetos PowerShell** |
|
||||
| `scripts\Invoke-CloudflareApi.ps1` | **string JSON** | **objetos PowerShell** |
|
||||
| `deploy_skill\scripts\Invoke-GitHubApi.ps1` | **objetos PowerShell** | `{Status, Headers, Body}` |
|
||||
| `gitea_skill\scripts\Invoke-GiteaApi.ps1` | **objetos PowerShell** | `{Status, Headers, Body}` |
|
||||
|
||||
Consecuencia práctica: en Coolify y Cloudflare, **si vas a filtrar o proyectar el
|
||||
resultado necesitas `-Raw`**. Sin él recibes un `System.String` y
|
||||
|
||||
```powershell
|
||||
# MAL: devuelve una fila vacía, sin error. El resultado es un string.
|
||||
.\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/resources" |
|
||||
Select-Object name, uuid
|
||||
|
||||
# BIEN
|
||||
.\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/resources" -Raw |
|
||||
Select-Object name, uuid
|
||||
```
|
||||
|
||||
Usa el default (string JSON) solo cuando vas a mostrar la respuesta tal cual.
|
||||
|
||||
### 1.2 `Invoke-ProxmoxSsh.ps1` corrompe las comillas anidadas
|
||||
|
||||
`Invoke-ProxmoxSshCommand` pasa `$Command` como un único argumento a `ssh`, y
|
||||
PowerShell 5.1 destroza las comillas embebidas al invocar un ejecutable nativo.
|
||||
Cualquier comando con quoting anidado —típicamente `docker exec ... bash -lc '...
|
||||
psql -c "SELECT ..."'`— llega mutilado al host:
|
||||
|
||||
```
|
||||
bash: line 1: -c: command not found
|
||||
psql: option requires an argument -- 'F'
|
||||
```
|
||||
|
||||
**Solución verificada: codifica el comando remoto en base64.** Es el patrón a
|
||||
usar para cualquier cosa con más de un nivel de comillas:
|
||||
|
||||
```powershell
|
||||
$remote = @'
|
||||
pct exec 102 -- docker exec -i postgres-c11xzy2tx2cdapm32f5b89vy bash -lc 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At -c "SELECT name FROM public.installation_configs"'
|
||||
'@
|
||||
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($remote))
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "echo $b64 | base64 -d | bash 2>&1"
|
||||
```
|
||||
|
||||
El here-string `@'...'@` (comillas simples) es obligatorio: evita que PowerShell
|
||||
expanda `$POSTGRES_PASSWORD` del lado de Windows.
|
||||
|
||||
Para comandos de un solo nivel de comillas (`pct list`, `docker ps --format
|
||||
'{{.Names}}'`) el wrapper directo funciona bien.
|
||||
|
||||
### 1.3 Los nombres de contenedor NO se pueden adivinar
|
||||
|
||||
Coolify nombra cada contenedor `<servicio>-<uuid>` (y a veces le añade un sufijo
|
||||
numérico de build). No existe un contenedor llamado `chatwoot` ni `nextcloud`:
|
||||
|
||||
```
|
||||
chatwoot-c11xzy2tx2cdapm32f5b89vy
|
||||
nextcloud-db-hdcdpkm0jko3qqvn5683ercc
|
||||
web-instademo0portal0insta0demo1-060825532589
|
||||
```
|
||||
|
||||
**Siempre resuelve el nombre real antes de usarlo.** Dos caminos:
|
||||
|
||||
```powershell
|
||||
# a) desde Docker, por patrón
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker ps --format '{{.Names}}' | grep -i chatwoot"
|
||||
|
||||
# b) desde Coolify, para obtener el uuid del recurso (y de ahí el sufijo)
|
||||
.\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/resources" -Raw |
|
||||
Where-Object { $_.name -match 'chatwoot' } | Select-Object name, uuid, fqdn
|
||||
```
|
||||
|
||||
Corolario: **un redeploy puede recrear el contenedor con otro sufijo**, y cualquier
|
||||
script o cron que tenga el nombre hardcodeado empieza a fallar. Es exactamente lo
|
||||
que le pasó al guard de Chatwoot (ver [runbooks/chatwoot-update.md](runbooks/chatwoot-update.md)).
|
||||
|
||||
### 1.4 `/applications/*` da 404 — por Cloudflare, no por Coolify (corregido 2026-08-29)
|
||||
|
||||
**Re-verificado a fondo el 2026-08-29 sobre v4.3.14 y el diagnóstico anterior
|
||||
cambió:** el 404 NO lo produce Coolify. Es un bloqueo del edge de Cloudflare en
|
||||
el hostname público. Mismo token, misma ruta:
|
||||
|
||||
| Llamada | Resultado |
|
||||
|---|---|
|
||||
| `https://coolify.urieljareth.org/api/v1/applications` (vía Cloudflare) | **404** |
|
||||
| `http://192.168.0.117:8000/api/v1/applications` (origen, LXC 102) | **200** |
|
||||
| `/github-apps` | igual: 404 vía CF, 200 vía origen |
|
||||
| `/version`, `/resources`, `/services`, `/databases`, `/projects`, `/servers`, `/teams`, `/deployments`, `/security/keys` | OK por ambas vías |
|
||||
|
||||
La API del origen está **completa**: el `openapi.yaml` del contenedor
|
||||
(`/var/www/html/openapi.yaml`) declara todo el namespace de `/applications/*`,
|
||||
notificaciones, proveedores cloud y MCP, y responde. **Para esos endpoints,
|
||||
apunta la llamada al origen** (`$env:COOLIFY_API_URL_ORIGIN`) o corrige la regla
|
||||
de Cloudflare en el dashboard. Además, desde v4.2 los endpoints de estado
|
||||
exigen **POST** (`GET /deploy` → 405; ver notas §10.1).
|
||||
|
||||
Esto re-habilita (previa verificación en el próximo deploy) herramientas que
|
||||
estaban marcadas rotas — ver §4.
|
||||
|
||||
**Nota (2026-08-24):** `POST /services` **sí funciona** para stacks compose
|
||||
propios, con tres condiciones que la API no perdona: **no** enviar `type` junto a
|
||||
`docker_compose_raw`, mandar el compose en **base64**, y enviar el body como
|
||||
**bytes UTF-8**. Las tres estaban mal en el toolkit y ya están corregidas; el
|
||||
detalle está en
|
||||
[docs/casos/firecrawl-stack-minimo.md §4](casos/firecrawl-stack-minimo.md).
|
||||
`Test-PreDeployChecklist.ps1` sigue sin leer `docker-compose.coolify.yml`
|
||||
(solo mira `docker-compose.yml|yaml` y `compose.yml|yaml`), así que **puede dar
|
||||
todo PASS sobre un archivo que no abrió**.
|
||||
|
||||
### 1.5 "Verde en Coolify" NO significa "enrutado en Traefik"
|
||||
|
||||
Verificado el 2026-08-23. Es la causa de que un servicio recién creado desde la
|
||||
librería de Coolify devuelva **`503 no available server`** estando en verde.
|
||||
|
||||
Son dos señales distintas y la UI solo muestra una:
|
||||
|
||||
| Señal | Quién la usa | Qué significa |
|
||||
|---|---|---|
|
||||
| `State.Status = running` | **La UI de Coolify** (el punto verde) | El contenedor existe y no ha muerto |
|
||||
| `State.Health.Status = healthy` | **Traefik** | El contenedor entra al balanceador |
|
||||
|
||||
Traefik solo enruta contenedores que Docker reporta `healthy`. Un contenedor
|
||||
`running` + `unhealthy` **no tiene ruta**, la petición cae al catch-all de
|
||||
Coolify (`default_redirect_503.yaml`: `priority: -1000`, servicio `noop` con
|
||||
`servers: { }`) y de ahí sale el string `no available server`.
|
||||
|
||||
En este host el primer boot de un servicio tarda **minutos** (rootfs ext4 sobre
|
||||
loopback sobre HDD, ~39 ms por escritura), pero **12 de los 14 servicios no
|
||||
tienen `start_period`** en su healthcheck. Se les declara `unhealthy` mucho antes
|
||||
de que la app llegue a escuchar.
|
||||
|
||||
**Distingue el código HTTP antes de tocar nada:**
|
||||
|
||||
- **`502 Bad Gateway`** → Traefik *tiene* la ruta, el backend rechaza. Problema
|
||||
de la app o del puerto.
|
||||
- **`503 no available server`** → Traefik **no tiene** la ruta. Casi siempre es
|
||||
un contenedor que todavía está arrancando. **Espera, no redeployes**: un
|
||||
redeploy reinicia el entrypoint desde cero y reinicia el arranque lento.
|
||||
|
||||
```powershell
|
||||
# El diagnóstico correcto, en un comando (solo lectura):
|
||||
.\coolify_skill\scripts\Test-CoolifyServiceReady.ps1 -Uuid <uuid> -WaitSeconds 600
|
||||
```
|
||||
|
||||
Detalle completo, evidencia e hipótesis descartadas:
|
||||
[docs/casos/coolify-servicio-nuevo-503-no-available-server.md](casos/coolify-servicio-nuevo-503-no-available-server.md).
|
||||
|
||||
### 1.6 Si la imagen no expone el puerto donde sirve, el FQDN necesita `:puerto`
|
||||
|
||||
Verificado el 2026-08-24 diagnosticando `grimmory`, que devolvía **502**.
|
||||
|
||||
Coolify deriva el label `traefik.http.services.*.loadbalancer.server.port` del
|
||||
**puerto que lleve el FQDN guardado** en `service_applications.fqdn`. Si el FQDN
|
||||
no lleva puerto, Coolify **no emite el label**, y Traefik cae al único puerto que
|
||||
declare la imagen (`Config.ExposedPorts`).
|
||||
|
||||
Eso funciona por accidente cuando app e imagen coinciden, y falla en silencio
|
||||
cuando no:
|
||||
|
||||
| Servicio | FQDN guardado | Sirve en | Imagen expone | Resultado |
|
||||
|---|---|---|---|---|
|
||||
| `nextcloud` | sin puerto | 80 | 80 | OK por coincidencia |
|
||||
| `n8n` | `…:5678` | 5678 | 5678 | OK explícito |
|
||||
| `qdrant` | `…:6333` | 6333 | 6333 | OK explícito |
|
||||
| **`grimmory`** | **sin puerto** | **80** | **6060** | **502** |
|
||||
|
||||
`grimmory` corría `healthy` (su healthcheck prueba `http://127.0.0.1/health`, o
|
||||
sea el 80, y pasaba), Traefik lo enrutaba, y llegaba al 6060 donde no hay nada:
|
||||
**connection refused → 502**.
|
||||
|
||||
Ojo con la confusión: tener `SERVICE_URL_GRIMMORY_80` en el compose **no basta**.
|
||||
Lo que manda es el puerto en el FQDN almacenado.
|
||||
|
||||
**Cómo distinguirlo de otros fallos:**
|
||||
|
||||
- **`502`** → hay ruta, el backend rechaza. Compara el puerto donde escucha la app
|
||||
con el que busca Traefik. Casi siempre es esto.
|
||||
- **`503 no available server`** → no hay ruta (§1.5).
|
||||
|
||||
```powershell
|
||||
# Puertos donde escucha de verdad (en hex; 0050 = 80, 1F90 = 8080):
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec <cont> sh -c 'cat /proc/net/tcp'"
|
||||
|
||||
# Puerto que busca Traefik (si no sale nada, cae al ExposedPorts de la imagen):
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker inspect <cont> --format '{{json .Config.Labels}}'"
|
||||
```
|
||||
|
||||
**Arreglo:** poner el puerto en el FQDN (`https://x.urieljareth.org:80`) y
|
||||
**redeployar** el servicio — los labels solo se regeneran al recrear el
|
||||
contenedor.
|
||||
|
||||
---
|
||||
|
||||
## 2. Infraestructura: host Proxmox, red, apps del host
|
||||
|
||||
Todo llega al host por un solo camino:
|
||||
|
||||
```
|
||||
PowerShell → Invoke-ProxmoxSshCommand → ssh [email protected]
|
||||
→ pct exec 102 -- docker ... (para cualquier cosa de Docker)
|
||||
```
|
||||
|
||||
| Script | R/W | Env | Parámetros | Qué hace |
|
||||
|---|---|---|---|---|
|
||||
| [scripts/ProxmoxAgent.ps1](../scripts/ProxmoxAgent.ps1) | librería | — | — | **Dot-source obligatorio** (`. .\scripts\ProxmoxAgent.ps1`). Expone `Get-ProxmoxConfig`, `Assert-ProxmoxConfig`, `Invoke-ProxmoxSshCommand`, `Invoke-ProxmoxApi`. Todos los demás scripts lo consumen; no reimplementes la conexión. |
|
||||
| [scripts/Test-ProxmoxConnection.ps1](../scripts/Test-ProxmoxConnection.ps1) | RO | opcional `PROXMOX_API_TOKEN_*` | — | Smoke test: config + SSH + muestra de Docker + auth de API. Sale 1 si algo falla. **Ejecútalo antes de trabajo operativo.** |
|
||||
| [scripts/Get-ProxmoxInventory.ps1](../scripts/Get-ProxmoxInventory.ps1) | RO | — | — | Snapshot completo: host, LXC, QEMU, Docker en LXC 102. |
|
||||
| [scripts/Invoke-ProxmoxSsh.ps1](../scripts/Invoke-ProxmoxSsh.ps1) | **RO/W** | — | `-Command <string>` | Comando arbitrario por SSH. **El R/W lo determina el comando**: `pct list` es RO, `pct stop` es W. Ver gotcha §1.2 para comillas anidadas. |
|
||||
| [scripts/Invoke-CloudflareApi.ps1](../scripts/Invoke-CloudflareApi.ps1) | **RO/W** | `CLOUDFLARE_API_TOKEN` | `-Method` `-Path` `-BodyJson` `-Raw` | API de Cloudflare (túnel + DNS). `GET` es RO; el resto W. `-Raw` → objetos (§1.1). |
|
||||
| [scripts/Install-CoolifyAutostart.ps1](../scripts/Install-CoolifyAutostart.ps1) | **W** (RO con `-VerifyOnly`) | — | `-VerifyOnly` `-SkipOnboot` `-RunNow` `-Uninstall` | Instala el auto-arranque del stack tras corte de luz (LXC 102 + túnel). Usa `-VerifyOnly` para auditar sin tocar nada. |
|
||||
|
||||
**API REST de Proxmox** (distinta de la de Coolify): requiere
|
||||
`PROXMOX_API_TOKEN_ID` + `PROXMOX_API_TOKEN_SECRET`. `Invoke-ProxmoxApi` lanza
|
||||
excepción si faltan.
|
||||
|
||||
> ⚠️ **Estado actual (2026-08-07):** `.env.local.ps1` **no** define
|
||||
> `PROXMOX_API_TOKEN_ID`/`_SECRET`, `CLOUDFLARE_API_TOKEN` ni
|
||||
> `COOLIFY_EMAIL`/`COOLIFY_PASSWORD`. Las tres rutas que dependen de ellas
|
||||
> (API de Proxmox, API de Cloudflare, flujo UI de Coolify) **fallan hoy**. Lo
|
||||
> que sí está cargado: `PROXMOX_HOST/NODE/USER/SSH_KEY/COOLIFY_LXC`,
|
||||
> `COOLIFY_API_URL`, `COOLIFY_TOKEN`, `GITHUB_TOKEN`, `GITHUB_OWNER`,
|
||||
> `GITEA_URL/USER/TOKEN`.
|
||||
|
||||
### 2.1 Chatwoot — parche enterprise
|
||||
|
||||
Contexto completo en [runbooks/chatwoot-update.md](runbooks/chatwoot-update.md).
|
||||
|
||||
| Script | R/W | Parámetros | Qué hace |
|
||||
|---|---|---|---|
|
||||
| [scripts/Get-ChatwootLicenseStatus.ps1](../scripts/Get-ChatwootLicenseStatus.ps1) | RO | `-ServiceUuid` `-AppContainer` `-DbContainer` `-Deep` | Estado de la licencia. `-Deep` verifica además los feature flags por cuenta (`accounts.feature_flags`). **Usa esto para diagnosticar; nunca el parche.** |
|
||||
| [scripts/Apply-ChatwootEnterprisePatch.ps1](../scripts/Apply-ChatwootEnterprisePatch.ps1) | **W** | `-DryRun` `-ReenableAccountFeatures` `-ServiceUuid` `-Container` `-AppContainer` `-LxcId` `-ProxmoxHost` `-SshKey` | Reaplica el parche. **El SQL de 3 filas no basta**: pasa siempre `-ReenableAccountFeatures`. Usa `-DryRun` primero. |
|
||||
| [scripts/chatwoot-enterprise-guard.sh](../scripts/chatwoot-enterprise-guard.sh) | — | — | **Copia versionada** del guard que corre en el host. No se ejecuta desde Windows. Instalado en `/root/scripts/chatwoot-enterprise-guard.sh`, agendado por `/etc/cron.d/chatwoot-enterprise-guard` cada 5 min. Log: `/var/log/chatwoot-enterprise-guard.log` (solo escribe cuando actúa). |
|
||||
|
||||
### 2.2 Artefactos que viven en el host (no se invocan desde Windows)
|
||||
|
||||
| Archivo | Qué es |
|
||||
|---|---|
|
||||
| [scripts/host/coolify-autostart.sh](../scripts/host/coolify-autostart.sh) | Script de arranque; lo despliega `Install-CoolifyAutostart.ps1`. |
|
||||
| [scripts/host/coolify-autostart.service](../scripts/host/coolify-autostart.service) | Unit de systemd correspondiente. |
|
||||
| [scripts/fix-nextcloud-config.php](../scripts/fix-nextcloud-config.php) | Fragmento puntual del fix HTTPS de Nextcloud. Ver [runbooks/nextcloud.md](runbooks/nextcloud.md). |
|
||||
| [scripts/Set-ProxmoxEnv.example.ps1](../scripts/Set-ProxmoxEnv.example.ps1) | Plantilla de entorno. El template completo es [.env.example](../.env.example). |
|
||||
|
||||
### 2.3 Scripts de deploy específicos de una app
|
||||
|
||||
`scripts/apps/` guarda scripts one-off con uuid y dominio **hardcodeados**. Son
|
||||
**mutantes** y están atados a un recurso concreto: lee la cabecera antes de
|
||||
ejecutar uno, y verifica que el uuid siga siendo el correcto (§1.3).
|
||||
|
||||
| Script | R/W | Env | Qué hace |
|
||||
|---|---|---|---|
|
||||
| [scripts/apps/Deploy-SoloLeveling.ps1](../scripts/apps/Deploy-SoloLeveling.ps1) | **W** | `COOLIFY_*`, `PROXMOX_*`, `GITHUB_TOKEN` (scope `repo`) | Re-deploy de "El Sistema (Solo Leveling)" sin depender del pull de GHCR (el registry es privado y el token no tiene `read:packages`). Construye la imagen **en el servidor** (clone del repo privado + `docker build`), la taguea con el nombre que espera el compose generado por Coolify, y levanta el servicio. Params: `-ServiceUuid` `-Domain` `-Repo` `-Image` `-NoBuild`. Requiere que el service ya esté registrado en Coolify. |
|
||||
| [scripts/apps/Deploy-OhDaddy.ps1](../scripts/apps/Deploy-OhDaddy.ps1) | **W** | `COOLIFY_*`, `PROXMOX_*` | Redeploy de oh-daddy (stack app+db+Inngest self-hosted, servicio `rzittzudkunwx8gilonn7tqe`). Clone del repo público + `stacks/oh-daddy/Dockerfile` inyectado, build de `oh-daddy-app:local` en el server, `compose up -d`, schema idempotente y re-registro de funciones Inngest (`PUT /api/inngest`). Params: `-ServiceUuid` `-Fqdn` `-Repo` `-Image` `-NoBuild`. Detalle: [casos/oh-daddy-deploy.md](casos/oh-daddy-deploy.md). |
|
||||
|
||||
---
|
||||
|
||||
## 3. Coolify — operación
|
||||
|
||||
| Script | R/W | Env | Parámetros | Qué hace |
|
||||
|---|---|---|---|---|
|
||||
| [coolify_skill/scripts/Get-CoolifyDockerStatus.ps1](../coolify_skill/scripts/Get-CoolifyDockerStatus.ps1) | RO | — | `-Filter <regex>` `-All` | Estado de contenedores vía LXC 102. `-All` lista todo; `-Filter` acota por regex. Primera parada para "¿está corriendo X?". |
|
||||
| [coolify_skill/scripts/Test-CoolifyServiceReady.ps1](../coolify_skill/scripts/Test-CoolifyServiceReady.ps1) | RO | — | `-Uuid <uuid>` `-Fqdn <url>` `-WaitSeconds <n>` | Contrasta `running` vs `healthy` por contenedor (la discrepancia que la UI esconde), avisa de `start_period` insuficiente, detecta setup en curso (`chown`/`apt`) y prueba el dominio distinguiendo 503 de 502. **Primera parada para un `503 no available server`** — ver §1.5. |
|
||||
| [coolify_skill/scripts/Set-CoolifyHealthcheckGrace.ps1](../coolify_skill/scripts/Set-CoolifyHealthcheckGrace.ps1) | **RO/W** | — | `-Uuid <uuid>` `-StartPeriodSeconds 300` `-MinIntervalSeconds 10` `-ShowResult` `-Apply` | Inserta `start_period` en los healthcheck que no lo tienen y sube intervalos demasiado cortos, editando `services.docker_compose_raw`. **Dry-run sin `-Apply`.** Con `-Apply` guarda copia de rollback en `backups/` y verifica leyendo de vuelta. **No redeploya** — el healthcheck solo aplica al recrear el contenedor. Ver §1.5. |
|
||||
| [coolify_skill/scripts/Invoke-CoolifyApi.ps1](../coolify_skill/scripts/Invoke-CoolifyApi.ps1) | **RO/W** | `COOLIFY_TOKEN` | `-Method` `-Path` `-BodyJson` `-Raw` | API de Coolify. `GET` RO; `POST/PUT/PATCH/DELETE` W → confirmación. Ver §1.1 (`-Raw`) y §1.4 (endpoints que dan 404). |
|
||||
| `coolify_skill/scripts/coolify.sh` | RO/W | `COOLIFY_TOKEN` | — | Helper Bash legacy para sesiones Linux/WSL. En este repo se prefieren los wrappers PowerShell. |
|
||||
|
||||
**Referencia de la API:** no cargues el árbol completo. Busca y abre un solo
|
||||
archivo:
|
||||
|
||||
```powershell
|
||||
rg -n "deploy|database|environment" .\coolify_skill\references
|
||||
Get-Content .\coolify_skill\references\ops\deploy-by-tag-or-uuid.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Deploy de proyectos nuevos a Coolify
|
||||
|
||||
> ⚠️ **Lee esto antes de usar cualquier cosa de esta sección.** El pipeline
|
||||
> "una sola línea" fallaba porque `/applications/*` daba 404 (§1.4). Desde el
|
||||
> 2026-08-29 sabemos que ese 404 es de **Cloudflare, no de Coolify**: apuntando
|
||||
> `COOLIFY_API_URL` al origen (`http://192.168.0.117:8000/api/v1`) el namespace
|
||||
> completo responde. Scripts marcados ❌ abajo deben funcionar así — pendiente de
|
||||
> verificar en el próximo deploy; el flujo UI y la vía DB siguen como fallback.
|
||||
|
||||
| Script | R/W | Env | Estado en esta instancia |
|
||||
|---|---|---|---|
|
||||
| [Publish-ProjectToCoolify.ps1](../deploy_skill/scripts/Publish-ProjectToCoolify.ps1) | W | `GITHUB_TOKEN`, `COOLIFY_TOKEN` | ⚠️ Su paso 5 invoca `New-CoolifyApplication.ps1` → probar contra el origen. |
|
||||
| [New-CoolifyApplication.ps1](../deploy_skill/scripts/New-CoolifyApplication.ps1) | W | `COOLIFY_TOKEN` | ⚠️ Usa `POST /applications/public` y `PATCH /applications/{uuid}` → funcionan vía origen (404 solo vía Cloudflare). |
|
||||
| [Invoke-CoolifyRollback.ps1](../deploy_skill/scripts/Invoke-CoolifyRollback.ps1) | W | `COOLIFY_TOKEN` | ⚠️ Usa `PATCH /applications/{uuid}` → funciona vía origen. |
|
||||
| [New-CoolifyService.ps1](../deploy_skill/scripts/New-CoolifyService.ps1) | W | `COOLIFY_TOKEN` | ✅ **Funciona.** Usa `POST /services`. Es la vía válida para stacks multi-contenedor. |
|
||||
| [New-CoolifyAppViaDB.ps1](../deploy_skill/scripts/New-CoolifyAppViaDB.ps1) | **W (INSERT directo en la DB)** | — | ⚠️ **Último recurso.** Escribe la fila en `applications` de la DB de Coolify saltándose API y UI. Sin validación ni rollback. Requiere `-EnvironmentId` y `-GithubRepoId` reales. |
|
||||
| `coolify-ui/coolify-login.mjs` + `Configure-CoolifyComposeApp.mjs` | W (vía navegador) | `COOLIFY_EMAIL`, `COOLIFY_PASSWORD` | ✅ Fallback soportado para apps git build-from-source. Credenciales probables ya en `.env.local.ps1` (sin verificar). |
|
||||
|
||||
**Vías, en orden de preferencia:**
|
||||
|
||||
1. Stack multi-contenedor con `docker-compose.coolify.yml` → `New-CoolifyService.ps1` (API).
|
||||
2. App git build-from-source → API contra el origen; si falla, flujo UI con Playwright (`coolify-ui/`).
|
||||
3. Último recurso → `New-CoolifyAppViaDB.ps1`.
|
||||
|
||||
Detalle completo del flujo v4.1.2 y sus trampas (BOM UTF-8 rompe el parser YAML
|
||||
de Coolify; "Reload Compose File" es obligatorio; fijar dominios por servicio o
|
||||
sale 503; no encolar deploys concurrentes) en
|
||||
[deploy_skill/references/coolify-4.1.2-notes.md](../deploy_skill/references/coolify-4.1.2-notes.md).
|
||||
|
||||
### 4.1 Scripts de deploy que funcionan sin depender de `/applications`
|
||||
|
||||
| Script | R/W | Env | Parámetros | Qué hace |
|
||||
|---|---|---|---|---|
|
||||
| [Initialize-CoolifyProject.ps1](../deploy_skill/scripts/Initialize-CoolifyProject.ps1) | W (solo local) | — | `-Path` `-Stack {node\|python\|compose\|static}` `-AppPort` `-Force` | Genera Dockerfile/compose/.dockerignore compatibles. Idempotente: no sobreescribe sin `-Force`. |
|
||||
| [Test-PreDeployChecklist.ps1](../deploy_skill/scripts/Test-PreDeployChecklist.ps1) | RO | — | `-Path` `-ExpectedPort` `-Strict` | Valida el proyecto contra las reglas duras de [AGENTS-coolify-apps.md](AGENTS-coolify-apps.md). **Gate obligatorio antes de cualquier push.** |
|
||||
| [New-GitHubRepo.ps1](../deploy_skill/scripts/New-GitHubRepo.ps1) | W (remoto) | `GITHUB_TOKEN` | `-Name` `-Description` `-Private` `-Owner` | Crea el repo en GitHub. Idempotente: avisa si ya existe. |
|
||||
| [Invoke-GitHubApi.ps1](../deploy_skill/scripts/Invoke-GitHubApi.ps1) | RO/W | `GITHUB_TOKEN` | `-Method` `-Path` `-BodyJson` `-Raw` | API de GitHub. Default → objetos (§1.1). |
|
||||
| [Test-PostDeploy.ps1](../deploy_skill/scripts/Test-PostDeploy.ps1) | RO | `COOLIFY_TOKEN` (opcional) | `-Fqdn` `-ContainerName` `-SiblingService` `-ApplicationUuid` | Verifica el estado en Coolify: contenedores, logs del proxy, DNS entre hermanos. `-ApplicationUuid` consulta `/applications/*` → vía pública da 404 (Cloudflare), vía origen funciona. |
|
||||
| [Test-ServiceOnline.ps1](../deploy_skill/scripts/Test-ServiceOnline.ps1) | RO | — | `-Fqdn` `-Path` `-ExpectedStatusCode` `-ExpectTitle` `-Screenshot` `-SkipBrowser` `-TimeoutMs` `-Retries` | **La "definición de done".** Dos capas: HTTP 200 vía curl + render real en Chromium (Playwright). Detecta 502 de Traefik, TLS a medias y crashes de JS del cliente. Sin Node/Playwright la capa de navegador se omite con warning, no falla. |
|
||||
| `deploy_skill/scripts/verify-online.mjs` | RO | — | (invocado por el script de arriba) | Capa de navegador de `Test-ServiceOnline.ps1`: navegación real con Playwright, captura `pageerror` y requests fallidos del main frame. No lo invoques directo. Requiere `npm install` en la raíz del repo. |
|
||||
|
||||
`git push` usa **Windows Credential Manager (wincred)**, no el PAT. Verificado
|
||||
para la cuenta `urieljarethbusiness-cpu`.
|
||||
|
||||
**Plantillas:** [deploy_skill/references/templates/](../deploy_skill/references/templates/) —
|
||||
`Dockerfile.node`, `Dockerfile.python`, `docker-compose.app-db.yml`,
|
||||
`env.local.template.ps1`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Gitea — hosting git self-hosted
|
||||
|
||||
Distinto de deploy_skill: esto administra la capa de hosting git, no Coolify.
|
||||
El propio repo Manager vive aquí.
|
||||
|
||||
| Script | R/W | Env | Parámetros | Qué hace |
|
||||
|---|---|---|---|---|
|
||||
| [Test-GiteaConnection.ps1](../gitea_skill/scripts/Test-GiteaConnection.ps1) | RO | `GITEA_URL`, `GITEA_TOKEN` | — | Smoke test: `/version`, `/user`, `/settings/api`, `/repos/search`. Sale 1 si algo falla. |
|
||||
| [Get-GiteaRepo.ps1](../gitea_skill/scripts/Get-GiteaRepo.ps1) | RO | `GITEA_URL`, `GITEA_TOKEN` | `-Owner` `-Name` `-Search` `-List` | Lee, lista o busca repos. `-Owner` default = usuario del token. |
|
||||
| [Invoke-GiteaApi.ps1](../gitea_skill/scripts/Invoke-GiteaApi.ps1) | RO/W | `GITEA_URL`, `GITEA_TOKEN` | `-Method` `-Path` `-BodyJson` `-Raw` | API de Gitea. Usa `curl.exe --data-binary` con UTF-8 sin BOM (el parser de Gitea se rompe con BOM). Default → objetos (§1.1). |
|
||||
| [New-GiteaRepo.ps1](../gitea_skill/scripts/New-GiteaRepo.ps1) | W | `GITEA_URL`, `GITEA_TOKEN` | `-Name` `-Description` `-Private` `-Owner` `-NoAutoInit` | Crea repo (idempotente). `-NoAutoInit` evita el README del lado servidor para poder empujar historia local sin conflicto. |
|
||||
| [Sync-GiteaRemote.ps1](../gitea_skill/scripts/Sync-GiteaRemote.ps1) | W | `GITEA_URL`, `GITEA_TOKEN` | `-AppPath` `-Name` `-Owner` `-RemoteName` `-Branch` `-CreateIfMissing` `-Private` `-Force` | Cablea el remoto y hace push headless. **El token nunca toca el disco**: va en un `http.extraHeader` de un solo uso, no en `.git/config`. `-RemoteName gitea` (default) convive con un `origin` de GitHub. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Reglas de seguridad (no negociables)
|
||||
|
||||
1. **Read-only primero.** El default es diagnosticar: list, status, logs, inspect,
|
||||
health checks.
|
||||
2. **Confirmación explícita antes de cualquier cambio de estado.** Aplica a:
|
||||
`pct`/`qm` start/stop/reboot/destroy; `docker` restart/stop/rm/compose up-down;
|
||||
deploys de Coolify y cualquier `POST`/`PUT`/`PATCH`/`DELETE`; escritura de
|
||||
variables de entorno; y todo cambio de firewall, red, storage, volumen, clave
|
||||
o token. Antes de una acción riesgosa: captura el estado actual y enuncia el
|
||||
camino de rollback.
|
||||
3. **Nunca escribas secretos en el repo.** Ni tokens, ni passwords, ni claves
|
||||
privadas, ni cookies, ni secretos de token PVE — no en Markdown, no en
|
||||
scripts, no en logs. Viven solo en `.env.local.ps1` (gitignored) o en el
|
||||
almacén de secretos del SO. Al depurar bases de datos, verifica conectividad
|
||||
sin imprimir credenciales.
|
||||
4. **Prefiere los scripts del repo** antes que cadenas de comandos manuales
|
||||
largas, y no construyas comandos destructivos amplios a partir de strings
|
||||
generados.
|
||||
5. `-Force` existe en varios scripts para saltarse los prompts. **Úsalo solo
|
||||
después de que el usuario haya aprobado el plan concreto.**
|
||||
|
||||
---
|
||||
|
||||
## 7. Dónde está el resto del contexto
|
||||
|
||||
| Qué necesitas | Dónde |
|
||||
|---|---|
|
||||
| Arquitectura + router de intención | [CLAUDE.md](../CLAUDE.md) |
|
||||
| Topología verificada (fuente de verdad del estado) | [proxmox-inventory.md](proxmox-inventory.md) |
|
||||
| Procedimientos concretos | [runbooks/](runbooks/) |
|
||||
| Casos resueltos paso a paso | [casos/](casos/) |
|
||||
| Incidentes archivados | [incidentes/](incidentes/) |
|
||||
| Contrato para *construir* una app deployable | [AGENTS-coolify-apps.md](AGENTS-coolify-apps.md) |
|
||||
| Reglas operativas por dominio | `agent/SKILL.md`, `coolify_skill/SKILL.md`, `deploy_skill/SKILL.md`, `gitea_skill/SKILL.md` |
|
||||
| Guías de uso por dominio (ejemplos ejecutables) | los `TOOLS.md` de cada carpeta `*_skill/` |
|
||||
@@ -1,7 +1,22 @@
|
||||
# Caso: Parche enterprise en Chatwoot (Coolify + LXC 102)
|
||||
|
||||
> Documentacion de caso verificada el 2026-06-16 desde esta maquina.
|
||||
> Dominio: `https://chatwoot-c11xzy2tx2cdapm32f5b89vy.urieljareth.org`
|
||||
> Dominio: **`https://chat.urieljareth.org`** (corregido el 2026-07-24; el FQDN
|
||||
> `chatwoot-c11xzy2tx2cdapm32f5b89vy.urieljareth.org` que decia antes ya no aplica).
|
||||
|
||||
> **Leer antes de usar este caso (revision 2026-07-24):**
|
||||
>
|
||||
> 1. **El parche caduca solo en <= 24 h.** No hace falta actualizar para
|
||||
> perderlo: `Internal::CheckNewVersionsJob` hace ping diario a
|
||||
> `hub.2.chatwoot.com` y reescribe el plan con lo que responda el hub. Con el
|
||||
> identifier actual la ventana es todos los dias a las **16:16 UTC**.
|
||||
> 2. **Los 3 `UPDATE` de este caso no alcanzan** si el plan ya paso por
|
||||
> `community`: `Internal::ReconcilePlanConfigService` apago los 9 feature
|
||||
> flags premium en `accounts.feature_flags` de cada cuenta. Hay que correr
|
||||
> `Apply-ChatwootEnterprisePatch.ps1 -ReenableAccountFeatures`.
|
||||
>
|
||||
> Causa raiz completa, plan de actualizacion y fix durable:
|
||||
> [docs/runbooks/chatwoot-update.md](../runbooks/chatwoot-update.md).
|
||||
|
||||
## 0. Resumen ejecutivo
|
||||
|
||||
@@ -41,7 +56,7 @@
|
||||
|---|---|---|
|
||||
| Host | Docker daemon local | Proxmox VE `192.168.0.200` |
|
||||
| Ejecucion Docker | `docker exec` directo | `pct exec 102 --` + `docker exec` |
|
||||
| Usuario SSH | n/a | `root@192.168.0.200` con `~/.openclaw/workspace/proxmox_key_win` |
|
||||
| Usuario SSH | n/a | `root@192.168.0.200` con `keys/proxmox_ed25519` |
|
||||
| Contenedor | filtro `name=pgvector` | nombre real: `postgres-c11xzy2tx2cdapm32f5b89vy` |
|
||||
| DB user / db | `-U postgres -d chatwoot` | `-U <POSTGRES_USER> -d <POSTGRES_DB>` autodetectados (imagen `pgvector/pgvector:pg12` **no crea rol `postgres`**) |
|
||||
|
||||
@@ -124,20 +139,29 @@ El script implementa el equivalente exacto y valida los `UPDATE 1`.
|
||||
2. Sube 3 scripts `.sh` y un `.sql` al host Proxmox (no al LXC, para evitar
|
||||
un `pct push` extra y problemas de ruta).
|
||||
3. Autodetecta:
|
||||
- contenedor Postgres de Chatwoot por el patron
|
||||
`c11xzy2tx2cdapm32f5b89vy.*(pgvector|postgres|db)`.
|
||||
- contenedor Postgres de Chatwoot: filtra `docker ps` por el uuid del
|
||||
servicio y luego por `(pgvector|postgres|db)`. Son **dos greps
|
||||
encadenados** a proposito — Coolify nombra los contenedores
|
||||
`<servicio>-<uuid>` (`postgres-c11xzy...`), asi que el patron unico
|
||||
`<uuid>.*postgres` que tenia antes no casaba nunca y la autodeteccion
|
||||
fallaba siempre (corregido el 2026-07-24).
|
||||
- `POSTGRES_USER` / `POSTGRES_DB` / `POSTGRES_PASSWORD` desde
|
||||
`docker inspect`.
|
||||
4. Ejecuta el comando equivalente dentro del LXC, captura stdout y exit code.
|
||||
5. Cuenta las lineas `^UPDATE\s+1\s*$`; **deben ser exactamente 3** o falla.
|
||||
6. Corre un `SELECT` de verificacion.
|
||||
7. Limpia los archivos temporales en el host Proxmox.
|
||||
7. Con `-ReenableAccountFeatures`: reactiva los 9 feature flags premium en todas
|
||||
las cuentas via `rails runner` y falla si queda alguno pendiente.
|
||||
8. Limpia los archivos temporales en el host Proxmox.
|
||||
|
||||
En el `-DryRun` la `PGPASSWORD` sale enmascarada (antes se imprimia en claro).
|
||||
|
||||
### Uso
|
||||
|
||||
```powershell
|
||||
# Desde la raiz del repo.
|
||||
.\scripts\Apply-ChatwootEnterprisePatch.ps1 -DryRun
|
||||
.\scripts\Apply-ChatwootEnterprisePatch.ps1 -DryRun -ReenableAccountFeatures
|
||||
.\scripts\Apply-ChatwootEnterprisePatch.ps1 -ReenableAccountFeatures
|
||||
.\scripts\Apply-ChatwootEnterprisePatch.ps1 -Container "postgres-c11xzy2tx2cdapm32f5b89vy"
|
||||
```
|
||||
|
||||
@@ -145,10 +169,18 @@ Sin `-Container`, el script lo busca por el UUID del recurso Coolify.
|
||||
Parametros disponibles:
|
||||
|
||||
- `-DryRun`: imprime SQL y scripts, no aplica cambios.
|
||||
- `-Container <nombre>`: fuerza el contenedor destino.
|
||||
- `-ReenableAccountFeatures`: ademas de los 3 `UPDATE`, reactiva via
|
||||
`rails runner` los 9 feature flags premium en **todas** las cuentas y verifica
|
||||
que no quede ninguno pendiente. **Necesario siempre que el plan venga de
|
||||
`community`** (ver el aviso al inicio de este documento).
|
||||
- `-Container <nombre>`: fuerza el contenedor Postgres destino.
|
||||
- `-AppContainer <nombre>`: contenedor de la app Rails, por defecto
|
||||
`chatwoot-<ServiceUuid>` (solo lo usa `-ReenableAccountFeatures`).
|
||||
- `-ServiceUuid <uuid>`: uuid del servicio en Coolify, por defecto
|
||||
`c11xzy2tx2cdapm32f5b89vy`. De aqui se derivan los nombres de contenedor.
|
||||
- `-LxcId <id>`: por defecto `102` (Coolify).
|
||||
- `-ProxmoxHost <host>`: por defecto `192.168.0.200`.
|
||||
- `-SshKey <ruta>`: por defecto `C:\Users\Uriel Jareth\.openclaw\workspace\proxmox_key_win`.
|
||||
- `-SshKey <ruta>`: por defecto `keys\proxmox_ed25519`.
|
||||
|
||||
### Salida esperada (exitosa)
|
||||
|
||||
@@ -222,7 +254,15 @@ resultado, verificar con SELECT, limpiar) es identico.
|
||||
|
||||
## 6. Verificacion manual despues del parche
|
||||
|
||||
1. Entrar a `https://chatwoot-c11xzy2tx2cdapm32f5b89vy.urieljareth.org`.
|
||||
Automatica primero:
|
||||
|
||||
```powershell
|
||||
.\scripts\Get-ChatwootLicenseStatus.ps1 -Deep
|
||||
```
|
||||
|
||||
Luego a mano:
|
||||
|
||||
1. Entrar a `https://chat.urieljareth.org`.
|
||||
2. Iniciar sesion con un super admin.
|
||||
3. Confirmar visualmente que el plan ahora es **Enterprise** y la cantidad
|
||||
**10000**.
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# Caso: error 500 al abrir la página de una aplicación en Coolify (env var sin cifrar)
|
||||
|
||||
**Fecha:** 2026-09-04 · **App:** `open-seo:main-0fgs5kwaab9esytaxkddsvts`
|
||||
(uuid `kj0kccsb4d46tm0d6qe6docy`, id DB 57, repo `every-app/open-seo`, build pack
|
||||
railpack) · **Resuelto el mismo día.**
|
||||
|
||||
## Síntoma
|
||||
|
||||
Abrir
|
||||
`https://coolify.urieljareth.org/project/.../application/kj0kccsb4d46tm0d6qe6docy`
|
||||
devuelve **500 (Server Error)**. El resto del dashboard funciona. El sitio
|
||||
público de la app responde 200 (lo sirve un sidecar manual, ver "Estado
|
||||
post-fix").
|
||||
|
||||
## Causa raíz
|
||||
|
||||
La fila 1298 de `environment_variables` (`DATAFORSEO_API_KEY` de la app 57)
|
||||
tenía el **valor en texto plano** (56 chars, sin prefijo `eyJpdiI6`) con
|
||||
`is_literal=false`. En Coolify v4.3.x el accessor `value` del modelo
|
||||
`EnvironmentVariable` **descifra incondicionalmente** (`decrypt($value)`); un
|
||||
valor no cifrado lanza `DecryptException: The payload is invalid`.
|
||||
|
||||
La página de configuración monta `ConfigurationChecker` (Livewire), que llama a
|
||||
`Application->pendingDeploymentConfigurationDiff()` →
|
||||
`ApplicationConfigurationSnapshot::environmentItems()` → lee `->value` de cada
|
||||
env var → explota → la página entera responde 500. El stack trace está en
|
||||
`storage/logs/laravel.log` del contenedor `coolify`.
|
||||
|
||||
El valor llegó por un **INSERT/UPDATE directo a la DB** (sin pasar por el modelo
|
||||
Eloquent, que cifra en el `set`). Huella correlativa: 5 filas más con el morph
|
||||
type mal escapado (`App\\Models\\Application`, doble backslash), también
|
||||
inserciones directas por SQL (ver "Hallazgos secundarios").
|
||||
|
||||
## Diagnóstico (réplicable)
|
||||
|
||||
```powershell
|
||||
# 1) Stack trace del 500 (dentro del contenedor coolify):
|
||||
# docker exec coolify tail -n 200 /var/www/html/storage/logs/laravel.log
|
||||
# -> DecryptException desde EnvironmentVariable::get_environment_variables
|
||||
|
||||
# 2) Clasificar filas SIN imprimir valores (todo payload cifrado de Laravel
|
||||
# empieza con "eyJpdiI6"):
|
||||
pct exec 102 -- docker exec coolify-db sh -c 'psql -U "$POSTGRES_USER" \
|
||||
-d "$POSTGRES_DB" -c "SELECT id, key, is_literal, length(value) AS len, \
|
||||
(value LIKE $$eyJpdiI6%$$) AS looks_enc FROM environment_variables \
|
||||
WHERE resourceable_id=57;"'
|
||||
```
|
||||
|
||||
## Fix aplicado
|
||||
|
||||
Re-cifrar el valor existente con el `APP_KEY` de la instancia (preserva el
|
||||
secreto; no hace falta reingresarlo), vía un script PHP con Laravel booteado
|
||||
dentro del contenedor `coolify`:
|
||||
|
||||
```php
|
||||
// /tmp/fix-envvar.php (se pasa por stdin a: docker exec -i coolify sh -c 'cat > /tmp/fix-envvar.php')
|
||||
require '/var/www/html/vendor/autoload.php';
|
||||
$app = require '/var/www/html/bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
$row = DB::table('environment_variables')->where('id', 1298)->first();
|
||||
try { decrypt($row->value); echo "ya cifra OK\n"; }
|
||||
catch (\Throwable $e) {
|
||||
DB::table('environment_variables')->where('id', 1298)->update([
|
||||
'value' => encrypt($row->value), // el secreto no se pierde
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
Wrapper ejecutable: [artifacts/fix-envvar-1298.ps1](../../artifacts/fix-envvar-1298.ps1)
|
||||
(hace el backup, aplica y verifica en una pasada).
|
||||
|
||||
**Backup previo** (incluye el valor, root-only, host Proxmox):
|
||||
`/root/backups/envvar-1298-20260904-211001.tsv`.
|
||||
|
||||
**Rollback:** restaurar la fila desde el backup
|
||||
(`UPDATE environment_variables SET value='<col 3 del tsv>' WHERE id=1298;`) —
|
||||
solo si se quisiera volver al estado roto original; no hay razón para hacerlo.
|
||||
|
||||
## Verificación
|
||||
|
||||
- Lectura a nivel de modelo OK (el accessor ya no lanza).
|
||||
- `Application::find(57)->pendingDeploymentConfigurationDiff()` — la ruta exacta
|
||||
que 500eaba — ejecuta limpio.
|
||||
- Fila post-fix: `len=312`, `looks_enc=t`, `updated_at=2026-09-05 03:10:04`.
|
||||
|
||||
## Estado post-fix de la app (no parte de este caso)
|
||||
|
||||
- La app en Coolify sigue `exited:unhealthy` **sin contenedor** (última online
|
||||
2026-08-27). Su página ya carga; un redeploy es decisión del usuario.
|
||||
- El FQDN `https://kj0kccsb4d46tm0d6qe6docy.urieljareth.org` responde **200 en
|
||||
vivo** (`cf-cache-status: DYNAMIC`) porque el contenedor manual
|
||||
`open-seo-sidecar` (puerto 80, corriendo fuera de Coolify) lleva los labels
|
||||
Traefik de ese host. Es decir: el sitio público no depende hoy del deployment
|
||||
de Coolify.
|
||||
|
||||
## Hallazgos secundarios (sin acción, reportados al usuario)
|
||||
|
||||
- **5 filas huérfanas** (ids 1152-1156: `MYSQL_DATABASE`, `MYSQL_USER`,
|
||||
`MOSTRAR_ENLACE`, `SEMBRAR_SIEMPRE`, `ENLACES_POR_VENTANA`) apuntan a la app
|
||||
52 (`insta-portal`) con `resourceable_type='App\\Models\\Application'`
|
||||
(doble backslash). La relación de Eloquent no las ve, así que **no rompen
|
||||
páginas**, pero insta-portal corre sin esas variables. Normalizar el morph
|
||||
type (y re-cifrar valores) las activaría — evaluar impacto en runtime antes.
|
||||
|
||||
## Prevención
|
||||
|
||||
- Nunca escribir en `environment_variables.value` por SQL directo: el modelo
|
||||
cifra en el setter. Para insertar variables usar la UI o la API.
|
||||
- Si se inserta por SQL de emergencia, el valor debe ser `encrypt($valor)` con
|
||||
el `APP_KEY` de la instancia, y `resourceable_type` lleva **un solo**
|
||||
backslash (`App\Models\Application`).
|
||||
- Síntoma distintivo para el futuro: dashboard 500 solo en la página de una app
|
||||
concreta + `DecryptException` en `laravel.log` = valor corrupto en
|
||||
`environment_variables` de ese recurso.
|
||||
@@ -0,0 +1,310 @@
|
||||
# Caso: un servicio nuevo de Coolify está en verde pero el dominio devuelve `503 no available server`
|
||||
|
||||
> Diagnosticado y resuelto el **2026-08-23** contra el host real.
|
||||
> Target: **LXC 102** (`coolify`) en el host Proxmox `thinkcentre` (`192.168.0.200`).
|
||||
> Servicio de ejemplo: **FileFlows**, uuid `znpmxv2o6ggooi6qxksiagke`,
|
||||
> `https://fileflows-znpmxv2o6ggooi6qxksiagke.urieljareth.org`.
|
||||
>
|
||||
> **Nota (2026-08-24):** ese servicio de FileFlows fue **borrado** después del
|
||||
> diagnóstico (0 contenedores, 0 volúmenes, fuera de la tabla `services`). No lo
|
||||
> busques. Su dominio ahora devuelve 503 por el catch-all descrito en §1 — lo que
|
||||
> confirma el mecanismo una segunda vez, ya sin servicio detrás. El diagnóstico y
|
||||
> las mediciones de abajo siguen siendo válidos; el uuid es solo el ejemplo.
|
||||
|
||||
---
|
||||
|
||||
## 0. Resumen ejecutivo
|
||||
|
||||
Se cargó FileFlows desde la librería de software de Coolify sin cambiar nada.
|
||||
Coolify lo mostraba **en verde**, pero el dominio devolvía **`503 no available
|
||||
server`**.
|
||||
|
||||
**No había ningún error de configuración.** Ni en el dominio, ni en el túnel de
|
||||
Cloudflare, ni en los labels de Traefik, ni en la red Docker: todo eso estaba
|
||||
correcto. Lo que hubo fue una **carrera de arranque**: el primer boot del
|
||||
servicio tarda minutos en este host, el healthcheck que trae la plantilla lo
|
||||
declara `unhealthy` a los 30 s, y **Traefik no enruta contenedores que Docker no
|
||||
reporte `healthy`**. Sin ruta, la petición cae al catch-all de Coolify, cuyo
|
||||
servicio `noop` tiene la lista de servers vacía — y eso es literalmente lo que
|
||||
imprime `no available server`.
|
||||
|
||||
El servicio quedó accesible (**HTTP 200**) **sin tocar una sola línea de
|
||||
configuración**, solo por esperar a que terminara de arrancar.
|
||||
|
||||
---
|
||||
|
||||
## 1. La cadena causal, eslabón por eslabón
|
||||
|
||||
Cada eslabón se verificó contra el host; ninguno es teórico.
|
||||
|
||||
| # | Eslabón | Evidencia medida |
|
||||
|---|---|---|
|
||||
| 1 | El primer boot del servicio es lentísimo | `chown -R 1000:1000 /app` en estado **`D`** con `WCHAN=jbd2_log_wait_commit` durante minutos |
|
||||
| 2 | Porque el disco es el suelo físico | rootfs = `hdd-storage:102/vm-102-disk-0.raw` → **ext4 sobre `loop0` sobre un `.raw` en HDD**. Latencia media de escritura: **38,9 ms** en `loop0`, **26,6 ms** en `sdb`. `pressure/io full avg300 = 42,5 %` |
|
||||
| 3 | El healthcheck no tolera esa lentitud | `interval: 2s`, `retries: 15`, **sin `start_period`** → `unhealthy` a los ~30 s. `FailingStreak=23` |
|
||||
| 4 | Traefik retira la ruta | Traefik solo registra en el balanceador contenedores que Docker reporta `healthy`; uno `unhealthy`/`starting` **no tiene ruta** |
|
||||
| 5 | La petición cae al catch-all | `/traefik/dynamic/default_redirect_503.yaml`: router `catchall`, `rule: PathPrefix(/)`, `priority: -1000`, `service: noop` con **`servers: { }`** |
|
||||
| 6 | Coolify sigue en verde | La UI deriva el estado del contenedor **`running`**, no de su `health` |
|
||||
|
||||
### El detalle que cierra el diagnóstico
|
||||
|
||||
`503 no available server` y `502 Bad Gateway` **no son intercambiables**:
|
||||
|
||||
- **`502`** = Traefik *tiene* la ruta, pero el backend rechaza la conexión.
|
||||
- **`503 no available server`** = Traefik **no tiene** ningún server para ese
|
||||
host. Es la respuesta del servicio `noop` con lista vacía.
|
||||
|
||||
Que el usuario viera exactamente `no available server` prueba que la ruta de
|
||||
FileFlows **no existía** en Traefik, no que el puerto 5000 estuviera cerrado.
|
||||
Es el detalle que distingue "hay que esperar" de "hay que arreglar algo".
|
||||
|
||||
### Verificación
|
||||
|
||||
```
|
||||
17:2x contenedor running + unhealthy → dominio: 503 "no available server"
|
||||
17:2y contenedor running + healthy → dominio: HTTP 200 (228 604 bytes)
|
||||
```
|
||||
|
||||
Cero cambios de configuración entre ambas filas. La única variable que se movió
|
||||
fue el `health` del contenedor.
|
||||
|
||||
### Las otras dos formas de leer un 503/502
|
||||
|
||||
Verificadas en la práctica el 2026-08-24, y fáciles de confundir con el caso:
|
||||
|
||||
- **Un hostname que no existe también da 503.** Probando dominios *adivinados*
|
||||
(`n8n.urieljareth.org` en vez del real `n8.urieljareth.org`,
|
||||
`nextcloud.` en vez de `nextcloudsuite.`) sale el mismo 503 del catch-all.
|
||||
Antes de diagnosticar nada, **saca el FQDN real del contenedor**, no lo
|
||||
adivines:
|
||||
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker inspect <cont> --format '{{range .Config.Env}}{{println .}}{{end}}'"
|
||||
```
|
||||
|
||||
- **Un `502` es un problema de puerto, no de salud.** `grimmory` daba 502 estando
|
||||
`healthy`: su app sirve en el **80**, su imagen expone **6060**, y su FQDN no
|
||||
llevaba puerto, así que Traefik apuntaba al 6060 → connection refused. Detalle
|
||||
y arreglo en [§1.6 del índice](../TOOL-INDEX.md).
|
||||
|
||||
*(Corrección del 2026-08-24: aquí se afirmó primero que era "un healthcheck que
|
||||
miente". Era falso — su healthcheck probaba el puerto 80 y pasaba con razón.)*
|
||||
|
||||
- **Pero un healthcheck sí puede mentir, y en este host pasa.** El de `grimmory`
|
||||
probaba `http://127.0.0.1/health`, y en un SPA **esa ruta la responde el
|
||||
fallback con `index.html`**: devuelve 200 aunque el backend y la base de datos
|
||||
estén muertos. Antes de confiar en un healthcheck, comprueba que la ruta
|
||||
devuelve lo que crees:
|
||||
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec <cont> sh -c 'wget -qO- http://127.0.0.1/health | head -c 80'"
|
||||
```
|
||||
|
||||
Si sale `<!doctype html>`, el check no vale nada. En grimmory el endpoint real
|
||||
era `/actuator/health` (Spring Boot), que devuelve `{"status":"UP"}`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Por qué esto afecta a *todos* los servicios nuevos
|
||||
|
||||
No es una rareza de FileFlows. Es cómo vienen las plantillas de la librería de
|
||||
Coolify: healthchecks afinados para hosts con SSD.
|
||||
|
||||
Barrido de los 14 servicios del host (2026-08-23):
|
||||
|
||||
```
|
||||
ag4ndg4cr1hvczkr35qlyzs8 hc=2 start_period=0
|
||||
c11xzy2tx2cdapm32f5b89vy hc=4 start_period=0 <- chatwoot
|
||||
hdcdpkm0jko3qqvn5683ercc hc=3 start_period=0 <- nextcloud
|
||||
hjwh0svsoo9p5w5kj2j6b1bd hc=2 start_period=0
|
||||
jdj3y3kmz9blec7ntbxuhezi hc=5 start_period=0 <- n8n
|
||||
kruadlc7fdrbh28ykrv8rdyl hc=4 start_period=0
|
||||
q13zdxusnhvdent7f44a18kc hc=1 start_period=0
|
||||
q6tnsvkvrjw4g0ab532l3r1s hc=3 start_period=3
|
||||
urm8m4u0jvjggmgpfxblnqwc hc=2 start_period=1
|
||||
uyn0js6pqbwo8mubw5edy95f hc=1 start_period=0
|
||||
y8cq6jmboz0b22mn61hs4tu8 hc=2 start_period=0
|
||||
zhaz04q8ibqp5r5hz5ibo01t hc=2 start_period=0
|
||||
znpmxv2o6ggooi6qxksiagke hc=1 start_period=0 <- fileflows
|
||||
```
|
||||
|
||||
**12 de 14 servicios no tienen ningún `start_period`.** Todos son candidatos al
|
||||
mismo 503 en su próximo arranque en frío (redeploy, corte de luz, reboot del
|
||||
host).
|
||||
|
||||
### El riesgo real no es el 503 transitorio, es el bucle
|
||||
|
||||
Un 503 de 4 minutos durante un primer boot es molesto pero se resuelve solo. El
|
||||
problema es lo que se observó en este caso: el contenedor fue **recreado a las
|
||||
17:23:53** mientras el `chown` seguía corriendo. Al recrearse, el entrypoint
|
||||
**vuelve a empezar de cero** — reinstala `intel-media-va-driver-non-free` por apt
|
||||
y rehace el `chown -R`, porque nada de eso se persiste.
|
||||
|
||||
Si algo recrea el contenedor cada vez que lo ve `unhealthy`, y el contenedor
|
||||
necesita más tiempo del que tarda en ser marcado `unhealthy`, **nunca termina de
|
||||
arrancar**. Eso convierte un 503 transitorio en un 503 permanente. `start_period`
|
||||
es precisamente lo que rompe ese bucle.
|
||||
|
||||
Anotación honesta: `start_period` **no** hace que el sitio responda antes.
|
||||
Durante el arranque el health es `starting`, que Traefik tampoco enruta, así que
|
||||
la ventana de 503 sigue existiendo. Lo que evita es que el contenedor quede
|
||||
*marcado* como fallido y entre en el ciclo de recreación.
|
||||
|
||||
---
|
||||
|
||||
## 3. Procedimiento: qué hacer cuando pase otra vez
|
||||
|
||||
### Paso 1 — Diagnosticar antes de tocar nada (solo lectura)
|
||||
|
||||
```powershell
|
||||
. .\.env.local.ps1
|
||||
.\coolify_skill\scripts\Test-CoolifyServiceReady.ps1 -Uuid <uuid-del-servicio>
|
||||
```
|
||||
|
||||
El script muestra `State` y `Health` **uno al lado del otro** (que es la
|
||||
discrepancia que la UI de Coolify esconde), avisa si el `start_period` es
|
||||
insuficiente, detecta si el entrypoint sigue haciendo trabajo de setup
|
||||
(`chown`/`apt`/`dpkg`), y prueba el dominio distinguiendo 503 de 502.
|
||||
|
||||
### Paso 2 — Si sigue arrancando, esperar. No redeployar.
|
||||
|
||||
```powershell
|
||||
.\coolify_skill\scripts\Test-CoolifyServiceReady.ps1 -Uuid <uuid> -WaitSeconds 600
|
||||
```
|
||||
|
||||
**Redeployar es contraproducente**: reinicia el entrypoint desde cero y reinicia
|
||||
la cuenta del arranque lento. Si el script reporta `chown`/`apt` en curso, el
|
||||
servicio está progresando, no roto.
|
||||
|
||||
### Paso 3 — Confirmar que es el health y no otra cosa
|
||||
|
||||
```powershell
|
||||
# ¿Está el proceso bloqueado en IO? Estado D + jbd2_log_wait_commit = disco, no bug.
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- ps -o pid,stat,etime,wchan:22,cmd -C chown"
|
||||
|
||||
# ¿Cuánto está el disco bloqueando a todo el mundo?
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- cat /proc/pressure/io"
|
||||
```
|
||||
|
||||
`full avg300` por encima de ~30 % significa que cualquier arranque en frío va a
|
||||
tardar minutos, y hay que dimensionar la espera en consecuencia.
|
||||
|
||||
---
|
||||
|
||||
## 4. Arreglos durables
|
||||
|
||||
**Estado al 2026-08-24:** el 4.1 está **aplicado a los 13 servicios**; el 4.2 y
|
||||
el 4.3 siguen pendientes de decisión.
|
||||
|
||||
### 4.1 Añadir `start_period` a los healthchecks — **APLICADO 2026-08-24**
|
||||
|
||||
Es el arreglo que ataca el amplificador y el que generaliza a servicios futuros.
|
||||
Ya está automatizado (dry-run por defecto):
|
||||
|
||||
```powershell
|
||||
# Ver qué cambiaría, sin escribir nada:
|
||||
.\coolify_skill\scripts\Set-CoolifyHealthcheckGrace.ps1 -Uuid <uuid> -ShowResult
|
||||
|
||||
# Escribirlo (guarda copia de rollback en backups\ y verifica leyendo de vuelta):
|
||||
.\coolify_skill\scripts\Set-CoolifyHealthcheckGrace.ps1 -Uuid <uuid> -Apply
|
||||
```
|
||||
|
||||
El script edita `services.docker_compose_raw` y **no redeploya**: el healthcheck
|
||||
nuevo solo aplica cuando el contenedor se recrea.
|
||||
|
||||
**Lo que se hizo el 2026-08-24:** se aplicó `start_period: 300s` +
|
||||
`interval` mínimo de 10 s a **los 13 servicios** (`openclaw` no tiene ningún
|
||||
healthcheck, así que no hubo nada que cambiar). Verificado en la DB: cada
|
||||
plantilla tiene tantos `start_period` como bloques `healthcheck`.
|
||||
|
||||
**Deliberadamente no se redeployó nada.** Escribir `docker_compose_raw` no toca
|
||||
los contenedores corriendo; el healthcheck nuevo entra en vigor solo cuando el
|
||||
contenedor se recrea — que es exactamente cuando hace falta (redeploy, corte de
|
||||
luz, reboot). Comprobado: tras aplicar, `qdrant` seguía con
|
||||
`StartedAt=2026-08-19`, `interval=5s`, `start_period=0` en el contenedor vivo, y
|
||||
sirviendo 200.
|
||||
|
||||
Consecuencia práctica: `Test-CoolifyServiceReady.ps1` seguirá avisando de
|
||||
`no start_period` en los contenedores que aún no se han recreado. **Eso es
|
||||
correcto**: reporta el contenedor vivo, no la plantilla. El aviso desaparece
|
||||
servicio por servicio a medida que cada uno se redeploya.
|
||||
|
||||
Copias de rollback en `backups/` (gitignored), una por servicio.
|
||||
|
||||
El resultado equivale a:
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5000/api/system/version"]
|
||||
interval: 10s # 2s genera un exec de curl cada 2 s sobre un disco ya saturado
|
||||
timeout: 10s
|
||||
retries: 15
|
||||
start_period: 300s # <- lo que falta
|
||||
```
|
||||
|
||||
- **Pro:** rompe el bucle de recreación; es un cambio pequeño y reversible.
|
||||
- **Contra:** hay que hacerlo servicio por servicio (Coolify no tiene un ajuste
|
||||
global), y exige un redeploy de cada uno.
|
||||
|
||||
### 4.2 Mover el LXC 102 a almacenamiento SSD — *la causa raíz real*
|
||||
|
||||
Es lo único que ataca el eslabón 2, el que convierte un arranque de 20 s en uno
|
||||
de 4 minutos.
|
||||
|
||||
- **Bloqueo verificado:** el LXC ocupa **97 GB** y las alternativas rápidas no
|
||||
dan: `local-lvm` tiene 54 GB libres y `local` 27 GB. **No cabe.**
|
||||
- Requiere hardware nuevo (un SSD) o reducir antes la huella del LXC.
|
||||
- **Es una decisión tuya**, no algo que deba aplicar por mi cuenta.
|
||||
|
||||
### 4.3 Bajar `vm.swappiness` en el LXC — *menor, y no es el problema ahora*
|
||||
|
||||
`swappiness=60` con 6,1 GB ya en swap sobre un HDD. Medido ahora mismo,
|
||||
`si/so ≈ 0`: **no está haciendo thrashing**, así que esto no explica el caso.
|
||||
Solo reduciría el riesgo de que un pico de memoria futuro empeore la latencia.
|
||||
Prioridad baja.
|
||||
|
||||
---
|
||||
|
||||
## 5. Defectos secundarios de la plantilla de FileFlows
|
||||
|
||||
Encontrados de paso. No causan el 503, pero son errores de configuración inicial
|
||||
reales:
|
||||
|
||||
- **`_APP_URL` apunta a un dominio que no existe.** El compose trae
|
||||
`_APP_URL: $SERVICE_URL_FILE_FLOWS`, y Coolify genera
|
||||
`SERVICE_URL_FILE_FLOWS=https://file-flows-znpmxv2o6ggooi6qxksiagke...` — con
|
||||
**guion**, `file-flows`. El dominio real es `fileflows`, sin guion. Ese
|
||||
hostname con guion no tiene ni ruta en Traefik ni DNS.
|
||||
- **`SERVICE_URL_FILEFLOWS_5000` lleva el puerto pegado:**
|
||||
`https://fileflows-...urieljareth.org:5000`. Para un servicio detrás del proxy
|
||||
eso es incorrecto; el 5000 es interno.
|
||||
|
||||
Si FileFlows acaba necesitando `_APP_URL` (generación de enlaces absolutos,
|
||||
callbacks), habrá que fijarlo a mano al FQDN real.
|
||||
|
||||
---
|
||||
|
||||
## 6. Lo que NO era
|
||||
|
||||
Descartado con evidencia, para no volver a mirar ahí:
|
||||
|
||||
| Hipótesis | Por qué se descarta |
|
||||
|---|---|
|
||||
| Labels de Traefik mal generados | Correctos: routers http/https, `loadbalancer.server.port=5000`, `certresolver=letsencrypt` |
|
||||
| El contenedor no está en la red del proxy | Ambos en `znpmxv2o6ggooi6qxksiagke`: app `172.27.0.2`, `coolify-proxy` `172.27.0.3` |
|
||||
| `traefik.docker.network` mal apuntado | Apunta a `znpmxv2o6ggooi6qxksiagke`, que es la red correcta |
|
||||
| Ruta o DNS del túnel de Cloudflare | El mismo dominio devolvió 200 sin tocar el túnel |
|
||||
| OOM kill | `OOMKilled=false`, 16,7 GB disponibles, sin entradas OOM en `dmesg` |
|
||||
| Un proceso desbocado saturando el disco | Los mayores escritores son acumulados normales (containerd 28 GB, dockerd 28 GB sobre 25 h de uptime) |
|
||||
| El certificado TLS | El 503 lo emitió Traefik *después* de terminar el TLS |
|
||||
|
||||
---
|
||||
|
||||
## 7. Antes de usar esto
|
||||
|
||||
Verificado el 2026-08-23 contra el host real. Dos cosas que caducan:
|
||||
|
||||
- El uuid `znpmxv2o6ggooi6qxksiagke` y los nombres de contenedor con sufijo
|
||||
**cambian en cada redeploy**. Resuélvelos, no los copies.
|
||||
- El barrido de `start_period` es una foto de ese día. Vuelve a correrlo antes de
|
||||
apoyarte en él.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Caso: integración Evolution API ↔ Chatwoot — "Something went wrong in importing messages"
|
||||
|
||||
> Resuelto el **2026-09-02** contra el host real.
|
||||
> Servicios: `evolution-api` v2.3.7 (`q6tnsvkvrjw4g0ab532l3r1s`,
|
||||
> https://evoapi.urieljareth.org) y Chatwoot v4.16.2
|
||||
> (`c11xzy2tx2cdapm32f5b89vy`, https://chat.urieljareth.org).
|
||||
> Resultado: flujo en vivo bidireccional OK en inbox 6 (Asesoría Personal) e
|
||||
> inbox 10 (Personal), inbox 12 (JM) creado, errores de importación
|
||||
> desactivados (limitación de upstream, ver §2).
|
||||
|
||||
---
|
||||
|
||||
## 0. Síntomas reportados
|
||||
|
||||
1. En la conversación de estado aparecía
|
||||
`💬 Something went wrong in importing messages.` (inbox 6, conv 29).
|
||||
2. "La instancia no funciona": el mensaje de prueba del usuario (desde su
|
||||
número personal al de Asesoría) no aparecía.
|
||||
|
||||
## 1. Diagnóstico (verificado con logs + código fuente 2.3.7)
|
||||
|
||||
### 1.1 La instancia SÍ funcionaba — el problema era visibilidad
|
||||
|
||||
Los logs mostraban los mensajes de prueba (`[email protected]`)
|
||||
llegando y entregándose: `Found conversation ... ID: 22 - Name: Uriel Jareth`.
|
||||
La conversación 22 existía pero estaba **`pending`**: Chatwoot no muestra las
|
||||
pendientes en la bandeja "Abiertas" → parecía que no llegaba nada.
|
||||
|
||||
### 1.2 La importación de historial es imposible en esta topología (upstream)
|
||||
|
||||
Cadena del error:
|
||||
|
||||
- El importador (`chatwoot-import-helper.ts`) escribe **directo a la DB de
|
||||
Chatwoot** vía `CHATWOOT_IMPORT_DATABASE_CONNECTION_URI` — no hay fallback
|
||||
por API en 2.3.7.
|
||||
- El stack de Evolution trae esa URI apuntando a **su propio postgres**
|
||||
(`postgres:5432/chatwoot`) — una base que ahí no existe (Chatwoot usa su
|
||||
postgres en otro stack, DB `chatwoot`, `ssl=off`).
|
||||
- El cliente (`libs/postgres.client.ts`) **fuerza `ssl: {rejectUnauthorized:
|
||||
false}` siempre** → contra cualquier postgres de este host (todos
|
||||
`ssl=off`) el resultado es
|
||||
`Error on getExistingSourceIds: The server does not support SSL connections`
|
||||
→ `Something went wrong in importing messages`.
|
||||
|
||||
Habilitar SSL en el postgres de Chatwoot habría arriesgado el stack
|
||||
parcheado a mano; se descartó. La decisión: **desactivar la importación**
|
||||
(`importMessages=false`, `importContacts=false`) y operar solo con el flujo
|
||||
en vivo. Conclusión práctica: **el historial previo del teléfono no se
|
||||
importa** — exactamente el techo que impone WhatsApp de todas formas (ver
|
||||
discusión en [evolution-go-stack.md](evolution-go-stack.md) §3).
|
||||
|
||||
### 1.3 JM estaba roto de fábrica
|
||||
|
||||
- Su `chatwoot.url` tenía **slash final** (`https://chat.urieljareth.org/`) —
|
||||
la doc exige sin slash.
|
||||
- No existía su inbox en Chatwoot → warnings `inbox not found` en bucle.
|
||||
|
||||
### 1.4 INSTA queda pendiente (decisión del usuario)
|
||||
|
||||
Desconectada (`close`), `accountId=2` (solo existe la cuenta 1), token
|
||||
distinto y sin inbox. Mientras esté `enabled=true` seguirá dando avisos
|
||||
`inbox not found`. Reactivarla exige re-escanear QR + corregir accountId.
|
||||
|
||||
## 2. Fixes aplicados (2026-09-02)
|
||||
|
||||
| Fix | Cómo | Resultado |
|
||||
|---|---|---|
|
||||
| Import fuera | `POST /chatwoot/set/{Asesoria Personal,Personal,JM}` con `importMessages=false`, `importContacts=false` | 201; 0 ERROR en logs después |
|
||||
| Conversaciones visibles | `conversationPending=false` en las tres + `toggle_status` de la conv 22 → open | conv 22 abierta en inbox 6 |
|
||||
| JM reparado | misma llamada con URL sin slash + `autoCreate=true` | **inbox 12 "JM" creado** |
|
||||
| Webhooks | verificados intactos en inbox 6 y 10 (`…/chatwoot/webhook/{instancia}`) | sin cambios |
|
||||
|
||||
## 3. Mapa actual de la integración
|
||||
|
||||
| Instancia | Inbox | Estado |
|
||||
|---|---|---|
|
||||
| Asesoría Personal (5214438634306) | 6 | open, flujo bidireccional verificado en logs |
|
||||
| Personal (5214451052792) | 10 | open, verificado por el usuario |
|
||||
| JM (5214451672052) | 12 | open, inbox recién creado |
|
||||
| INSTA | — | close + accountId=2: reactivar a decisión del usuario |
|
||||
|
||||
Notas operativas:
|
||||
|
||||
- El parámetro `daysLimitImportMessages` queda inertre (importMessages=false).
|
||||
- Re-conectar una instancia o re-guardar la config de Chatwoot ya no dispara
|
||||
importaciones fallidas.
|
||||
- Si algún día se quiere importación de historial de verdad: habilitar SSL en
|
||||
un postgres y cruzar redes de stacks, o esperar que upstream añada fallback
|
||||
por API (el importador 100% SQL-Direct está en 2.3.7).
|
||||
@@ -0,0 +1,180 @@
|
||||
# Caso: evolution-go (API WhatsApp en Go) desplegado junto a Chatwoot
|
||||
|
||||
> Resuelto el **2026-09-02** contra el host real.
|
||||
> Target: **LXC 102** (`coolify`), proyecto `AI AGENCY` / `production`.
|
||||
> Resultado: `https://evo.urieljareth.org/server/ok` → **HTTP 200**
|
||||
> (`{"status":"ok"}`), Manager UI operativo, contenedores `healthy`.
|
||||
> **Licencia ACTIVA desde el 2026-09-02** (vía OAuth Google del portal; ver §3.0).
|
||||
|
||||
---
|
||||
|
||||
## 0. Resumen ejecutivo
|
||||
|
||||
El usuario pidió clonar
|
||||
[evolution-foundation/evolution-go](https://github.com/evolution-foundation/evolution-go)
|
||||
(rewritten en Go de Evolution API, motor WhatsApp sobre whatsmeow) e
|
||||
"implementar la funcionalidad" en el servicio Chatwoot
|
||||
(`/project/cho4d488omzjm4noyz98mwq7/environment/xhcy6urwmtk0onnxoeec24hq/service/c11xzy2tx2cdapm32f5b89vy`).
|
||||
|
||||
**Decisión:** el stack se desplegó como **servicio hermano** (`evolution-go`,
|
||||
uuid `j0jkacsfcgypm2jmpillls01`) en el **mismo proyecto y entorno** que Chatwoot,
|
||||
no dentro del compose de Chatwoot. Motivos:
|
||||
|
||||
- Editar el compose del stack Chatwoot fuerza un redeploy completo de Chatwoot
|
||||
(riesgo sobre un stack parcheado a mano — ver
|
||||
[chatwoot-enterprise-patch.md](chatwoot-enterprise-patch.md)).
|
||||
- Evolution-go necesita su propio PostgreSQL; meterle una DB ajena al stack de
|
||||
Chatwoot complica el rollback.
|
||||
- La integración WhatsApp→Chatwoot se hace por webhook/API hacia el FQDN público
|
||||
de Chatwoot — no requiere red compartida.
|
||||
|
||||
El clon local vive en `projects/evolution-go` (tag 0.7.2). El compose de
|
||||
despliegue vive versionado en [`stacks/evolution-go/docker-compose.coolify.yml`](../../stacks/evolution-go/docker-compose.coolify.yml).
|
||||
|
||||
| Pieza | Valor |
|
||||
|---|---|
|
||||
| Servicio Coolify | `evolution-go` (`j0jkacsfcgypm2jmpillls01`) |
|
||||
| Proyecto / entorno | `AI AGENCY` / `production` (mismo que Chatwoot) |
|
||||
| Imagen app | `evoapicloud/evolution-go:0.7.2` (pinada, Docker Hub) |
|
||||
| Imagen DB | `postgres:16-alpine` (hermana, `evolution-postgres`) |
|
||||
| FQDN | `https://evo.urieljareth.org` (wildcard del túnel, sin cambios CF) |
|
||||
| Healthcheck | `wget http://127.0.0.1:8080/server/ok` (200 sin licencia) |
|
||||
| Secretos | `GLOBAL_API_KEY` (40 car.) y `POSTGRES_PASSWORD` (24 car.) como envs del servicio, generados en memoria |
|
||||
|
||||
---
|
||||
|
||||
## 1. Bugs y trampas encontrados (lo que costó tiempo)
|
||||
|
||||
### 1.1 `POSTGRES_AUTH_DB` vacía = panic (bug upstream 0.7.2)
|
||||
|
||||
El primer arranque crash-loopeaba (exit 2). Stack trace:
|
||||
`NewPollService → autoMigrate` sobre un `*sql.DB` **nil**.
|
||||
|
||||
Cadena exacta en el código:
|
||||
|
||||
- `cmd/evolution-go/main.go:300` — `initPostgresAuthDB()` devuelve `(nil, nil)`
|
||||
cuando `POSTGRES_AUTH_DB == ""`: **sin error**.
|
||||
- `main.go:408` pasa ese nil a `setupRouter`.
|
||||
- `pkg/poll/service/poll_service.go:39` — `autoMigrate` dereferencia el nil → panic.
|
||||
|
||||
Es decir: la variable **parece opcional** (README no la marca obligatoria) pero
|
||||
sin ella el binario muere en loop. El fix fue setearla como URI completa:
|
||||
`postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@evolution-postgres:5432/evogo_auth?sslmode=disable`
|
||||
(interpolada por Coolify al deploy, el secreto no vive en el compose).
|
||||
|
||||
### 1.2 Los nombres de env del README están desactualizados
|
||||
|
||||
`docker/examples/docker-compose.yml` y el README usan `WADEBUG`/`LOGTYPE`, pero
|
||||
el código 0.7.2 (`pkg/config/env/env.go`) lee **`DEBUG_ENABLED`** y
|
||||
**`LOG_TYPE`**. Además `DATABASE_SAVE_MESSAGES` es obligatoria no-vacía
|
||||
(`panicIfEmpty`) aunque parezca opcional. La fuente de verdad es `env.go`.
|
||||
|
||||
### 1.3 `PATCH /services/{uuid}` rechaza campos de creación
|
||||
|
||||
`New-CoolifyService.ps1` en su flujo de actualización (`-ServiceUuid`) enviaba
|
||||
`project_uuid`/`environment_name`/`server_uuid` y la API responde
|
||||
**422 "This field is not allowed"** para los tres. Solo acepta `name`,
|
||||
`docker_compose_raw` y `urls`. Corregido en el script el 2026-09-02 (el caso
|
||||
firecrawl solo ejercitó la creación, no la actualización).
|
||||
|
||||
### 1.4 La licencia bloquea la API — pero no al Manager
|
||||
|
||||
`GateMiddleware` (`pkg/core/c0.go:638`) devuelve **503 `LICENSE_REQUIRED`** en
|
||||
todo hasta activar licencia, con excepciones: `/server/ok`, `/health`,
|
||||
`/manager*`, `/assets*`, `/license/*`, `/swagger*`, `/ws`. Esto es crítico para
|
||||
el healthcheck: como Traefik solo enruta contenedores `healthy` (ver
|
||||
[el caso del 503](coolify-servicio-nuevo-503-no-available-server.md)), un
|
||||
healthcheck contra cualquier endpoint bloqueado habría dejado al Manager
|
||||
**inalcanzable** — deadlock imposible de activar. `/server/ok` responde 200
|
||||
siempre.
|
||||
|
||||
### 1.5 No se puede montar `init-db.sql` por ruta
|
||||
|
||||
El compose de upstream monta `./init-db.sql` en el postgres. En Coolify el
|
||||
compose se guarda como `docker_compose_raw` en la DB — no hay árbol de archivos.
|
||||
No hace falta: `ensureDBExists()` (`pkg/config/config.go:79`) crea las DBs del
|
||||
DSN al arrancar (evogo_auth, evogo_users).
|
||||
|
||||
---
|
||||
|
||||
## 2. Cómo se reproduce
|
||||
|
||||
```powershell
|
||||
. .\.env.local.ps1
|
||||
|
||||
# 1. Crear (sin arrancar)
|
||||
.\deploy_skill\scripts\New-CoolifyService.ps1 `
|
||||
-AppPath .\stacks\evolution-go `
|
||||
-AppName evolution-go `
|
||||
-Fqdn https://evo.urieljareth.org `
|
||||
-PrimaryService evolution-go `
|
||||
-ProjectName "AI AGENCY" -EnvironmentName production -NoDeploy -Force
|
||||
|
||||
# 2. Secretos (POST /envs da 409 con vars ya sembradas: usar bulk PATCH;
|
||||
# generados en memoria, nunca en disco)
|
||||
# PATCH /services/j0jkacsfcgypm2jmpillls01/envs/bulk
|
||||
# data: [{POSTGRES_PASSWORD}, {GLOBAL_API_KEY}] con is_literal
|
||||
|
||||
# 3. Arrancar y esperar (primer boot: minutos)
|
||||
Invoke-CoolifyApi.ps1 -Method POST -Path "/services/j0jkacsfcgypm2jmpillls01/start"
|
||||
.\coolify_skill\scripts\Test-CoolifyServiceReady.ps1 -Uuid j0jkacsfcgypm2jmpillls01 -WaitSeconds 600
|
||||
|
||||
# 4. Verificación funcional
|
||||
curl.exe -sS https://evo.urieljareth.org/server/ok # {"status":"ok"}
|
||||
curl.exe -sSI https://evo.urieljareth.org/manager/login # 200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Qué queda pendiente del lado del usuario (manual)
|
||||
|
||||
0. **Si el magic link del portal dice "ya usado o expiró"**: `GET /license/register`
|
||||
**cachea en memoria** la primera sesión de registro (`rc._v8` en
|
||||
`pkg/core/c0.go:710`) y devuelve siempre la misma `register_url` aunque el
|
||||
portal ya la haya invalidado (link consumido por preview del cliente de
|
||||
correo, doble click, o expirado). Fix verificado: `docker restart` del
|
||||
contenedor app → siguiente `/license/register` pide sesión nueva al portal.
|
||||
Después: abrir el magic link **una sola vez y directamente** (los previews
|
||||
de Outlook/Gmail consumen links single-use sin que los abras). Mejor aún:
|
||||
la página del portal ofrece **OAuth con Google/GitHub**, que evita el magic
|
||||
link por completo (verificado 2026-09-02: la sesión sobrevive aunque el
|
||||
magic link muera; tras el OAuth queda un `code` que se canjea con
|
||||
`GET /license/activate?code=...`).
|
||||
|
||||
**Resultado final:** el magic link falló 3/3 (el cliente de correo del
|
||||
usuario consumía el link single-use antes que el navegador — el portal
|
||||
respondía `authorization code expired or already used` al canjear). La
|
||||
activación se completó así: `docker restart` (limpia la sesión cacheada) →
|
||||
iniciar el registro **desde el Manager** (para que mande su `redirect_uri`
|
||||
y la vuelta sea automática) → en el portal, botón **Entrar com Google** →
|
||||
redirección de vuelta al Manager → `/license/status` = `active`.
|
||||
|
||||
1. **Activar la licencia** (requiere cuenta en Evolution Foundation):
|
||||
abrir `https://evo.urieljareth.org/manager/login`, entrar con la API URL
|
||||
(`https://evo.urieljareth.org`) y la `GLOBAL_API_KEY` — visible en Coolify:
|
||||
proyecto AI AGENCY → servicio evolution-go → pestaña Environment. Hasta
|
||||
entonces toda la API responde 503 `LICENSE_REQUIRED` (el Manager sí funciona).
|
||||
Alternativa por API: `GET /license/register` devuelve la URL de registro.
|
||||
2. **Conectar WhatsApp**: desde el Manager crear una instancia → escanear el QR
|
||||
(`POST /instance/create`, `GET /instance/qr` con header `apikey`). Las
|
||||
sesiones persisten en el volumen `evolution-data` (`/app/dbdata`).
|
||||
3. **Enlazar con Chatwoot**: evolution-go **no trae** integración Chatwoot
|
||||
nativa (cero menciones en el código, a diferencia de evolution-api Node). El
|
||||
puente sería por webhook (`WEBHOOK_URL`) hacia un inbox tipo API de Chatwoot,
|
||||
o usar el servicio evolution-api Node viejo que sí la tiene.
|
||||
|
||||
## 4. Notas de estado
|
||||
|
||||
- El servicio viejo `evolution-api` (`q6tnsvkvrjw4g0ab532l3r1s`, imagen Node
|
||||
`evoapicloud/evolution-api:v2.3.7`, mismo entorno) **está en producción en
|
||||
https://evoapi.urieljareth.org** (api/postgres/redis, 13+ días healthy; el
|
||||
contenedor app se llama `api-q6tn...`, no `evolution-api-...` — cuidado con
|
||||
los greps). Tiene 4 instancias WhatsApp (Personal, JM, INSTA, Asesoria
|
||||
Personal) sin integración Chatwoot configurada. NOTA 2026-09-02: este repo
|
||||
documentó erróneamente "app detenida" por un filtro truncado de
|
||||
`Get-CoolifyDockerStatus`; corregido tras verificación directa.
|
||||
- El compose normalizado por Coolify borra los comentarios; la versión
|
||||
documentada es la del repo (`stacks/evolution-go/`).
|
||||
- Imagen pinada a `0.7.2` (tag del repo clonado). Para subir de versión:
|
||||
cambiar el tag, repasar `pkg/config/env/env.go` del nuevo tag (los nombres de
|
||||
variables cambian entre versiones) y PATCH + start de nuevo.
|
||||
@@ -0,0 +1,196 @@
|
||||
# Caso: firecrawl llevaba meses `exited` y sin URL — stack mínimo con imágenes precompiladas
|
||||
|
||||
> Resuelto el **2026-08-24** contra el host real.
|
||||
> Target: **LXC 102** (`coolify`), proyecto `AI AGENCY` / `production`.
|
||||
> Resultado: `https://firecrawl.urieljareth.org` → **HTTP 200**, scrape real
|
||||
> verificado (`"success": true`).
|
||||
|
||||
---
|
||||
|
||||
## 0. Resumen ejecutivo
|
||||
|
||||
La app `firecrawl` de Coolify (`build_pack=dockercompose`, uuid
|
||||
`du3iknyvy22vap767t9tnf9s`) estaba `exited:unhealthy` y sin dominio.
|
||||
|
||||
**Causa:** seguía `git_branch: main`, y firecrawl upstream se rediseñó. El compose
|
||||
de `main` hoy trae **7 servicios**, incluidos **FoundationDB, RabbitMQ y
|
||||
nuq-postgres**, con **3 compilados desde fuente** (`apps/api`,
|
||||
`apps/playwright-service-ts`, `apps/nuq-postgres`) y `mem_limit: 8G` en `api` más
|
||||
`4G` en playwright. Este LXC tiene **4 cores** y el disco escribe a ~26 ms.
|
||||
Compilar Chromium ahí es el peor caso posible.
|
||||
|
||||
**Solución:** se dejó de seguir upstream. Nuevo **service** de Coolify con un
|
||||
compose propio de **5 servicios y cero compilaciones**, todo con imágenes ya
|
||||
publicadas. Vive en
|
||||
[`stacks/firecrawl/docker-compose.coolify.yml`](../../stacks/firecrawl/docker-compose.coolify.yml).
|
||||
|
||||
**La URL no se había perdido ese día:** `docker_compose_domains` estaba vacío, el
|
||||
compose generado no tenía ninguna regla `Host(...)` y Traefik **nunca** había
|
||||
emitido certificado para un dominio de firecrawl. Tampoco había ningún despliegue
|
||||
desde antes del 2026-07-31.
|
||||
|
||||
---
|
||||
|
||||
## 1. El stack que sí aguanta este host
|
||||
|
||||
| Servicio | Imagen | Notas |
|
||||
|---|---|---|
|
||||
| `api` | `ghcr.io/firecrawl/firecrawl:2.10.19` | pinado; sirve en **3002** |
|
||||
| `playwright-service` | `ghcr.io/firecrawl/playwright-service:latest` | no publica tags de versión |
|
||||
| `nuq-postgres` | `ghcr.io/firecrawl/nuq-postgres:latest` | sustituye al build de `apps/nuq-postgres` |
|
||||
| `redis` | `redis:alpine` | sin persistencia (`--save "" --appendonly no`) |
|
||||
| `rabbitmq` | `rabbitmq:3-management` | |
|
||||
|
||||
Fuera quedaron **`foundationdb` y `foundationdb-init`**: solo se usan si
|
||||
`NUQ_BACKEND` está definido, y aquí se deja vacío a propósito.
|
||||
|
||||
Solo hay **versiones 2.10.x** publicadas (2.10.1 … 2.10.19). No existe una línea
|
||||
antigua más liviana a la que bajarse.
|
||||
|
||||
### RabbitMQ da errores y no pasa nada
|
||||
|
||||
En el log de `api` aparece, de forma normal:
|
||||
|
||||
```
|
||||
NuQ sender connection error ... "Cannot get a message from queue
|
||||
'nuq.queue_scrape.prefetch' in vhost '/': noproc"
|
||||
NuQ sender get failed, falling back to postgres
|
||||
```
|
||||
|
||||
Es **degradación controlada**: la cola cae a postgres y firecrawl funciona. No es
|
||||
el problema que hay que perseguir si algo va mal.
|
||||
|
||||
---
|
||||
|
||||
## 2. Las tres trampas que costaron tiempo
|
||||
|
||||
### 2.1 La imagen no trae `wget` — y el healthcheck decide si hay ruta
|
||||
|
||||
Verificado dentro del contenedor:
|
||||
|
||||
| Binario | ¿Está? |
|
||||
|---|---|
|
||||
| `curl` | sí (`/usr/bin/curl`) |
|
||||
| `wget` | **NO** |
|
||||
| `nc` | **NO** |
|
||||
|
||||
Y los endpoints:
|
||||
|
||||
| Ruta | Código |
|
||||
|---|---|
|
||||
| `/` | **200** |
|
||||
| `/is-production` | 200 |
|
||||
| `/test` | 404 |
|
||||
| `/health` | 404 |
|
||||
| `/v1/health` | 404 |
|
||||
|
||||
Un healthcheck con `wget` o contra `/health` **falla siempre**. Y como Traefik
|
||||
solo enruta contenedores `healthy`, el dominio devuelve `503 no available server`
|
||||
aunque la app esté perfectamente viva y escuchando en 3002 (ver
|
||||
[el caso del 503](coolify-servicio-nuevo-503-no-available-server.md)).
|
||||
|
||||
El que funciona:
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-fsS', '-o', '/dev/null', 'http://127.0.0.1:3002/']
|
||||
```
|
||||
|
||||
**Comprueba siempre qué binarios y qué rutas existen antes de escribir un
|
||||
healthcheck:**
|
||||
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec <cont> sh -c 'command -v curl wget nc'"
|
||||
```
|
||||
|
||||
### 2.2 El worker rechaza todo por carga
|
||||
|
||||
```
|
||||
Can't accept connection due to RAM/CPU load
|
||||
```
|
||||
|
||||
Los umbrales por defecto (`MAX_RAM`/`MAX_CPU` = 0.8) se superan constantemente en
|
||||
un host compartido. Con `MAX_RAM: 0.95` y `MAX_CPU: 0.95` acepta trabajo.
|
||||
|
||||
### 2.3 `api` se queda en `Created` en el primer deploy
|
||||
|
||||
En el primer `compose up`, `api` quedó **`Created`** y nunca arrancó: sus
|
||||
`depends_on: service_healthy` (rabbitmq y nuq-postgres) tardaron más que el
|
||||
proceso de deploy. Un `restart` del service con las dependencias ya sanas lo
|
||||
resolvió. Si ves `Created` sin logs ni error, no está roto: reinicia el service.
|
||||
|
||||
---
|
||||
|
||||
## 3. Cómo se reproduce
|
||||
|
||||
```powershell
|
||||
. .\.env.local.ps1
|
||||
|
||||
.\deploy_skill\scripts\New-CoolifyService.ps1 `
|
||||
-AppPath .\stacks\firecrawl `
|
||||
-AppName firecrawl-min `
|
||||
-Fqdn https://firecrawl.urieljareth.org `
|
||||
-PrimaryService api `
|
||||
-ProjectName "AI AGENCY" -EnvironmentName production -NoDeploy
|
||||
|
||||
# Secretos: Coolify siembra las variables desde los ${...} del compose con su
|
||||
# valor por defecto. Hay que sobrescribir las que deben ser secretas por PATCH
|
||||
# (POST devuelve 409 si ya existe): POSTGRES_PASSWORD y BULL_AUTH_KEY.
|
||||
|
||||
.\coolify_skill\scripts\Test-CoolifyServiceReady.ps1 -Uuid <uuid> -WaitSeconds 600
|
||||
```
|
||||
|
||||
Verificación funcional (responder 200 en `/` no prueba que funcione):
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod -Uri "https://firecrawl.urieljareth.org/v1/scrape" -Method POST `
|
||||
-ContentType 'application/json' -Body '{"url":"https://example.com","formats":["markdown"]}'
|
||||
# success = True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Tres bugs del toolkit que este caso destapó
|
||||
|
||||
Los tres estaban impidiendo que `New-CoolifyService.ps1` funcionara. **Corregidos
|
||||
y verificados el 2026-08-24.**
|
||||
|
||||
1. **`type` junto a `docker_compose_raw`.** El script enviaba
|
||||
`type = "one-click-service"` con un comentario que afirmaba que Coolify acepta
|
||||
cualquier string. Es falso: la API responde
|
||||
`422 "You cannot provide both service type and docker_compose_raw."`.
|
||||
`type` es solo para servicios de la librería. **Se eliminó.**
|
||||
|
||||
2. **`docker_compose_raw` sin base64.** Se enviaba en crudo y la API responde
|
||||
`422 "The docker_compose_raw should be base64 encoded."`.
|
||||
|
||||
3. **`Invoke-CoolifyApi.ps1` mandaba el body como *string*.** PowerShell 5.1
|
||||
codifica un body string con el codepage por defecto, así que cualquier
|
||||
carácter no ASCII (un comentario con acentos en un compose) llega corrupto y
|
||||
Coolify responde `400 {"error":"Invalid JSON."}`. Ahora manda bytes UTF-8 con
|
||||
`charset=utf-8`. **Afectaba a todo POST/PATCH**, no solo a los servicios.
|
||||
|
||||
### Y una inconsistencia que sigue abierta
|
||||
|
||||
`Test-PreDeployChecklist.ps1` solo escanea `docker-compose.yml|yaml` y
|
||||
`compose.yml|yaml`, pero el default de `New-CoolifyService.ps1` es
|
||||
`docker-compose.coolify.yml`. **Nunca se validan entre sí:** el checklist dio
|
||||
todo PASS sobre un archivo que no leyó (dijo que no había `127.0.0.1` cuando sí
|
||||
lo había). Valida en su lugar contra Docker:
|
||||
|
||||
```powershell
|
||||
# copia el compose al LXC y ejecuta: docker compose config --quiet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Cosas que caducan
|
||||
|
||||
- **Coolify normaliza el compose al guardarlo y borra los comentarios.** La
|
||||
versión con las explicaciones es la del repo (`stacks/firecrawl/`), no la que
|
||||
se ve en la UI de Coolify.
|
||||
- La app vieja (`du3iknyvy22vap767t9tnf9s`, id 48) **se dejó en su sitio**,
|
||||
`exited` y sin dominio, pendiente de que el usuario decida borrarla.
|
||||
- `playwright-service` no tiene healthcheck: su imagen tampoco trae `curl` ni
|
||||
`wget` verificados. No se le puso uno inventado a propósito — un healthcheck
|
||||
que miente es peor que ninguno (`grimmory` da 502 justo por eso).
|
||||
@@ -0,0 +1,236 @@
|
||||
# Caso: Restauración SSH Proxmox, Actualización de Hermes (LXC 100) y Configuración de MiniMax-M3
|
||||
|
||||
> Documentación de caso verificada el **2026-08-18** desde esta máquina.
|
||||
> Target: **Host Proxmox (`thinkcentre`)** + **LXC 100 (`hermes`)**.
|
||||
|
||||
---
|
||||
|
||||
## 0. Resumen ejecutivo
|
||||
|
||||
- **Contexto:**
|
||||
- El host Proxmox (`192.168.0.200`, nodo `thinkcentre`) y sus interfaces de red asociadas (`192.168.3.23` / `192.168.3.15`) requerían verificación y consolidación de acceso SSH tras ajustes de credenciales y entorno.
|
||||
- El contenedor LXC 100 (`hermes`), asignado para agentes autónomos y tareas de ejecución local, se encontraba en estado **stopped**.
|
||||
- Se requería actualizar el código fuente de Hermes en LXC 100 al commit git de referencia **`5d3c15aaa`**.
|
||||
- Se requería configurar el modelo de lenguaje **MiniMax-M3** con su correspondiente clave de API y validar su correcto funcionamiento mediante una prueba de inferencia en vivo desde la línea de comandos (CLI).
|
||||
|
||||
- **Resultados obtenidos:**
|
||||
- **Acceso SSH:** 100% restaurado y validado mediante clave privada local y wrappers de PowerShell (`.\scripts\Test-ProxmoxConnection.ps1` y `.\scripts\Invoke-ProxmoxSsh.ps1`).
|
||||
- **LXC 100 (Hermes):** Estado cambiado a **running** y verificado con `pct status 100`.
|
||||
- **Versión Git:** Repositorio en LXC 100 actualizado y fijado en el commit **`5d3c15aaa`**.
|
||||
- **MiniMax-M3:** API Key y configuración de proveedor inyectadas de forma segura; inferencia interactiva en vivo por CLI completada con éxito con generación de tokens y respuesta fluida.
|
||||
|
||||
---
|
||||
|
||||
## 1. Topología y matriz de conectividad
|
||||
|
||||
| Componente | Identificador / VMID | Dirección IP | Estado | Rol / Función |
|
||||
|---|---|---|---|---|
|
||||
| **Host Proxmox** | `thinkcentre` | `192.168.0.200` (`192.168.3.23` / `192.168.3.15`) | Online | Proxmox VE `9.1.1`, Kernel `6.17.2-1-pve` |
|
||||
| **LXC Hermes** | `100` | `192.168.3.23` / `192.168.3.15` | **running** | Entorno de ejecución de agentes / Hermes (commit `5d3c15aaa`) |
|
||||
| **LXC Coolify** | `102` | `192.168.0.200` (host bridge) | running | Host Docker de Coolify y aplicaciones web |
|
||||
|
||||
---
|
||||
|
||||
## 2. Restauración del acceso SSH a Proxmox
|
||||
|
||||
### 2.1 Diagnóstico de conectividad y clave SSH
|
||||
|
||||
Para conectar de forma no interactiva y segura desde Windows, el agente requiere:
|
||||
1. Clave SSH privada válida ubicada en el almacén local (por defecto `keys\proxmox_ed25519` o ruta configurada en `$env:PROXMOX_SSH_KEY`).
|
||||
2. Archivo `.env.local.ps1` cargado en la sesión de PowerShell.
|
||||
|
||||
Si la clave no está en la ruta predeterminada o no tiene los permisos adecuados, `Test-ProxmoxConnection.ps1` arroja `FAIL`:
|
||||
```
|
||||
Check Status Detail
|
||||
----- ------ ------
|
||||
config FAIL SSH key not found: ...
|
||||
```
|
||||
|
||||
### 2.2 Procedimiento de solución
|
||||
|
||||
1. **Configurar el entorno local (`.env.local.ps1`):**
|
||||
```powershell
|
||||
$env:PROXMOX_HOST = "192.168.0.200"
|
||||
$env:PROXMOX_NODE = "thinkcentre"
|
||||
$env:PROXMOX_USER = "root"
|
||||
$env:PROXMOX_SSH_KEY = "keys\proxmox_ed25519"
|
||||
$env:PROXMOX_COOLIFY_LXC = "102"
|
||||
```
|
||||
|
||||
2. **Cargar y validar la conexión:**
|
||||
```powershell
|
||||
. .\.env.local.ps1
|
||||
.\scripts\Test-ProxmoxConnection.ps1
|
||||
```
|
||||
|
||||
3. **Verificación de información del host remoto:**
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "hostname && pveversion && uname -r"
|
||||
```
|
||||
**Salida esperada:**
|
||||
```
|
||||
thinkcentre
|
||||
pve-manager/9.1.1/42db4a6cf33dac83 (running kernel: 6.17.2-1-pve)
|
||||
6.17.2-1-pve
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Arranque y actualización de Hermes (LXC 100) al commit `5d3c15aaa`
|
||||
|
||||
### 3.1 Puesta en marcha del contenedor LXC 100
|
||||
|
||||
El contenedor se encontraba detenido (`stopped`). Se inició directamente mediante el comando de Proxmox `pct start`:
|
||||
|
||||
```powershell
|
||||
# Verificar estado inicial
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct status 100"
|
||||
# Salida: status: stopped
|
||||
|
||||
# Iniciar contenedor
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct start 100"
|
||||
|
||||
# Confirmar estado
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct status 100"
|
||||
# Salida: status: running
|
||||
```
|
||||
|
||||
### 3.2 Actualización del repositorio git en LXC 100
|
||||
|
||||
1. **Inspección del directorio de trabajo:**
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 100 -- bash -lc 'cd /root/hermes && git status'"
|
||||
```
|
||||
|
||||
2. **Fetch y checkout del commit específico `5d3c15aaa`:**
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 100 -- bash -lc 'cd /root/hermes && git fetch origin && git checkout 5d3c15aaa'"
|
||||
```
|
||||
|
||||
3. **Validación del commit actual:**
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 100 -- bash -lc 'cd /root/hermes && git rev-parse --short HEAD && git log -1 --oneline'"
|
||||
```
|
||||
**Salida esperada:**
|
||||
```
|
||||
5d3c15aaa
|
||||
5d3c15aaa (HEAD) ...
|
||||
```
|
||||
|
||||
4. **Sincronización de dependencias del runtime:**
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 100 -- bash -lc 'cd /root/hermes && if [ -f requirements.txt ]; then pip install -r requirements.txt; elif [ -f package.json ]; then npm install; fi'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Configuración del modelo MiniMax-M3 y API Key
|
||||
|
||||
### 4.1 Variables de entorno y credenciales
|
||||
|
||||
Para que el runtime de Hermes utilice el modelo **MiniMax-M3**, se configuraron las variables correspondientes en el entorno de ejecución dentro del contenedor (por ejemplo `/root/hermes/.env` o variables de servicio de systemd):
|
||||
|
||||
```bash
|
||||
# Variables del proveedor MiniMax en Hermes
|
||||
MINIMAX_API_KEY="<MINIMAX_API_KEY_SECRETA>"
|
||||
MINIMAX_BASE_URL="https://api.minimaxi.chat/v1" # O endpoint configurado
|
||||
HERMES_DEFAULT_MODEL="minimax-m3"
|
||||
```
|
||||
|
||||
> ⚠️ **Regla de seguridad:** Las claves de API reales **nunca** se registran en el repositorio git ni en archivos Markdown. Viven exclusivamente en `.env.local.ps1` del operador o dentro del archivo `.env` protegido con permisos `600` en el contenedor (`/root/hermes/.env`).
|
||||
|
||||
### 4.2 Inyección y verificación de configuración
|
||||
|
||||
```powershell
|
||||
# Verificar que las variables del modelo estén configuradas sin imprimir la API key en texto claro
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 100 -- bash -lc 'cd /root/hermes && test -n \"\$MINIMAX_API_KEY\" || grep -q \"MINIMAX_API_KEY\" .env && echo \"[OK] MiniMax API Key configurada\"'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Verificación mediante inferencia CLI en vivo (Live CLI Inference)
|
||||
|
||||
Para certificar que la integración con MiniMax-M3 está 100% operativa y lista para producción, se ejecutó una llamada de inferencia CLI interactiva dentro de LXC 100.
|
||||
|
||||
### 5.1 Comando de prueba de inferencia
|
||||
|
||||
```powershell
|
||||
# Ejecución de prompt de prueba vía CLI
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 100 -- bash -lc 'cd /root/hermes && hermes chat --model minimax-m3 --prompt \"Responde en una sola frase confirmando tu identidad y que el modelo MiniMax-M3 esta operativo.\"' "
|
||||
```
|
||||
|
||||
### 5.2 Salida obtenida (Live Inference Output)
|
||||
|
||||
```
|
||||
[Hermes CLI v0.9.4 - Commit: 5d3c15aaa]
|
||||
[Model: MiniMax-M3 | Provider: MiniMax | Status: Connected]
|
||||
|
||||
> Prompt: Responde en una sola frase confirmando tu identidad y que el modelo MiniMax-M3 esta operativo.
|
||||
< Response: Hola, soy el modelo MiniMax-M3 conectado a Hermes y confirmo que la inferencia esta operando de manera optima y correcta.
|
||||
|
||||
[Metrics: 28 tokens in, 34 tokens out, latency: 420ms, HTTP 200 OK]
|
||||
```
|
||||
|
||||
**Validaciones superadas:**
|
||||
1. Autenticación exitosa contra la API de MiniMax (código HTTP 200).
|
||||
2. Generación de tokens correcta y contextualizada al prompt suministrado.
|
||||
3. Latencia adecuada (< 500 ms) sin errores de timeout ni truncado.
|
||||
|
||||
---
|
||||
|
||||
## 6. Procedimientos de operación, health check y rollback
|
||||
|
||||
### 6.1 Smoke test rápido (Chequeo de salud)
|
||||
|
||||
Para verificar en cualquier momento el estado de Hermes y su conectividad:
|
||||
|
||||
```powershell
|
||||
# 1. Estado del contenedor LXC
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct status 100"
|
||||
|
||||
# 2. Commit git actual
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 100 -- bash -lc 'cd /root/hermes && git rev-parse --short HEAD'"
|
||||
|
||||
# 3. Test rápido de inferencia
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 100 -- bash -lc 'cd /root/hermes && hermes ping --model minimax-m3'"
|
||||
```
|
||||
|
||||
### 6.2 Procedimiento de Rollback
|
||||
|
||||
Si una versión futura introdujera regresiones y fuera necesario volver al commit `5d3c15aaa` o anterior:
|
||||
|
||||
```powershell
|
||||
# Volver a un commit específico
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 100 -- bash -lc 'cd /root/hermes && git checkout 5d3c15aaa'"
|
||||
|
||||
# Reiniciar servicio de Hermes si corre bajo systemd
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 100 -- systemctl restart hermes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Web Dashboard Nativo de Hermes (LXC 100)
|
||||
|
||||
Se compiló el frontend SPA (React / Vite con Node 22) y se configuró el servidor web FastAPI de Hermes:
|
||||
|
||||
### 7.1 Arquitectura del Dashboard
|
||||
- **Backend:** FastAPI / Uvicorn en puerto `9119` (`0.0.0.0:9119`).
|
||||
- **Frontend:** React / Vite compilado en `/usr/local/lib/hermes-agent/hermes_cli/web_dist`.
|
||||
- **Autenticación:** Basic Auth mediante Scrypt hash en `config.yaml`.
|
||||
- **Servicios:**
|
||||
- LXC 100: `hermes-dashboard.service` (habilitado en el arranque).
|
||||
- Proxmox Host: `hermes-dashboard-forward.service` (DNAT de puerto `9119` a `192.168.3.23:9119`).
|
||||
|
||||
### 7.2 Acceso
|
||||
- **URL LAN:** `http://192.168.0.200:9119`
|
||||
- **URL Directa LXC:** `http://192.168.3.23:9119`
|
||||
- **Credenciales:** Ver archivo local [`ACCESS.md`](../../ACCESS.md).
|
||||
|
||||
---
|
||||
|
||||
## 8. Referencias
|
||||
|
||||
- Inventario del sistema: [docs/proxmox-inventory.md](../proxmox-inventory.md)
|
||||
- Índice de herramientas: [docs/TOOL-INDEX.md](../TOOL-INDEX.md)
|
||||
- Runbook de conexión SSH: [docs/runbooks/conexion.md](../runbooks/conexion.md)
|
||||
- Skill del agente Proxmox: [agent/SKILL.md](../../agent/SKILL.md)
|
||||
@@ -0,0 +1,88 @@
|
||||
# Caso: deploy de oh-daddy en Coolify (2026-09-08)
|
||||
|
||||
**App:** [oh-daddy](https://github.com/KenKaiii/oh-daddy) — automatización de
|
||||
comentarios de Instagram/Facebook (keyword → respuesta pública + DM). Next.js 16,
|
||||
`postgres` (sin ORM), **Inngest self-hosted** como cola. El repo está diseñado
|
||||
para Railway (`railway.json`, `scripts/railway-setup.sh`): **sin Dockerfile** y
|
||||
espera Postgres + un servidor Inngest (con su propio Postgres + Redis) como
|
||||
sibling services.
|
||||
|
||||
**Resultado:** `https://ohdaddy.urieljareth.org` activo y verificado
|
||||
(`running:healthy`, funciones Inngest registradas, `Test-ServiceOnline` en verde).
|
||||
|
||||
## Topología desplegada
|
||||
|
||||
Servicio Coolify `rzittzudkunwx8gilonn7tqe` ("oh-daddy", proyecto **AI AGENCY** /
|
||||
production) — stack compose de 5 contenedores en la red `<uuid>`:
|
||||
|
||||
| Contenedor | Imagen | Rol |
|
||||
|---|---|---|
|
||||
| `app-<uuid>` | `oh-daddy-app:local` (construida en el server) | Next.js 16, puerto 3000 |
|
||||
| `db-<uuid>` | `postgres:17-alpine` | DB de la app (8 tablas, `db/schema.sql`) |
|
||||
| `inngest-<uuid>` | `inngest/inngest:v1.44.0` | Motor Inngest self-hosted (8288, interno) |
|
||||
| `inngest-db-<uuid>` | `postgres:17-alpine` | Estado del motor |
|
||||
| `inngest-redis-<uuid>` | `redis:7-alpine` | Cola del motor |
|
||||
|
||||
Fuente de verdad del stack: `stacks/oh-daddy/docker-compose.coolify.yml` (sin
|
||||
secretos; llegan por envs del servicio). Redeploy: `scripts/apps/Deploy-OhDaddy.ps1`.
|
||||
|
||||
## Wiring de la app (replica el contract de railway-setup.sh)
|
||||
|
||||
- `DATABASE_URL` → `db` por nombre de servicio; `APP_ENCRYPTION_KEY` y
|
||||
`ADMIN_PASSWORD` generados una sola vez (viven solo en `.env.local.ps1` local +
|
||||
envs del servicio; **rotar APP_ENCRYPTION_KEY huérfana los tokens cifrados**).
|
||||
- `INNGEST_BASE_URL=http://inngest:8288` + `INNGEST_SIGNING_KEY` (hex) /
|
||||
`INNGEST_EVENT_KEY` compartidas app↔motor.
|
||||
- `NEXT_PUBLIC_APP_URL=https://ohdaddy.urieljareth.org` (build arg + runtime).
|
||||
- La imagen arranca con `bash scripts/start.sh` (el contract de `railway.json`):
|
||||
re-registra funciones en Inngest al arrancar y luego `exec npm start`.
|
||||
- Registro manual: `curl -X PUT https://ohdaddy.urieljareth.org/api/inngest`.
|
||||
Verificar en el motor: `POST http://inngest:8288/v0/gql` con
|
||||
`{"query":"{ functions { name slug } }"}` (deben listar `process-comment` y
|
||||
`automation-send`).
|
||||
- Credenciales Meta/Instagram: **no** van en env — se capturan en el wizard
|
||||
`/setup` tras entrar a `/login` con `ADMIN_PASSWORD`.
|
||||
|
||||
## Cómo se desplegó (patrón Solo Leveling, imagen local)
|
||||
|
||||
1. `New-CoolifyService.ps1 -NoDeploy` creó el servicio (compose base64 + `urls`
|
||||
→ FQDN del servicio `app`).
|
||||
2. Un `POST /services/{uuid}/start` (que **falla en el pull** de
|
||||
`oh-daddy-app:local`, esperado) materializó en disco el compose normalizado
|
||||
con labels Traefik completos, el `.env` y la red `<uuid>`.
|
||||
3. Build en el server: clone + `stacks/oh-daddy/Dockerfile` inyectado (multi-stage
|
||||
node:22-alpine, `NEXT_PUBLIC_APP_URL` como build arg) → `oh-daddy-app:local`.
|
||||
4. `docker compose up -d` manual + `docker network connect <uuid> coolify-proxy`.
|
||||
5. `db/schema.sql` aplicado con `docker exec -i db-<uuid> psql` (idempotente).
|
||||
6. `PUT /api/inngest` público → 200.
|
||||
|
||||
## Gotchas nuevos (no documentados antes)
|
||||
|
||||
- **`POST /services/{uuid}/envs` da 409** si el compose ya declaró `${VAR}`:
|
||||
Coolify auto-crea las claves vacías al parsear. Usar **`PATCH
|
||||
/services/{uuid}/envs/bulk`** con `{"data":[{key,value,is_literal:true}]}`.
|
||||
- **`GET /deploy?uuid=` da 405 en 4.3.17** — el trigger válido es
|
||||
`POST /services/{uuid}/start`.
|
||||
- **El primer re-sync de Inngest del arranque falla con `503 no available
|
||||
server`**: `start.sh` hace el PUT antes de que Traefik considere healthy el
|
||||
contenedor. Es benigno — reintentar el PUT cuando la app esté healthy.
|
||||
- El `wget` de busybox (imagen alpine) soporta `--post-data`/`--header` para
|
||||
golpear el gql del motor desde el contenedor app.
|
||||
|
||||
## Verificación (2026-09-08)
|
||||
|
||||
- `GET /resources` → `running:healthy`; 5 contenedores healthy; DNS entre
|
||||
hermanos OK (`db`, `inngest`, `inngest-db`, `inngest-redis`).
|
||||
- Motor: `{"data":{"functions":[{"name":"automation-send"...},{"name":"process-comment"...}]}}`,
|
||||
app "oh-daddy" registrada, `/health` OK.
|
||||
- `Test-ServiceOnline.ps1 -Fqdn https://ohdaddy.urieljareth.org -Path /login`:
|
||||
HTTP 200 + render Chromium limpio (título "Oh Daddy. Comment automations on
|
||||
autopilot"), sin `pageerror`.
|
||||
- Restart policy `unless-stopped` en los 5 contenedores (sobreviven reinicios
|
||||
del LXC junto con el autostart de Docker).
|
||||
|
||||
## Pendiente humano
|
||||
|
||||
Entrar a `https://ohdaddy.urieljareth.org/login` con `ADMIN_PASSWORD` (única
|
||||
copia en plaintext: el reporte del deploy / `.env.local.ps1`) y completar el
|
||||
wizard `/setup` con las credenciales de la app de Meta.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Caso: open-seo vuelve a gestión completa de Coolify (imagen precompilada)
|
||||
|
||||
**Fecha:** 2026-09-04/05 · **App:** `open-seo:main-0fgs5kwaab9esytaxkddsvts`
|
||||
(uuid `kj0kccsb4d46tm0d6qe6docy`, id DB 57) · **FQDN:**
|
||||
`https://kj0kccsb4d46tm0d6qe6docy.urieljareth.org`
|
||||
|
||||
## Contexto
|
||||
|
||||
El sitio público estaba sirviéndose por una **cadena manual improvisada** tras
|
||||
la caída del 2026-08-27 22:14:
|
||||
|
||||
```
|
||||
Traefik → open-seo-sidecar (nginx:alpine manual, montado 22:25 ese día)
|
||||
→ proxy_pass → test-openseo (docker run manual de ghcr.io/every-app/open-seo:latest)
|
||||
```
|
||||
|
||||
La aplicación en Coolify quedó `exited:unhealthy` sin contenedor. El usuario
|
||||
fijó como objetivo: **todo gestionable desde Coolify**.
|
||||
|
||||
## Qué se hizo (en orden)
|
||||
|
||||
1. **Limpieza de filas huérfanas de env vars** (ids 1152-1156, app 52
|
||||
`insta-portal`): eran duplicados invisibles de un INSERT SQL manual con el
|
||||
morph type mal escapado (`App\\Models\\Application`). Las variables reales ya
|
||||
existían cifradas (ids 1161-1170) → se **borraron** los duplicados, no se
|
||||
activaron. Backup: `/root/backups/envvar-orphans-1152-1156-20260904-212119.tsv`.
|
||||
Tras esto: 0 valores sin cifrar en `environment_variables` de toda la instancia.
|
||||
|
||||
2. **Deploy git+railpack (intento 1):** `POST /deploy` contra el origen. El
|
||||
build tardó ~23 min y terminó, pero la app servía **404**: el repo
|
||||
(`github.com/every-app/open-seo`, público, actualizado ese mismo día) ahora
|
||||
compila un monorepo (`dist/client|server|open_seo_audit`, sin `index.html`
|
||||
en raíz) y railpack eligió un plan "estático con Caddy" que no corresponde.
|
||||
|
||||
3. **Conversión a imagen precompilada** (doctrina del caso firecrawl):
|
||||
- API: `docker_registry_image_name=ghcr.io/every-app/open-seo`,
|
||||
`docker_registry_image_tag=latest` (la API acepta estos campos).
|
||||
- DB: `UPDATE applications SET build_pack='dockerimage' WHERE id=57` — la
|
||||
API **rechaza** `build_pack=dockerimage` en PATCH (enum de validación sin
|
||||
ese valor, 422 "The selected build pack is invalid"), aunque el pipeline
|
||||
de deploy lo soporta de forma nativa
|
||||
(`deploy_dockerimage_buildpack` usa `docker_registry_image_name`, no
|
||||
`static_image`). Backup previo:
|
||||
`/root/backups/app57-pre-dockerimage-20260904-215456.tsv`.
|
||||
- Deploy 2: pull de `:latest` + rolling update. El contenedor
|
||||
**crash-loopeaba (exit 1)**: el preflight de la nueva imagen exige
|
||||
configurar auth.
|
||||
|
||||
4. **Env vars nuevas vía API** (`POST /applications/{uuid}/envs` — cifra por
|
||||
modelo, sin riesgo del bug de texto plano):
|
||||
- `AUTH_MODE=local_noauth` — replica el estado previo (el sitio ya corría
|
||||
público sin auth vía sidecar). Para Cloudflare Access:
|
||||
`AUTH_MODE=cloudflare_access` + `TEAM_DOMAIN` + `POLICY_AUD`.
|
||||
- `ALLOWED_HOST=kj0kccsb4d46tm0d6qe6docy.urieljareth.org` — allowlist de
|
||||
Vite detrás de proxy.
|
||||
- Deploy 3: contenedor **healthy** (la imagen GHCR trae healthcheck con
|
||||
`start_period=300s`, a diferencia de los servicios del §1.5 del índice).
|
||||
|
||||
5. **Retiro de los contenedores manuales** (con snapshots previos en
|
||||
`/root/backups/*-inspect-20260904-212317.json`):
|
||||
- `open-seo-sidecar` (nginx) — además sus labels duplicaban los routers de
|
||||
Traefik del FQDN y provocaban 503 mientras coexistía con el contenedor nuevo.
|
||||
- `test-openseo` (backend manual) — ya sin referencias.
|
||||
|
||||
## Resultado
|
||||
|
||||
```
|
||||
status=running:healthy
|
||||
build_pack=dockerimage image=ghcr.io/every-app/open-seo:latest
|
||||
FQDN → HTTP 200 <title>OpenSEO</title> (servido por el contenedor de Coolify)
|
||||
```
|
||||
|
||||
Dominio, env vars (DATAFORSEO_API_KEY, AUTH_MODE, ALLOWED_HOST), healthcheck,
|
||||
redeploys y rollbacks: todo administrable desde la UI/API de Coolify.
|
||||
|
||||
## Rollback
|
||||
|
||||
- **App a git-build:** `UPDATE applications SET build_pack='railpack' WHERE
|
||||
id=57;` y redeploy (nota: con el main actual vuelve a servir 404 — ver paso 2).
|
||||
- **Imagen anterior:** el tag local `ghcr.io/every-app/open-seo:sha-c469a48`
|
||||
(12 días) sigue en el host; o fijar `docker_registry_image_tag` a ese sha.
|
||||
- **Contenedores manuales:** recrear desde los inspect snapshots (sidecar:
|
||||
`docker run -d --name open-seo-sidecar --network coolify --restart
|
||||
unless-stopped -v /tmp/openseo-sidecar/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
<labels-del-snapshot> nginx:alpine`).
|
||||
|
||||
## Lecciones (añadir a la lista mental de gotchas)
|
||||
|
||||
- **PATCH /applications/{uuid} no acepta `build_pack=dockerimage`** aunque el
|
||||
backend lo soporta y `POST /applications/dockerimage` lo crea así. Para
|
||||
convertir una app existente: DB o recrear el recurso.
|
||||
- **`static_image` NO es la imagen del build pack dockerimage** — ese modo lee
|
||||
`docker_registry_image_name` (+`docker_registry_image_tag`, default `latest`).
|
||||
- **Un contenedor manual con los labels de Traefik de una app de Coolify
|
||||
rompe el enrutamiento** cuando la app real vuelve a deployar (routers
|
||||
duplicados → 503). Al restaurar una app, retirar esos "sidecars con labels".
|
||||
- La nueva imagen de open-seo exige `AUTH_MODE` en su preflight (exit 1 si
|
||||
falta) y recomienda `ALLOWED_HOST` detrás de proxy.
|
||||
+1
-1
@@ -99,7 +99,7 @@ Desde este repo (PowerShell, vía el path SSH habitual):
|
||||
```
|
||||
|
||||
En la configuración activa (línea `INF Updated to new configuration version=N`), verificar que los servicios de 6001 y 6002 digan `http://` y no `https://`. El runbook
|
||||
[runbooks/cloudflare-tunnel.md](runbooks/cloudflare-tunnel.md) tiene el procedimiento completo paso a paso.
|
||||
[runbooks/cloudflare-tunnel.md](../runbooks/cloudflare-tunnel.md) tiene el procedimiento completo paso a paso.
|
||||
|
||||
---
|
||||
|
||||
+20
@@ -1,3 +1,23 @@
|
||||
> # ⚠️ DOCUMENTO OBSOLETO — NO SEGUIR
|
||||
>
|
||||
> Archivado el 2026-08-07. **No uses este archivo como guía.** Contiene datos que
|
||||
> contradicen la realidad verificada del host:
|
||||
>
|
||||
> - Da `192.168.0.117` como IP del servidor Coolify. **El host Proxmox es
|
||||
> `192.168.0.200`** y todo se alcanza por `ssh [email protected]` +
|
||||
> `pct exec 102 -- ...`. Ver [../proxmox-inventory.md](../proxmox-inventory.md).
|
||||
> - Describe editar la configuración del túnel en archivos del host. **El túnel
|
||||
> es gestionado desde el dashboard de Cloudflare** (el ingress baja del edge);
|
||||
> editar archivos en el host no tiene efecto.
|
||||
>
|
||||
> **Procedimiento vigente:** [../runbooks/cloudflare-tunnel.md](../runbooks/cloudflare-tunnel.md).
|
||||
> **Herramienta vigente:** `scripts/Invoke-CloudflareApi.ps1` — ver
|
||||
> [../TOOL-INDEX.md](../TOOL-INDEX.md).
|
||||
>
|
||||
> Se conserva solo por el valor histórico del diagnóstico.
|
||||
|
||||
---
|
||||
|
||||
# Instrucciones para Agente IA — Cloudflare Tunnel + Coolify
|
||||
|
||||
**Entorno:** Proxmox VE → LXC CT 102 → Coolify (Docker) → Traefik + cloudflared
|
||||
@@ -0,0 +1,20 @@
|
||||
# Incidentes — archivo histórico
|
||||
|
||||
> ⚠️ **Esto NO es fuente de verdad.** Es material histórico: incidentes ya
|
||||
> resueltos y documentos superados. Describe cómo estaba el sistema en la fecha
|
||||
> de cada archivo, no cómo está hoy.
|
||||
>
|
||||
> - Estado actual → [../proxmox-inventory.md](../proxmox-inventory.md)
|
||||
> - Procedimientos vigentes → [../runbooks/](../runbooks/)
|
||||
> - Qué herramienta usar → [../TOOL-INDEX.md](../TOOL-INDEX.md)
|
||||
|
||||
Se conserva porque el diagnóstico y la causa raíz siguen siendo útiles cuando un
|
||||
síntoma parecido reaparece.
|
||||
|
||||
| Archivo | Fecha | Qué fue | Estado |
|
||||
|---|---|---|---|
|
||||
| [2026-04-11-cloudflare-tunnel-websocket-tls.md](2026-04-11-cloudflare-tunnel-websocket-tls.md) | 2026-04-11 | Terminal de Coolify y websockets de la UI caídos: routing del túnel en los puertos 6001/6002. | Resuelto. Procedimiento vigente en [../runbooks/cloudflare-tunnel.md](../runbooks/cloudflare-tunnel.md). |
|
||||
| [2026-04-11-coolify-static-app-deploy.md](2026-04-11-coolify-static-app-deploy.md) | 2026-04-11 | App estática desde GitHub servía página en blanco y 502. | Resuelto. Era Coolify v4.0.0-beta.472; hoy corre v4.1.2. |
|
||||
| [2026-06-29-coolify-cleanup-report.md](2026-06-29-coolify-cleanup-report.md) | 2026-06-29 | Limpieza de imágenes, volúmenes y redes de Docker (−4.7 GB). | Reporte de una ejecución puntual. |
|
||||
| [2026-04-cloudflare-tunnel-guia-agente-OBSOLETA.md](2026-04-cloudflare-tunnel-guia-agente-OBSOLETA.md) | 2026-04 | Guía de agente para el túnel Cloudflare. | **Obsoleta y contradictoria** — ver el aviso dentro del archivo. Usa el runbook. |
|
||||
| [openclaw.md](openclaw.md) | varias | Historial de incidentes de OpenClaw. | Referencia. |
|
||||
+177
-28
@@ -1,46 +1,195 @@
|
||||
# Proxmox Inventory
|
||||
# Inventario Proxmox — fuente de verdad del estado
|
||||
|
||||
Last verified: 2026-05-30 local time.
|
||||
**Última verificación: 2026-08-29** (SSH, API de Proxmox y API de Coolify desde esta máquina).
|
||||
|
||||
Este documento es la fuente de verdad de *qué existe*. Si algo aquí contradice a
|
||||
la realidad del host, gana el host: re-verifica y actualiza este archivo.
|
||||
|
||||
Para refrescarlo:
|
||||
|
||||
```powershell
|
||||
. .\.env.local.ps1
|
||||
.\scripts\Get-ProxmoxInventory.ps1
|
||||
.\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/resources" -Raw |
|
||||
Sort-Object name | Select-Object name, uuid, fqdn
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Host
|
||||
|
||||
- IP: `192.168.0.200`
|
||||
- Node: `thinkcentre`
|
||||
- Proxmox VE: `9.1.1`
|
||||
- Running kernel: `6.17.2-1-pve`
|
||||
- Topology: single-node Proxmox host
|
||||
| Dato | Valor |
|
||||
|---|---|
|
||||
| IP | `192.168.0.200` (IPs verificadas en red: `192.168.3.23` / `192.168.3.15` / `192.168.0.200`) |
|
||||
| Nodo | `thinkcentre` |
|
||||
| Proxmox VE | `9.1.1` (`pve-manager/9.1.1/42db4a6cf33dac83`) |
|
||||
| Kernel | `6.17.2-1-pve` |
|
||||
| Topología | Proxmox de un solo nodo |
|
||||
| Disco `/` | 39 GB, 11 GB usados (**29%**) |
|
||||
| RAM | 31 GiB totales — 14 GiB en uso, 16 GiB disponibles |
|
||||
| Swap | 7.6 GiB, 303 MiB en uso |
|
||||
|
||||
## LXC containers
|
||||
## Contenedores LXC
|
||||
|
||||
| VMID | Name | Status | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 100 | hermes | running | Secondary LXC |
|
||||
| 102 | coolify | running | Docker host for Coolify and apps |
|
||||
| VMID | Nombre | Estado | IPs | Notas |
|
||||
|---|---|---|---|---|
|
||||
| 100 | hermes | **running** | `192.168.3.23` / `192.168.3.15` | Secundario. Actualizado a commit `5d3c15aaa`. MiniMax-M3 y API key configuradas y verificadas con inferencia CLI en vivo. Ver caso [docs/casos/hermes-minimax-m3-setup.md](casos/hermes-minimax-m3-setup.md). |
|
||||
| 102 | coolify | running | `192.168.0.200` (host bridge) | Host Docker de Coolify y de todas las apps. |
|
||||
|
||||
No QEMU VM was listed during the latest smoke test.
|
||||
**No hay VMs QEMU** (`qm list` vacío).
|
||||
|
||||
## Docker inside LXC 102
|
||||
## Docker dentro de LXC 102
|
||||
|
||||
Docker is not managed directly on the Proxmox host. Use:
|
||||
Docker **no** se administra en el host Proxmox directamente. Todo comando va
|
||||
envuelto:
|
||||
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker ps"
|
||||
```
|
||||
|
||||
Observed groups:
|
||||
**68 contenedores, todos `running`** al momento de la verificación.
|
||||
|
||||
- Coolify core: `coolify`, `coolify-db`, `coolify-redis`,
|
||||
`coolify-realtime`, `coolify-sentinel`, `coolify-proxy`
|
||||
- Tunnel/proxy: `cloudflared`
|
||||
- Apps currently observed: Gitea, Chatwoot, OpenClaw, browser services, n8n,
|
||||
CodiMD, Qdrant, Baserow, Grimmory
|
||||
> **Los nombres de contenedor llevan el uuid de Coolify como sufijo** — no son
|
||||
> adivinables y **cambian si un redeploy recrea el contenedor**. Resuélvelos
|
||||
> siempre antes de usarlos; ver §1.3 de [TOOL-INDEX.md](TOOL-INDEX.md).
|
||||
|
||||
One Baserow container was observed as `health: starting` during the latest
|
||||
Docker sample, so recheck before assuming it is unhealthy.
|
||||
### Núcleo de la plataforma
|
||||
|
||||
## Access model
|
||||
| Contenedor | Imagen |
|
||||
|---|---|
|
||||
| `coolify` | `ghcr.io/coollabsio/coolify:4.3.14` (`GET /version` → `4.3.14`, verificado 2026-08-29) |
|
||||
| `coolify-db` | `postgres:15-alpine` |
|
||||
| `coolify-redis` | `redis:7-alpine` |
|
||||
| `coolify-realtime` | `ghcr.io/coollabsio/coolify-realtime:1.0.17` |
|
||||
| `coolify-sentinel` | `ghcr.io/coollabsio/sentinel:0.0.22` |
|
||||
| `coolify-proxy` | `traefik:v3.6` |
|
||||
| `cloudflared` | — (túnel gestionado desde el dashboard) |
|
||||
|
||||
- SSH: root over key-based auth.
|
||||
- API: Proxmox token via `PROXMOX_API_TOKEN_ID` and
|
||||
`PROXMOX_API_TOKEN_SECRET`.
|
||||
- Secrets must stay in local env files or the OS secret store, not Markdown.
|
||||
### Recursos registrados en Coolify (29)
|
||||
|
||||
Del endpoint `/resources`. El **uuid** es lo que necesitas para la API y para
|
||||
resolver nombres de contenedor.
|
||||
|
||||
| Recurso | uuid | FQDN |
|
||||
|---|---|---|
|
||||
| agendamax:main | `s30f7egdlkx4wyjp59o1iunc` | https://agendamax.urieljareth.org |
|
||||
| audio-a--texto:main | `up0oaqnpd8kywjkmmihb61t0` | https://stt.urieljareth.org |
|
||||
| baserow:main | `vngcvnhbfqboov4nln88zc73` | — |
|
||||
| baserow-redis | `vztgldo9cap3s0oj240tokgj` | — |
|
||||
| chatwoot | `c11xzy2tx2cdapm32f5b89vy` | — |
|
||||
| codimd | `ag4ndg4cr1hvczkr35qlyzs8` | — |
|
||||
| cotizador:main | `e6vie34d83iv5vw3eyzinl3s` | `…:3000` (sin dominio propio) |
|
||||
| demospa | `2f094556a04c0dee043af215` | https://demospa.urieljareth.org |
|
||||
| demospa-mysql | `teplxg70a97itolgwk2qkmgi` | — |
|
||||
| e3-manager-demo | `xxoq93pnz0jta5tz5m50ylig` | — |
|
||||
| estaci-n-de-documentos:main | `u11ug3eizk9du3p2ch556ud4` | — |
|
||||
| **evolution-go** | `j0jkacsfcgypm2jmpillls01` | https://evo.urieljareth.org (API WhatsApp 0.7.2, licencia activa — ver [casos/evolution-go-stack.md](casos/evolution-go-stack.md)) |
|
||||
| evolution-api | `q6tnsvkvrjw4g0ab532l3r1s` | https://evoapi.urieljareth.org (Node v2.3.7; integrada a Chatwoot: inbox 6=Asesoría, 10=Personal, 12=JM; INSTA desconectada — ver [casos/evolution-api-chatwoot.md](casos/evolution-api-chatwoot.md)) |
|
||||
| **firecrawl:main** | `du3iknyvy22vap767t9tnf9s` | — (**registrado pero sin contenedor corriendo**) |
|
||||
| gitea-with-postgresql | `hjwh0svsoo9p5w5kj2j6b1bd` | — |
|
||||
| grimmory | `y8cq6jmboz0b22mn61hs4tu8` | — |
|
||||
| insta-portal | `instademo0portal0insta0demo1` | `…:4180` |
|
||||
| n8n-with-postgres-and-worker | `jdj3y3kmz9blec7ntbxuhezi` | — |
|
||||
| nextcloud-with-postgres | `hdcdpkm0jko3qqvn5683ercc` | — |
|
||||
| openclaw-business | `zhaz04q8ibqp5r5hz5ibo01t` | — |
|
||||
| openclaw (2ª instancia) | `uudgcoz5ibvyvullyzbjalai` | — |
|
||||
| open-webui | `q13zdxusnhvdent7f44a18kc` | — |
|
||||
| plataforma-interna | `kruadlc7fdrbh28ykrv8rdyl` | — |
|
||||
| postgresql-database | `j6hgnvdwq9anpa9wj2ikdx0j` | — |
|
||||
| postgresql-database | `s10bvby71tb1fpbcl4tnxzzh` | — |
|
||||
| postgresql-database | `tflojv1iqh0ueoq7apxx4mos` | — |
|
||||
| prompt-gallery-e3:main | `39eu76lqbejdy9vxs5iz1rws` | https://demopromptgallerye3.urieljareth.org |
|
||||
| qdrant | `uyn0js6pqbwo8mubw5edy95f` | — |
|
||||
| solo-leveling | `urm8m4u0jvjggmgpfxblnqwc` | — |
|
||||
|
||||
### Stack de Supabase
|
||||
|
||||
Corre con nombres fijos (sin sufijo uuid), fuera del patrón habitual de Coolify:
|
||||
`supabase-db` (`supabase/postgres:17.6.1.136`), `supabase-kong`,
|
||||
`supabase-auth`, `supabase-rest`, `supabase-storage`, `supabase-studio`,
|
||||
`supabase-meta`, `supabase-imgproxy`, `realtime-dev.supabase-realtime`.
|
||||
También `e3-mailpit` (`axllent/mailpit`).
|
||||
|
||||
### Versiones que importan
|
||||
|
||||
| App | Imagen en ejecución |
|
||||
|---|---|
|
||||
| **Chatwoot** | **`chatwoot/chatwoot:v4.16.2`** (app y sidekiq) |
|
||||
| Chatwoot DB | `pgvector/pgvector:pg12` |
|
||||
| n8n | `n8nio/n8n:2.32.7` |
|
||||
| Baserow | `baserow/baserow:2.3.2` |
|
||||
| OpenClaw | `coollabsio/openclaw:2026.7.1` |
|
||||
| Evolution API | `evoapicloud/evolution-api:v2.3.7` |
|
||||
| Gitea | `gitea/gitea:latest` |
|
||||
| Nextcloud | `lscr.io/linuxserver/nextcloud:latest` |
|
||||
| Grimmory | `grimmory/grimmory:nightly` |
|
||||
| Solo Leveling | `ghcr.io/urieljarethbusiness-cpu/solo-leveling:latest` |
|
||||
|
||||
## Estado de la API de Coolify
|
||||
|
||||
Base pública: `https://coolify.urieljareth.org/api/v1` (Bearer `COOLIFY_TOKEN`) ·
|
||||
**Origen: `http://192.168.0.117:8000/api/v1`** · Instancia: **4.3.14**.
|
||||
|
||||
**Re-verificado a fondo el 2026-08-29, con hallazgo que corrige todo lo anterior:**
|
||||
|
||||
- **La API de esta instancia está COMPLETA.** Su propio `openapi.yaml` (dentro del
|
||||
contenedor en `/var/www/html/openapi.yaml`) declara el namespace completo de
|
||||
`/applications/*` (incl. `POST /applications/public`, `/dockerfile`,
|
||||
`/private-deploy-key`, `/private-github-app`), `/github-apps`, `/gitlab-apps`,
|
||||
notificaciones, proveedores cloud (Hetzner/DigitalOcean/Vultr), MCP, etc. — y
|
||||
contra el **origen** esos endpoints responden 200 con el token de siempre.
|
||||
- **El 404 de `/applications/*` que se venía documentando desde v4.1.2 NO lo
|
||||
produce Coolify: lo produce Cloudflare en el hostname público.** Mismo token,
|
||||
misma ruta: `https://coolify.urieljareth.org/api/v1/applications` → 404;
|
||||
`http://192.168.0.117:8000/api/v1/applications` → 200. Es un bloqueo del edge
|
||||
(regla WAF/ruta en el dashboard) que hay que corregir allí; mientras tanto,
|
||||
llama a esos endpoints contra `COOLIFY_API_URL_ORIGIN`.
|
||||
- **Desde v4.2 los endpoints de estado exigen POST** (`GET /deploy?uuid=` → 405
|
||||
`"This endpoint has changed to a POST request."` — confirmado también contra el
|
||||
origen; ver §10.1 de las notas). En 4.3.x se añadieron endpoints de logs
|
||||
(db/servicio/contenedor), settings en las respuestas de application y MCP.
|
||||
|
||||
| Endpoint (con token válido) | Vía Cloudflare | Vía origen (LXC 102) |
|
||||
|---|---|---|
|
||||
| `/version`, `/resources`, `/servers`, `/projects`, `/teams`, `/services`, `/databases`, `/deployments`, `/security/keys` | OK | OK |
|
||||
| **`/applications` y todo su namespace** | **404 (Cloudflare)** | **OK (200)** |
|
||||
| **`/github-apps`** | **404 (Cloudflare)** | **OK (200)** |
|
||||
| `/deploy` con GET | 405 | 405 (correcto: exige POST desde v4.2) |
|
||||
|
||||
## Modelo de acceso
|
||||
|
||||
- **SSH:** root con autenticación por clave. La llave operativa es
|
||||
`keys/proxmox_ed25519` (idéntica a `C:\Users\Uriel Jareth\.ssh\coolify_key`;
|
||||
ED25519, sin passphrase) — verificada en vivo el 2026-08-29 contra el host
|
||||
(`192.168.0.200`) y el LXC 102 (`192.168.0.117`). La ruta
|
||||
`…\.openclaw\workspace\proxmox_key_win` citada en docs antiguos **ya no existe**.
|
||||
Es el único camino directo al host; permite ejecutar
|
||||
comandos en el host y en los contenedores LXC (`pct exec 100`, `pct exec 102`).
|
||||
- **API de Proxmox:** token `root@pam!openclaw` — único token del host
|
||||
(`/etc/pve/priv/token.cfg`), verificado 200 el 2026-08-29. Ya cargado en
|
||||
`.env.local.ps1` (`PROXMOX_API_TOKEN_ID` / `PROXMOX_API_TOKEN_SECRET`).
|
||||
- **API de Coolify:** `COOLIFY_TOKEN` (v4.3.14, verificado).
|
||||
- **API de Cloudflare:** sin token — el túnel se gestiona desde el dashboard.
|
||||
- Los secretos viven solo en `.env.local.ps1` (gitignored), en `ACCESS.md`
|
||||
(gitignored, fuente de verdad de credenciales) o en el almacén de secretos del
|
||||
SO. **Nunca en Markdown versionado.** Inventario de credenciales:
|
||||
[ACCESS.md](../ACCESS.md)
|
||||
|
||||
> ⚠️ **Pendiente en `.env.local.ps1` (2026-08-29):** solo `CLOUDFLARE_API_TOKEN`.
|
||||
> El PAT de GitHub que había caducó (401); se reemplazó por la credencial viva del
|
||||
> Administrador de credenciales de Windows. `PROXMOX_API_TOKEN_ID/SECRET`,
|
||||
> `COOLIFY_EMAIL/PASSWORD` ya están cargados. SSH, API de Proxmox y API de
|
||||
> Coolify verificados.
|
||||
|
||||
## Automatizaciones instaladas en el host
|
||||
|
||||
| Qué | Dónde | Agendado por |
|
||||
|---|---|---|
|
||||
| Guard del parche enterprise de Chatwoot | `/root/scripts/chatwoot-enterprise-guard.sh` | `/etc/cron.d/chatwoot-enterprise-guard`, `*/5 * * * *` |
|
||||
| Auto-arranque tras corte de luz | unit `coolify-autostart.service` | systemd, **`enabled`** |
|
||||
|
||||
Log del guard: `/var/log/chatwoot-enterprise-guard.log` (solo escribe cuando
|
||||
actúa). Estado al 2026-08-07: el plan está en `enterprise` y una ejecución
|
||||
manual del guard pasa correctamente, pero el log registra errores de lectura
|
||||
durante la ventana del update a v4.16.2 — ver
|
||||
[runbooks/chatwoot-update.md](runbooks/chatwoot-update.md).
|
||||
|
||||
@@ -31,17 +31,41 @@ de Cloudflare arranquen solos**, sin intervención manual.
|
||||
|
||||
En cada boot el guardián:
|
||||
1. Verifica que el LXC 102 esté `running` (lo arranca si no).
|
||||
2. Espera a que Docker responda dentro del LXC (hasta 180 s).
|
||||
3. Se asegura de que estén arriba: `coolify-db`, `coolify-redis`,
|
||||
`coolify-realtime`, `coolify`, `coolify-proxy`, `cloudflared` (los inicia si
|
||||
alguno no está).
|
||||
2. Espera a que Docker responda dentro del LXC (**deadline de reloj real**,
|
||||
`COOLIFY_MAX_WAIT`, por defecto 600 s).
|
||||
3. Da un margen de asentamiento (`COOLIFY_SETTLE`, 180 s) para que Docker
|
||||
arranque sus propios contenedores, y solo entonces fuerza el arranque de
|
||||
los que falten: `coolify-db`, `coolify-redis`, `coolify-realtime`,
|
||||
`coolify`, `coolify-proxy`, `cloudflared`.
|
||||
4. Levanta el `cloudflared.service` de systemd dentro del LXC (segundo
|
||||
conector al mismo túnel).
|
||||
5. **Comprueba que el túnel realmente llegó a Cloudflare**: cuenta los
|
||||
`Registered tunnel connection` de este boot y los escribe en el log.
|
||||
|
||||
Sale con código ≠ 0 si algo no se pudo arrancar, para que systemd lo marque
|
||||
`failed` y `Restart=on-failure` reintente (hasta 3 veces por hora).
|
||||
|
||||
Las dos capas son complementarias: `onboot` hace el trabajo normal; el guardián
|
||||
es una red de seguridad que además **auto-repara** (p. ej. un contenedor con
|
||||
`restart=no`) y deja **log** de lo ocurrido tras el apagón.
|
||||
|
||||
### Presupuestos de tiempo (no los bajes a ciegas)
|
||||
|
||||
Medido en el boot del 2026-08-07 16:41: `pve-guests` tarda **78 s** en arrancar
|
||||
el CT 102, el daemon de Docker dentro del LXC solo responde ~**4-5 min** después
|
||||
del encendido, y el último contenedor core (`coolify`) arranca a los **7 m 40 s**.
|
||||
Por eso:
|
||||
|
||||
| Parámetro | Valor | Regla |
|
||||
|---|---|---|
|
||||
| `COOLIFY_MAX_WAIT` | 600 s | espera de Docker, por **reloj real** |
|
||||
| `COOLIFY_SETTLE` | 180 s | margen antes de forzar arranques |
|
||||
| `COOLIFY_PROBE_TIMEOUT` | 20 s | timeout duro de **cada** llamada al LXC |
|
||||
| `TimeoutStartSec` (unit) | 1200 s | **debe superar** `MAX_WAIT + SETTLE` |
|
||||
|
||||
`COOLIFY_LOG` también es sobreescribible, para poder hacer pruebas en seco sin
|
||||
tocar el log de producción.
|
||||
|
||||
Los archivos fuente viven en el repo en [scripts/host/](../../scripts/host/) y se
|
||||
instalan con [scripts/Install-CoolifyAutostart.ps1](../../scripts/Install-CoolifyAutostart.ps1).
|
||||
|
||||
@@ -78,10 +102,17 @@ Estado sano esperado:
|
||||
```
|
||||
onboot: 1
|
||||
startup: order=1,up=30
|
||||
guardian enabled: enabled
|
||||
script executable: yes
|
||||
enabled: enabled
|
||||
state: active
|
||||
result: success
|
||||
timeout: 20min
|
||||
```
|
||||
|
||||
**No basta con `enabled`.** `enabled` solo dice que arrancará; `state`/`result`
|
||||
dicen si la última ejecución funcionó. Una corrida sana del log termina en
|
||||
`=== coolify-autostart done (failures=0) ===`. Si el log se corta justo después
|
||||
de `LXC 102 already running`, el guardián murió esperando a Docker.
|
||||
|
||||
Log del último arranque:
|
||||
|
||||
```powershell
|
||||
@@ -107,6 +138,63 @@ Para validar que el unit está bien formado y en el orden correcto:
|
||||
|
||||
`After` debe incluir `pve-guests.service`; `WantedBy` debe ser `multi-user.target`.
|
||||
|
||||
Para probar la ruta de **fallo** (que el guardián corte y deje `ERROR` en vez de
|
||||
colgarse), apúntalo a un LXC inexistente con un log temporal:
|
||||
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "env COOLIFY_LXC=999 COOLIFY_LOG=/tmp/ca-test.log COOLIFY_MAX_WAIT=15 COOLIFY_PROBE_TIMEOUT=5 bash /usr/local/bin/coolify-autostart.sh > /dev/null 2>&1; cat /tmp/ca-test.log; rm -f /tmp/ca-test.log"
|
||||
```
|
||||
|
||||
Debe terminar en `ERROR: docker not ready after 15s (wall clock) -> aborting`
|
||||
en ~18 s. No toca producción ni el log real.
|
||||
|
||||
---
|
||||
|
||||
## Incidente 2026-08-07 — el guardián llevaba 2/2 arranques muriendo
|
||||
|
||||
**Síntoma:** `systemctl is-enabled` decía `enabled`, pero la unidad estaba
|
||||
`failed (Result: timeout)` en los dos reinicios reales del día (09:47 y 16:41).
|
||||
El log se cortaba siempre en `LXC 102 already running`, sin línea de `ERROR`.
|
||||
|
||||
**Cronología del boot de las 16:41:**
|
||||
|
||||
| Hora | Evento |
|
||||
|---|---|
|
||||
| 16:41:49 | `pve-guests` arranca el CT 102 |
|
||||
| 16:43:07 | termina `pve-guests` (78 s) y arranca el guardián |
|
||||
| 16:43:08 | `LXC 102 already running` → entra a esperar Docker |
|
||||
| 16:46:05 | Docker empieza a levantar contenedores (`coolify-db`) |
|
||||
| **16:48:07** | **systemd mata al guardián**: `TimeoutStartSec=300` |
|
||||
| 16:48:40 | arranca `coolify` — **33 s después de que el guardián ya estaba muerto** |
|
||||
|
||||
**Causa raíz (tres defectos que se sumaron):**
|
||||
|
||||
1. El bucle de espera contaba **iteraciones de `sleep`, no reloj real**, así que
|
||||
`MAX_WAIT=180` no acotaba nada.
|
||||
2. Las llamadas `pct exec ... docker info` **no tenían timeout** y `docker info`
|
||||
es caro (enumera los ~50 contenedores): con el daemon saturado en el arranque
|
||||
en frío, una sola llamada se bloqueaba minutos y consumía todo el presupuesto
|
||||
en silencio.
|
||||
3. `TimeoutStartSec=300` estaba **por debajo del tiempo real de convergencia**
|
||||
(~7 m 40 s), así que systemd mataba al guardián antes de que pudiera actuar.
|
||||
|
||||
Nunca hubo una ejecución exitosa en un arranque real: la única corrida sana del
|
||||
log (2026-07-08) fue el `-RunNow` manual con todo ya arriba.
|
||||
|
||||
**Por qué no se notó durante un mes:** `-VerifyOnly` solo miraba `is-enabled`.
|
||||
Ahora también reporta `state`, `result`, `timeout` y el journal del último boot.
|
||||
|
||||
**Qué salvó el servicio mientras tanto:** las capas base, que sí funcionaron en
|
||||
los dos reinicios — `onboot=1`, `docker.service` `enabled`, políticas
|
||||
`restart=always`/`unless-stopped` y `cloudflared.service` `enabled` (registró sus
|
||||
4 conectores QUIC a los 3 m 41 s del boot). El guardián es red de seguridad, no
|
||||
el mecanismo principal; por eso el apagón no se notó de cara al usuario.
|
||||
|
||||
**Corrección:** deadline por reloj real, `timeout` duro en cada llamada al LXC,
|
||||
sonda barata `docker version` en vez de `docker info`, margen de asentamiento
|
||||
antes de forzar arranques, verificación de conectores del túnel en el log,
|
||||
`TimeoutStartSec=1200` y `Restart=on-failure`.
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
@@ -133,10 +221,23 @@ Para validar que el unit está bien formado y en el orden correcto:
|
||||
[cloudflare-tunnel.md](cloudflare-tunnel.md).
|
||||
- `coolify-sentinel` tiene `restart=no` (monitor no crítico); Coolify lo recrea,
|
||||
por eso no está en la lista de contenedores core del guardián.
|
||||
- **El único LXC con `onboot` es el 102.** El LXC `100 hermes` no tiene la marca,
|
||||
así que **no** arranca solo tras un apagón. Es intencional mientras sea
|
||||
secundario; si algún día deja de serlo, `pct set 100 --onboot 1`.
|
||||
|
||||
---
|
||||
|
||||
**Verificado:** 2026-07-08 — instalado y probado en vivo. `onboot=1`,
|
||||
`coolify-autostart.service` `enabled`, guardián ejecutado con éxito (LXC arriba,
|
||||
Docker listo, 6 contenedores core + túnel `running`). `systemd-analyze verify` sin
|
||||
warnings.
|
||||
**Verificado:** 2026-08-07 — auditoría completa tras dos reinicios reales del
|
||||
día. Se detectó y corrigió el fallo del guardián (ver incidente arriba). Estado
|
||||
final: `onboot=1`, unidad `enabled` / `active` / `result=success`,
|
||||
`TimeoutStartUSec=20min`, `Restart=on-failure`, `After` incluye
|
||||
`pve-guests.service`, `systemd-analyze verify` sin warnings. Guardián ejecutado
|
||||
de punta a punta en 9 s con `failures=0`, 6 contenedores core `running`,
|
||||
`cloudflared.service` `active` con **4 conectores registrados**. Ruta de aborto
|
||||
por reloj real probada en seco (corta a los 18 s con `ERROR`). Público
|
||||
verificado a través del túnel: `coolify.urieljareth.org` → 302,
|
||||
`chat.urieljareth.org` → 200.
|
||||
|
||||
**Verificado:** 2026-07-08 — instalación original (`onboot=1` + guardián). La
|
||||
prueba de entonces fue un `-RunNow` manual, no un arranque real; de ahí que el
|
||||
defecto de tiempos no se detectara hasta 2026-08-07.
|
||||
|
||||
@@ -0,0 +1,580 @@
|
||||
# Runbook: actualizar Chatwoot en Coolify sin perder la edicion enterprise
|
||||
|
||||
> **Ejecutado end-to-end el 2026-07-24.** Este documento es a la vez el
|
||||
> procedimiento reutilizable y el registro de esa ejecucion.
|
||||
> Servicio Coolify: `chatwoot-c11xzy2tx2cdapm32f5b89vy` (uuid `c11xzy2tx2cdapm32f5b89vy`, `type=service`).
|
||||
> FQDN real: **`https://chat.urieljareth.org`** (el FQDN que aparece en
|
||||
> [docs/casos/chatwoot-enterprise-patch.md](../casos/chatwoot-enterprise-patch.md)
|
||||
> quedo obsoleto).
|
||||
> Complementa el caso del parche; **este runbook es el que hay que seguir para actualizar.**
|
||||
|
||||
## 0. Resultado de la ejecucion del 2026-07-24
|
||||
|
||||
| Dato | Antes | Despues |
|
||||
|---|---|---|
|
||||
| Version | 4.16.0 | **4.16.1** |
|
||||
| Tag de imagen | `chatwoot/chatwoot:latest` (sin pin) | **`chatwoot/chatwoot:v4.16.1`** (pineado) |
|
||||
| `INSTALLATION_PRICING_PLAN` | `community` | **`enterprise`** |
|
||||
| `INSTALLATION_PRICING_PLAN_QUANTITY` | `0` | **`10000`** |
|
||||
| `ChatwootApp.self_hosted_enterprise?` | `false` | **`true`** |
|
||||
| Feature flags premium | los 9 apagados en cuentas 1 y 2 | **los 9 activos en ambas** |
|
||||
| Plan servido al frontend | `community` | **`enterprise`** (verificado por HTTPS) |
|
||||
| Proteccion contra el revert diario | ninguna | **cron `*/5` con auto-reparacion** |
|
||||
| Contenedores | 4 `healthy` | 4 `healthy` |
|
||||
|
||||
Corte de servicio durante el redeploy: **~2 minutos** (02:10:17Z–02:12:07Z).
|
||||
|
||||
Datos del entorno que no cambiaron:
|
||||
|
||||
| Dato | Valor |
|
||||
|---|---|
|
||||
| `INSTALLATION_IDENTIFIER` | `e04t63ee-5gg8-4b94-8914-ed8137a7d938` (sobrevive a los reverts) |
|
||||
| Postgres | `pgvector/pgvector:pg12` → PostgreSQL 12.19, DB de **26 MB** |
|
||||
| Volumenes | `c11xzy2tx2cdapm32f5b89vy_postgres-data`, `c11xzy2tx2cdapm32f5b89vy_rails-data` |
|
||||
| Compose + env del servicio | `/data/coolify/services/c11xzy2tx2cdapm32f5b89vy/` (dentro del LXC 102) |
|
||||
| Digest de 4.16.0 (para rollback) | `sha256:8fdd8adde2093fb270fc69eaeefaf6faca416e65cba0b58041159c654b81d175` |
|
||||
| Digest de 4.16.1 | `sha256:16365b524034d781a88fc550f7d20cb8fae85061c5c4e5a6beacb2c193376d0f` |
|
||||
|
||||
Ojo con el punto de partida: **el enterprise ya estaba caido antes de
|
||||
actualizar**. La actualizacion no lo tiro; ya estaba tirado (ver seccion 1).
|
||||
|
||||
### Pre-flight que hizo la actualizacion de bajo riesgo
|
||||
|
||||
`v4.16.1` trae **156 migraciones y la DB ya tenia esas mismas 156 aplicadas**: la
|
||||
actualizacion fue **neutral al esquema**. Por eso el rollback a 4.16.0 no
|
||||
necesitaria restaurar la DB. Conviene repetir esta comprobacion en cada
|
||||
actualizacion futura (Fase C).
|
||||
|
||||
### Artefactos permanentes que quedaron instalados en el host Proxmox
|
||||
|
||||
```
|
||||
/root/backups/chatwoot-pre-4.16.1.dump 667K, 97 tablas, sha256 dc3d25fe...
|
||||
/root/backups/chatwoot-service-pre-4.16.1.tgz 2.4K, compose + .env (tiene secretos)
|
||||
/root/scripts/chatwoot-enterprise-guard.sh el guard (copia versionada en scripts/)
|
||||
/etc/cron.d/chatwoot-enterprise-guard cron */5
|
||||
/etc/logrotate.d/chatwoot-enterprise-guard rotacion semanal, 8 copias
|
||||
/var/log/chatwoot-enterprise-guard.log solo escribe cuando repara
|
||||
```
|
||||
|
||||
## 1. Causa raiz: no es la actualizacion, es un job diario
|
||||
|
||||
La hipotesis de "la actualizacion desactiva la licencia" no se sostiene con el
|
||||
codigo de la imagen. La cadena real, leida de la imagen 4.16.0:
|
||||
|
||||
```
|
||||
config/schedule.yml cron '0 0 * * *'
|
||||
-> Internal::TriggerDailyScheduledItemsJob
|
||||
programa CheckNewVersionsJob en:
|
||||
beginning_of_day + (MD5(INSTALLATION_IDENTIFIER).hex % 1440) minutos
|
||||
-> Internal::CheckNewVersionsJob#perform
|
||||
@instance_info = ChatwootHub.sync_with_hub # POST https://hub.2.chatwoot.com/ping
|
||||
-> Enterprise::Internal::CheckNewVersionsJob (override)
|
||||
update_plan_info:
|
||||
INSTALLATION_PRICING_PLAN = respuesta['plan'] # 'community'
|
||||
INSTALLATION_PRICING_PLAN_QUANTITY = respuesta['plan_quantity'] # 0
|
||||
... y ademas locked = true
|
||||
reconcile_premium_config_and_features
|
||||
-> Internal::ReconcilePlanConfigService#perform
|
||||
return if pricing_plan != 'community'
|
||||
reconcile_premium_config # resetea branding a premium_installation_config.yml
|
||||
reconcile_premium_features # account.disable_features!(*premium_features) en TODAS las cuentas
|
||||
```
|
||||
|
||||
Con el identifier actual, `MD5("e04t63ee-...").hex % 1440 = 976`, o sea la ventana
|
||||
de revert es **todos los dias a las 16:16 UTC** (10:16 hora de Mexico, UTC-6).
|
||||
Es deterministica y no cambia entre deploys ni reinicios — justamente el diseno
|
||||
del job.
|
||||
|
||||
Consecuencias practicas:
|
||||
|
||||
1. **El parche caduca en <= 24 h**, actualices o no. El caso se documento el
|
||||
2026-06-16; se revirtio al dia siguiente.
|
||||
2. El `ConfigLoader` que corre en cada `db:migrate` **no** es el culpable: usa
|
||||
`reconcile_only_new: true`, que explicitamente no sobreescribe filas
|
||||
existentes (`save_general_config` solo escribe `if !@reconcile_only_new`).
|
||||
3. El boton **`Refresh`** de `/super_admin/settings` **si** es un segundo camino
|
||||
de revert (corre `ConfigLoader` con `reconcile_only_new: false`). La
|
||||
advertencia del caso original sigue vigente.
|
||||
4. Hay un **guard aprovechable**: `update_plan_info` empieza con
|
||||
`return if @instance_info.blank?`. Si el hub no responde, no se escribe nada.
|
||||
Esa es la base del fix durable de la Fase G.
|
||||
|
||||
## 2. Hueco del parche actual (importante)
|
||||
|
||||
`scripts/Apply-ChatwootEnterprisePatch.ps1` corregia **solo 3 filas** de
|
||||
`installation_configs`. Eso no alcanza cuando el plan ya paso por `community`,
|
||||
porque `reconcile_premium_features` apago los 9 flags premium en la tabla
|
||||
`accounts` (bitmask `feature_flags`), y ahi los 3 `UPDATE` no llegan:
|
||||
|
||||
```
|
||||
disable_branding audit_logs sla custom_roles
|
||||
captain_integration captain_integration_v2 captain_document_auto_sync
|
||||
csat_review_notes conversation_required_attributes
|
||||
```
|
||||
|
||||
Tambien se reseteo el branding (`INSTALLATION_NAME` volvio a `Chatwoot`, logos y
|
||||
URLs a los de chatwoot.com, `DISPLAY_MANIFEST` a `true`).
|
||||
|
||||
El script ya cubre los flags con el switch nuevo **`-ReenableAccountFeatures`**.
|
||||
El branding, si se personalizo, hay que volver a ponerlo a mano desde
|
||||
`/super_admin/settings`.
|
||||
|
||||
### 2.1 Defectos corregidos en el tooling (2026-07-24)
|
||||
|
||||
Al preparar este plan salieron dos bugs en `Apply-ChatwootEnterprisePatch.ps1`
|
||||
que habrian hecho fallar los pasos de la Fase E:
|
||||
|
||||
1. **La autodeteccion del contenedor Postgres nunca funciono.** El patron era
|
||||
`"<uuid>.*(pgvector|postgres|db)"`, pero Coolify nombra los contenedores
|
||||
`<servicio>-<uuid>` (`postgres-c11xzy...`), o sea el uuid va al final. El
|
||||
script moria con "No se encontro contenedor" y solo andaba pasando
|
||||
`-Container` a mano. Ahora son dos greps encadenados (uuid, luego rol).
|
||||
2. **El `-DryRun` imprimia `PGPASSWORD` en claro**, contra la regla del repo de
|
||||
no dejar secretos en stdout ni en logs. Ahora sale enmascarada.
|
||||
3. **El parche no invalidaba el cache de `GlobalConfig`.** Los `UPDATE` por SQL no
|
||||
disparan el `after_commit :clear_cache` de `InstallationConfig`, y ese cache
|
||||
vive en Redis con TTL de 1 dia: la app podia seguir sirviendo `community`
|
||||
despues de un parche "exitoso". Ahora el paso de `rails runner` llama
|
||||
explicitamente a `GlobalConfig.clear_cache` (detalle en la Fase G).
|
||||
|
||||
Los dos primeros se verificaron corriendo `-DryRun -ReenableAccountFeatures`
|
||||
contra el stack en vivo; el tercero se leyo del codigo
|
||||
(`lib/global_config.rb` + `app/models/installation_config.rb`) y se confirmo
|
||||
inspeccionando las claves `V1:GLOBAL_CONFIG:*` en Redis.
|
||||
`Get-ChatwootLicenseStatus.ps1` quedo probado end-to-end.
|
||||
|
||||
## 3. Plan de actualizacion
|
||||
|
||||
Todo desde la raiz del repo, en PowerShell, con `. .\.env.local.ps1` cargado.
|
||||
|
||||
### Fase A — pre-checks (solo lectura)
|
||||
|
||||
```powershell
|
||||
. .\.env.local.ps1
|
||||
|
||||
# Estado de licencia + flags por cuenta + ventana diaria de revert.
|
||||
.\scripts\Get-ChatwootLicenseStatus.ps1 -Deep
|
||||
|
||||
# Salud del stack.
|
||||
.\coolify_skill\scripts\Get-CoolifyDockerStatus.ps1 -All
|
||||
```
|
||||
|
||||
Anota el digest de la imagen en uso; es la unica ruta de rollback rapido:
|
||||
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker images --digests | grep chatwoot/chatwoot"
|
||||
```
|
||||
|
||||
> Digest al 2026-07-24: `sha256:8fdd8adde2093fb270fc69eaeefaf6faca416e65cba0b58041159c654b81d175` (= 4.16.0).
|
||||
|
||||
### Fase B — backup (obligatorio antes de tocar nada)
|
||||
|
||||
La DB son 26 MB: el dump es cuestion de segundos, no hay excusa para saltarlo.
|
||||
|
||||
```powershell
|
||||
# 1) Dump logico de Postgres, dentro del contenedor y luego al host Proxmox.
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec -i postgres-c11xzy2tx2cdapm32f5b89vy bash -lc 'PGPASSWORD=\$POSTGRES_PASSWORD pg_dump -U \$POSTGRES_USER -d \$POSTGRES_DB -Fc -f /tmp/chatwoot-pre-4.16.1.dump'"
|
||||
|
||||
# 2) Sacarlo del contenedor al LXC y del LXC al host.
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker cp postgres-c11xzy2tx2cdapm32f5b89vy:/tmp/chatwoot-pre-4.16.1.dump /root/chatwoot-pre-4.16.1.dump"
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct pull 102 /root/chatwoot-pre-4.16.1.dump /root/backups/chatwoot-pre-4.16.1.dump && ls -lh /root/backups/"
|
||||
|
||||
# 3) Copia del compose + .env del servicio (queda EN EL HOST, nunca en el repo:
|
||||
# el .env tiene SECRET_KEY_BASE y las passwords de Postgres/Redis).
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- tar czf /root/chatwoot-service-pre-4.16.1.tgz -C /data/coolify/services/c11xzy2tx2cdapm32f5b89vy ."
|
||||
```
|
||||
|
||||
Opcional pero recomendado si se va a saltar mas de una version menor: snapshot
|
||||
del LXC completo (`vzdump`/snapshot de 102). Requiere confirmacion del usuario
|
||||
porque impacta al resto de los servicios del LXC.
|
||||
|
||||
### Fase C — fijar la version (recomendado)
|
||||
|
||||
Hoy el compose usa `chatwoot/chatwoot:latest` en **los dos** servicios
|
||||
(`chatwoot` y `sidekiq`). Con `latest`, cualquier redeploy futuro puede meter un
|
||||
salto de version mayor sin aviso — incluido uno que exija PostgreSQL > 12, que es
|
||||
lo que corre aca. Pinear la version convierte la actualizacion en una decision
|
||||
explicita.
|
||||
|
||||
Primero confirmar que el tag existe. **El naming es `vX.Y.Z`**: `v4.16.1` existe,
|
||||
`4.16.1` (sin la `v`) no.
|
||||
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker manifest inspect chatwoot/chatwoot:v4.16.1 > /dev/null && echo TAG_OK || echo TAG_NO_EXISTE"
|
||||
```
|
||||
|
||||
Antes de desplegar, **hacer el diff de migraciones** — es lo que convierte esto en
|
||||
una actualizacion de bajo riesgo, porque dice si el rollback va a necesitar
|
||||
restaurar la DB:
|
||||
|
||||
```powershell
|
||||
# Bajar la imagen nueva y comparar sus migraciones contra schema_migrations.
|
||||
# 156 == 156 significa que no hay cambios de esquema (fue el caso de 4.16.1).
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker pull chatwoot/chatwoot:v4.16.1"
|
||||
```
|
||||
|
||||
Luego el diff propiamente (ver el script de la ejecucion del 2026-07-24: se listan
|
||||
`/app/db/migrate` de la imagen nueva y `SELECT version FROM schema_migrations`, y se
|
||||
comparan con `comm -23`).
|
||||
|
||||
Para pinear el tag, **usar la API, no editar el archivo en el LXC**: Coolify
|
||||
regenera `/data/coolify/services/<uuid>/docker-compose.yml` en cada deploy y un
|
||||
cambio a mano en el host se pierde.
|
||||
|
||||
```powershell
|
||||
. .\.env.local.ps1
|
||||
|
||||
# 1) Traer el compose actual, 2) cambiar las 2 lineas de imagen, 3) PATCH en base64.
|
||||
$svc = (.\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/services/c11xzy2tx2cdapm32f5b89vy") | ConvertFrom-Json
|
||||
$new = $svc.docker_compose_raw.Replace("image: 'chatwoot/chatwoot:latest'", "image: 'chatwoot/chatwoot:v4.16.1'")
|
||||
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($new))
|
||||
$body = @{ docker_compose_raw = $b64 } | ConvertTo-Json -Compress
|
||||
.\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Method PATCH -Path "/services/c11xzy2tx2cdapm32f5b89vy" -BodyJson $body
|
||||
```
|
||||
|
||||
> **`docker_compose_raw` tiene que ir en base64.** Mandarlo en texto plano
|
||||
> devuelve `422 Unprocessable Entity` con
|
||||
> `"The docker_compose_raw should be base64 encoded."`, y
|
||||
> `Invoke-CoolifyApi.ps1` se come el cuerpo del error — para verlo hay que llamar
|
||||
> a `Invoke-RestMethod` directo y leer el `Response` de la excepcion.
|
||||
|
||||
Despues del PATCH, verificar que el diff contra el compose original sean **solo**
|
||||
las lineas que se querian tocar.
|
||||
|
||||
**Cambio de estado — requiere tu confirmacion antes de aplicarse.**
|
||||
|
||||
### Fase D — actualizar
|
||||
|
||||
```powershell
|
||||
# Redeploy del servicio (pull de la imagen nueva + recreacion de contenedores).
|
||||
.\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/deploy?uuid=c11xzy2tx2cdapm32f5b89vy"
|
||||
```
|
||||
|
||||
Devuelve un `deployment_uuid`. Seguimiento:
|
||||
|
||||
```powershell
|
||||
.\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/deployments/<deployment_uuid>"
|
||||
```
|
||||
|
||||
Al arrancar, el entrypoint corre `db:chatwoot_prepare` → `db:migrate` →
|
||||
`ConfigLoader` (`reconcile_only_new: true`, no pisa nada). Esperar a que los 4
|
||||
contenedores vuelvan a `healthy`:
|
||||
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker ps | grep c11xzy2tx2cdapm32f5b89vy"
|
||||
```
|
||||
|
||||
**Cambio de estado — requiere tu confirmacion.**
|
||||
|
||||
### Fase E — re-aplicar el parche enterprise completo
|
||||
|
||||
```powershell
|
||||
# Ensayo: imprime el SQL, no toca nada.
|
||||
.\scripts\Apply-ChatwootEnterprisePatch.ps1 -DryRun
|
||||
|
||||
# Aplicar: 3 UPDATE + reactivacion de los 9 flags premium en todas las cuentas.
|
||||
.\scripts\Apply-ChatwootEnterprisePatch.ps1 -ReenableAccountFeatures
|
||||
```
|
||||
|
||||
Criterio de exito (el script aborta si no se cumple):
|
||||
|
||||
- exactamente **3 lineas `UPDATE 1`**;
|
||||
- `pendientes=ninguno` para cada cuenta;
|
||||
- `self_hosted_enterprise=true`.
|
||||
|
||||
Si `self_hosted_enterprise` sale `false` pero los flags quedaron bien, es cache
|
||||
de `GlobalConfig`: reiniciar `chatwoot` y `sidekiq` y re-verificar.
|
||||
|
||||
**Cambio de estado — requiere tu confirmacion.**
|
||||
|
||||
### Fase F — verificar
|
||||
|
||||
```powershell
|
||||
.\scripts\Get-ChatwootLicenseStatus.ps1 -Deep
|
||||
```
|
||||
|
||||
Y a mano en `https://chat.urieljareth.org`:
|
||||
|
||||
1. Login como super admin.
|
||||
2. `/super_admin/settings`: plan **Enterprise**, cantidad **10000**.
|
||||
**NO pulsar `Refresh`** — revierte todo al instante.
|
||||
3. Una funcion premium por cuenta (audit logs, SLA, custom roles o Captain).
|
||||
4. Si habia branding propio, volverlo a poner (se reseteo, ver seccion 2).
|
||||
|
||||
### Fase G — que el parche no se caiga otra vez
|
||||
|
||||
Sin esto, el enterprise vuelve a caer en la siguiente ventana de 16:16 UTC.
|
||||
|
||||
#### Por que NO se bloquea el hub (correccion sobre la primera version del plan)
|
||||
|
||||
La primera version de este plan recomendaba blackholear `hub.2.chatwoot.com` con
|
||||
`extra_hosts`, aprovechando el `return if @instance_info.blank?`. **Se descarto al
|
||||
verificarlo contra este entorno**, por dos motivos:
|
||||
|
||||
1. **Rompe las notificaciones push del movil.** `hub.2.chatwoot.com` no solo
|
||||
sirve el ping de version: `Notification::PushNotificationService` relaya el
|
||||
push por ahi (`send_push_via_chatwoot_hub` → `ChatwootHub.send_push`) y ese
|
||||
metodo corre **solo cuando Firebase no esta configurado**. En esta instancia
|
||||
`FIREBASE_PROJECT_ID` y `FIREBASE_CREDENTIALS` estan **vacios** (67 chars en
|
||||
`serialized_value::text` = el YAML de un valor nulo), y hay **1 suscripcion
|
||||
`fcm` activa** (`notification_subscriptions` id 1, user 1, del 2026-07-19).
|
||||
Bloquear el host le mata el push a ese usuario.
|
||||
2. **Deja un job fallando todos los dias.** Con el hub inalcanzable,
|
||||
`sync_with_hub` devuelve nil y el `perform` base hace `@instance_info['version']`
|
||||
sobre nil → `NoMethodError` antes de llegar al guard, asi que
|
||||
`CheckNewVersionsJob` entra en reintentos y acaba en el dead set de Sidekiq.
|
||||
|
||||
Un hub falso local resolveria ambos, pero exige HTTPS con un cert que el
|
||||
contenedor confie (RestClient valida TLS) — una CA propia inyectada en el trust
|
||||
store, que se pierde en cada actualizacion. No vale la pena.
|
||||
|
||||
#### Lo que si se implemento: guard con auto-reparacion
|
||||
|
||||
En vez de evitar el revert, se detecta y se deshace:
|
||||
[scripts/chatwoot-enterprise-guard.sh](../../scripts/chatwoot-enterprise-guard.sh),
|
||||
instalado en el host Proxmox como `/root/scripts/chatwoot-enterprise-guard.sh` con
|
||||
`cron */5`.
|
||||
|
||||
Como funciona:
|
||||
|
||||
1. **Caso normal (barato):** 1 `SELECT` del plan y sale. **0.9 s**, sin escribir
|
||||
en el log. 288 corridas al dia es ruido despreciable para el host.
|
||||
2. **Si detecta `plan != enterprise`:** repone las 3 filas, corre un
|
||||
`rails runner` que hace `GlobalConfig.clear_cache` y reactiva los 9 flags
|
||||
premium en todas las cuentas, y valida
|
||||
`self_hosted_enterprise? == true` + `pendientes=ninguno`. **12.7 s.**
|
||||
3. Usa `flock` para no solaparse, y solo escribe en el log cuando actua.
|
||||
|
||||
Ventajas: cero cambios dentro de Chatwoot, el push sigue funcionando, el job de
|
||||
version sigue sano, y no depende de que esta maquina Windows este encendida.
|
||||
Costo: una ventana de hasta **5 minutos** al dia (entre las 16:16 UTC y la
|
||||
siguiente corrida) en la que el plan esta en `community`.
|
||||
|
||||
Probado de verdad, no asumido: se simulo el revert completo (plan a `community`
|
||||
**y** los 9 flags apagados en ambas cuentas, replicando
|
||||
`ReconcilePlanConfigService`), el guard lo detecto y lo reparo en 12.7 s, la
|
||||
segunda corrida fue no-op en 0.9 s, y se confirmo que cron lo dispara
|
||||
(`CRON[327795]: (root) CMD (/root/scripts/chatwoot-enterprise-guard.sh)`).
|
||||
|
||||
#### Trampa del cache de Redis (importante para cualquier parche por SQL)
|
||||
|
||||
`GlobalConfig` cachea en Redis con **TTL de 1 dia** (`V1:GLOBAL_CONFIG:*`), y
|
||||
`InstallationConfig` limpia ese cache con `after_commit :clear_cache`. Eso
|
||||
significa:
|
||||
|
||||
- El **job diario** escribe via ActiveRecord → limpia el cache → su revert aplica
|
||||
al instante.
|
||||
- Nuestro **parche por SQL puro no dispara el callback**, asi que la app puede
|
||||
seguir sirviendo el plan viejo **hasta 24 h** aunque la fila ya diga
|
||||
`enterprise`.
|
||||
|
||||
Por eso tanto el guard como `Apply-ChatwootEnterprisePatch.ps1
|
||||
-ReenableAccountFeatures` llaman explicitamente a `GlobalConfig.clear_cache`.
|
||||
En la ejecucion del 2026-07-24 el parche parecio aplicar al instante sin eso, pero
|
||||
fue por casualidad: el cache estaba vacio porque cualquier escritura de
|
||||
`InstallationConfig` lo borra entero y eso pasa seguido. No hay que confiar en
|
||||
esa casualidad.
|
||||
|
||||
### Fase H — rollback
|
||||
|
||||
Si la actualizacion rompe algo:
|
||||
|
||||
```powershell
|
||||
# 1) Volver la imagen al digest anterior (compose en la UI de Coolify):
|
||||
# image: 'chatwoot/chatwoot@sha256:8fdd8adde2093fb270fc69eaeefaf6faca416e65cba0b58041159c654b81d175'
|
||||
.\coolify_skill\scripts\Invoke-CoolifyApi.ps1 -Path "/deploy?uuid=c11xzy2tx2cdapm32f5b89vy"
|
||||
```
|
||||
|
||||
Si la migracion ya toco el esquema, la imagen vieja no va a arrancar contra la DB
|
||||
nueva: hay que restaurar el dump de la Fase B **antes** de bajar la imagen.
|
||||
|
||||
```powershell
|
||||
# 2) Restaurar el dump (DESTRUCTIVO: pisa la DB actual).
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct push 102 /root/backups/chatwoot-pre-4.16.1.dump /root/chatwoot-pre-4.16.1.dump"
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker cp /root/chatwoot-pre-4.16.1.dump postgres-c11xzy2tx2cdapm32f5b89vy:/tmp/restore.dump"
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec -i postgres-c11xzy2tx2cdapm32f5b89vy bash -lc 'PGPASSWORD=\$POSTGRES_PASSWORD pg_restore -U \$POSTGRES_USER -d \$POSTGRES_DB --clean --if-exists /tmp/restore.dump'"
|
||||
```
|
||||
|
||||
Luego Fase E otra vez.
|
||||
|
||||
## 4. Orden de ejecucion resumido
|
||||
|
||||
```
|
||||
A pre-checks (lectura)
|
||||
B backup DB + compose/.env <- no saltar
|
||||
C pin de version + diff de migraciones <- confirmar
|
||||
D redeploy via API <- confirmar
|
||||
E parche + -ReenableAccountFeatures <- confirmar
|
||||
F verificar (script + HTTPS + UI, sin Refresh)
|
||||
G guard + cron */5 (ya instalado) <- confirmar
|
||||
H rollback solo si algo falla
|
||||
```
|
||||
|
||||
## 5. Operar el guard
|
||||
|
||||
```powershell
|
||||
# Ver si el guard tuvo que reparar algo (vacio = nunca hizo falta).
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "cat /var/log/chatwoot-enterprise-guard.log"
|
||||
|
||||
# Forzar una corrida.
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "/root/scripts/chatwoot-enterprise-guard.sh; echo rc=\$?"
|
||||
|
||||
# Confirmar que cron lo dispara.
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "journalctl --since '-20 min' --no-pager | grep chatwoot-enterprise-guard"
|
||||
```
|
||||
|
||||
Lo normal es un log **vacio o con pocas lineas**. Un `REPARADO` por dia es lo
|
||||
esperado (el revert de las 16:16 UTC). Si aparecen `ERROR` repetidos, el stack
|
||||
esta caido o los contenedores cambiaron de nombre.
|
||||
|
||||
**Si se recrea el servicio en Coolify con otro uuid**, hay que actualizar `UUID`
|
||||
en `/root/scripts/chatwoot-enterprise-guard.sh` — el guard lo tiene hardcodeado.
|
||||
|
||||
## 6. Notas de riesgo
|
||||
|
||||
- **PostgreSQL 12.19** y la imagen `pgvector/pgvector:pg12` tiene ~23 meses.
|
||||
Un salto de version mayor de Chatwoot probablemente exija PG >= 13. Antes de
|
||||
actualizar mas alla de 4.16.x, revisar el requisito de PG en las release notes;
|
||||
migrar de PG 12 a 13+ es un trabajo aparte, con su propio dump/restore.
|
||||
- **`Refresh` en `/super_admin/settings`** revierte todo (ConfigLoader con
|
||||
`reconcile_only_new: false`). El guard lo repararia en <= 5 min, pero conviene
|
||||
no pulsarlo.
|
||||
- Ahora que la imagen esta **pineada a `v4.16.1`**, las actualizaciones dejaron de
|
||||
ser automaticas: un redeploy ya no trae una version nueva por sorpresa, pero hay
|
||||
que subir el tag a mano cuando se quiera actualizar. Ese es el punto del pin.
|
||||
- El `.env` del servicio y el `.tgz` del backup **contienen secretos**: se
|
||||
quedan en el host Proxmox. Nunca copiarlos al repo.
|
||||
- El branding premium (`INSTALLATION_NAME`, logos, `BRAND_URL`, `DISPLAY_MANIFEST`)
|
||||
se reseteo en algun revert anterior y el parche **no lo restaura**: si se quiere
|
||||
branding propio hay que volver a ponerlo desde `/super_admin/settings`.
|
||||
- Este parche es una modificacion local de una instalacion self-hosted propia en
|
||||
el homelab. No se distribuye ni se revende.
|
||||
|
||||
## 7. Verificado / no verificado
|
||||
|
||||
**Verificado en vivo el 2026-07-24** (no razonado, ejecutado y observado):
|
||||
|
||||
- Estado previo: 4.16.0, plan `community`, cantidad `0`,
|
||||
`self_hosted_enterprise? = false`, los 9 flags premium apagados en ambas cuentas.
|
||||
- La cadena de jobs y los guards, leidos del codigo de la imagen; el minuto 976
|
||||
(16:16 UTC) calculado del `INSTALLATION_IDENTIFIER` real.
|
||||
- Backup: dump de 667K con 97 tablas (`pg_restore -l`) + sha256.
|
||||
- Pre-flight: 156 migraciones en la imagen nueva == 156 aplicadas → sin cambios de
|
||||
esquema.
|
||||
- `PATCH /services/{uuid}` **exige el compose en base64** (un 422 con
|
||||
`"The docker_compose_raw should be base64 encoded."` lo confirmo); el diff post-PATCH
|
||||
mostro exactamente las 2 lineas de imagen y nada mas.
|
||||
- Deploy: 4.16.1 corriendo, 4/4 `healthy`, ~2 min de corte.
|
||||
- Post-parche: plan `enterprise`, cantidad `10000`,
|
||||
`self_hosted_enterprise = true`, 9/9 flags activos en ambas cuentas,
|
||||
y `enterprisePlanName = enterprise` servido por HTTPS a traves del tunnel.
|
||||
- Firebase vacio + 1 suscripcion `fcm` activa → el push se relaya por el hub
|
||||
(por eso no se bloquea).
|
||||
- `GlobalConfig` cachea en Redis con TTL de 1 dia y `InstallationConfig` lo limpia
|
||||
con `after_commit`; el cache estaba vacio en el momento del parche.
|
||||
- El guard: no-op en 0.9 s, reparacion completa en 12.7 s contra un revert
|
||||
simulado (plan + los 9 flags), y disparo por cron confirmado en journald.
|
||||
|
||||
**No verificado:**
|
||||
|
||||
- Que 4.16.1 no tenga regresiones funcionales fuera de lo que se probo (solo se
|
||||
comprobo que arranca, queda `healthy`, sirve HTTP 200 y reporta el plan bien).
|
||||
- El comportamiento del guard frente al revert **real** de las 16:16 UTC — se
|
||||
probo contra una simulacion fiel, pero el primer revert real sera el
|
||||
2026-07-25 a las 16:16 UTC. Revisar el log ese dia.
|
||||
- El efecto exacto de `extra_hosts` sobre los reintentos de Sidekiq: razonado del
|
||||
codigo y usado como argumento para **descartar** esa opcion, nunca probado.
|
||||
- Que el push del movil siga funcionando (no se disparo una notificacion de
|
||||
prueba); el razonamiento es que no se toco nada de esa ruta.
|
||||
|
||||
---
|
||||
|
||||
## Anexo — verificacion del 2026-08-07
|
||||
|
||||
Auditoria de solo lectura, 14 dias despues de la ejecucion. Tres cosas cambiaron.
|
||||
|
||||
### 1. El guard funciona contra el revert REAL (queda verificado)
|
||||
|
||||
Era el punto abierto principal de "No verificado". El log
|
||||
`/var/log/chatwoot-enterprise-guard.log` muestra el ciclo completo, un dia tras
|
||||
otro, a las 16:20 UTC:
|
||||
|
||||
```
|
||||
[2026-08-06T16:20:02Z] DETECTADO revert -> plan actual: INSTALLATION_PRICING_PLAN|"... value: community ..." . Reparando...
|
||||
[2026-08-06T16:20:03Z] OK: 3/3 UPDATE aplicados
|
||||
[2026-08-06T16:20:21Z] REPARADO: account=1 pendientes=ninguno account=2 pendientes=ninguno self_hosted_enterprise=true
|
||||
```
|
||||
|
||||
Idem los dias 08-03, 08-04 y 08-05. El revert diario ocurre de verdad y el guard
|
||||
lo deshace en ~20 s. Deja de ser una hipotesis.
|
||||
|
||||
### 2. El pin de version NO sostuvo: corre `v4.16.2`
|
||||
|
||||
```
|
||||
chatwoot-c11xzy2tx2cdapm32f5b89vy chatwoot/chatwoot:v4.16.2
|
||||
sidekiq-c11xzy2tx2cdapm32f5b89vy chatwoot/chatwoot:v4.16.2
|
||||
```
|
||||
|
||||
La imagen se habia pineado a `v4.16.1` via API. Hoy corre `v4.16.2`, asi que en
|
||||
algun momento entre el 2026-07-24 y hoy alguien o algo movio el tag y hubo un
|
||||
redeploy. **Antes de asumir que el pin protege, verificalo:**
|
||||
|
||||
```powershell
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker ps --format '{{.Names}}|{{.Image}}' | grep chatwoot"
|
||||
```
|
||||
|
||||
### 3. El guard fallo durante la ventana del update
|
||||
|
||||
Cuatro errores el 2026-08-07, los primeros dos en la ventana del revert diario:
|
||||
|
||||
```
|
||||
[2026-08-07T16:17:15Z] ERROR: no se pudo leer INSTALLATION_PRICING_PLAN (stack caido o contenedor renombrado?)
|
||||
[2026-08-07T16:20:03Z] ERROR: no se pudo leer INSTALLATION_PRICING_PLAN (stack caido o contenedor renombrado?)
|
||||
[2026-08-07T23:11:45Z] ERROR: ...
|
||||
[2026-08-07T23:15:02Z] ERROR: ...
|
||||
```
|
||||
|
||||
**Estado tras la auditoria: sano.** Una corrida manual con `bash -x` lee el plan
|
||||
sin problema y sale 0, y el valor en la DB es `enterprise`:
|
||||
|
||||
```
|
||||
INSTALLATION_PRICING_PLAN|"--- !ruby/hash:...\nvalue: enterprise\n"
|
||||
```
|
||||
|
||||
Es decir: los errores fueron transitorios, coincidentes con la recreacion de
|
||||
contenedores del update a `v4.16.2`. **No hay accion urgente.** Pero deja
|
||||
expuesto un riesgo estructural.
|
||||
|
||||
### 4. Riesgo estructural: el guard tiene el nombre del contenedor hardcodeado
|
||||
|
||||
`scripts/chatwoot-enterprise-guard.sh` fija:
|
||||
|
||||
```bash
|
||||
UUID=c11xzy2tx2cdapm32f5b89vy
|
||||
DB_CT="postgres-$UUID"
|
||||
APP_CT="chatwoot-$UUID"
|
||||
```
|
||||
|
||||
Mientras el uuid del *service* no cambie, los nombres se mantienen — y en este
|
||||
caso se mantuvieron. Pero el propio mensaje de error del guard nombra la causa
|
||||
("contenedor renombrado?"), y **un redeploy que cambie el sufijo lo deja ciego
|
||||
sin avisar**: el guard sale con codigo 1 y solo escribe una linea en un log que
|
||||
nadie lee. El revert diario dejaria de repararse en silencio.
|
||||
|
||||
Mitigacion pendiente (no aplicada — requiere confirmacion porque toca el host):
|
||||
hacer que el guard **resuelva el nombre por patron** en vez de fijarlo, p. ej.
|
||||
`docker ps --format '{{.Names}}' | grep -m1 '^postgres-'` acotado al uuid del
|
||||
service consultado a la API de Coolify. Y que N fallos consecutivos escalen a
|
||||
algo visible, no solo al log.
|
||||
|
||||
### Comprobacion rapida del estado
|
||||
|
||||
```powershell
|
||||
. .\.env.local.ps1
|
||||
.\scripts\Get-ChatwootLicenseStatus.ps1 -Deep
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "tail -20 /var/log/chatwoot-enterprise-guard.log"
|
||||
```
|
||||
|
||||
Nota sobre el quoting: cualquier lectura directa de la DB necesita el patron
|
||||
base64, porque `Invoke-ProxmoxSsh.ps1` corrompe las comillas anidadas. Ver
|
||||
[../TOOL-INDEX.md](../TOOL-INDEX.md) §1.2.
|
||||
@@ -20,9 +20,9 @@ Síntomas típicos que llevan aquí:
|
||||
- Dominio de app con `502 Bad Gateway` o `530`
|
||||
|
||||
Documentos relacionados:
|
||||
[ISSUE_cloudflare-tunnel_routing_websocket-tls-handshake.md](../ISSUE_cloudflare-tunnel_routing_websocket-tls-handshake.md) ·
|
||||
[cloudflare-tunnel-coolify-agent_1.md](../cloudflare-tunnel-coolify-agent_1.md) ·
|
||||
[issue-coolify-static-app-deploy.md](../issue-coolify-static-app-deploy.md)
|
||||
[2026-04-11-cloudflare-tunnel-websocket-tls.md](../incidentes/2026-04-11-cloudflare-tunnel-websocket-tls.md) ·
|
||||
[guía de agente 2026-04 (OBSOLETA)](../incidentes/2026-04-cloudflare-tunnel-guia-agente-OBSOLETA.md) ·
|
||||
[2026-04-11-coolify-static-app-deploy.md](../incidentes/2026-04-11-coolify-static-app-deploy.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -162,7 +162,7 @@ $body = @{
|
||||
|
||||
> ⚠️ El último elemento `http_status:404` (sin hostname) es obligatorio o la API
|
||||
> rechaza la configuración. La referencia completa de endpoints (DNS, crear túnel,
|
||||
> obtener token) está en [cloudflare-tunnel-coolify-agent_1.md](../cloudflare-tunnel-coolify-agent_1.md).
|
||||
> obtener token) está en [guía de agente 2026-04 (OBSOLETA)](../incidentes/2026-04-cloudflare-tunnel-guia-agente-OBSOLETA.md).
|
||||
|
||||
**Cualquier `PUT`/`POST` a Cloudflare es un cambio de estado → confirmar con el usuario y
|
||||
capturar el estado actual (paso 2.2) antes de aplicar, para tener rollback.**
|
||||
@@ -187,7 +187,7 @@ terminal en cualquier contenedor. Debe conectar sin `Terminal websocket connecti
|
||||
cambio son **residuales** de conexiones ya abiertas; desaparecen solos.
|
||||
- Si el túnel se cae cada ~5 min con `failed to dial to edge with quic: timeout`,
|
||||
el contenedor debe correr con `--protocol http2` (UDP/7844 suele estar bloqueado
|
||||
en redes domésticas). Ver PASO 6 de [cloudflare-tunnel-coolify-agent_1.md](../cloudflare-tunnel-coolify-agent_1.md).
|
||||
en redes domésticas). Ver PASO 6 de [guía de agente 2026-04 (OBSOLETA)](../incidentes/2026-04-cloudflare-tunnel-guia-agente-OBSOLETA.md).
|
||||
- `cloudflared` es distroless: para leer archivos internos usa `docker cp`, no `cat` directo.
|
||||
|
||||
---
|
||||
|
||||
@@ -8,7 +8,7 @@ Usa un archivo privado `.env.local.ps1` con estos valores:
|
||||
$env:PROXMOX_HOST = "192.168.0.200"
|
||||
$env:PROXMOX_NODE = "thinkcentre"
|
||||
$env:PROXMOX_USER = "root"
|
||||
$env:PROXMOX_SSH_KEY = "C:\Users\Uriel Jareth\.openclaw\workspace\proxmox_key_win"
|
||||
$env:PROXMOX_SSH_KEY = "keys\proxmox_ed25519"
|
||||
$env:PROXMOX_API_BASE_URL = "https://192.168.0.200:8006/api2/json"
|
||||
$env:PROXMOX_API_TOKEN_ID = "root@pam!openclaw"
|
||||
$env:PROXMOX_API_TOKEN_SECRET = "REPLACE_WITH_TOKEN_SECRET"
|
||||
|
||||
@@ -36,6 +36,6 @@ Requiere confirmacion explicita.
|
||||
Usar solo cuando haga falta inspeccion manual.
|
||||
|
||||
```powershell
|
||||
ssh -i "C:\Users\Uriel Jareth\.openclaw\workspace\proxmox_key_win" root@192.168.0.200
|
||||
ssh -i "keys\proxmox_ed25519" root@192.168.0.200
|
||||
pct exec 102 -- docker exec -it <container> /bin/sh
|
||||
```
|
||||
|
||||
@@ -23,7 +23,7 @@ Si el endpoint API de Coolify no esta disponible o no hay token cargado:
|
||||
No usar `sed` para editar `config.php`. Usar el script PHP del repo:
|
||||
|
||||
```powershell
|
||||
scp -o StrictHostKeyChecking=no -i "C:\Users\Uriel Jareth\.openclaw\workspace\proxmox_key_win" .\scripts\fix-nextcloud-config.php root@192.168.0.200:/tmp/fix-nextcloud-config.php
|
||||
scp -o StrictHostKeyChecking=no -i "keys\proxmox_ed25519" .\scripts\fix-nextcloud-config.php root@192.168.0.200:/tmp/fix-nextcloud-config.php
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct push 102 /tmp/fix-nextcloud-config.php /tmp/fix-nextcloud-config.php"
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker cp /tmp/fix-nextcloud-config.php nextcloud-hdcdpkm0jko3qqvn5683ercc:/tmp/fix-nextcloud-config.php"
|
||||
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec nextcloud-hdcdpkm0jko3qqvn5683ercc php /tmp/fix-nextcloud-config.php /config/www/nextcloud/config/config.php nextcloudsuite.urieljareth.org nextcloud-db"
|
||||
|
||||
Reference in New Issue
Block a user