Actualiza toolkit operativo y documentación
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user