Actualiza toolkit operativo y documentación

This commit is contained in:
urieljareth
2026-09-10 20:53:50 -06:00
parent 3b7209dcc1
commit 714057bfc8
69 changed files with 6023 additions and 384 deletions
@@ -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.