14 KiB
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 Proxmoxthinkcentre(192.168.0.200). Servicio de ejemplo: FileFlows, uuidznpmxv2o6ggooi6qxksiagke,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 servicionoopcon 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.orgen vez del realn8.urieljareth.org,nextcloud.en vez denextcloudsuite.) sale el mismo 503 del catch-all. Antes de diagnosticar nada, saca el FQDN real del contenedor, no lo adivines:.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker inspect <cont> --format '{{range .Config.Env}}{{println .}}{{end}}'" -
Un
502es un problema de puerto, no de salud.grimmorydaba 502 estandohealthy: 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.(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
grimmoryprobabahttp://127.0.0.1/health, y en un SPA esa ruta la responde el fallback conindex.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:.\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)
. .\.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.
.\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
# ¿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):
# 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:
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-lvmtiene 54 GB libres ylocal27 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_URLapunta a un dominio que no existe. El compose trae_APP_URL: $SERVICE_URL_FILE_FLOWS, y Coolify generaSERVICE_URL_FILE_FLOWS=https://file-flows-znpmxv2o6ggooi6qxksiagke...— con guion,file-flows. El dominio real esfileflows, sin guion. Ese hostname con guion no tiene ni ruta en Traefik ni DNS.SERVICE_URL_FILEFLOWS_5000lleva 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
znpmxv2o6ggooi6qxksiagkey los nombres de contenedor con sufijo cambian en cada redeploy. Resuélvelos, no los copies. - El barrido de
start_periodes una foto de ese día. Vuelve a correrlo antes de apoyarte en él.