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-commiteadata/database.jsontras cada mutación - El repo local se queda detrás del remoto
- Siempre hacer
git pull --rebaseantes de push (nunca merge — mantiene historia limpia) - Síntoma:
! [rejected] main -> main (fetch first)
Pitfall: No verificar en producción
- NaN tiene polling de ~1-5 min y cache de Cloudflare
- Un push exitoso no significa que el cambio esté servido
- Siempre verificar con curl antes de decir "está listo":
curl -s https://<app>.apps.nan.builders/ | grep "cambio esperado" # O para APIs: curl -s https://<app>.apps.nan.builders/api/entrenamientos/0 | python3 -c "import sys,json; print(json.load(sys.stdin))" - Si el cambio no está vivo tras 3 min, forzar con
git commit --allow-empty -m "chore: trigger redeploy" && git push
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:
// ❌ Miente: solo verifica que exista
checks.ors_api = !!process.env.ORS_API_KEY;
// ✅ Real: verifica que la key funcione (llamada mínima)
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; // NO sobreescribe
}
}
}
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:
# 1. Añadir al .gitignore
echo -e "\ndata/gtfs/\ndata/gtfs-cache/\n*.json\n!data/ciudades-*.json\n!data/codigos-postales-spain.json" >> .gitignore
# 2. Quitar del tracking (mantiene archivos locales)
git rm -r --cached data/gtfs/ data/gtfs-cache/
git rm --cached data/poblacion-cp.json data/salarios-*.json data/precios-vivienda.json
# 3. Commit y push
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 .
# ... copiar solo archivos necesarios
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/letusada antes de declaración → crash silencioso → Cloudflare 502- Prevention: ordenar todas las
constal 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 remotosgit 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 7070en 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 usa Kaniko para construir imágenes Docker. Si el repo no tiene
Dockerfileen la raíz, el build falla con:Error: error resolving dockerfile path: please provide a valid path to a Dockerfile within the build context with --dockerfile - Síntoma: el build history muestra "failed" sin mensaje claro de error, solo el usage de Kaniko
- Fix: crear
Dockerfileen la raíz del repo antes de conectar NaN. Mínimo viable:FROM node:20-alpine WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --only=production COPY . . EXPOSE 4040 CMD ["node", "server.js"] - Pitfall: si el repo se conectó a NaN antes de tener Dockerfile, NaN no lo detecta retroactivamente. Hay que hacer un push nuevo para triggerear el build.
- Pitfall:
npm cirequierepackage-lock.json. Si no existe, usarnpm installen su lugar. - Pitfall: no incluir
node_modules/en.gitignorehace que el build suba 600+ archivos innecesarios (como pasó con el primer push del Mastermind Dashboard). Añadir.gitignoreconnode_modules/y hacergit rm -r --cached node_modulespara limpiar.
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,fspara 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).
- Local (microVM, puerto 4040): backend con datos reales del sistema (CPU, RAM, procesos, ChromaDB, crons). Usa
- 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/skillsdebe devolver{ status: 'disconnected' }gracefulmente - El contenedor de NaN no ve procesos del host →
/api/processesdebe 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.gitignoreantes del primer commit o limpiar congit rm -r --cached node_modulesdespués - Cuidado: el Dockerfile debe estar en el repo ANTES de que NaN intente construir, o el build fallará con "Dockerfile not found"
- Crear repo privado en GitHub via API REST (
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:
# Verificar que el archivo cambió
git diff --stat
# O verificar contenido específico
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
cronjobtool no existe en el entorno actual — no haycrontab, 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 -ey 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 pushal 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
cronjobtool — 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 -cpor elementos clave en ambos archivos- Verificar git status y últimos commits
- Verificar endpoints secundarios
Checklist de integridad HTML
-
loadData()definida y llamada -
renderDashboard()definida - Hero quick status (heroPeso, heroPerdido, heroRitmo)
- Botones de acción rápida con
switchTab - Tabs con
display:noneexcepto tab activo - Sin código de dark mode residual
Pitfalls
- NaN bloquea curl desde ciertas IPs (403) → usar
curl -A "Mozilla" - Tamaños iguales ≠ contenido idéntico → usar
diff
5. Error 502 por desajuste de puertos
NaN.builders tiene 3 lugares donde se define el puerto, y deben coincidir:
| Lugar | Dónde está | Ejemplo |
|---|---|---|
| Container Port | UI de NaN → Settings del espacio | 6060 |
EXPOSE |
Dockerfile |
EXPOSE 6060 |
process.env.PORT |
server.js |
`const PORT = process.env.PORT |
Diagnóstico rápido
grep EXPOSE Dockerfile
grep 'PORT' server.js
grep -A2 HEALTHCHECK Dockerfile
Fix recomendado
Alinear todo al Container Port de NaN. O escuchar en ambos puertos (tolerante a cambios).
Healthcheck: regla de oro
El HEALTHCHECK NUNCA debe apuntar a un endpoint protegido por auth. Crear /healthz ANTES del middleware de auth.
NaN containers están AISLADOS del host
No pueden ver procesos, crons, skills, sessions ni archivos del host. Sincronizar datos vía Git.
6. Aurora Nightly — Mejora Continua Nocturna
Pipeline de 4 jobs nocturnos para mejora continua del CSS Aurora:
- Job #1 (01:00): Investigación web de tendencias CSS
- Job #2 (02:00): Análisis gap + Mejora CSS #1
- Job #3 (03:00): Mejora CSS #2
- Job #4 (04:00): Mejora CSS #3 + Reaprendizaje
Ver aurora-nightly para el procedimiento completo con investigación RSS, patrones de mejora CSS, y pitfall de no reescribir packs enteros.
GitHub Actions Nightly Pipeline
Para proyectos con CI/CD: lint → build → test → deploy en cron nocturno. Ver aurora-nightly-pipeline para el workflow YAML completo.
7. GitHub Pages + Vite
Pitfalls comunes al desplegar proyectos Vite en GitHub Pages. Referencia completa: references/vite-github-pages-deploy.md.
Checklist rápida:
-
vite.config.jstienebase: '/RepoName/'(coincide con el repo) - No hay
crossoriginen<script>ni<link>del HTML build - El JS fuente no tiene strings sin cerrar (comillas simples sin par)
- GitHub Pages activado y build desplegado en
gh-pages - El repo es público (requisito en plan free)
🔥 GitHub Pages CDN cachea JS agresivamente — versión vieja tras push
Causa: GitHub Pages usa un CDN (Fastly/Cloudflare) que cachea archivos estáticos (JS, CSS) con max-age prolongado. Cuando haces push con cambios en JS, el CDN puede seguir sirviendo la versión vieja durante minutos. El curl desde terminal sirve el HTML nuevo, pero los archivos JS referenciados en el HTML siguen siendo los viejos.
Síntomas:
curl -s https://user.github.io/repo/muestra HTML con<script src="js/main.js?v=1">(nuevo)- Pero el contenido de
js/main.js?v=1es la versión vieja (el CDN no invalidó) - El navegador ejecuta código JS obsoleto → NaN, bugs fantasma
- Los features nuevos no aparecen aunque el commit está en main
Fix: Cache busting con version query en script tags
<!-- ❌ MAL — CDN cachea el archivo sin version -->
<script type="module" src="js/main.js"></script>
<!-- ✅ BIEN — version query fuerza descarga nueva -->
<script type="module" src="js/main.js?v=4"></script>
Patrón: Incrementar ?v=N en CADA push que modifique JS/CSS:
<script type="module" src="js/main.js?v=4"></script>
<link rel="stylesheet" href="css/style.css?v=3">
Verificación de que el browser cargó la versión nueva:
// En browser console:
document.querySelectorAll('script[type="module"]')[0].src
// Debe mostrar "?v=4" (la versión nueva), no "?v=1" (la vieja)
Verificación de que el CDN sirve el contenido nuevo:
# El HTML puede estar cacheado — verificar el JS directamente
curl -s "https://user.github.io/repo/js/main.js?v=4" | head -5
# Debe mostrar el código nuevo, no el viejo
Pitfall: HTML cacheado por el browser
A veces el browser cachea el HTML mismo (no solo el JS). Aunque el CDN tiene el HTML nuevo, el browser sigue con el viejo. Fix: ?t=timestamp en la URL del HTML:
https://user.github.io/repo/index.html?t=20260630
O hard refresh: Ctrl+Shift+R / Cmd+Shift+R.
Pitfall: curl sirve nuevo pero browser sirve viejo
El curl bypassa el browser cache pero no el CDN cache. Si curl muestra el HTML nuevo pero el browser no, es browser cache. Si curl también muestra viejo, es CDN cache (esperar 2-5 min o forzar con otro push).
🔥 GitHub Pages NO funciona en repos privados con plan free
2026-06-18 (AtlasMadrid2024): POST /repos/{owner}/{repo}/pages devuelve 422: "Your current plan does not support GitHub Pages for this repository." Incluso tras crear el sitio de Pages vía API, el workflow de GitHub Actions falla en "Setup Pages" con el mismo error.
Fix: Cambiar el repo a público antes de activar Pages:
curl -X PATCH -H "Authorization: token $TOKEN" \
"https://api.github.com/repos/OWNER/REPO" \
-d '{"private":false}'
# Esperar ~5 segundos
curl -X POST -H "Authorization: token $TOKEN" \
"https://api.github.com/repos/OWNER/REPO/pages" \
-d '{"build_type":"workflow","source":{"branch":"main","path":"/"}}'
Workflows que fallan sin Pages habilitado: El step "Setup Pages" (actions/configure-pages@v5) falla → "Upload artifact" y "Deploy" se saltan. No hay error claro de "Pages not enabled" — solo falla genérica.
Detección: Si el workflow falla en "Setup Pages" y el repo es privado → Pages no está soportado en este plan.
Alternativa si no se puede hacer público: Deploy manual a gh-pages branch con git subtree o deploy a otro hosting (NaN.builders, Vercel, Netlify).
Patrón de deploy manual (cuando no hay GitHub Actions):
npm run build
sed -i 's/ crossorigin//g' dist/index.html
git init /tmp/gh-deploy && cp -r dist/* /tmp/gh-deploy/
git -C /tmp/gh-deploy remote add origin https://github.com/USER/REPO.git
git -C /tmp/gh-deploy push origin master:gh-pages --force