@@ -0,0 +1,580 @@
# Runbook: actualizar Chatwoot en Coolify sin perder la edicion enterprise
> **Ejecutado end-to-end el 2026-07-24.** Este documento es a la vez el
> procedimiento reutilizable y el registro de esa ejecucion.
> Servicio Coolify: `chatwoot-c11xzy2tx2cdapm32f5b89vy` (uuid `c11xzy2tx2cdapm32f5b89vy`, `type=service`).
> FQDN real: **`https://chat.urieljareth.org`** (el FQDN que aparece en
> [docs/casos/chatwoot-enterprise-patch.md](../casos/chatwoot-enterprise-patch.md)
> quedo obsoleto).
> Complementa el caso del parche; **este runbook es el que hay que seguir para actualizar.**
## 0. Resultado de la ejecucion del 2026-07-24
| Dato | Antes | Despues |
|---|---|---|
| Version | 4.16.0 | **4.16.1 ** |
| Tag de imagen | `chatwoot/chatwoot:latest` (sin pin) | * * `chatwoot/chatwoot:v4.16.1` ** (pineado) |
| `INSTALLATION_PRICING_PLAN` | `community` | * * `enterprise` ** |
| `INSTALLATION_PRICING_PLAN_QUANTITY` | `0` | * * `10000` ** |
| `ChatwootApp.self_hosted_enterprise?` | `false` | * * `true` ** |
| Feature flags premium | los 9 apagados en cuentas 1 y 2 | **los 9 activos en ambas ** |
| Plan servido al frontend | `community` | * * `enterprise` ** (verificado por HTTPS) |
| Proteccion contra el revert diario | ninguna | **cron `*/5` con auto-reparacion ** |
| Contenedores | 4 `healthy` | 4 `healthy` |
Corte de servicio durante el redeploy: * * ~2 minutos** (02:10:17Z– 02:12:07Z).
Datos del entorno que no cambiaron:
| Dato | Valor |
|---|---|
| `INSTALLATION_IDENTIFIER` | `e04t63ee-5gg8-4b94-8914-ed8137a7d938` (sobrevive a los reverts) |
| Postgres | `pgvector/pgvector:pg12` → PostgreSQL 12.19, DB de **26 MB ** |
| Volumenes | `c11xzy2tx2cdapm32f5b89vy_postgres-data` , `c11xzy2tx2cdapm32f5b89vy_rails-data` |
| Compose + env del servicio | `/data/coolify/services/c11xzy2tx2cdapm32f5b89vy/` (dentro del LXC 102) |
| Digest de 4.16.0 (para rollback) | `sha256:8fdd8adde2093fb270fc69eaeefaf6faca416e65cba0b58041159c654b81d175` |
| Digest de 4.16.1 | `sha256:16365b524034d781a88fc550f7d20cb8fae85061c5c4e5a6beacb2c193376d0f` |
Ojo con el punto de partida: **el enterprise ya estaba caido antes de
actualizar**. La actualizacion no lo tiro; ya estaba tirado (ver seccion 1).
### Pre-flight que hizo la actualizacion de bajo riesgo
`v4.16.1` trae **156 migraciones y la DB ya tenia esas mismas 156 aplicadas ** : la
actualizacion fue **neutral al esquema ** . Por eso el rollback a 4.16.0 no
necesitaria restaurar la DB. Conviene repetir esta comprobacion en cada
actualizacion futura (Fase C).
### Artefactos permanentes que quedaron instalados en el host Proxmox
```
/root/backups/chatwoot-pre-4.16.1.dump 667K, 97 tablas, sha256 dc3d25fe...
/root/backups/chatwoot-service-pre-4.16.1.tgz 2.4K, compose + .env (tiene secretos)
/root/scripts/chatwoot-enterprise-guard.sh el guard (copia versionada en scripts/)
/etc/cron.d/chatwoot-enterprise-guard cron */5
/etc/logrotate.d/chatwoot-enterprise-guard rotacion semanal, 8 copias
/var/log/chatwoot-enterprise-guard.log solo escribe cuando repara
```
## 1. Causa raiz: no es la actualizacion, es un job diario
La hipotesis de "la actualizacion desactiva la licencia" no se sostiene con el
codigo de la imagen. La cadena real, leida de la imagen 4.16.0:
```
config/schedule.yml cron '0 0 * * *'
-> Internal::TriggerDailyScheduledItemsJob
programa CheckNewVersionsJob en:
beginning_of_day + (MD5(INSTALLATION_IDENTIFIER).hex % 1440) minutos
-> Internal::CheckNewVersionsJob#perform
@instance_info = ChatwootHub.sync_with_hub # POST https://hub.2.chatwoot.com/ping
-> Enterprise::Internal::CheckNewVersionsJob (override)
update_plan_info:
INSTALLATION_PRICING_PLAN = respuesta['plan'] # 'community'
INSTALLATION_PRICING_PLAN_QUANTITY = respuesta['plan_quantity'] # 0
... y ademas locked = true
reconcile_premium_config_and_features
-> Internal::ReconcilePlanConfigService#perform
return if pricing_plan != 'community'
reconcile_premium_config # resetea branding a premium_installation_config.yml
reconcile_premium_features # account.disable_features!(*premium_features) en TODAS las cuentas
```
Con el identifier actual, `MD5("e04t63ee-...").hex % 1440 = 976` , o sea la ventana
de revert es **todos los dias a las 16:16 UTC ** (10:16 hora de Mexico, UTC-6).
Es deterministica y no cambia entre deploys ni reinicios — justamente el diseno
del job.
Consecuencias practicas:
1. **El parche caduca en <= 24 h ** , actualices o no. El caso se documento el
2026-06-16; se revirtio al dia siguiente.
2. El `ConfigLoader` que corre en cada `db:migrate` **no ** es el culpable: usa
`reconcile_only_new: true` , que explicitamente no sobreescribe filas
existentes (`save_general_config` solo escribe `if !@reconcile_only_new` ).
3. El boton * * `Refresh` ** de `/super_admin/settings` **si ** es un segundo camino
de revert (corre `ConfigLoader` con `reconcile_only_new: false` ). La
advertencia del caso original sigue vigente.
4. Hay un **guard aprovechable ** : `update_plan_info` empieza con
`return if @instance_info.blank?` . Si el hub no responde, no se escribe nada.
Esa es la base del fix durable de la Fase G.
## 2. Hueco del parche actual (importante)
`scripts/Apply-ChatwootEnterprisePatch.ps1` corregia **solo 3 filas ** de
`installation_configs` . Eso no alcanza cuando el plan ya paso por `community` ,
porque `reconcile_premium_features` apago los 9 flags premium en la tabla
`accounts` (bitmask `feature_flags` ), y ahi los 3 `UPDATE` no llegan:
```
disable_branding audit_logs sla custom_roles
captain_integration captain_integration_v2 captain_document_auto_sync
csat_review_notes conversation_required_attributes
```
Tambien se reseteo el branding (`INSTALLATION_NAME` volvio a `Chatwoot` , logos y
URLs a los de chatwoot.com, `DISPLAY_MANIFEST` a `true` ).
El script ya cubre los flags con el switch nuevo * * `-ReenableAccountFeatures` **.
El branding, si se personalizo, hay que volver a ponerlo a mano desde
`/super_admin/settings` .
### 2.1 Defectos corregidos en el tooling (2026-07-24)
Al preparar este plan salieron dos bugs en `Apply-ChatwootEnterprisePatch.ps1`
que habrian hecho fallar los pasos de la Fase E:
1. **La autodeteccion del contenedor Postgres nunca funciono. ** El patron era
`"<uuid>.*(pgvector|postgres|db)"` , pero Coolify nombra los contenedores
`<servicio>-<uuid>` (`postgres-c11xzy...` ), o sea el uuid va al final. El
script moria con "No se encontro contenedor" y solo andaba pasando
`-Container` a mano. Ahora son dos greps encadenados (uuid, luego rol).
2. **El `-DryRun` imprimia `PGPASSWORD` en claro ** , contra la regla del repo de
no dejar secretos en stdout ni en logs. Ahora sale enmascarada.
3. **El parche no invalidaba el cache de `GlobalConfig`. ** Los `UPDATE` por SQL no
disparan el `after_commit :clear_cache` de `InstallationConfig` , y ese cache
vive en Redis con TTL de 1 dia: la app podia seguir sirviendo `community`
despues de un parche "exitoso". Ahora el paso de `rails runner` llama
explicitamente a `GlobalConfig.clear_cache` (detalle en la Fase G).
Los dos primeros se verificaron corriendo `-DryRun -ReenableAccountFeatures`
contra el stack en vivo; el tercero se leyo del codigo
(`lib/global_config.rb` + `app/models/installation_config.rb` ) y se confirmo
inspeccionando las claves `V1:GLOBAL_CONFIG:*` en Redis.
`Get-ChatwootLicenseStatus.ps1` quedo probado end-to-end.
## 3. Plan de actualizacion
Todo desde la raiz del repo, en PowerShell, con `. .\.env.local.ps1` cargado.
### Fase A — pre-checks (solo lectura)
``` powershell
. . \ . env . local . ps1
# Estado de licencia + flags por cuenta + ventana diaria de revert.
. \ scripts \ Get-ChatwootLicenseStatus . ps1 -Deep
# Salud del stack.
. \ coolify_skill \ scripts \ Get-CoolifyDockerStatus . ps1 -All
```
Anota el digest de la imagen en uso; es la unica ruta de rollback rapido:
``` powershell
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct exec 102 -- docker images --digests | grep chatwoot/chatwoot "
```
> Digest al 2026-07-24: `sha256:8fdd8adde2093fb270fc69eaeefaf6faca416e65cba0b58041159c654b81d175` (= 4.16.0).
### Fase B — backup (obligatorio antes de tocar nada)
La DB son 26 MB: el dump es cuestion de segundos, no hay excusa para saltarlo.
``` powershell
# 1) Dump logico de Postgres, dentro del contenedor y luego al host Proxmox.
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct exec 102 -- docker exec -i postgres-c11xzy2tx2cdapm32f5b89vy bash -lc 'PGPASSWORD=\ $POSTGRES_PASSWORD pg_dump -U \ $POSTGRES_USER -d \ $POSTGRES_DB -Fc -f /tmp/chatwoot-pre-4.16.1.dump' "
# 2) Sacarlo del contenedor al LXC y del LXC al host.
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct exec 102 -- docker cp postgres-c11xzy2tx2cdapm32f5b89vy:/tmp/chatwoot-pre-4.16.1.dump /root/chatwoot-pre-4.16.1.dump "
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct pull 102 /root/chatwoot-pre-4.16.1.dump /root/backups/chatwoot-pre-4.16.1.dump && ls -lh /root/backups/ "
# 3) Copia del compose + .env del servicio (queda EN EL HOST, nunca en el repo:
# el .env tiene SECRET_KEY_BASE y las passwords de Postgres/Redis).
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct exec 102 -- tar czf /root/chatwoot-service-pre-4.16.1.tgz -C /data/coolify/services/c11xzy2tx2cdapm32f5b89vy . "
```
Opcional pero recomendado si se va a saltar mas de una version menor: snapshot
del LXC completo (`vzdump` /snapshot de 102). Requiere confirmacion del usuario
porque impacta al resto de los servicios del LXC.
### Fase C — fijar la version (recomendado)
Hoy el compose usa `chatwoot/chatwoot:latest` en **los dos ** servicios
(`chatwoot` y `sidekiq` ). Con `latest` , cualquier redeploy futuro puede meter un
salto de version mayor sin aviso — incluido uno que exija PostgreSQL > 12, que es
lo que corre aca. Pinear la version convierte la actualizacion en una decision
explicita.
Primero confirmar que el tag existe. **El naming es `vX.Y.Z` ** : `v4.16.1` existe,
`4.16.1` (sin la `v` ) no.
``` powershell
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct exec 102 -- docker manifest inspect chatwoot/chatwoot:v4.16.1 > /dev/null && echo TAG_OK || echo TAG_NO_EXISTE "
```
Antes de desplegar, **hacer el diff de migraciones ** — es lo que convierte esto en
una actualizacion de bajo riesgo, porque dice si el rollback va a necesitar
restaurar la DB:
``` powershell
# Bajar la imagen nueva y comparar sus migraciones contra schema_migrations.
# 156 == 156 significa que no hay cambios de esquema (fue el caso de 4.16.1).
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct exec 102 -- docker pull chatwoot/chatwoot:v4.16.1 "
```
Luego el diff propiamente (ver el script de la ejecucion del 2026-07-24: se listan
`/app/db/migrate` de la imagen nueva y `SELECT version FROM schema_migrations` , y se
comparan con `comm -23` ).
Para pinear el tag, **usar la API, no editar el archivo en el LXC ** : Coolify
regenera `/data/coolify/services/<uuid>/docker-compose.yml` en cada deploy y un
cambio a mano en el host se pierde.
``` powershell
. . \ . env . local . ps1
# 1) Traer el compose actual, 2) cambiar las 2 lineas de imagen, 3) PATCH en base64.
$svc = ( . \ coolify_skill \ scripts \ Invoke-CoolifyApi . ps1 -Path " /services/c11xzy2tx2cdapm32f5b89vy " ) | ConvertFrom-Json
$new = $svc . docker_compose_raw . Replace ( " image: 'chatwoot/chatwoot:latest' " , " image: 'chatwoot/chatwoot:v4.16.1' " )
$b64 = [ Convert ] :: ToBase64String ( [ Text.Encoding ] :: UTF8 . GetBytes ( $new ) )
$body = @ { docker_compose_raw = $b64 } | ConvertTo-Json -Compress
. \ coolify_skill \ scripts \ Invoke-CoolifyApi . ps1 -Method PATCH -Path " /services/c11xzy2tx2cdapm32f5b89vy " -BodyJson $body
```
> **`docker_compose_raw` tiene que ir en base64.** Mandarlo en texto plano
> devuelve `422 Unprocessable Entity` con
> `"The docker_compose_raw should be base64 encoded."`, y
> `Invoke-CoolifyApi.ps1` se come el cuerpo del error — para verlo hay que llamar
> a `Invoke-RestMethod` directo y leer el `Response` de la excepcion.
Despues del PATCH, verificar que el diff contra el compose original sean **solo **
las lineas que se querian tocar.
**Cambio de estado — requiere tu confirmacion antes de aplicarse. **
### Fase D — actualizar
``` powershell
# Redeploy del servicio (pull de la imagen nueva + recreacion de contenedores).
. \ coolify_skill \ scripts \ Invoke-CoolifyApi . ps1 -Path " /deploy?uuid=c11xzy2tx2cdapm32f5b89vy "
```
Devuelve un `deployment_uuid` . Seguimiento:
``` powershell
. \ coolify_skill \ scripts \ Invoke-CoolifyApi . ps1 -Path " /deployments/<deployment_uuid> "
```
Al arrancar, el entrypoint corre `db:chatwoot_prepare` → `db:migrate` →
`ConfigLoader` (`reconcile_only_new: true` , no pisa nada). Esperar a que los 4
contenedores vuelvan a `healthy` :
``` powershell
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct exec 102 -- docker ps | grep c11xzy2tx2cdapm32f5b89vy "
```
**Cambio de estado — requiere tu confirmacion. **
### Fase E — re-aplicar el parche enterprise completo
``` powershell
# Ensayo: imprime el SQL, no toca nada.
. \ scripts \ Apply-ChatwootEnterprisePatch . ps1 -DryRun
# Aplicar: 3 UPDATE + reactivacion de los 9 flags premium en todas las cuentas.
. \ scripts \ Apply-ChatwootEnterprisePatch . ps1 -ReenableAccountFeatures
```
Criterio de exito (el script aborta si no se cumple):
- exactamente **3 lineas `UPDATE 1` ** ;
- `pendientes=ninguno` para cada cuenta;
- `self_hosted_enterprise=true` .
Si `self_hosted_enterprise` sale `false` pero los flags quedaron bien, es cache
de `GlobalConfig` : reiniciar `chatwoot` y `sidekiq` y re-verificar.
**Cambio de estado — requiere tu confirmacion. **
### Fase F — verificar
``` powershell
. \ scripts \ Get-ChatwootLicenseStatus . ps1 -Deep
```
Y a mano en `https://chat.urieljareth.org` :
1. Login como super admin.
2. `/super_admin/settings` : plan **Enterprise ** , cantidad **10000 ** .
**NO pulsar `Refresh` ** — revierte todo al instante.
3. Una funcion premium por cuenta (audit logs, SLA, custom roles o Captain).
4. Si habia branding propio, volverlo a poner (se reseteo, ver seccion 2).
### Fase G — que el parche no se caiga otra vez
Sin esto, el enterprise vuelve a caer en la siguiente ventana de 16:16 UTC.
#### Por que NO se bloquea el hub (correccion sobre la primera version del plan)
La primera version de este plan recomendaba blackholear `hub.2.chatwoot.com` con
`extra_hosts` , aprovechando el `return if @instance_info.blank?` . **Se descarto al
verificarlo contra este entorno**, por dos motivos:
1. **Rompe las notificaciones push del movil. ** `hub.2.chatwoot.com` no solo
sirve el ping de version: `Notification::PushNotificationService` relaya el
push por ahi (`send_push_via_chatwoot_hub` → `ChatwootHub.send_push` ) y ese
metodo corre **solo cuando Firebase no esta configurado ** . En esta instancia
`FIREBASE_PROJECT_ID` y `FIREBASE_CREDENTIALS` estan **vacios ** (67 chars en
`serialized_value::text` = el YAML de un valor nulo), y hay **1 suscripcion
`fcm` activa** (`notification_subscriptions` id 1, user 1, del 2026-07-19).
Bloquear el host le mata el push a ese usuario.
2. **Deja un job fallando todos los dias. ** Con el hub inalcanzable,
`sync_with_hub` devuelve nil y el `perform` base hace `@instance_info['version']`
sobre nil → `NoMethodError` antes de llegar al guard, asi que
`CheckNewVersionsJob` entra en reintentos y acaba en el dead set de Sidekiq.
Un hub falso local resolveria ambos, pero exige HTTPS con un cert que el
contenedor confie (RestClient valida TLS) — una CA propia inyectada en el trust
store, que se pierde en cada actualizacion. No vale la pena.
#### Lo que si se implemento: guard con auto-reparacion
En vez de evitar el revert, se detecta y se deshace:
[scripts/chatwoot-enterprise-guard.sh ](../../scripts/chatwoot-enterprise-guard.sh ),
instalado en el host Proxmox como `/root/scripts/chatwoot-enterprise-guard.sh` con
`cron */5` .
Como funciona:
1. **Caso normal (barato): ** 1 `SELECT` del plan y sale. **0.9 s ** , sin escribir
en el log. 288 corridas al dia es ruido despreciable para el host.
2. **Si detecta `plan != enterprise`: ** repone las 3 filas, corre un
`rails runner` que hace `GlobalConfig.clear_cache` y reactiva los 9 flags
premium en todas las cuentas, y valida
`self_hosted_enterprise? == true` + `pendientes=ninguno` . **12.7 s. **
3. Usa `flock` para no solaparse, y solo escribe en el log cuando actua.
Ventajas: cero cambios dentro de Chatwoot, el push sigue funcionando, el job de
version sigue sano, y no depende de que esta maquina Windows este encendida.
Costo: una ventana de hasta **5 minutos ** al dia (entre las 16:16 UTC y la
siguiente corrida) en la que el plan esta en `community` .
Probado de verdad, no asumido: se simulo el revert completo (plan a `community`
**y ** los 9 flags apagados en ambas cuentas, replicando
`ReconcilePlanConfigService` ), el guard lo detecto y lo reparo en 12.7 s, la
segunda corrida fue no-op en 0.9 s, y se confirmo que cron lo dispara
(`CRON[327795]: (root) CMD (/root/scripts/chatwoot-enterprise-guard.sh)` ).
#### Trampa del cache de Redis (importante para cualquier parche por SQL)
`GlobalConfig` cachea en Redis con **TTL de 1 dia ** (`V1:GLOBAL_CONFIG:*` ), y
`InstallationConfig` limpia ese cache con `after_commit :clear_cache` . Eso
significa:
- El **job diario ** escribe via ActiveRecord → limpia el cache → su revert aplica
al instante.
- Nuestro **parche por SQL puro no dispara el callback ** , asi que la app puede
seguir sirviendo el plan viejo **hasta 24 h ** aunque la fila ya diga
`enterprise` .
Por eso tanto el guard como `Apply-ChatwootEnterprisePatch.ps1
-ReenableAccountFeatures` llaman explicitamente a `GlobalConfig.clear_cache` .
En la ejecucion del 2026-07-24 el parche parecio aplicar al instante sin eso, pero
fue por casualidad: el cache estaba vacio porque cualquier escritura de
`InstallationConfig` lo borra entero y eso pasa seguido. No hay que confiar en
esa casualidad.
### Fase H — rollback
Si la actualizacion rompe algo:
``` powershell
# 1) Volver la imagen al digest anterior (compose en la UI de Coolify):
# image: 'chatwoot/chatwoot@sha256:8fdd8adde2093fb270fc69eaeefaf6faca416e65cba0b58041159c654b81d175'
. \ coolify_skill \ scripts \ Invoke-CoolifyApi . ps1 -Path " /deploy?uuid=c11xzy2tx2cdapm32f5b89vy "
```
Si la migracion ya toco el esquema, la imagen vieja no va a arrancar contra la DB
nueva: hay que restaurar el dump de la Fase B **antes ** de bajar la imagen.
``` powershell
# 2) Restaurar el dump (DESTRUCTIVO: pisa la DB actual).
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct push 102 /root/backups/chatwoot-pre-4.16.1.dump /root/chatwoot-pre-4.16.1.dump "
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct exec 102 -- docker cp /root/chatwoot-pre-4.16.1.dump postgres-c11xzy2tx2cdapm32f5b89vy:/tmp/restore.dump "
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct exec 102 -- docker exec -i postgres-c11xzy2tx2cdapm32f5b89vy bash -lc 'PGPASSWORD=\ $POSTGRES_PASSWORD pg_restore -U \ $POSTGRES_USER -d \ $POSTGRES_DB --clean --if-exists /tmp/restore.dump' "
```
Luego Fase E otra vez.
## 4. Orden de ejecucion resumido
```
A pre-checks (lectura)
B backup DB + compose/.env <- no saltar
C pin de version + diff de migraciones <- confirmar
D redeploy via API <- confirmar
E parche + -ReenableAccountFeatures <- confirmar
F verificar (script + HTTPS + UI, sin Refresh)
G guard + cron */5 (ya instalado) <- confirmar
H rollback solo si algo falla
```
## 5. Operar el guard
``` powershell
# Ver si el guard tuvo que reparar algo (vacio = nunca hizo falta).
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " cat /var/log/chatwoot-enterprise-guard.log "
# Forzar una corrida.
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " /root/scripts/chatwoot-enterprise-guard.sh; echo rc=\ $ ? "
# Confirmar que cron lo dispara.
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " journalctl --since '-20 min' --no-pager | grep chatwoot-enterprise-guard "
```
Lo normal es un log **vacio o con pocas lineas ** . Un `REPARADO` por dia es lo
esperado (el revert de las 16:16 UTC). Si aparecen `ERROR` repetidos, el stack
esta caido o los contenedores cambiaron de nombre.
**Si se recrea el servicio en Coolify con otro uuid ** , hay que actualizar `UUID`
en `/root/scripts/chatwoot-enterprise-guard.sh` — el guard lo tiene hardcodeado.
## 6. Notas de riesgo
- **PostgreSQL 12.19** y la imagen `pgvector/pgvector:pg12` tiene ~23 meses.
Un salto de version mayor de Chatwoot probablemente exija PG >= 13. Antes de
actualizar mas alla de 4.16.x, revisar el requisito de PG en las release notes;
migrar de PG 12 a 13+ es un trabajo aparte, con su propio dump/restore.
- **`Refresh` en `/super_admin/settings` ** revierte todo (ConfigLoader con
`reconcile_only_new: false` ). El guard lo repararia en <= 5 min, pero conviene
no pulsarlo.
- Ahora que la imagen esta **pineada a `v4.16.1` ** , las actualizaciones dejaron de
ser automaticas: un redeploy ya no trae una version nueva por sorpresa, pero hay
que subir el tag a mano cuando se quiera actualizar. Ese es el punto del pin.
- El `.env` del servicio y el `.tgz` del backup **contienen secretos ** : se
quedan en el host Proxmox. Nunca copiarlos al repo.
- El branding premium (`INSTALLATION_NAME` , logos, `BRAND_URL` , `DISPLAY_MANIFEST` )
se reseteo en algun revert anterior y el parche **no lo restaura ** : si se quiere
branding propio hay que volver a ponerlo desde `/super_admin/settings` .
- Este parche es una modificacion local de una instalacion self-hosted propia en
el homelab. No se distribuye ni se revende.
## 7. Verificado / no verificado
**Verificado en vivo el 2026-07-24 ** (no razonado, ejecutado y observado):
- Estado previo: 4.16.0, plan `community` , cantidad `0` ,
`self_hosted_enterprise? = false` , los 9 flags premium apagados en ambas cuentas.
- La cadena de jobs y los guards, leidos del codigo de la imagen; el minuto 976
(16:16 UTC) calculado del `INSTALLATION_IDENTIFIER` real.
- Backup: dump de 667K con 97 tablas (`pg_restore -l` ) + sha256.
- Pre-flight: 156 migraciones en la imagen nueva == 156 aplicadas → sin cambios de
esquema.
- `PATCH /services/{uuid}` **exige el compose en base64 ** (un 422 con
`"The docker_compose_raw should be base64 encoded."` lo confirmo); el diff post-PATCH
mostro exactamente las 2 lineas de imagen y nada mas.
- Deploy: 4.16.1 corriendo, 4/4 `healthy` , ~2 min de corte.
- Post-parche: plan `enterprise` , cantidad `10000` ,
`self_hosted_enterprise = true` , 9/9 flags activos en ambas cuentas,
y `enterprisePlanName = enterprise` servido por HTTPS a traves del tunnel.
- Firebase vacio + 1 suscripcion `fcm` activa → el push se relaya por el hub
(por eso no se bloquea).
- `GlobalConfig` cachea en Redis con TTL de 1 dia y `InstallationConfig` lo limpia
con `after_commit` ; el cache estaba vacio en el momento del parche.
- El guard: no-op en 0.9 s, reparacion completa en 12.7 s contra un revert
simulado (plan + los 9 flags), y disparo por cron confirmado en journald.
**No verificado: **
- Que 4.16.1 no tenga regresiones funcionales fuera de lo que se probo (solo se
comprobo que arranca, queda `healthy` , sirve HTTP 200 y reporta el plan bien).
- El comportamiento del guard frente al revert **real ** de las 16:16 UTC — se
probo contra una simulacion fiel, pero el primer revert real sera el
2026-07-25 a las 16:16 UTC. Revisar el log ese dia.
- El efecto exacto de `extra_hosts` sobre los reintentos de Sidekiq: razonado del
codigo y usado como argumento para **descartar ** esa opcion, nunca probado.
- Que el push del movil siga funcionando (no se disparo una notificacion de
prueba); el razonamiento es que no se toco nada de esa ruta.
---
## Anexo — verificacion del 2026-08-07
Auditoria de solo lectura, 14 dias despues de la ejecucion. Tres cosas cambiaron.
### 1. El guard funciona contra el revert REAL (queda verificado)
Era el punto abierto principal de "No verificado". El log
`/var/log/chatwoot-enterprise-guard.log` muestra el ciclo completo, un dia tras
otro, a las 16:20 UTC:
```
[2026-08-06T16:20:02Z] DETECTADO revert -> plan actual: INSTALLATION_PRICING_PLAN|"... value: community ..." . Reparando...
[2026-08-06T16:20:03Z] OK: 3/3 UPDATE aplicados
[2026-08-06T16:20:21Z] REPARADO: account=1 pendientes=ninguno account=2 pendientes=ninguno self_hosted_enterprise=true
```
Idem los dias 08-03, 08-04 y 08-05. El revert diario ocurre de verdad y el guard
lo deshace en ~20 s. Deja de ser una hipotesis.
### 2. El pin de version NO sostuvo: corre `v4.16.2`
```
chatwoot-c11xzy2tx2cdapm32f5b89vy chatwoot/chatwoot:v4.16.2
sidekiq-c11xzy2tx2cdapm32f5b89vy chatwoot/chatwoot:v4.16.2
```
La imagen se habia pineado a `v4.16.1` via API. Hoy corre `v4.16.2` , asi que en
algun momento entre el 2026-07-24 y hoy alguien o algo movio el tag y hubo un
redeploy. **Antes de asumir que el pin protege, verificalo: **
``` powershell
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " pct exec 102 -- docker ps --format '{{.Names}}|{{.Image}}' | grep chatwoot "
```
### 3. El guard fallo durante la ventana del update
Cuatro errores el 2026-08-07, los primeros dos en la ventana del revert diario:
```
[2026-08-07T16:17:15Z] ERROR: no se pudo leer INSTALLATION_PRICING_PLAN (stack caido o contenedor renombrado?)
[2026-08-07T16:20:03Z] ERROR: no se pudo leer INSTALLATION_PRICING_PLAN (stack caido o contenedor renombrado?)
[2026-08-07T23:11:45Z] ERROR: ...
[2026-08-07T23:15:02Z] ERROR: ...
```
**Estado tras la auditoria: sano. ** Una corrida manual con `bash -x` lee el plan
sin problema y sale 0, y el valor en la DB es `enterprise` :
```
INSTALLATION_PRICING_PLAN|"--- !ruby/hash:...\nvalue: enterprise\n"
```
Es decir: los errores fueron transitorios, coincidentes con la recreacion de
contenedores del update a `v4.16.2` . **No hay accion urgente. ** Pero deja
expuesto un riesgo estructural.
### 4. Riesgo estructural: el guard tiene el nombre del contenedor hardcodeado
`scripts/chatwoot-enterprise-guard.sh` fija:
``` bash
UUID = c11xzy2tx2cdapm32f5b89vy
DB_CT = " postgres- $UUID "
APP_CT = " chatwoot- $UUID "
```
Mientras el uuid del * service * no cambie, los nombres se mantienen — y en este
caso se mantuvieron. Pero el propio mensaje de error del guard nombra la causa
("contenedor renombrado?"), y **un redeploy que cambie el sufijo lo deja ciego
sin avisar**: el guard sale con codigo 1 y solo escribe una linea en un log que
nadie lee. El revert diario dejaria de repararse en silencio.
Mitigacion pendiente (no aplicada — requiere confirmacion porque toca el host):
hacer que el guard **resuelva el nombre por patron ** en vez de fijarlo, p. ej.
`docker ps --format '{{.Names}}' | grep -m1 '^postgres-'` acotado al uuid del
service consultado a la API de Coolify. Y que N fallos consecutivos escalen a
algo visible, no solo al log.
### Comprobacion rapida del estado
``` powershell
. . \ . env . local . ps1
. \ scripts \ Get-ChatwootLicenseStatus . ps1 -Deep
. \ scripts \ Invoke-ProxmoxSsh . ps1 -Command " tail -20 /var/log/chatwoot-enterprise-guard.log "
```
Nota sobre el quoting: cualquier lectura directa de la DB necesita el patron
base64, porque `Invoke-ProxmoxSsh.ps1` corrompe las comillas anidadas. Ver
[../TOOL-INDEX.md ](../TOOL-INDEX.md ) §1.2.