OpenSEO — stack self-host en Coolify
Resuelto el 2026-08-27 contra el host real. Target: LXC 102 (
coolify), servicio compose, FQDNhttps://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=railpackclonóevery-app/open-seo@main, construyó imagen local con el mismo SHAc469a48ae90ab58413b198fe3d1ac1aa90a9b070y 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 (
/Caddyfileconroot * /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:
- Cloudflare Zero Trust → Networks → Tunnels → tunnel
urieljareth→ Configure → Public hostname. - 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)
- Subdomain:
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-c469a48se queda viejo. Cuando el upstream publique un SHA más reciente, actualizar elimage:endocker-compose.coolify.ymly redeployar.v0.1.6también existe (publicado 8 días antes). - El entrypoint vuelve a buildear
distcada vez que algún env var del prefijoVITE_*/AUTH_MODE/POSTHOG_*/TURNSTILE_SITE_KEY/BYPASS_EMAIL_VERIFICATIONcambie. 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.