311 lines
14 KiB
Markdown
311 lines
14 KiB
Markdown
# 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.
|