Actualiza toolkit operativo y documentación
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Caso: error 500 al abrir la página de una aplicación en Coolify (env var sin cifrar)
|
||||
|
||||
**Fecha:** 2026-09-04 · **App:** `open-seo:main-0fgs5kwaab9esytaxkddsvts`
|
||||
(uuid `kj0kccsb4d46tm0d6qe6docy`, id DB 57, repo `every-app/open-seo`, build pack
|
||||
railpack) · **Resuelto el mismo día.**
|
||||
|
||||
## Síntoma
|
||||
|
||||
Abrir
|
||||
`https://coolify.urieljareth.org/project/.../application/kj0kccsb4d46tm0d6qe6docy`
|
||||
devuelve **500 (Server Error)**. El resto del dashboard funciona. El sitio
|
||||
público de la app responde 200 (lo sirve un sidecar manual, ver "Estado
|
||||
post-fix").
|
||||
|
||||
## Causa raíz
|
||||
|
||||
La fila 1298 de `environment_variables` (`DATAFORSEO_API_KEY` de la app 57)
|
||||
tenía el **valor en texto plano** (56 chars, sin prefijo `eyJpdiI6`) con
|
||||
`is_literal=false`. En Coolify v4.3.x el accessor `value` del modelo
|
||||
`EnvironmentVariable` **descifra incondicionalmente** (`decrypt($value)`); un
|
||||
valor no cifrado lanza `DecryptException: The payload is invalid`.
|
||||
|
||||
La página de configuración monta `ConfigurationChecker` (Livewire), que llama a
|
||||
`Application->pendingDeploymentConfigurationDiff()` →
|
||||
`ApplicationConfigurationSnapshot::environmentItems()` → lee `->value` de cada
|
||||
env var → explota → la página entera responde 500. El stack trace está en
|
||||
`storage/logs/laravel.log` del contenedor `coolify`.
|
||||
|
||||
El valor llegó por un **INSERT/UPDATE directo a la DB** (sin pasar por el modelo
|
||||
Eloquent, que cifra en el `set`). Huella correlativa: 5 filas más con el morph
|
||||
type mal escapado (`App\\Models\\Application`, doble backslash), también
|
||||
inserciones directas por SQL (ver "Hallazgos secundarios").
|
||||
|
||||
## Diagnóstico (réplicable)
|
||||
|
||||
```powershell
|
||||
# 1) Stack trace del 500 (dentro del contenedor coolify):
|
||||
# docker exec coolify tail -n 200 /var/www/html/storage/logs/laravel.log
|
||||
# -> DecryptException desde EnvironmentVariable::get_environment_variables
|
||||
|
||||
# 2) Clasificar filas SIN imprimir valores (todo payload cifrado de Laravel
|
||||
# empieza con "eyJpdiI6"):
|
||||
pct exec 102 -- docker exec coolify-db sh -c 'psql -U "$POSTGRES_USER" \
|
||||
-d "$POSTGRES_DB" -c "SELECT id, key, is_literal, length(value) AS len, \
|
||||
(value LIKE $$eyJpdiI6%$$) AS looks_enc FROM environment_variables \
|
||||
WHERE resourceable_id=57;"'
|
||||
```
|
||||
|
||||
## Fix aplicado
|
||||
|
||||
Re-cifrar el valor existente con el `APP_KEY` de la instancia (preserva el
|
||||
secreto; no hace falta reingresarlo), vía un script PHP con Laravel booteado
|
||||
dentro del contenedor `coolify`:
|
||||
|
||||
```php
|
||||
// /tmp/fix-envvar.php (se pasa por stdin a: docker exec -i coolify sh -c 'cat > /tmp/fix-envvar.php')
|
||||
require '/var/www/html/vendor/autoload.php';
|
||||
$app = require '/var/www/html/bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
$row = DB::table('environment_variables')->where('id', 1298)->first();
|
||||
try { decrypt($row->value); echo "ya cifra OK\n"; }
|
||||
catch (\Throwable $e) {
|
||||
DB::table('environment_variables')->where('id', 1298)->update([
|
||||
'value' => encrypt($row->value), // el secreto no se pierde
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
Wrapper ejecutable: [artifacts/fix-envvar-1298.ps1](../../artifacts/fix-envvar-1298.ps1)
|
||||
(hace el backup, aplica y verifica en una pasada).
|
||||
|
||||
**Backup previo** (incluye el valor, root-only, host Proxmox):
|
||||
`/root/backups/envvar-1298-20260904-211001.tsv`.
|
||||
|
||||
**Rollback:** restaurar la fila desde el backup
|
||||
(`UPDATE environment_variables SET value='<col 3 del tsv>' WHERE id=1298;`) —
|
||||
solo si se quisiera volver al estado roto original; no hay razón para hacerlo.
|
||||
|
||||
## Verificación
|
||||
|
||||
- Lectura a nivel de modelo OK (el accessor ya no lanza).
|
||||
- `Application::find(57)->pendingDeploymentConfigurationDiff()` — la ruta exacta
|
||||
que 500eaba — ejecuta limpio.
|
||||
- Fila post-fix: `len=312`, `looks_enc=t`, `updated_at=2026-09-05 03:10:04`.
|
||||
|
||||
## Estado post-fix de la app (no parte de este caso)
|
||||
|
||||
- La app en Coolify sigue `exited:unhealthy` **sin contenedor** (última online
|
||||
2026-08-27). Su página ya carga; un redeploy es decisión del usuario.
|
||||
- El FQDN `https://kj0kccsb4d46tm0d6qe6docy.urieljareth.org` responde **200 en
|
||||
vivo** (`cf-cache-status: DYNAMIC`) porque el contenedor manual
|
||||
`open-seo-sidecar` (puerto 80, corriendo fuera de Coolify) lleva los labels
|
||||
Traefik de ese host. Es decir: el sitio público no depende hoy del deployment
|
||||
de Coolify.
|
||||
|
||||
## Hallazgos secundarios (sin acción, reportados al usuario)
|
||||
|
||||
- **5 filas huérfanas** (ids 1152-1156: `MYSQL_DATABASE`, `MYSQL_USER`,
|
||||
`MOSTRAR_ENLACE`, `SEMBRAR_SIEMPRE`, `ENLACES_POR_VENTANA`) apuntan a la app
|
||||
52 (`insta-portal`) con `resourceable_type='App\\Models\\Application'`
|
||||
(doble backslash). La relación de Eloquent no las ve, así que **no rompen
|
||||
páginas**, pero insta-portal corre sin esas variables. Normalizar el morph
|
||||
type (y re-cifrar valores) las activaría — evaluar impacto en runtime antes.
|
||||
|
||||
## Prevención
|
||||
|
||||
- Nunca escribir en `environment_variables.value` por SQL directo: el modelo
|
||||
cifra en el setter. Para insertar variables usar la UI o la API.
|
||||
- Si se inserta por SQL de emergencia, el valor debe ser `encrypt($valor)` con
|
||||
el `APP_KEY` de la instancia, y `resourceable_type` lleva **un solo**
|
||||
backslash (`App\Models\Application`).
|
||||
- Síntoma distintivo para el futuro: dashboard 500 solo en la página de una app
|
||||
concreta + `DecryptException` en `laravel.log` = valor corrupto en
|
||||
`environment_variables` de ese recurso.
|
||||
Reference in New Issue
Block a user