197 lines
7.2 KiB
Markdown
197 lines
7.2 KiB
Markdown
# 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 <cont> 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 <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).
|