# Caso: firecrawl llevaba meses `exited` y sin URL — stack mínimo con imágenes precompiladas > Resuelto el **2026-08-24** contra el host real. > Target: **LXC 102** (`coolify`), proyecto `AI AGENCY` / `production`. > Resultado: `https://firecrawl.urieljareth.org` → **HTTP 200**, scrape real > verificado (`"success": true`). --- ## 0. Resumen ejecutivo La app `firecrawl` de Coolify (`build_pack=dockercompose`, uuid `du3iknyvy22vap767t9tnf9s`) estaba `exited:unhealthy` y sin dominio. **Causa:** seguía `git_branch: main`, y firecrawl upstream se rediseñó. El compose de `main` hoy trae **7 servicios**, incluidos **FoundationDB, RabbitMQ y nuq-postgres**, con **3 compilados desde fuente** (`apps/api`, `apps/playwright-service-ts`, `apps/nuq-postgres`) y `mem_limit: 8G` en `api` más `4G` en playwright. Este LXC tiene **4 cores** y el disco escribe a ~26 ms. Compilar Chromium ahí es el peor caso posible. **Solución:** se dejó de seguir upstream. Nuevo **service** de Coolify con un compose propio de **5 servicios y cero compilaciones**, todo con imágenes ya publicadas. Vive en [`stacks/firecrawl/docker-compose.coolify.yml`](../../stacks/firecrawl/docker-compose.coolify.yml). **La URL no se había perdido ese día:** `docker_compose_domains` estaba vacío, el compose generado no tenía ninguna regla `Host(...)` y Traefik **nunca** había emitido certificado para un dominio de firecrawl. Tampoco había ningún despliegue desde antes del 2026-07-31. --- ## 1. El stack que sí aguanta este host | Servicio | Imagen | Notas | |---|---|---| | `api` | `ghcr.io/firecrawl/firecrawl:2.10.19` | pinado; sirve en **3002** | | `playwright-service` | `ghcr.io/firecrawl/playwright-service:latest` | no publica tags de versión | | `nuq-postgres` | `ghcr.io/firecrawl/nuq-postgres:latest` | sustituye al build de `apps/nuq-postgres` | | `redis` | `redis:alpine` | sin persistencia (`--save "" --appendonly no`) | | `rabbitmq` | `rabbitmq:3-management` | | Fuera quedaron **`foundationdb` y `foundationdb-init`**: solo se usan si `NUQ_BACKEND` está definido, y aquí se deja vacío a propósito. Solo hay **versiones 2.10.x** publicadas (2.10.1 … 2.10.19). No existe una línea antigua más liviana a la que bajarse. ### RabbitMQ da errores y no pasa nada En el log de `api` aparece, de forma normal: ``` NuQ sender connection error ... "Cannot get a message from queue 'nuq.queue_scrape.prefetch' in vhost '/': noproc" NuQ sender get failed, falling back to postgres ``` Es **degradación controlada**: la cola cae a postgres y firecrawl funciona. No es el problema que hay que perseguir si algo va mal. --- ## 2. Las tres trampas que costaron tiempo ### 2.1 La imagen no trae `wget` — y el healthcheck decide si hay ruta Verificado dentro del contenedor: | Binario | ¿Está? | |---|---| | `curl` | sí (`/usr/bin/curl`) | | `wget` | **NO** | | `nc` | **NO** | Y los endpoints: | Ruta | Código | |---|---| | `/` | **200** | | `/is-production` | 200 | | `/test` | 404 | | `/health` | 404 | | `/v1/health` | 404 | Un healthcheck con `wget` o contra `/health` **falla siempre**. Y como Traefik solo enruta contenedores `healthy`, el dominio devuelve `503 no available server` aunque la app esté perfectamente viva y escuchando en 3002 (ver [el caso del 503](coolify-servicio-nuevo-503-no-available-server.md)). El que funciona: ```yaml healthcheck: test: ['CMD', 'curl', '-fsS', '-o', '/dev/null', 'http://127.0.0.1:3002/'] ``` **Comprueba siempre qué binarios y qué rutas existen antes de escribir un healthcheck:** ```powershell .\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec sh -c 'command -v curl wget nc'" ``` ### 2.2 El worker rechaza todo por carga ``` Can't accept connection due to RAM/CPU load ``` Los umbrales por defecto (`MAX_RAM`/`MAX_CPU` = 0.8) se superan constantemente en un host compartido. Con `MAX_RAM: 0.95` y `MAX_CPU: 0.95` acepta trabajo. ### 2.3 `api` se queda en `Created` en el primer deploy En el primer `compose up`, `api` quedó **`Created`** y nunca arrancó: sus `depends_on: service_healthy` (rabbitmq y nuq-postgres) tardaron más que el proceso de deploy. Un `restart` del service con las dependencias ya sanas lo resolvió. Si ves `Created` sin logs ni error, no está roto: reinicia el service. --- ## 3. Cómo se reproduce ```powershell . .\.env.local.ps1 .\deploy_skill\scripts\New-CoolifyService.ps1 ` -AppPath .\stacks\firecrawl ` -AppName firecrawl-min ` -Fqdn https://firecrawl.urieljareth.org ` -PrimaryService api ` -ProjectName "AI AGENCY" -EnvironmentName production -NoDeploy # Secretos: Coolify siembra las variables desde los ${...} del compose con su # valor por defecto. Hay que sobrescribir las que deben ser secretas por PATCH # (POST devuelve 409 si ya existe): POSTGRES_PASSWORD y BULL_AUTH_KEY. .\coolify_skill\scripts\Test-CoolifyServiceReady.ps1 -Uuid -WaitSeconds 600 ``` Verificación funcional (responder 200 en `/` no prueba que funcione): ```powershell Invoke-RestMethod -Uri "https://firecrawl.urieljareth.org/v1/scrape" -Method POST ` -ContentType 'application/json' -Body '{"url":"https://example.com","formats":["markdown"]}' # success = True ``` --- ## 4. Tres bugs del toolkit que este caso destapó Los tres estaban impidiendo que `New-CoolifyService.ps1` funcionara. **Corregidos y verificados el 2026-08-24.** 1. **`type` junto a `docker_compose_raw`.** El script enviaba `type = "one-click-service"` con un comentario que afirmaba que Coolify acepta cualquier string. Es falso: la API responde `422 "You cannot provide both service type and docker_compose_raw."`. `type` es solo para servicios de la librería. **Se eliminó.** 2. **`docker_compose_raw` sin base64.** Se enviaba en crudo y la API responde `422 "The docker_compose_raw should be base64 encoded."`. 3. **`Invoke-CoolifyApi.ps1` mandaba el body como *string*.** PowerShell 5.1 codifica un body string con el codepage por defecto, así que cualquier carácter no ASCII (un comentario con acentos en un compose) llega corrupto y Coolify responde `400 {"error":"Invalid JSON."}`. Ahora manda bytes UTF-8 con `charset=utf-8`. **Afectaba a todo POST/PATCH**, no solo a los servicios. ### Y una inconsistencia que sigue abierta `Test-PreDeployChecklist.ps1` solo escanea `docker-compose.yml|yaml` y `compose.yml|yaml`, pero el default de `New-CoolifyService.ps1` es `docker-compose.coolify.yml`. **Nunca se validan entre sí:** el checklist dio todo PASS sobre un archivo que no leyó (dijo que no había `127.0.0.1` cuando sí lo había). Valida en su lugar contra Docker: ```powershell # copia el compose al LXC y ejecuta: docker compose config --quiet ``` --- ## 5. Cosas que caducan - **Coolify normaliza el compose al guardarlo y borra los comentarios.** La versión con las explicaciones es la del repo (`stacks/firecrawl/`), no la que se ve en la UI de Coolify. - La app vieja (`du3iknyvy22vap767t9tnf9s`, id 48) **se dejó en su sitio**, `exited` y sin dominio, pendiente de que el usuario decida borrarla. - `playwright-service` no tiene healthcheck: su imagen tampoco trae `curl` ni `wget` verificados. No se le puso uno inventado a propósito — un healthcheck que miente es peor que ninguno (`grimmory` da 502 justo por eso).