# 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 --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 sh -c 'wget -qO- http://127.0.0.1/health | head -c 80'" ``` Si sale ``, 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 ``` 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 -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 -ShowResult # Escribirlo (guarda copia de rollback en backups\ y verifica leyendo de vuelta): .\coolify_skill\scripts\Set-CoolifyHealthcheckGrace.ps1 -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.