Actualiza toolkit operativo y documentación
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user