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,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.
|
||||
@@ -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.
|
||||
@@ -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` |
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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