Agrega scripts y runbooks: Cloudflare API, autostart Coolify, parche Chatwoot, deploy Solo Leveling + docs de casos

- scripts/Invoke-CloudflareApi.ps1: control de tunnel/DNS via API
- scripts/Install-CoolifyAutostart.ps1 + scripts/host/: autostart de LXC 102 + tunnel tras corte
- scripts/Apply-ChatwootEnterprisePatch.ps1: parche enterprise (autodetecta creds)
- Deploy-SoloLeveling.ps1: deploy build-on-server verificado
- docs/runbooks/cloudflare-tunnel.md y autostart-coolify.md
- docs/casos/chatwoot-enterprise-patch.md (credenciales redactadas)
- docs/AGENTS-coolify-apps.md + issues y reportes
- deploy_skill/references/coolify-4.1.2-notes.md: hallazgos verificados (seccion 7)
- package.json para verify-online.mjs del coolify-deploy skill
- .gitignore: excluye .claude/ y .opencode/ (config local de herramientas)
This commit is contained in:
urieljareth
2026-07-18 11:31:31 -06:00
parent e7d1c33cd5
commit f587a8baa2
23 changed files with 2312 additions and 2 deletions
@@ -0,0 +1,143 @@
# ISSUE: cloudflare-tunnel · routing · websocket-tls-handshake
**ID:** `cloudflare-tunnel_routing_websocket-tls-handshake`
**Tags:** `cloudflare-tunnel` `routing` `websocket` `tls` `coolify` `terminal` `realtime` `6001` `6002`
**Severidad:** Alta — terminal de Coolify inoperativa, websockets de la UI rotos
**Fecha:** 2026-04-11
---
## Síntoma
- La terminal dentro de Coolify muestra `Terminal websocket connection lost`
- La UI de Coolify se comporta de forma inestable (reconexiones constantes)
- Los logs de `cloudflared` muestran:
```
tls: first record does not look like a TLS handshake
originService=https://192.168.0.117:6001
tls: first record does not look like a TLS handshake
originService=https://192.168.0.117:6002
```
---
## Causa Raíz
Las rutas del Cloudflare Tunnel para los puertos 6001 (realtime/websocket) y 6002 (terminal/websocket) están configuradas con `https://` como prefijo del servicio de origen.
Estos puertos **no hablan HTTPS** — solo HTTP plano. Cloudflared intenta establecer un handshake TLS con un servicio que responde en texto plano, causando el error.
---
## Dónde vive la configuración (importante)
El túnel está **gestionado desde el dashboard de Cloudflare** (remotely-managed),
no por el `config.yml` local. El `/etc/cloudflared/config.yml` dentro del contenedor
solo tiene el UUID del túnel, las credenciales y el comodín `*.urieljareth.org`:
```yaml
tunnel: 5f0e5c5b-a180-46f2-a090-44d151006a62
credentials-file: /etc/cloudflared/credentials.json
ingress:
- hostname: "*.urieljareth.org"
service: https://192.168.0.117:443
- service: http_status:404
```
**Las rutas de `coolify.urieljareth.org` (incluidas 6001/6002) NO están en ese
archivo — vienen del dashboard/API.** La firma de esto en los logs es la línea
`INF Updated to new configuration version=N`. Consecuencia práctica: **este problema
no se arregla editando archivos en el servidor**, sino en el dashboard (rápido) o
por la API de Cloudflare (control total — ver runbook).
---
## Configuración Correcta del Tunnel (coolify-tunnel)
El orden importa: Cloudflare evalúa las rutas de **arriba hacia abajo**. Las rutas específicas deben ir ANTES del comodín.
| # | Dominio | Ruta | Servicio | Protocolo |
|---|---------|------|---------|-----------|
| 1 | `coolify.dominio.com` | `/build/*` | `http://192.168.0.117:8000` | HTTP |
| 2 | `coolify.dominio.com` | `/app/*` | `http://192.168.0.117:6001` | HTTP ⚠️ |
| 3 | `coolify.dominio.com` | `/terminal/ws*` | `http://192.168.0.117:6002` | HTTP ⚠️ |
| 4 | `coolify.dominio.com` | `*` | `http://192.168.0.117:8000` | HTTP |
| 5 | `*.dominio.com` | `*` | `https://192.168.0.117:443` | HTTPS |
### Reglas Críticas
- Los puertos **6001 y 6002 deben usar `http://`**, nunca `https://`
- La ruta `/terminal/ws*` se escribe **sin barra final** (no `/terminal/ws/*`)
- El comodín `*.dominio.com` va **siempre al final**
- El comodín de Coolify (ruta 4) también usa `http://`
---
## Diagnóstico
Dentro del CT 102 (referencia):
```bash
# Ver errores actuales del tunnel
docker logs cloudflared --tail 30
# Confirmar configuración activa del tunnel
docker logs cloudflared --tail 50 | grep "Updated to new configuration"
```
Desde este repo (PowerShell, vía el path SSH habitual):
```powershell
# Errores recientes del túnel
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker logs cloudflared --tail 40"
# Configuración activa (toma el version=N más alto)
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker logs cloudflared --tail 60" |
Select-String "Updated to new configuration"
```
En la configuración activa (línea `INF Updated to new configuration version=N`), verificar que los servicios de 6001 y 6002 digan `http://` y no `https://`. El runbook
[runbooks/cloudflare-tunnel.md](runbooks/cloudflare-tunnel.md) tiene el procedimiento completo paso a paso.
---
## Solución
1. Ir a **Cloudflare Zero Trust → Networks → Tunnels → coolify-tunnel → Public Hostnames**
2. Editar la ruta `/app/*` → cambiar servicio de `https://IP:6001` a `http://IP:6001`
3. Editar la ruta `/terminal/ws*` → cambiar servicio de `https://IP:6002` a `http://IP:6002`
4. Guardar — Cloudflared aplica el cambio automáticamente sin reinicio
**Verificación:**
```bash
docker logs cloudflared --tail 10
```
Debe aparecer `INF Updated to new configuration version=<N>` con los servicios correctos en el JSON.
---
## Notas Adicionales
- Los errores previos en los logs son residuales de conexiones ya abiertas — desaparecen solos
- Si el orden de las rutas también está mal (comodín antes que las específicas), el tráfico de websocket nunca llega a las rutas correctas
- Este error es silencioso desde la perspectiva del usuario — solo ve que la terminal no conecta
---
## Reincidencia verificada (2026-06-03)
El problema volvió a presentarse (`Terminal websocket connection lost`). Hallazgos
de esa intervención:
- Config activa (`version=14`) tenía **`/app/*` (6001) y `/terminal/ws/` (6002) en `https://`**.
- Variante observada: la ruta de terminal estaba como **`/terminal/ws/`** (con barra
final, sin comodín), además del `https://` — ambos errores juntos.
- Solución aplicada desde el **dashboard**: 6001 y 6002 a `http://`, y path de terminal
corregido a `/terminal/ws*`. Cloudflared aplicó solo (`version=14 → 15 → 16`).
- Resultado: terminal de Coolify operativa. Los errores TLS residuales desaparecieron solos.
- Pendiente menor: `app/*` quedó sin barra inicial (`/app/*` sería lo ideal); no es
crítico porque el protocolo ya es correcto.
+232
View File
@@ -0,0 +1,232 @@
# Caso: Parche enterprise en Chatwoot (Coolify + LXC 102)
> Documentacion de caso verificada el 2026-06-16 desde esta maquina.
> Dominio: `https://chatwoot-c11xzy2tx2cdapm32f5b89vy.urieljareth.org`
## 0. Resumen ejecutivo
- **Problema:** Chatwoot se distribuye bajo una licencia que bloquea
funcionalidades enterprise desde la UI. Para una auto-hospedaje legitimo en
entorno de desarrollo propio, el parche conocido es actualizar 3 filas de
la tabla `public.installation_configs` en la base de datos Postgres.
- **Comando original (Bash, dentro del host Docker):**
```bash
docker exec -i "$(docker ps -q --filter "name=pgvector")" \
psql -U postgres -d chatwoot -c "
UPDATE public.installation_configs
SET serialized_value = '\"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: enterprise\n\"'
WHERE name = 'INSTALLATION_PRICING_PLAN';
UPDATE public.installation_configs
SET serialized_value = '\"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: 10000\n\"'
WHERE name = 'INSTALLATION_PRICING_PLAN_QUANTITY';
UPDATE public.installation_configs
SET serialized_value = '\"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: e04t63ee-5gg8-4b94-8914-ed8137a7d938\n\"'
WHERE name = 'INSTALLATION_IDENTIFIER';"
```
- **Criterio de exito:** psql imprime exactamente **3 lineas `UPDATE 1`**,
una por sentencia. Despues de esto todas las funcionalidades enterprise
quedan disponibles.
- **Restriccion critica:** una vez aplicado, **no pulsar el boton `Refresh`**
en `/super_admin/settings`, o el plan vuelve a Community.
- **Stack local:** Proxmox `192.168.0.200` -> LXC `102` (Coolify) -> Docker ->
contenedor Postgres `pgvector/pgvector:pg12` de Chatwoot.
## 1. Equivalencia entre el comando original y este entorno
| Capa | Comando original | Este entorno |
|---|---|---|
| Host | Docker daemon local | Proxmox VE `192.168.0.200` |
| Ejecucion Docker | `docker exec` directo | `pct exec 102 --` + `docker exec` |
| Usuario SSH | n/a | `root@192.168.0.200` con `~/.openclaw/workspace/proxmox_key_win` |
| Contenedor | filtro `name=pgvector` | nombre real: `postgres-c11xzy2tx2cdapm32f5b89vy` |
| DB user / db | `-U postgres -d chatwoot` | `-U <POSTGRES_USER> -d <POSTGRES_DB>` autodetectados (imagen `pgvector/pgvector:pg12` **no crea rol `postgres`**) |
El identificador `c11xzy2tx2cdapm32f5b89vy` es el UUID que Coolify asigna al
recurso; aparece como prefijo del FQDN y de todos los contenedores del stack
(chatwoot, sidekiq, redis, postgres).
## 2. Iteracion de descubrimiento (para reproducir en otro caso)
Estos son los pasos exactos que se siguieron para llegar al equivalente. Son
utiles como plantilla para **cualquier** aplicacion dentro de Coolify en
LXC 102.
### Iteracion 1: Validar SSH y encontrar el stack
```bash
# Listar contenedores en el LXC 102.
pct exec 102 -- docker ps --format "{{.Names}} | {{.Image}} | {{.Status}}"
```
Salida relevante (recortada):
```
sidekiq-c11xzy2tx2cdapm32f5b89vy | chatwoot/chatwoot:latest | Up 2 days
chatwoot-c11xzy2tx2cdapm32f5b89vy | chatwoot/chatwoot:latest | Up 2 days
redis-c11xzy2tx2cdapm32f5b89vy | redis:alpine | Up 2 days
postgres-c11xzy2tx2cdapm32f5b89vy | pgvector/pgvector:pg12 | Up 2 days
```
El contenedor buscado es `postgres-c11xzy2tx2cdapm32f5b89vy`, no el generico
`pgvector` del filtro original.
### Iteracion 2: Autodetectar credenciales reales
El filtro `-U postgres` del comando original **asume un rol por defecto que la
imagen `pgvector/pgvector:pg12` no crea**. Hay que leer el rol del `Config.Env`
del contenedor:
```bash
pct exec 102 -- docker inspect postgres-c11xzy2tx2cdapm32f5b89vy \
--format '{{range .Config.Env}}{{println .}}{{end}}' | grep -Ei POSTGRES
```
Salida tipica:
```
POSTGRES_DB=chatwoot
POSTGRES_USER=<slug-aleatorio-generado-por-coolify>
POSTGRES_PASSWORD=<secreto-no-se-commitea-ver-docker-inspect>
POSTGRES_HOST=postgres
```
`POSTGRES_DB=chatwoot` coincide con el `-d chatwoot` del comando original.
`POSTGRES_USER` es un slug aleatorio (Coolify lo genera por instalacion), asi
que **no se puede hardcodear**. Hay que inyectar `PGPASSWORD` y pasar el
`POSTGRES_USER` real a `psql -U`.
### Iteracion 3: Equivalente ejecutable
El comando final, ya alineado al original y verificado:
```bash
pct exec 102 -- docker exec -i postgres-c11xzy2tx2cdapm32f5b89vy \
env PGPASSWORD="$POSTGRES_PASSWORD" \
psql -U "$POSTGRES_USER" -d chatwoot -v ON_ERROR_STOP=1 \
< chatwoot-enterprise-patch.sql
```
donde `chatwoot-enterprise-patch.sql` contiene los 3 `UPDATE` identicos al
comando original.
## 3. Aplicacion automatica: `scripts/Apply-ChatwootEnterprisePatch.ps1`
El script implementa el equivalente exacto y valida los `UPDATE 1`.
### Que hace
1. Abre SSH contra `192.168.0.200` con `BatchMode=yes`,
`ConnectTimeout=15`, `StrictHostKeyChecking=no` y la clave del proyecto.
2. Sube 3 scripts `.sh` y un `.sql` al host Proxmox (no al LXC, para evitar
un `pct push` extra y problemas de ruta).
3. Autodetecta:
- contenedor Postgres de Chatwoot por el patron
`c11xzy2tx2cdapm32f5b89vy.*(pgvector|postgres|db)`.
- `POSTGRES_USER` / `POSTGRES_DB` / `POSTGRES_PASSWORD` desde
`docker inspect`.
4. Ejecuta el comando equivalente dentro del LXC, captura stdout y exit code.
5. Cuenta las lineas `^UPDATE\s+1\s*$`; **deben ser exactamente 3** o falla.
6. Corre un `SELECT` de verificacion.
7. Limpia los archivos temporales en el host Proxmox.
### Uso
```powershell
# Desde la raiz del repo.
.\scripts\Apply-ChatwootEnterprisePatch.ps1 -DryRun
.\scripts\Apply-ChatwootEnterprisePatch.ps1 -Container "postgres-c11xzy2tx2cdapm32f5b89vy"
```
Sin `-Container`, el script lo busca por el UUID del recurso Coolify.
Parametros disponibles:
- `-DryRun`: imprime SQL y scripts, no aplica cambios.
- `-Container <nombre>`: fuerza el contenedor destino.
- `-LxcId <id>`: por defecto `102` (Coolify).
- `-ProxmoxHost <host>`: por defecto `192.168.0.200`.
- `-SshKey <ruta>`: por defecto `C:\Users\Uriel Jareth\.openclaw\workspace\proxmox_key_win`.
### Salida esperada (exitosa)
```
[+] Contenedor: postgres-c11xzy2tx2cdapm32f5b89vy
[+] PG user=AQz03AGLKg9HaOZS db=chatwoot password=********************************
[*] Aplicando 3 UPDATE en postgres-c11xzy2tx2cdapm32f5b89vy ...
----- psql output -----
UPDATE 1
UPDATE 1
UPDATE 1
-----------------------
UPDATE 1 count = 3
[*] Verificando valores finales ...
name | serialized_value
------------------------------------+------------------------------------------------------------
INSTALLATION_IDENTIFIER | "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: e04t63ee-5gg8-4b94-8914-ed8137a7d938\n"
INSTALLATION_PRICING_PLAN | "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: enterprise\n"
INSTALLATION_PRICING_PLAN_QUANTITY | "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: 10000\n"
(3 rows)
[OK] Parche enterprise aplicado correctamente (3/3 UPDATE 1).
NO pulsar 'Refresh' en /super_admin/settings.
```
## 4. Errores tipicos durante la iteracion (lecciones)
Para evitar que un agente repita los mismos tropiezos:
1. **PowerShell y comillas en strings remotos**: dentro de un script PS, un
`RemoteCmd = "pct exec 102 -- sh -lc 'docker ps --format \"{{.Names}}\"'"`
rompe el parser o el shell remoto. **Regla:** cualquier comando no trivial
que se envia por SSH se sube como archivo `.sh` y se ejecuta con
`bash /tmp/archivo.sh`. Asi se elimina el problema de escaping.
2. **BOM UTF-8 al escribir archivos con `Set-Content` / `WriteAllText`**: el
BOM antepuesto a `#!/bin/bash` rompe el shebang en Linux.
**Regla:** usar `New-Object System.Text.UTF8Encoding($false)`.
3. **Shell por defecto en el host Proxmox**: si el usuario `root` no tiene
`bash` como login shell, los `pct exec ... -- bash -lc '...'` fallan con
`Syntax error: "(" unexpected`. **Regla:** dentro de los scripts remotos
usar `bash -lc` o `bash -c` siempre, no `sh`.
4. **Imagen `pgvector/pgvector:pg12` no crea rol `postgres`**: rompe el
comando original `-U postgres`. **Regla:** autodetectar
`POSTGRES_USER` / `POSTGRES_DB` / `POSTGRES_PASSWORD` desde
`docker inspect ... --format '{{range .Config.Env}}...{{end}}'`.
5. **Pipe por stdin vs `pct push`**: `pct push` deposita el archivo dentro
del filesystem del LXC, pero bash se ejecuta en el host y `<` resuelve en
el host, no en el LXC. **Regla:** subir el `.sql` al **host** Proxmox y
pipe con `pct exec 102 -- docker exec -i <ct> psql ... < /tmp/file.sql`.
6. **Validacion post-condicional**: en lugar de confiar en el exit code,
contar las lineas `UPDATE 1` para asegurar que se aplicaron las 3
sentencias. Si el contador no es 3, abortar antes de la verificacion.
## 5. Plantilla generica para otro stack en Coolify
Para cualquier otra aplicacion desplegada en Coolify con Postgres propio,
sustituir en el script:
- `Container`: contenedor Postgres de la app (suele llamarse
`postgres-<UUID-coolify>`).
- `pgUser`, `pgDb`, `pgPass`: lectura via `docker inspect ... Config.Env`.
- SQL: el de la app objetivo.
El resto del flujo (autodetectar, subir, ejecutar por SSH, contar lineas de
resultado, verificar con SELECT, limpiar) es identico.
## 6. Verificacion manual despues del parche
1. Entrar a `https://chatwoot-c11xzy2tx2cdapm32f5b89vy.urieljareth.org`.
2. Iniciar sesion con un super admin.
3. Confirmar visualmente que el plan ahora es **Enterprise** y la cantidad
**10000**.
4. Probar una opcion enterprise (por ejemplo, auditoria de equipos o
custom branding).
5. **NO pulsar el boton `Refresh` de `/super_admin/settings`** despues de
aplicar; si se pulsa, hay que volver a ejecutar este caso.
+405
View File
@@ -0,0 +1,405 @@
# Instrucciones para Agente IA — Cloudflare Tunnel + Coolify
**Entorno:** Proxmox VE → LXC CT 102 → Coolify (Docker) → Traefik + cloudflared
**Dominio:** urieljareth.org
**IP interna del servidor Coolify:** 192.168.0.117
---
## HERRAMIENTAS DISPONIBLES
El agente tiene acceso a:
1. **SSH a Proxmox** — para ejecutar comandos en el servidor
2. **API de Cloudflare** — para configurar DNS, Tunnel y Zero Trust sin usar la UI
Credenciales necesarias antes de comenzar:
- `CF_API_TOKEN` — token con permisos: `Zona → DNS → Editar` y `Zero Trust → Editar` para urieljareth.org
- `CF_ACCOUNT_ID` — ID de cuenta de Cloudflare (visible en el panel principal)
- `CF_ZONE_ID` — ID de zona DNS (visible en la sección DNS del dominio)
- `TUNNEL_TOKEN` — token del túnel cloudflared (se obtiene o regenera via API)
- Acceso SSH al host Proxmox o directamente al CT 102 (root@192.168.0.117)
---
## PASO 1 — Verificar conectividad básica desde el servidor
Conectarse por SSH al contenedor de Coolify y ejecutar:
```bash
# Verificar que TCP/443 saliente funciona
curl -v https://cloudflare.com 2>&1 | head -10
# Verificar que UDP/7844 NO está disponible (es común en redes domésticas)
nc -zv 198.41.192.37 7844
```
**Resultado esperado:**
- `curl` debe mostrar `Connected to cloudflare.com`
- `nc` debe mostrar `Connection timed out` (UDP bloqueado — esto es normal y se resuelve en el Paso 3)
---
## PASO 2 — Configurar DNS en Cloudflare via API
### 2.1 Verificar registros existentes
```bash
curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" | jq '.result[] | {name, type, content, proxied}'
```
### 2.2 Crear registro CNAME wildcard (obligatorio)
Reemplazar `<TUNNEL_ID>` con el ID del túnel (formato: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`):
```bash
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"type": "CNAME",
"name": "*",
"content": "<TUNNEL_ID>.cfargotunnel.com",
"proxied": true,
"ttl": 1
}'
```
### 2.3 Crear registro CNAME para coolify
```bash
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"type": "CNAME",
"name": "coolify",
"content": "<TUNNEL_ID>.cfargotunnel.com",
"proxied": true,
"ttl": 1
}'
```
> ⚠️ Ambos registros deben tener `proxied: true` (ícono naranja en la UI). Sin el wildcard `*`, los subdominios de las apps desplegadas devuelven `DNS_PROBE_FINISHED_NXDOMAIN`.
---
## PASO 3 — Obtener o regenerar el token del túnel
### 3.1 Listar túneles existentes
```bash
curl -s -X GET "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result[] | {id, name, status}'
```
### 3.2 Obtener el token del túnel existente
```bash
curl -s -X GET "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/<TUNNEL_ID>/token" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq -r '.result'
```
Guardar el resultado como `TUNNEL_TOKEN`.
### 3.3 (Alternativa) Crear un túnel nuevo
Solo si no existe un túnel previo:
```bash
curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"name": "coolify-tunnel",
"tunnel_secret": "'$(openssl rand -base64 32)'"
}' | jq '{id: .result.id, name: .result.name}'
```
Luego obtener el token con el endpoint del paso 3.2.
---
## PASO 4 — Configurar las rutas del túnel via API
Este es el paso más crítico. Las rutas se llaman "ingress rules" y deben configurarse en orden exacto.
### Orden obligatorio de rutas
| # | Hostname | Path | Servicio | Protocolo | Notas |
|---|----------|------|----------|-----------|-------|
| 1 | coolify.urieljareth.org | /build/* | 192.168.0.117:8000 | http | Build logs |
| 2 | coolify.urieljareth.org | /project/* | 192.168.0.117:8000 | http | **CRÍTICO: UI de apps** |
| 3 | coolify.urieljareth.org | /app/* | 192.168.0.117:6001 | http | Websocket realtime |
| 4 | coolify.urieljareth.org | /terminal/ws* | 192.168.0.117:6002 | http | Terminal websocket |
| 5 | coolify.urieljareth.org | * | 192.168.0.117:8000 | http | Catch-all Coolify UI |
| 6 | *.urieljareth.org | * | 192.168.0.117:443 | https | Apps via Traefik |
### Aplicar configuración via API
```bash
curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/<TUNNEL_ID>/configurations" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"config": {
"ingress": [
{
"hostname": "coolify.urieljareth.org",
"path": "/build/*",
"service": "http://192.168.0.117:8000"
},
{
"hostname": "coolify.urieljareth.org",
"path": "/project/*",
"service": "http://192.168.0.117:8000"
},
{
"hostname": "coolify.urieljareth.org",
"path": "/app/*",
"service": "http://192.168.0.117:6001"
},
{
"hostname": "coolify.urieljareth.org",
"path": "/terminal/ws*",
"service": "http://192.168.0.117:6002"
},
{
"hostname": "coolify.urieljareth.org",
"service": "http://192.168.0.117:8000"
},
{
"hostname": "*.urieljareth.org",
"service": "https://192.168.0.117:443",
"originRequest": {
"noTLSVerify": true
}
},
{
"service": "http_status:404"
}
]
}
}'
```
> ⚠️ El último elemento `http_status:404` es obligatorio. La API rechaza la configuración si no hay un catch-all final sin hostname.
### Verificar que las rutas se aplicaron correctamente
```bash
curl -s -X GET "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/<TUNNEL_ID>/configurations" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result.config.ingress[] | {hostname, path, service}'
```
---
## PASO 5 — Reglas críticas de las rutas (no negociables)
### Puertos 6001 y 6002 — siempre HTTP, nunca HTTPS
Los puertos `6001` (websocket realtime) y `6002` (terminal websocket) de Coolify NO hablan TLS internamente. Si se configura `https://` para estas rutas, cloudflared devuelve:
```
tls: first record does not look like a TLS handshake
```
Siempre usar `http://192.168.0.117:6001` y `http://192.168.0.117:6002`.
### Ruta /project/* — va ANTES de /app/*
Sin la regla `/project/*` en posición 2, cualquier URL con `/application/` en el path (como `/project/xxx/environment/xxx/application/xxx`) es capturada por `/app/*` y enviada al websocket del puerto 6001. La UI de Coolify carga en blanco.
### Ruta /terminal/ws* — sin barra al final
Escribir `/terminal/ws*` y NO `/terminal/ws/*`. La variante con barra no hace match con las conexiones del terminal.
### Ruta *.urieljareth.org — siempre al final con noTLSVerify
El wildcard debe ir después de todas las rutas específicas de `coolify.urieljareth.org`. Apunta a Traefik en puerto 443 con HTTPS y requiere `noTLSVerify: true` porque Traefik usa certificados autofirmados internamente.
---
## PASO 6 — Desplegar cloudflared en el servidor
Ejecutar via SSH en el contenedor de Coolify:
```bash
# Detener y eliminar el contenedor anterior si existe
docker stop cloudflared 2>/dev/null
docker rm cloudflared 2>/dev/null
# Crear el contenedor con protocolo HTTP2 forzado
docker run -d \
--name cloudflared \
--restart always \
-e TUNNEL_EDGE_PORT=443 \
cloudflare/cloudflared:latest \
tunnel --no-autoupdate --protocol http2 run --token $TUNNEL_TOKEN
```
### Por qué --protocol http2 es obligatorio
En redes domésticas, el puerto UDP/7844 que usa QUIC (el protocolo por defecto de cloudflared) está bloqueado por la mayoría de routers e ISPs. Sin `--protocol http2`, el túnel se conecta brevemente y cae en timeout cada ~5 minutos con el error:
```
failed to dial to edge with quic: timeout: no recent network activity
```
`-e TUNNEL_EDGE_PORT=443` fuerza la conexión TCP por el puerto 443, que siempre está abierto.
### Verificar que el túnel está activo
```bash
docker logs cloudflared --tail 20
```
Resultado correcto — deben aparecer 4 líneas como esta:
```
INF Registered tunnel connection connIndex=0 ... protocol=http2
INF Registered tunnel connection connIndex=1 ... protocol=http2
INF Registered tunnel connection connIndex=2 ... protocol=http2
INF Registered tunnel connection connIndex=3 ... protocol=http2
```
Si aparece `Provided Tunnel token is not valid`, el token se truncó. Repetir el Paso 3.2 para obtenerlo completo.
---
## PASO 7 — Configurar Traefik con DNS Challenge
Esto elimina la dependencia de validación HTTP para obtener certificados SSL. Es necesario cuando "Always Use HTTPS" está activo en Cloudflare (que redirige las validaciones HTTP de Let's Encrypt antes de que lleguen a Traefik).
### 7.1 Crear token de API para DNS Challenge
El token debe tener permisos: `Zona → DNS → Editar` para `urieljareth.org`.
```bash
# Verificar que el token tiene los permisos correctos
curl -s -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer $CF_DNS_API_TOKEN" | jq '{status: .result.status}'
```
### 7.2 Editar el docker-compose de Traefik via SSH
```bash
# Hacer backup primero
cp /data/coolify/proxy/docker-compose.yml /data/coolify/proxy/docker-compose.yml.bak
# Editar
nano /data/coolify/proxy/docker-compose.yml
```
Agregar dentro del servicio `traefik`, sección `environment`:
```yaml
environment:
- CF_DNS_API_TOKEN=<token-dns-api>
```
Reemplazar las líneas de `httpchallenge` en `command` por:
```yaml
- '--certificatesresolvers.letsencrypt.acme.dnschallenge=true'
- '--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare'
- '--certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53'
- '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json'
```
### 7.3 Limpiar certificados anteriores y reiniciar
```bash
echo '{}' > /data/coolify/proxy/acme.json
chmod 600 /data/coolify/proxy/acme.json
docker compose -f /data/coolify/proxy/docker-compose.yml up -d --force-recreate
```
> ⚠️ Si hay error 429 en los logs de coolify-proxy, Let's Encrypt aplicó rate limit por intentos fallidos previos. Esperar 1 hora antes de reintentar.
---
## PASO 8 — Verificación final
### Verificar estado de todos los servicios
```bash
# Contenedores corriendo
docker ps --format "table {{.Names}}\t{{.Status}}" | grep -E "coolify|cloudflared|traefik"
# Túnel con 4 conexiones activas
docker logs cloudflared --tail 5 | grep "Registered tunnel"
# Traefik sin errores de certificado
docker logs coolify-proxy --tail 50 2>&1 | grep -i "error\|certificate\|acme\|429"
```
### Verificar rutas desde afuera
```bash
# Coolify UI debe responder 200
curl -s -o /dev/null -w "%{http_code}" https://coolify.urieljareth.org
# Una app desplegada debe responder 200 o 301
curl -s -o /dev/null -w "%{http_code}" https://miapp.urieljareth.org
```
---
## DIAGNÓSTICO DE ERRORES COMUNES
| Error | Causa más probable | Verificar |
|-------|--------------------|-----------|
| `530` — Unregistered from Argo Tunnel | cloudflared caído | `docker logs cloudflared --tail 20` |
| `502 Bad Gateway` | Traefik sin cert SSL o puerto 3000 | Logs de coolify-proxy |
| Página en blanco al abrir app en Coolify | Falta regla `/project/*` | Verificar rutas del túnel |
| `tls: first record does not look like TLS` | https:// en puerto 6001 o 6002 | Corregir a http:// en ingress rules |
| `DNS_PROBE_FINISHED_NXDOMAIN` | Falta CNAME wildcard | Verificar registro `*` en DNS |
| Túnel cae cada 5 min | UDP/7844 bloqueado | Confirmar `--protocol http2` en el contenedor |
| `429` en logs de Traefik | Rate limit de Let's Encrypt | Esperar 1h, luego reiniciar Traefik |
| `Provided Tunnel token is not valid` | Token truncado | Obtener token completo via API (Paso 3.2) |
---
## FLUJO PARA DESPLEGAR UNA APP NUEVA
Por un bug en Coolify beta.472, la UI de apps individuales no carga tras la creación. Usar este flujo:
```bash
# 1. Obtener el ID y UUID de la app recién creada
docker exec coolify-db psql -U coolify -d coolify \
-c "SELECT id, uuid, name, fqdn FROM applications ORDER BY id DESC LIMIT 3;"
# 2. Actualizar dominio y puerto antes del deploy
docker exec coolify-db psql -U coolify -d coolify \
-c "UPDATE applications SET fqdn='https://miapp.urieljareth.org', ports_exposes='80' WHERE id=<ID>;"
# 3. Hacer deploy via API de Coolify
curl -X POST "http://localhost:8000/api/v1/deploy?uuid=<UUID>&force=false" \
-H "Authorization: Bearer <COOLIFY_API_TOKEN>"
# 4. Verificar que Traefik apunta al puerto correcto
grep "server.port" /data/coolify/applications/<UUID>/docker-compose.yaml
```
Si el puerto sigue siendo 3000 en lugar de 80:
```bash
sed -i 's/loadbalancer.server.port=3000/loadbalancer.server.port=80/g' \
/data/coolify/applications/<UUID>/docker-compose.yaml
docker compose -f /data/coolify/applications/<UUID>/docker-compose.yaml up -d --force-recreate
```
---
## ARCHIVOS DE REFERENCIA EN EL SERVIDOR
| Archivo | Ruta |
|---------|------|
| Docker-compose de Traefik | `/data/coolify/proxy/docker-compose.yml` |
| Backup de Traefik | `/data/coolify/proxy/docker-compose.yml.bak` |
| Certificados SSL | `/data/coolify/proxy/acme.json` |
| Docker-compose de cada app | `/data/coolify/applications/<UUID>/docker-compose.yaml` |
+88
View File
@@ -0,0 +1,88 @@
# Coolify cleanup report
> **Date:** 2026-06-29
> **Target:** self-hosted Coolify inside Proxmox LXC `102` (`192.168.0.200`)
> **Scope:** Docker images, volumes, networks, and stale `/data/coolify/applications/` directories.
## Executive summary
| Item | Before | After | Δ |
|------|-------:|------:|---|
| Docker images | 41 | 38 | **3** (≈ **4.7 GB**) |
| Orphan volumes | 2 | 0 | 2 |
| Orphan networks | 1 | 0 | 1 |
| Orphan `/data/coolify/applications/*` dirs | 4 | 0 | 4 |
| LXC disk usage | **71 GB / 99 GB (75%)** | **≈ 66 GB / 99 GB (≈ 67%)** | **≈ 5 GB freed** |
All deletions were verified against the Coolify API (`/api/v1/{services,applications,databases,projects}`) and the running container set: every removed item had **zero matching container and zero matching API resource**.
## Methodology
1. Enumerated Docker images, containers, volumes, networks from inside LXC 102.
2. Pulled the authoritative list of Coolify resources from the API (services, applications, databases, projects).
3. Cross-referenced every image `repo:tag` and every volume/network against the running container set and the API.
4. Cross-referenced `/data/coolify/applications/*` directory UUIDs against the same sets.
5. Tagged anything with no active consumer as orphan.
## Items removed
### Docker images (no container references)
| Image | ID | Size | Age | Reason |
|-------|----|-----:|-----|--------|
| `e6vie34d83iv5vw3eyzinl3s:ab190560...` | `ae3586b9690a` | 1.5 GB | 2 weeks | App `cotizadormain` deleted from Coolify |
| `e6vie34d83iv5vw3eyzinl3s:4ee87f1a...` | `d36f87274b9d` | 1.5 GB | 2 weeks | App `cotizadormain` deleted from Coolify |
| `u11ug3eizk9du3p2ch556ud4_app:0df05644...` | `0e831f095231` | 1.65 GB | 5 weeks | Older build of the still-running `station` app (new build `:7361f7ced...` retained) |
> `ghcr.io/coollabsio/coolify-helper:1.0.14` (581 MB) is **kept** — it's not referenced by any container, but it's used by the Coolify build pipeline. Deleting it could break future builds of apps with build packs. Re-evaluate on the next Coolify upgrade.
### Docker volumes (no container references)
| Volume | Reason |
|--------|--------|
| `ff38d8acc367314a2f26b6ca041819ce55b72e07b3b72c89d99bab2df171f62c` | No container uses it; left over from a deleted app |
| `m7d87ukrkgcag7g2ij9m7f1i_station-data` | App's UUID prefix (`m7d87...`) no longer present in any container, volume, network, or API resource |
> The other hash-named volumes (`2aa23d9d...` and `b76a6b81...`) **were checked** and **kept** — they are still mounted by `rabbitmq-du3iknyvy22vap767t9tnf9s` and `nuq-postgres-du3iknyvy22vap767t9tnf9s` respectively (the `du3iknyvy22vap767t9tnf9s` AI stack). Their names are auto-generated because that compose didn't declare explicit volume names.
### Docker networks (no container references)
| Network | Reason |
|---------|--------|
| `a7dwp54y5ohnu855x9rrlui2` | UUID prefix not present in any container, volume, or Coolify resource. Stale bridge network from a deleted app. |
### `/data/coolify/applications/*` directories (no matching Coolify API resource)
| UUID | Last deployment | App (per embedded compose labels) | Reason |
|------|-----------------|----------------------------------|--------|
| `dc1rvdq1wt4rx4ezw2bunhij` | 2026-04-05 | `rag-google:main` (project `rag`) | Deleted from Coolify UI; left a `.env`, `docker-compose.yaml`, `README.md` |
| `mi86mdsztulzydw0i3a6r2cs` | 2026-04-11 | `chatweb:main` | Deleted from Coolify UI |
| `ra20xgpxukp1z6tz0570risk` | 2026-04-10 | `mk-editor:main` | Deleted from Coolify UI |
| `e6vie34d83iv5vw3eyzinl3s` | 2026-06-11 | `cotizadormain` (project `ai-agency`) | Deleted from Coolify UI; the two 1.5 GB images were the only artifacts left behind |
> The remaining four application directories (`du3iknyvy22vap767t9tnf9s`, `pugr2d3moykf6q9gjvz015dg`, `u11ug3eizk9du3p2ch556ud4`, `vngcvnhbfqboov4nln88zc73`) were **kept** — they are still referenced by running containers, even though they don't appear in `/api/v1/applications` (Coolify's API only lists `service_type` resources; these are legacy "application" resources).
## Coolify settings adjusted
Inside `/data/coolify/sentinel/.../settings` (Coolify DB row) the server had `delete_unused_volumes: false` and `delete_unused_networks: false`. These two flags control whether Coolify's sentinel garbage-collects Docker resources on its daily cleanup cron. **Switched both to `true`** to prevent the same drift from recurring.
## Verification
```
docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 38 37 60.05GB 55.24GB (91%)
Containers 47 47 2.64GB 0B (0%)
Local Volumes 42 41 9.615GB 36.86kB (0%)
Build Cache 0 0 0B 0B
docker ps --format '{{.Names}}' | wc -l # 47 → 47 (unchanged)
```
All 47 containers that were running before the cleanup are still running.
## Caveats / follow-ups
- **`coolify-helper:1.0.14`** is left in place intentionally. Delete after upgrading Coolify to a version that ships a newer helper.
- **Disk pressure:** the LXC is at ~67% used after the cleanup. The big remaining consumers are image layers (≈ 60 GB total, ≈ 55 GB of "reclaimable" history baked into tags). If pressure rises further, consider expanding the LXC rootfs or enabling `docker image prune` on a schedule.
- **Coolify API ↔ filesystem drift:** the four legacy "application" directories still hold compose/env files for `du3iknyvy22vap767t9tnf9s`, `pugr2d3moykf6q9gjvz015dg`, `u11ug3eizk9du3p2ch556ud4`, `vngcvnhbfqboov4nln88zc73`. Coolify's API no longer lists them as `application` resources (they predate the service model). They are still functional, but cannot be managed from the Coolify UI. Plan to recreate them as `service_type` resources or migrate them.
+169
View File
@@ -0,0 +1,169 @@
# Issue: Despliegue de App Estática en Coolify — Página en Blanco y 502
**Fecha:** 2026-04-11
**Entorno:** Coolify v4.0.0-beta.472 — Proxmox LXC CT 102 — Cloudflare Tunnel
**Estado:** Resuelto ✅
---
## Síntomas
- Al crear una app desde GitHub y dar clic en **Continue**, la UI redirige a una URL como:
`coolify.urieljareth.org/project/{uuid}/environment/{uuid}/application/{uuid}`
y muestra **página en blanco** o **HTTP 404**.
- Al hacer clic en una app existente desde la lista de proyectos, también carga en blanco.
- El dominio de la app desplegada devuelve **502 Bad Gateway** desde Cloudflare.
---
## Causas Raíz (múltiples)
### 1. Regla del Cloudflare Tunnel capturaba rutas de la UI incorrectamente
La ruta `/app/*` del tunnel (usada para websockets del puerto 6001) hacía match parcial con `/application/...` porque Cloudflare evalúa prefijos. Cualquier URL con `/application/` en el path era enviada al puerto 6001 (websocket) en lugar del puerto 8000 (Coolify UI).
**Solución:** Agregar una regla explícita `/project/*` antes de `/app/*` para que las rutas de la UI de Coolify sean enrutadas correctamente al puerto 8000.
### 2. Certificados SSL fallaban por "Always Use HTTPS" en Cloudflare
Let's Encrypt valida dominios por HTTP (`/.well-known/acme-challenge/`). Con "Usar siempre HTTPS" activo en Cloudflare, esas solicitudes eran redirigidas a HTTPS antes de llegar a Traefik, causando error 502 en la validación y rate limit 429.
**Solución:** Configurar Traefik para usar **DNS Challenge via API de Cloudflare** en lugar de HTTP Challenge. Esto elimina la dependencia de validación HTTP para siempre.
### 3. Puerto incorrecto en etiquetas de Traefik
Coolify asignaba puerto 3000 por defecto a apps nuevas. Las apps nginx escuchan en puerto 80. Traefik enviaba tráfico al 3000 y obtenía 502.
**Solución:** Actualizar `ports_exposes` en la base de datos a `80` antes del primer deploy.
### 4. Dominio UUID autogenerado
Coolify genera un UUID como subdominio cuando no detecta el dominio durante la creación. Esos subdominios UUID no tienen registro DNS y no funcionan.
**Solución:** Actualizar `fqdn` en la base de datos inmediatamente después de crear la app.
---
## Solución Permanente
### Paso 1 — Configurar DNS Challenge en Traefik
Crear token de API en Cloudflare con permisos `Zona → DNS → Editar` para `urieljareth.org`.
Editar `/data/coolify/proxy/docker-compose.yml`:
```yaml
environment:
- CF_DNS_API_TOKEN=<token>
command:
# Reemplazar httpchallenge por dnschallenge:
- '--certificatesresolvers.letsencrypt.acme.dnschallenge=true'
- '--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare'
- '--certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53'
- '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json'
```
Limpiar certificados anteriores y reiniciar:
```bash
echo '{}' > /data/coolify/proxy/acme.json && chmod 600 /data/coolify/proxy/acme.json
docker compose -f /data/coolify/proxy/docker-compose.yml up -d --force-recreate
```
### Paso 2 — Configurar rutas del Cloudflare Tunnel
Orden correcto en **Zero Trust → Networks → Tunnels → coolify-tunnel → Rutas de aplicación publicada:**
| # | Dominio | Ruta | Servicio | Notas |
|---|---------|------|---------|-------|
| 1 | coolify.urieljareth.org | /build/* | http://192.168.0.117:8000 | Build logs |
| 2 | coolify.urieljareth.org | /project/* | http://192.168.0.117:8000 | **Crítico: UI de apps** |
| 3 | coolify.urieljareth.org | /app/* | http://192.168.0.117:6001 | Websocket realtime |
| 4 | coolify.urieljareth.org | /terminal/ws* | http://192.168.0.117:6002 | Terminal websocket |
| 5 | coolify.urieljareth.org | * | http://192.168.0.117:8000 | Catch-all Coolify |
| 6 | *.urieljareth.org | * | https://192.168.0.117:443 | Apps via Traefik |
> ⚠️ La regla `/project/*` en posición 2 es crítica. Sin ella, `/application/...` es capturada por `/app/*` y la UI falla.
---
## Flujo de Trabajo para Nuevas Apps (mientras dure el bug de UI)
Debido a un bug de Coolify beta.472, la UI de apps individuales no carga correctamente tras la creación. El flujo correcto es:
**1. Crear la app en la UI** (seleccionar repo, Build Pack: Static, continuar)
**2. Obtener el ID de la app recién creada:**
```bash
docker exec coolify-db psql -U coolify -d coolify \
-c "SELECT id, name, fqdn FROM applications ORDER BY id DESC LIMIT 3;"
```
**3. Actualizar dominio y puerto antes del deploy:**
```bash
docker exec coolify-db psql -U coolify -d coolify \
-c "UPDATE applications SET fqdn='https://miapp.urieljareth.org', ports_exposes='80' WHERE id=<ID>;"
```
**4. Hacer deploy via API:**
```bash
curl -X POST "http://localhost:8000/api/v1/deploy?uuid=<UUID>&force=false" \
-H "Authorization: Bearer <TOKEN>"
```
**5. Verificar que el puerto en el docker-compose generado sea 80:**
```bash
grep "server.port" /data/coolify/applications/<UUID>/docker-compose.yaml
```
Si sigue mostrando 3000, corregir manualmente:
```bash
sed -i 's/loadbalancer.server.port=3000/loadbalancer.server.port=80/g' \
/data/coolify/applications/<UUID>/docker-compose.yaml
sed -i 's/upstreams 3000/upstreams 80/g' \
/data/coolify/applications/<UUID>/docker-compose.yaml
docker compose -f /data/coolify/applications/<UUID>/docker-compose.yaml up -d --force-recreate
```
---
## Comandos de Diagnóstico
```bash
# Ver estado de todos los contenedores Coolify
docker ps --format "table {{.Names}}\t{{.Status}}" | grep coolify
# Ver logs de Traefik (certificados, errores)
docker logs coolify-proxy --tail 50 2>&1 | grep -i "error\|certificate\|acme"
# Ver logs del tunnel
docker logs cloudflared --tail 30
# Verificar etiquetas Traefik de una app
docker inspect <nombre-contenedor> | grep "server.port\|rule\|certresolver"
# Listar apps en la base de datos
docker exec coolify-db psql -U coolify -d coolify \
-c "SELECT id, name, fqdn, ports_exposes FROM applications;"
```
---
## Lecciones Aprendidas
| Problema | Causa | Solución |
|----------|-------|---------|
| UI página en blanco al abrir app | `/app/*` captura `/application/...` en Cloudflare | Agregar regla `/project/*` antes de `/app/*` |
| 502 en dominio de app | Traefik sin certificado SSL | DNS Challenge con token Cloudflare |
| 502 en dominio de app | Puerto 3000 en lugar de 80 | Actualizar `ports_exposes` en DB |
| UUID como subdominio | Dominio no configurado al crear | Actualizar `fqdn` en DB antes del deploy |
| Rate limit 429 de Let's Encrypt | Intentos fallidos de validación HTTP | DNS Challenge elimina la validación HTTP |
---
## Configuración de Referencia
**Archivo:** `/data/coolify/proxy/docker-compose.yml`
**Backup:** `/data/coolify/proxy/docker-compose.yml.bak`
**Certificados:** `/data/coolify/proxy/acme.json`
**Apps:** `/data/coolify/applications/<UUID>/docker-compose.yaml`
+142
View File
@@ -0,0 +1,142 @@
# Runbook: Auto-arranque de Coolify tras apagón
Garantiza que, después de un corte de luz y al volver a encender el servidor
Proxmox (`192.168.0.200`, nodo `thinkcentre`), **el LXC 102 (Coolify) y su túnel
de Cloudflare arranquen solos**, sin intervención manual.
> **Causa raíz que resolvió esto (2026-07-08):** el LXC 102 **no tenía la marca
> `onboot`** (por defecto `0`), así que el host encendía pero el contenedor se
> quedaba apagado y, con él, todo el stack. Todo lo demás *dentro* del LXC ya
> estaba bien encadenado (Docker `enabled`, contenedores con `restart=always` /
> `unless-stopped`, `cloudflared.service` `enabled`).
---
## Arquitectura de la solución (dos capas)
1. **Base nativa de Proxmox — `onboot`.** El LXC 102 arranca solo al encender el
host, gestionado por `pve-guests.service` (que ya está `enabled`):
```
pct set 102 --onboot 1 --startup order=1,up=30
```
2. **Guardián auto-reparable — `coolify-autostart.service`.** Un servicio systemd
*oneshot* en el host que corre en **cada arranque**, **después** de
`pve-guests.service`, y garantiza el stack completo:
- `/usr/local/bin/coolify-autostart.sh` — script idempotente.
- `/etc/systemd/system/coolify-autostart.service` — unit (`WantedBy=multi-user.target`).
- Log: `/var/log/coolify-autostart.log`.
En cada boot el guardián:
1. Verifica que el LXC 102 esté `running` (lo arranca si no).
2. Espera a que Docker responda dentro del LXC (hasta 180 s).
3. Se asegura de que estén arriba: `coolify-db`, `coolify-redis`,
`coolify-realtime`, `coolify`, `coolify-proxy`, `cloudflared` (los inicia si
alguno no está).
4. Levanta el `cloudflared.service` de systemd dentro del LXC (segundo
conector al mismo túnel).
Las dos capas son complementarias: `onboot` hace el trabajo normal; el guardián
es una red de seguridad que además **auto-repara** (p. ej. un contenedor con
`restart=no`) y deja **log** de lo ocurrido tras el apagón.
Los archivos fuente viven en el repo en [scripts/host/](../../scripts/host/) y se
instalan con [scripts/Install-CoolifyAutostart.ps1](../../scripts/Install-CoolifyAutostart.ps1).
---
## Instalar / reinstalar
```powershell
# Instala onboot + guardián y lo prueba una vez (idempotente y seguro)
.\scripts\Install-CoolifyAutostart.ps1 -RunNow
```
Opciones:
- `-VerifyOnly` — solo reporta estado (onboot, unit, log). No cambia nada.
- `-SkipOnboot` — instala solo el guardián, sin tocar la marca `onboot`.
- `-RunNow` — tras instalar, dispara el guardián una vez y muestra el log.
- `-Uninstall` — quita el guardián (deja `onboot` intacto).
El instalador empuja los archivos por SSH en base64 (normaliza CRLF→LF), inyecta
el `COOLIFY_LXC` correcto en el unit, activa `onboot`, habilita el servicio y
verifica.
---
## Verificar estado (solo lectura)
```powershell
.\scripts\Install-CoolifyAutostart.ps1 -VerifyOnly
```
Estado sano esperado:
```
onboot: 1
startup: order=1,up=30
guardian enabled: enabled
script executable: yes
```
Log del último arranque:
```powershell
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "tail -n 25 /var/log/coolify-autostart.log"
```
---
## Probar sin apagar producción
**No reinicies el host** solo para probar (tumbaría todos los servicios). En su
lugar, dispara el guardián manualmente — solo *arranca* cosas, nunca las detiene:
```powershell
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "systemctl start coolify-autostart.service; systemctl is-active coolify-autostart.service; tail -n 25 /var/log/coolify-autostart.log"
```
Para validar que el unit está bien formado y en el orden correcto:
```powershell
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "systemd-analyze verify /etc/systemd/system/coolify-autostart.service && systemctl show coolify-autostart.service -p After -p WantedBy"
```
`After` debe incluir `pve-guests.service`; `WantedBy` debe ser `multi-user.target`.
---
## Rollback
```powershell
# Quitar el guardián (deja onboot como esté)
.\scripts\Install-CoolifyAutostart.ps1 -Uninstall
# Y si además quieres que el LXC deje de arrancar solo:
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct set 102 --onboot 0"
```
---
## Notas
- El guardián es **idempotente**: correrlo con todo ya arriba no hace nada
destructivo, solo lo confirma en el log.
- Hay **dos** conectores `cloudflared` al mismo túnel
(`5f0e5c5b-a180-46f2-a090-44d151006a62`): el contenedor Docker `cloudflared`
(`restart=unless-stopped`) y el `cloudflared.service` de systemd dentro del LXC
(`enabled`). Ambos arrancan solos; es redundante pero inofensivo. Si algún día
se consolida en uno, ajustar el paso 4 del script y esta nota. Ver
[cloudflare-tunnel.md](cloudflare-tunnel.md).
- `coolify-sentinel` tiene `restart=no` (monitor no crítico); Coolify lo recrea,
por eso no está en la lista de contenedores core del guardián.
---
**Verificado:** 2026-07-08 — instalado y probado en vivo. `onboot=1`,
`coolify-autostart.service` `enabled`, guardián ejecutado con éxito (LXC arriba,
Docker listo, 6 contenedores core + túnel `running`). `systemd-analyze verify` sin
warnings.
+197
View File
@@ -0,0 +1,197 @@
# Runbook: Cloudflare Tunnel (coolify-tunnel)
Procedimiento para diagnosticar y corregir el túnel de Cloudflare que expone
Coolify (`coolify.urieljareth.org`) y las apps (`*.urieljareth.org`).
> **Dato clave:** el túnel está **gestionado desde el dashboard de Cloudflare**
> (remotely-managed), no por el `config.yml` local. El archivo
> `/etc/cloudflared/config.yml` dentro del contenedor solo contiene el UUID del
> túnel, las credenciales y el comodín `*.urieljareth.org`. **Todas las rutas de
> `coolify.urieljareth.org` (incluidas 6001/6002) viven en el dashboard / la API
> de Cloudflare.** Por eso un cambio de rutas NO se arregla editando archivos en
> el servidor — se arregla en el dashboard (rápido) o por la API (control total).
> La firma de esto en los logs es la línea `INF Updated to new configuration version=N`.
Síntomas típicos que llevan aquí:
- En la terminal de Coolify: `Terminal websocket connection lost. Reconnecting...`
- UI de Coolify inestable / reconexiones constantes / página en blanco al abrir una app
- En logs de `cloudflared`: `tls: first record does not look like a TLS handshake`
- Dominio de app con `502 Bad Gateway` o `530`
Documentos relacionados:
[ISSUE_cloudflare-tunnel_routing_websocket-tls-handshake.md](../ISSUE_cloudflare-tunnel_routing_websocket-tls-handshake.md) ·
[cloudflare-tunnel-coolify-agent_1.md](../cloudflare-tunnel-coolify-agent_1.md) ·
[issue-coolify-static-app-deploy.md](../issue-coolify-static-app-deploy.md)
---
## 1. Cargar entorno y verificar conexión
```powershell
# Secretos privados (gitignored) — necesario solo para llamadas a API
. .\.env.local.ps1
# Verifica config + SSH + Docker + API (sale 1 si algo falla)
.\scripts\Test-ProxmoxConnection.ps1
```
El SSH funciona con los defaults aunque no cargues `.env.local.ps1`.
---
## 2. Diagnóstico rápido (solo lectura)
### 2.1 Logs recientes del túnel
```powershell
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker logs cloudflared --tail 40"
```
Buscar:
- `Registered tunnel connection ... protocol=http2` (x4) → túnel arriba y sano
- `tls: first record does not look like a TLS handshake` → ruta 6001/6002 en `https://`
- `Lost connection with the edge` / `failed to dial to edge with quic` → ver nota de `--protocol http2`
### 2.2 Ver la configuración ACTIVA del túnel (lo que realmente aplica)
```powershell
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker logs cloudflared --tail 60" |
Select-String "Updated to new configuration"
```
Toma la línea con el `version=N` **más alto** (es la config vigente) y revisa el
JSON `ingress`. **Los puertos 6001 y 6002 deben decir `http://`, nunca `https://`.**
Cada vez que se guarda un cambio en el dashboard/API, `version` se incrementa.
### 2.3 Ver el `config.yml` local (opcional)
La imagen `cloudflared` es **distroless** (no tiene `cat`/`sh` propios para leer el
archivo directo), así que se copia con `docker cp` y se lee desde el LXC:
```powershell
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- sh -c 'docker cp cloudflared:/etc/cloudflared/config.yml /tmp/cf-config.yml && cat /tmp/cf-config.yml; rm -f /tmp/cf-config.yml'"
```
Esperado: solo `tunnel:`, `credentials-file:` y el comodín. Si aquí NO aparecen
las rutas de `coolify.urieljareth.org`, confirma que el túnel es dashboard-managed.
> Nunca leas `credentials.json` ni lo imprimas — contiene el secreto del túnel.
---
## 3. Reglas de oro de las rutas (ingress)
Cloudflare evalúa las rutas **de arriba hacia abajo**; las específicas van ANTES
del comodín. Esta es la configuración correcta (orden exacto):
| # | Hostname | Path | Servicio | Protocolo | Notas |
|---|----------|------|----------|-----------|-------|
| 1 | coolify.urieljareth.org | `/build/*` | `192.168.0.117:8000` | **http** | Build logs |
| 2 | coolify.urieljareth.org | `/project/*` | `192.168.0.117:8000` | **http** | **Crítico: UI de apps** |
| 3 | coolify.urieljareth.org | `/app/*` | `192.168.0.117:6001` | **http** ⚠️ | Websocket realtime |
| 4 | coolify.urieljareth.org | `/terminal/ws*` | `192.168.0.117:6002` | **http** ⚠️ | Terminal websocket |
| 5 | coolify.urieljareth.org | `*` | `192.168.0.117:8000` | **http** | Catch-all Coolify UI |
| 6 | `*.urieljareth.org` | `*` | `192.168.0.117:443` | **https** | Apps via Traefik (`noTLSVerify: true`) |
Reglas no negociables:
- **6001 y 6002 → `http://` siempre.** Esos puertos no hablan TLS; con `https://`
aparece `tls: first record does not look like a TLS handshake` y la terminal/realtime no conecta.
- **`/project/*` va ANTES de `/app/*`.** Si no, las URL con `/application/...` las
captura `/app/*` (puerto 6001) y la UI carga en blanco.
- **`/terminal/ws*` sin barra final** (no `/terminal/ws/` ni `/terminal/ws/*`).
- **`*.urieljareth.org` siempre al final**, con `noTLSVerify: true` (Traefik usa cert autofirmado interno).
---
## 4. Solución vía Dashboard (rápida, sin token)
1. Entrar a **Cloudflare Zero Trust → Networks → Tunnels → `coolify-tunnel` → Public Hostnames**.
2. Corregir cada ruta que esté mal según la tabla del paso 3. Lo más común:
- `/app/*` (6001): Service `HTTPS`**`HTTP`**.
- `/terminal/ws*` (6002): Service `HTTPS`**`HTTP`**; path sin barra final.
3. Guardar. **Cloudflared aplica el cambio solo, sin reinicio** (verás un nuevo `version=N`).
---
## 5. Solución vía API de Cloudflare (control total, requiere token)
Para gestionar el túnel sin tocar el dashboard (automatizable). Requiere un token
y los IDs cargados en `.env.local.ps1` (ver [Set-ProxmoxEnv.example.ps1](../../scripts/Set-ProxmoxEnv.example.ps1)):
```powershell
$env:CLOUDFLARE_API_TOKEN = "<token con Account:Cloudflare Tunnel:Edit + Zone:DNS:Edit>"
$env:CLOUDFLARE_ACCOUNT_ID = "<account id>"
$env:CLOUDFLARE_ZONE_ID = "<zone id de urieljareth.org>"
$env:CLOUDFLARE_TUNNEL_ID = "5f0e5c5b-a180-46f2-a090-44d151006a62"
```
Wrapper PowerShell: [scripts/Invoke-CloudflareApi.ps1](../../scripts/Invoke-CloudflareApi.ps1)
(mismo patrón que `Invoke-CoolifyApi.ps1`: Bearer token, `Invoke-RestMethod`).
```powershell
. .\.env.local.ps1
# Verificar que el token es válido y sus permisos
.\scripts\Invoke-CloudflareApi.ps1 -Path "/user/tokens/verify"
# Leer la configuración (ingress) actual del túnel
.\scripts\Invoke-CloudflareApi.ps1 `
-Path "/accounts/$($env:CLOUDFLARE_ACCOUNT_ID)/cfd_tunnel/$($env:CLOUDFLARE_TUNNEL_ID)/configurations"
# Aplicar la configuración corregida (PUT) — CONFIRMAR antes de ejecutar
$body = @{
config = @{
ingress = @(
@{ hostname = "coolify.urieljareth.org"; path = "/build/*"; service = "http://192.168.0.117:8000" }
@{ hostname = "coolify.urieljareth.org"; path = "/project/*"; service = "http://192.168.0.117:8000" }
@{ hostname = "coolify.urieljareth.org"; path = "/app/*"; service = "http://192.168.0.117:6001" }
@{ hostname = "coolify.urieljareth.org"; path = "/terminal/ws*"; service = "http://192.168.0.117:6002" }
@{ hostname = "coolify.urieljareth.org"; service = "http://192.168.0.117:8000" }
@{ hostname = "*.urieljareth.org"; service = "https://192.168.0.117:443"; originRequest = @{ noTLSVerify = $true } }
@{ service = "http_status:404" }
)
}
} | ConvertTo-Json -Depth 10
.\scripts\Invoke-CloudflareApi.ps1 -Method PUT `
-Path "/accounts/$($env:CLOUDFLARE_ACCOUNT_ID)/cfd_tunnel/$($env:CLOUDFLARE_TUNNEL_ID)/configurations" `
-BodyJson $body
```
> ⚠️ El último elemento `http_status:404` (sin hostname) es obligatorio o la API
> rechaza la configuración. La referencia completa de endpoints (DNS, crear túnel,
> obtener token) está en [cloudflare-tunnel-coolify-agent_1.md](../cloudflare-tunnel-coolify-agent_1.md).
**Cualquier `PUT`/`POST` a Cloudflare es un cambio de estado → confirmar con el usuario y
capturar el estado actual (paso 2.2) antes de aplicar, para tener rollback.**
---
## 6. Verificación
```powershell
# Debe aparecer un version=N mayor con 6001/6002 ya en http://
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker logs cloudflared --tail 10"
```
Luego en el navegador: recargar la UI de Coolify con **Ctrl+Shift+R** y abrir una
terminal en cualquier contenedor. Debe conectar sin `Terminal websocket connection lost`.
---
## Notas
- Los errores `tls: first record does not look like a TLS handshake` previos al
cambio son **residuales** de conexiones ya abiertas; desaparecen solos.
- Si el túnel se cae cada ~5 min con `failed to dial to edge with quic: timeout`,
el contenedor debe correr con `--protocol http2` (UDP/7844 suele estar bloqueado
en redes domésticas). Ver PASO 6 de [cloudflare-tunnel-coolify-agent_1.md](../cloudflare-tunnel-coolify-agent_1.md).
- `cloudflared` es distroless: para leer archivos internos usa `docker cp`, no `cat` directo.
---
**Verificado:** 2026-06-03 — corregidas rutas `/app/*` (6001) y `/terminal/ws*` (6002)
de `https://` a `http://` desde el dashboard; terminal de Coolify operativa
(config `version=16`).
+46
View File
@@ -91,6 +91,52 @@ Resultado verificado tras recuperacion:
- container Nextcloud: `running healthy`
- redirects HTTPS probados sin `Location: http://...`
## Si el contenedor esta healthy pero la UI sale vacia
Verificar primero desde fuera:
```powershell
curl.exe -k -sS --max-time 30 https://nextcloudsuite.urieljareth.org/status.php
curl.exe -k -sS -o NUL -w "status=%{http_code} total=%{time_total} starttransfer=%{time_starttransfer}`n" --max-time 30 https://nextcloudsuite.urieljareth.org/login
```
Verificar estado interno:
```powershell
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec nextcloud-hdcdpkm0jko3qqvn5683ercc php /app/www/public/occ status"
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec nextcloud-hdcdpkm0jko3qqvn5683ercc php /app/www/public/occ user:list"
```
Si el usuario existe pero no aparecen archivos, reindexar solo ese usuario:
```powershell
.\scripts\Invoke-ProxmoxSsh.ps1 -Command "pct exec 102 -- docker exec nextcloud-hdcdpkm0jko3qqvn5683ercc php /app/www/public/occ files:scan --path='urieljareth/files'"
```
Resultado verificado el 2026-06-10:
- `status.php`: `installed=true`, `maintenance=false`
- `/login`: `200 OK`, ~0.4s
- `urieljareth/files`: 143 archivos, 151 MB
- `files:scan`: 143 archivos, 0 errores
## PHP-FPM saturado
Los logs han mostrado:
```text
server reached pm.max_children setting (5), consider raising it
```
El override persistente esta en:
```text
/config/php/www2.conf
```
Ese cambio requiere recargar/reiniciar solo el contenedor Nextcloud para aplicar.
No requiere reiniciar `coolify-proxy`, Cloudflare ni otros proyectos.
## Pendiente tras reset de volumenes
Si el volumen de configuracion/base de datos fue eliminado, Nextcloud queda como