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:
@@ -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.
|
||||
@@ -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`).
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user