# 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 -d ` 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= POSTGRES_PASSWORD= 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 `: fuerza el contenedor destino. - `-LxcId `: por defecto `102` (Coolify). - `-ProxmoxHost `: por defecto `192.168.0.200`. - `-SshKey `: 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 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-`). - `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.