| name | devops-operations |
| description | Patrones operativos de DevOps: deploy en NaN.builders (502, cache, env vars), cron jobs con scripts (no_agent=True), pipeline de digest estático, y GitHub Pages deployment. Todo para mantener apps en producción. |
| version | 1.2.0 |
| author | Hermes Agent |
| tags | ["devops","nan","deploy","cron","scripts","automation","production","github-pages"] |
DevOps Operations — Patrones de Producción
Patrones operativos para mantener aplicaciones en producción.
Tabla de Contenidos
- Código → NaN: Push y Verificación — edit → commit → pull --rebase → push → verify
- NaN Deploy Troubleshooting — 502, cache, TDZ, OOM
- Cron Jobs con Scripts — no_agent=True, Python scripts
- Static Digest Pipeline — Fetch API → scoring → JSON → HTML → Pages
- GitHub Pages + Vite — base path, crossorigin, deploys
0. Código → NaN: Flujo de Push y Verificación
⚠️ Regla de oro: Después de modificar código de un app desplegada en NaN, el trabajo NO está completo hasta que el push y la verificación en producción están hechos.
El flujo completo para cualquier cambio de código en apps NaN:
[1] Hacer cambios en local (dashboard.html, server.js, etc.)
[2] Verificar que funcionan localmente (curl localhost, revisar sintaxis)
[3] git add -A && git commit -m "fix: descripción clara"
[4] git pull --rebase origin main ← CRÍTICO: la app auto-commitea database.json vía syncGitHub
[5] git push origin main
[6] Esperar ~2 min a que NaN detecte el push y redeployee
[7] Verificar en producción: curl -s https://<app>.apps.nan.builders/healthz
[8] Verificar el cambio específico: curl endpoint, grep en HTML, etc.
Pitfall: git push rechazado por cambios remotos
- La app tiene
syncGitHub() que auto-commitea data/database.json tras cada mutación
- El repo local se queda detrás del remoto
- Siempre hacer
git pull --rebase antes de push (nunca merge — mantiene historia limpia)
- Síntoma:
! [rejected] main -> main (fetch first)
Pitfall: No verificar en producción
Pitfall: Asumir que el HTML se actualiza automáticamente
- NaN reconstruye el contenedor completo con Kaniko
- Los archivos estáticos (HTML, CSS, JS) están dentro de la imagen Docker
- No hay hot-reload — cada cambio requiere nuevo build
- Tiempo típico build+deploy: 1-5 min
🔥 Health check que miente: key existe pero no funciona
Patrón común: El endpoint /healthz verifica que la variable de entorno ORS_API_KEY exista en .env (string truthy check), pero NO hace una llamada real a la API para verificar que la key sea válida.
Síntoma: curl /healthz devuelve {"ors_api": true} pero las llamadas reales a la API retornan 403 Access disallowed o 401 Unauthorized.
Fix: El healthcheck debe hacer una llamada real (o al menos validar el formato de la key) además de verificar que exista:
checks.ors_api = !!process.env.ORS_API_KEY;
const testResp = await fetch('https://api.openrouteservice.org/v2/isochrones/driving-car', {
method: 'POST',
headers: { 'Authorization': process.env.ORS_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ locations: [[0,0]], range: [1] })
});
checks.ors_api = testResp.ok;
Pitfall: Hacer la llamada real en cada request de healthcheck es lento (200-500ms). Mejor cachear el resultado y re-validar cada 5 minutos.
🔥 .env loader manual en Node.js (sin dotenv)
Patrón: Node.js NO carga .env automáticamente (salvo --env-file=.env en Node 20.6+). Si no quieres dependencia de dotenv, añade un loader manual al inicio de server.mjs:
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf-8');
for (const line of envContent.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIdx = trimmed.indexOf('=');
if (eqIdx > 0) {
const key = trimmed.slice(0, eqIdx).trim();
const val = trimmed.slice(eqIdx + 1).trim();
if (!process.env[key]) process.env[key] = val;
}
}
}
Pitfall: El loader SOLO carga variables que NO existen en process.env. Si haces source .env en bash ANTES de lanzar el servidor, las variables ya existen y el loader las ignora. Para testing: simplemente node server.mjs (el loader hace el trabajo).
🔥 Git push falla por archivos grandes (OOM)
Patrón: Proyectos con datos generados (GTFS, JSON grandes, caches) pueden acumular cientos de MB en data/. Git intenta hacer push de todo el history y muere con pack-objects died of signal 9 (OOM en VMs con 2GB RAM).
Síntoma: git push falla con pack-objects died of signal 9 o remote end hung up unexpectedly.
Fix inmediato:
echo -e "\ndata/gtfs/\ndata/gtfs-cache/\n*.json\n!data/ciudades-*.json\n!data/codigos-postales-spain.json" >> .gitignore
git rm -r --cached data/gtfs/ data/gtfs-cache/
git rm --cached data/poblacion-cp.json data/salarios-*.json data/precios-vivienda.json
git add -A && git commit -m "chore: remove large data files from tracking"
git push origin main
Si el history es demasiado grande (>100MB): Crear repo fresco con solo código fuente:
cd /tmp && mkdir fresh-repo && cd fresh-repo && git init
cp -r /path/to/original/js . && cp -r /path/to/original/css .
cp /path/to/original/server.mjs /path/to/original/index.html .
git remote add origin <url> && git push --force origin main
Prevención: SIEMPRE añadir data/ grande a .gitignore ANTES del primer commit. Los archivos GTFS raw pueden ocupar 750MB+.
1. NaN Deploy Troubleshooting
TDZ (Temporal Dead Zone) — #1 causa de 502:
const/let usada antes de declaración → crash silencioso → Cloudflare 502
- Prevention: ordenar todas las
const al inicio de la función
NaN cachea contenedor Docker:
- Después de cambios JS/CSS, NaN puede servir versión antigua durante horas
- Soluciones: cambiar Dockerfile → renombrar repo → eliminar/recrear espacio
Scripts de verificación:
verify-nan-deploy.sh <base-url> — compara hashes MD5 locales vs remotos
git commit --allow-empty -m "chore: trigger redeploy" && git push — trigger de redeploy
Server crash silencioso:
- Container "Running" pero endpoints devuelven 502 con ~3-4s
- Fix:
process.on('uncaughtException') + fallback en route handlers
🔥 Browser tool cache agresivo — HTML nuevo no se sirve
Error real (2026-06-11): Tras hacer commit+push y redeploy en NaN, el browser tool seguía sirviendo HTML con JS antiguo. typeof THREE === 'undefined' aunque el CDN estaba en el HTML nuevo. El browser tool cachea el HTML y los scripts inline agresivamente.
Síntoma: curl desde terminal muestra HTML nuevo, pero browser_console(expression='typeof THREE') devuelve undefined. Los scripts CDN aparecen en el HTML pero no se ejecutan.
Soluciones (en orden de efectividad):
- Forzar redeploy con un commit mínimo en
database.json (o cualquier archivo servido por el server) → invalida cache de NaN
- Navegar con timestamp:
browser_navigate(url + '?t=' + Date.now()) — fuerza recarga del HTML
- Esperar 2-3 minutos — el cache de Cloudflare/NaN se expira
- Verificar con curl antes de confiar en el browser tool:
curl -s https://app.apps.nan.builders/ | grep 'three.min.js'
Regla: Si el HTML sirve correctamente (verificado con curl) pero el browser tool muestra comportamiento antiguo → es cache. No buscar bugs donde no los hay.
Puerto del Dockerfile ≠ Container port de NaN — causa común de 502:
- NaN tiene un campo Container port en la config del espacio (Settings > Container port)
- Si el servidor escucha en otro puerto (ej. 3000) pero NaN espera 7070 → 502 inmediato
- Fix: sincronizar ambos. Opción A: cambiar
ENV PORT=7070 + EXPOSE 7070 en Dockerfile. Opción B: cambiar Container port en NaN a 3000
- Recomendado: Opción A (Dockerfile), así el build es autónomo y no depende de config manual de NaN
- Verificar:
curl -s -o /dev/null -w "%{http_code}" https://<app>.apps.nan.builders/ debe dar 200 tras el build
Dockerfile faltante — error silencioso de Kaniko:
NaN build succeeded pero deployment stuck en "pending":
- A veces el build de Kaniko termina con éxito (imagen creada en registry) pero NaN no despliega el contenedor y se queda en estado "pending" indefinidamente.
- Síntoma: build history muestra "succeeded" con imagen, pero la URL pública da 404 y el status de la app es "pending".
- Causa probable: NaN no asigna recursos al contenedor (problema de orquestación interna) o el webhook de deploy no se dispara tras el build.
- Fixes:
- Desde la UI de NaN, darle a "Deploy" o "Restart" manualmente
- Cambiar el Container port en Settings y hacer deploy de nuevo (fuerza re-asignación)
- Cambiar el puerto en el código (
server.js + Dockerfile), pushear, y esperar nuevo build+deploy
- Si nada funciona, borrar la app y crearla de nuevo desde cero
- Prevención: no hay forma segura de evitarlo — es un problema de la plataforma NaN, no del código.
Dashboard dual: local backend + NaN frontend:
- El dashboard de control (monitorización del sistema) tiene dos caras:
- Local (microVM, puerto 4040): backend con datos reales del sistema (CPU, RAM, procesos, ChromaDB, crons). Usa
execSync, os, fs para datos en vivo.
- NaN (contenedor): versión visual que consume APIs del local. Como el contenedor no ve el sistema real, los endpoints deben tener fallbacks graceful (try/catch con datos de ejemplo).
- Flujo de creación:
- Desarrollar y testear localmente primero (el microVM tiene todos los datos reales)
- Crear Dockerfile y subir a GitHub
- Conectar repo a NaN como app
- El contenedor de NaN no tiene acceso a ChromaDB local → el endpoint
/api/skills debe devolver { status: 'disconnected' } gracefulmente
- El contenedor de NaN no ve procesos del host →
/api/processes debe tener fallback
- Puerto: elegir uno que no choque con otras apps. El puerto 4000 está ocupado por el ESIOS Dashboard (u otro proyecto de David). Usar 4040 o 6060 para nuevos dashboards.
- Auth: Basic Auth con contraseña vía env var
DASH_PASSWORD
- Auto-refresh: frontend con
setInterval(fetch, 5000) para datos en vivo
- Repo privado:
github.com/Ntizar/Mastermind-Dashboard
- Flujo de creación de dashboard desde cero:
- Crear repo privado en GitHub via API REST (
curl -X POST -H "Authorization: token $GITHUB_TOKEN" ...)
- Inicializar git local, hacer primer commit, pushear
- Desarrollar backend (Express) y frontend (HTML+CSS+JS) localmente
- Testear en localhost con datos reales del microVM
- Crear Dockerfile y entrypoint.sh
- Pushear todo → NaN detecta el push y construye automáticamente
- Cuidado: el primer push NO debe incluir
node_modules/ — añadir .gitignore antes del primer commit o limpiar con git rm -r --cached node_modules después
- Cuidado: el Dockerfile debe estar en el repo ANTES de que NaN intente construir, o el build fallará con "Dockerfile not found"
Infinite recursion → OOM:
- 4GB RAM hard limit, sin swap
- Fix: reemplazar recursión con fetch único
🔥 Verificar que patch realmente modificó el archivo
El tool patch puede reportar éxito sin modificar el archivo (fuzzy matching no encontró el string exacto, o el archivo fue leído parcialmente con offset/limit). Siempre verificar después de cada patch:
git diff --stat
grep "nuevo_contenido" js/archivo.js
Síntoma: patch dice success: true pero el archivo en disco sigue igual. El commit pusha código viejo. El deploy sirve versión obsoleta. Bugs "fantasma" que no se explican.
Fix: Si el patch no aplicó, usar write_file para reescribir el archivo completo en vez de intentar otro patch. Es más seguro para archivos pequeños (<500 líneas).
8. Cron Jobs con Scripts
⚠️ cronjob tool no disponible en esta VM:
- El
cronjob tool no existe en el entorno actual — no hay crontab, no hay daemon cron, no hay systemd timers
- Los scripts de mantenimiento se guardan en
/hermes-home/scripts/ y se ejecutan manualmente o desde un cron externo (SSH desde otra máquina)
- Para automatizar: configurar cron en máquina local que SSH al VM, o usar systemd timer en el VM
- Ejemplo: script
/hermes-home/scripts/mastermind-weekly-maintenance.sh (Domingo 05:00 UTC)
Patrón de scripts de mantenimiento:
- Script en
/hermes-home/scripts/ con shebang #!/bin/bash
- Script usa
set -e y loguea a /var/log/<name>.log
- Script incluye health checks antes y después de cada paso
- Script hace
git add -A && git commit && git push al final
Pitfalls:
- Script path: SOLO nombre de archivo (el scheduler añade el prefix)
- Schedule usa UTC
- Scripts ejecutan en sesión aislada → no tienen contexto de chat
- Scripts deben incluir retry logic para APIs externas (3 intentos, 2s delay)
- No usar
cronjob tool — no existe en esta VM. Crear scripts bash ejecutables manualmente.
3. Static Digest Pipeline
Pipeline para feeds periódicos:
- Fetch API externa
- Normalización y scoring heurístico
- Generar JSON + HTML
- Deploy a GitHub Pages
Ver skill devops/static-digest-pipeline para la implementación completa.
4. Deploy Audit — Verificación de despliegues
Procedimiento sistemático para auditar y verificar despliegues en NaN.builders.
Pasos
- Descargar deploy remoto y comparar con local (
diff)
grep -c por elementos clave en ambos archivos
- Verificar git status y últimos commits
- Verificar endpoints secundarios
Checklist de integridad HTML
Pitfalls
- NaN bloquea curl desde ciertas IPs (403) → usar
curl -A "Mozilla"
- Tamaños iguales ≠ contenido idéntico → usar
diff