Files
Proxmox-Coolify-Manager/docs/runbooks/chatwoot-update.md
T

27 KiB
Raw Blame History

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 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)

. .\.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:

.\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.

# 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.

.\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:

# 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.

. .\.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

# 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:

.\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:

.\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

# 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

.\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, 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:

# 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.

# 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

# 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:

.\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:

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

. .\.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 §1.2.