Files
Proxmox-Coolify-Manager/docs/casos/coolify-servicio-nuevo-503-no-available-server.md

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 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:

    .\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.

    (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:

    .\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-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.