581 lines
27 KiB
Markdown
581 lines
27 KiB
Markdown
# 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.
|