27 KiB
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(uuidc11xzy2tx2cdapm32f5b89vy,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:
- El parche caduca en <= 24 h, actualices o no. El caso se documento el 2026-06-16; se revirtio al dia siguiente.
- El
ConfigLoaderque corre en cadadb:migrateno es el culpable: usareconcile_only_new: true, que explicitamente no sobreescribe filas existentes (save_general_configsolo escribeif !@reconcile_only_new). - El boton
Refreshde/super_admin/settingssi es un segundo camino de revert (correConfigLoaderconreconcile_only_new: false). La advertencia del caso original sigue vigente. - Hay un guard aprovechable:
update_plan_infoempieza conreturn 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:
- 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-Containera mano. Ahora son dos greps encadenados (uuid, luego rol). - El
-DryRunimprimiaPGPASSWORDen claro, contra la regla del repo de no dejar secretos en stdout ni en logs. Ahora sale enmascarada. - El parche no invalidaba el cache de
GlobalConfig. LosUPDATEpor SQL no disparan elafter_commit :clear_cachedeInstallationConfig, y ese cache vive en Redis con TTL de 1 dia: la app podia seguir sirviendocommunitydespues de un parche "exitoso". Ahora el paso derails runnerllama explicitamente aGlobalConfig.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_rawtiene que ir en base64. Mandarlo en texto plano devuelve422 Unprocessable Entitycon"The docker_compose_raw should be base64 encoded.", yInvoke-CoolifyApi.ps1se come el cuerpo del error — para verlo hay que llamar aInvoke-RestMethoddirecto y leer elResponsede 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=ningunopara 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:
- Login como super admin.
/super_admin/settings: plan Enterprise, cantidad 10000. NO pulsarRefresh— revierte todo al instante.- Una funcion premium por cuenta (audit logs, SLA, custom roles o Captain).
- 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:
- Rompe las notificaciones push del movil.
hub.2.chatwoot.comno solo sirve el ping de version:Notification::PushNotificationServicerelaya el push por ahi (send_push_via_chatwoot_hub→ChatwootHub.send_push) y ese metodo corre solo cuando Firebase no esta configurado. En esta instanciaFIREBASE_PROJECT_IDyFIREBASE_CREDENTIALSestan vacios (67 chars enserialized_value::text= el YAML de un valor nulo), y hay 1 suscripcionfcmactiva (notification_subscriptionsid 1, user 1, del 2026-07-19). Bloquear el host le mata el push a ese usuario. - Deja un job fallando todos los dias. Con el hub inalcanzable,
sync_with_hubdevuelve nil y elperformbase hace@instance_info['version']sobre nil →NoMethodErrorantes de llegar al guard, asi queCheckNewVersionsJobentra 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:
- Caso normal (barato): 1
SELECTdel plan y sale. 0.9 s, sin escribir en el log. 288 corridas al dia es ruido despreciable para el host. - Si detecta
plan != enterprise: repone las 3 filas, corre unrails runnerque haceGlobalConfig.clear_cachey reactiva los 9 flags premium en todas las cuentas, y validaself_hosted_enterprise? == true+pendientes=ninguno. 12.7 s. - Usa
flockpara 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:pg12tiene ~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. Refreshen/super_admin/settingsrevierte todo (ConfigLoader conreconcile_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
.envdel servicio y el.tgzdel 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, cantidad0,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_IDENTIFIERreal. - 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, cantidad10000,self_hosted_enterprise = true, 9/9 flags activos en ambas cuentas, yenterprisePlanName = enterpriseservido por HTTPS a traves del tunnel. - Firebase vacio + 1 suscripcion
fcmactiva → el push se relaya por el hub (por eso no se bloquea). GlobalConfigcachea en Redis con TTL de 1 dia yInstallationConfiglo limpia conafter_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_hostssobre 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.