# Caso: Parche enterprise en Chatwoot (Coolify + LXC 102) > Documentacion de caso verificada el 2026-06-16 desde esta maquina. > Dominio: **`https://chat.urieljareth.org`** (corregido el 2026-07-24; el FQDN > `chatwoot-c11xzy2tx2cdapm32f5b89vy.urieljareth.org` que decia antes ya no aplica). > **Leer antes de usar este caso (revision 2026-07-24):** > > 1. **El parche caduca solo en <= 24 h.** No hace falta actualizar para > perderlo: `Internal::CheckNewVersionsJob` hace ping diario a > `hub.2.chatwoot.com` y reescribe el plan con lo que responda el hub. Con el > identifier actual la ventana es todos los dias a las **16:16 UTC**. > 2. **Los 3 `UPDATE` de este caso no alcanzan** si el plan ya paso por > `community`: `Internal::ReconcilePlanConfigService` apago los 9 feature > flags premium en `accounts.feature_flags` de cada cuenta. Hay que correr > `Apply-ChatwootEnterprisePatch.ps1 -ReenableAccountFeatures`. > > Causa raiz completa, plan de actualizacion y fix durable: > [docs/runbooks/chatwoot-update.md](../runbooks/chatwoot-update.md). ## 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 `keys/proxmox_ed25519` | | 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: filtra `docker ps` por el uuid del servicio y luego por `(pgvector|postgres|db)`. Son **dos greps encadenados** a proposito — Coolify nombra los contenedores `-` (`postgres-c11xzy...`), asi que el patron unico `.*postgres` que tenia antes no casaba nunca y la autodeteccion fallaba siempre (corregido el 2026-07-24). - `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. Con `-ReenableAccountFeatures`: reactiva los 9 feature flags premium en todas las cuentas via `rails runner` y falla si queda alguno pendiente. 8. Limpia los archivos temporales en el host Proxmox. En el `-DryRun` la `PGPASSWORD` sale enmascarada (antes se imprimia en claro). ### Uso ```powershell # Desde la raiz del repo. .\scripts\Apply-ChatwootEnterprisePatch.ps1 -DryRun -ReenableAccountFeatures .\scripts\Apply-ChatwootEnterprisePatch.ps1 -ReenableAccountFeatures .\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. - `-ReenableAccountFeatures`: ademas de los 3 `UPDATE`, reactiva via `rails runner` los 9 feature flags premium en **todas** las cuentas y verifica que no quede ninguno pendiente. **Necesario siempre que el plan venga de `community`** (ver el aviso al inicio de este documento). - `-Container `: fuerza el contenedor Postgres destino. - `-AppContainer `: contenedor de la app Rails, por defecto `chatwoot-` (solo lo usa `-ReenableAccountFeatures`). - `-ServiceUuid `: uuid del servicio en Coolify, por defecto `c11xzy2tx2cdapm32f5b89vy`. De aqui se derivan los nombres de contenedor. - `-LxcId `: por defecto `102` (Coolify). - `-ProxmoxHost `: por defecto `192.168.0.200`. - `-SshKey `: por defecto `keys\proxmox_ed25519`. ### 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 Automatica primero: ```powershell .\scripts\Get-ChatwootLicenseStatus.ps1 -Deep ``` Luego a mano: 1. Entrar a `https://chat.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.