Files
Proxmox-Coolify-Manager/stacks/open-seo

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

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

.\coolify_skill\scripts\Test-CoolifyServiceReady.ps1 -Uuid <uuid> -WaitSeconds 600

4. Verificar

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