Actualiza toolkit operativo y documentación

This commit is contained in:
urieljareth
2026-09-10 20:53:50 -06:00
parent 3b7209dcc1
commit 714057bfc8
69 changed files with 6023 additions and 384 deletions
+22
View File
@@ -0,0 +1,22 @@
# OpenSEO — env vars para Coolify (template, sin secretos reales).
#
# Copia este archivo a `stacks/open-seo/.env.coolify`, rellena lo que aplique,
# y pásalo a `New-CoolifyService.ps1 -EnvFile`. El script empuja cada línea
# KEY=VALUE como env var del servicio; las referencias `${VAR}` dentro del
# compose se resuelven en runtime desde esas vars.
#
# Nunca commitees el archivo `.env.coolify` real — está en .gitignore.
# SEO data (opcional). Base64 de `email:password` de dataforseo.com — NO la
# dashboard API key. Vacío = la app arranca, los workflows SEO muestran "no
# data". Ver https://openseo.so/docs/DATAFORSEO_API_KEY.md
DATAFORSEO_API_KEY=
# Telemetry opt-out. "1" para desactivar el heartbeat anónimo y el beacon de
# fallo de preflight. Por defecto encendido.
OPENSEO_TELEMETRY_DISABLED=1
DO_NOT_TRACK=1
# SAM, el agente SEO dentro de la app (opcional). Oculto si vacío.
OPENROUTER_API_KEY=
OPENROUTER_MODEL=
+154
View File
@@ -0,0 +1,154 @@
# OpenSEO — stack self-host en Coolify
> Resuelto el **2026-08-27** contra el host real.
> Target: **LXC 102** (`coolify`), servicio compose, FQDN
> **`https://openseo.urieljareth.org`**.
> Imagen: **`ghcr.io/every-app/open-seo:sha-c469a48`** (mismo SHA que el deploy
> fallido anterior — esta vez la build de Vite sí corre, en el entrypoint).
---
## Por qué existe este stack
El deploy previo (`uuid kj0kccsb4d46tm0d6qe6docy`, `name=open-seo:main-...`,
deployment `fkojyfkqzp69hcba6miy8oer`) terminó con Coolify marcando verde y
**Caddy respondiendo 404 a todo**. Diagnóstico:
- `build_pack=railpack` clonó `every-app/open-seo@main`, construyó imagen local
con el mismo SHA `c469a48ae90ab58413b198fe3d1ac1aa90a9b070` y la cacheó.
- En el redeploy: `No build configuration changed & image found (...) Build
step skipped` → la imagen cacheada **no tenía `/app/dist`** (los artefactos
del build de Vite) y Coolify la reusó.
- Caddy (`/Caddyfile` con `root * /app/dist` + SPA fallback a `/index.html`)
no encontró nada y devolvió 404 a `/`, `/robots.txt`, `/health`.
- El README upstream lo dice textual: *"We recommend self-hosting with
Cloudflare as opposed to Railway, Coolify or Dokploy. We plan to make it
simpler to host on those platforms in the next few months."*
**Solución:** dejar de seguir upstream y consumir la **imagen prebuilt** que el
propio equipo publica en GHCR. Esa imagen tiene la cadena correcta:
`docker-entrypoint.sh` corre preflight → migrations → `pnpm run build` (que sí
genera `/app/dist`) → `vite preview` en el puerto `3001`. Y usa un fingerprint
para no reconstruir cuando los env vars relevantes no cambiaron.
Stack: **un solo servicio** (OpenSEO es self-contained: SQLite vía workerd en
`/app/.wrangler`, volumen `openseo-data`). Sin DB externa.
---
## Archivos
| Archivo | Para qué |
|---|---|
| `docker-compose.coolify.yml` | Compose que consume Coolify vía `POST /services` |
| `.env.example` | Template de env vars (sin secretos) |
| `.env.coolify` | **No committed.** Lo crea el operador con `cp .env.example .env.coolify` y rellena |
---
## Variables de entorno
Hardcoded en el compose (porque son decisión de arquitectura, no secretos):
| Var | Valor | Por qué |
|---|---|---|
| `PORT` | `3001` | Es donde escucha `vite preview` (per `Dockerfile.selfhost`) |
| `AUTH_MODE` | `local_noauth` | Single admin, sin pantalla de login. Aquí no tenemos `TEAM_DOMAIN`/`POLICY_AUD` de Cloudflare Access |
| `ALLOWED_HOST` | `openseo.urieljareth.org` | Sin esto, Vite bloquea toda petición externa con "Blocked request" |
| `CLOUDFLARE_INCLUDE_PROCESS_ENV` | `true` | Lo exige el runtime workerd para que process.env llegue a los bindings |
Suministradas vía `.env.coolify` (env vars del servicio en Coolify):
| Var | Default | Efecto |
|---|---|---|
| `DATAFORSEO_API_KEY` | vacío | **WARN** del preflight (no FAIL). Vacío = la app arranca, los workflows SEO devuelven "no data" |
| `OPENSEO_TELEMETRY_DISABLED` | `1` | Apaga el heartbeat anónimo |
| `DO_NOT_TRACK` | `1` | Alias del anterior |
| `OPENROUTER_API_KEY` | vacío | Habilita a SAM (el agente SEO integrado) si se setea |
| `OPENROUTER_MODEL` | vacío | Modelo a usar con SAM |
---
## Deploy
### 1. (Manual, una sola vez) Ingress del túnel de Cloudflare
El token de Cloudflare **no está** en `.env.local.ps1`, así que esto se hace en
el dashboard:
1. Cloudflare Zero Trust → Networks → Tunnels → tunnel `urieljareth` →
Configure → Public hostname.
2. Add a public hostname:
- Subdomain: `openseo`
- Domain: `urieljareth.org`
- Service: **HTTP** (no HTTPS, lo gestiona Coolify/Traefik)
- URL: `coolify.urieljareth.org` (o la IP interna del proxy de Coolify —
misma que usan los demás subdominios)
### 2. Crear el servicio en Coolify
```powershell
. .\.env.local.ps1
# Crear el archivo de env real (gitignored)
Copy-Item .\stacks\open-seo\.env.example .\stacks\open-seo\.env.coolify
# Editar .\stacks\open-seo\.env.coolify si quieres setear DATAFORSEO_API_KEY
.\deploy_skill\scripts\New-CoolifyService.ps1 `
-AppPath .\stacks\open-seo `
-AppName open-seo `
-Fqdn https://openseo.urieljareth.org `
-PrimaryService app `
-ProjectName "AI AGENCY" -EnvironmentName production `
-EnvFile .\stacks\open-seo\.env.coolify `
-InstantDeploy
```
### 3. Esperar al primer arranque
El primer `up` tarda **1-2 min**: preflight + migrations + vite build + arranque
de `vite preview`. Traefik no enruta hasta que el contenedor esté `healthy`.
```powershell
.\coolify_skill\scripts\Test-CoolifyServiceReady.ps1 -Uuid <uuid> -WaitSeconds 600
```
### 4. Verificar
```powershell
# 1. Endpoint público responde (TLS emitido, Traefik enrutando)
curl.exe -k -sSI https://openseo.urieljareth.org/
# 2. Status del contenedor
.\coolify_skill\scripts\Get-CoolifyDockerStatus.ps1 -Filter openseo
# 3. Logs del entrypoint (debería verse "Preflight passed")
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker logs <cont> --tail 60"
# 4. Preflight reporta lo que falta
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec <cont> wget -qO- http://127.0.0.1:3001/api/health"
```
---
## Rollback / limpieza
| Acción | Comando |
|---|---|
| Parar la app rota original | `docker stop kj0kccsb4d46tm0d6qe6docy-055629993145` (vía `pct exec 102 --`) |
| Borrar la app rota de Coolify | UI → Service `kj0kccsb4d46tm0d6qe6docy` → Delete |
| Re-deployar | UI → Service `open-seo` → Redeploy |
---
## Cosas que caducan
- **El tag `:sha-c469a48` se queda viejo.** Cuando el upstream publique un SHA
más reciente, actualizar el `image:` en `docker-compose.coolify.yml` y
redeployar. `v0.1.6` también existe (publicado 8 días antes).
- **El entrypoint vuelve a buildear `dist`** cada vez que algún env var del
prefijo `VITE_*` / `AUTH_MODE` / `POSTHOG_*` / `TURNSTILE_SITE_KEY` /
`BYPASS_EMAIL_VERIFICATION` cambie. Es intencional — el fingerprint está
ahí para no rehacer cuando nada relevante cambió.
- **Coolify normaliza el compose al guardarlo y borra los comentarios.** La
versión con explicaciones es la del repo, no la que se ve en la UI.
@@ -0,0 +1,78 @@
# OpenSEO self-host — Coolify stack (LXC 102)
#
# Por qué este compose en lugar del repo upstream (`every-app/open-seo`) vía
# railpack:
# - el deploy anterior (uuid kj0kccsb4d46tm0d6qe6docy) terminó con Caddy
# respondiendo 404 a todo porque /app/dist no existía en la imagen cacheada
# (Coolify saltó el build: "No build configuration changed & image found ...
# Build step skipped").
# - el README upstream lo dice explícitamente: "We recommend self-hosting
# with Cloudflare as opposed to Railway, Coolify or Dokploy. We plan to
# make it simpler to host on those platforms in the next few months."
# - esta imagen (`ghcr.io/every-app/open-seo:sha-c469a48`) es la build del
# mismo commit, pero el build de Vite corre en `docker-entrypoint.sh` al
# arrancar el contenedor, no en el build de la imagen. Y el entrypoint ya
# tiene la lógica de fingerprint para no reconstruir cuando los env vars
# relevantes no cambiaron.
#
# Contrato: docs/AGENTS-coolify-apps.md
# - sin publicar 80/443: solo `expose`, Traefik enruta (§2.3)
# - SECRETOS vía env vars inyectados por Coolify (§2.5)
# - volumen con nombre para /app/.wrangler (SQLite que sobrevive al redeploy, §5)
# - healthcheck independiente de servicios externos al boot
# - AUTH_MODE=local_noauth porque aquí no hay TEAM_DOMAIN/POLICY_AUD de
# Cloudflare Access. Si más adelante se quiere proteger con auth, cambiar
# a AUTH_MODE=cloudflare_access + TEAM_DOMAIN + POLICY_AUD.
# - ALLOWED_HOST es obligatorio detrás del túnel: sin él Vite bloquea toda
# petición externa con "Blocked request" (ver preflight info level).
services:
app:
image: 'ghcr.io/every-app/open-seo:sha-c469a48'
environment:
# Puerto en el que escucha `vite preview` (per Dockerfile.selfhost / entrypoint).
- PORT=3001
# Single admin user, sin pantalla de login. NO exponer públicamente sin
# poner tu propia auth delante — el preflight lo dice literal.
- AUTH_MODE=local_noauth
# Host header permitido. Es el FQDN público por el que llega el tráfico
# desde el túnel de Cloudflare.
- ALLOWED_HOST=openseo.urieljareth.org
# Requerido por el runtime workerd para exponer process.env a los
# bindings (lo exige el compose upstream).
- CLOUDFLARE_INCLUDE_PROCESS_ENV=true
# SEO data (opcional). Vacío = la app arranca, los workflows SEO
# devuelven "no data". Se setea después vía Coolify env.
- DATAFORSEO_API_KEY=${DATAFORSEO_API_KEY:-}
# Telemetry opt-out (también vía DO_NOT_TRACK). Por defecto apagado.
- OPENSEO_TELEMETRY_DISABLED=${OPENSEO_TELEMETRY_DISABLED:-}
- DO_NOT_TRACK=${DO_NOT_TRACK:-}
# AI features (SAM, el agente SEO integrado). Vacío = SAM deshabilitado.
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
- OPENROUTER_MODEL=${OPENROUTER_MODEL:-}
expose:
- '3001'
# El endpoint /api/health lo sirve el propio preflight (ver
# src/lib/selfhost-preflight.ts) sin auth — seguro para el healthcheck.
# Usamos `node` directamente porque la imagen es node:22 y el HEALTHCHECK
# upstream hace exactamente esto.
healthcheck:
test:
- CMD-SHELL
- "node -e \"fetch('http://127.0.0.1:3001/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""
interval: 30s
timeout: 10s
retries: 5
# Primer arranque: preflight + migrations + vite build (1-2 min).
start_period: 300s
volumes:
- 'openseo-data:/app/.wrangler'
restart: unless-stopped
logging:
driver: json-file
options:
max-size: '10m'
max-file: '3'
volumes:
openseo-data: {}