esios-nan-deploy
Procedimiento de deploy de proyectos Node.js en NaN.builders — Dockerfile, Kaniko, variables de entorno, puertos y troubleshooting.
소스 정보
- 저장소
- Ntizar/MasterMind
- 최근 소스 활동
- 2026년 9월 4일 10:27
- 감지된 SKILL.md 언어
- 스페인어
- 스타
- 2
- 포크
- 0
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
파일 탐색기
10 개 파일SKILL.md 표시 중
SKILL.md
소스 지침 · 읽기 전용 미리보기- name
- esios-nan-deploy
- description
- Procedimiento de deploy de proyectos Node.js en NaN.builders — Dockerfile, Kaniko, variables de entorno, puertos y troubleshooting.
- version
- 2.5.0
- author
- Ntizar
- tags
- ["esios","deploy","nan","docker"]
- related_skills
- ["nan-puerto-desajuste","nan-pending-root-cause"]
# Deploy en NaN.builders
Guía para desplegar proyectos Node.js en la plataforma NaN.builders (microVMs KVM/QEMU).
## Estructura del deploy
1. **Push a GitHub** → NaN detecta el cambio y hace build con Kaniko
2. **Kaniko build** → construye la imagen Docker sin daemon
3. **Auto-deploy** → si el build funciona, se despliega automáticamente
### Patrón de archivos .env (CRÍTICO)
Cada proyecto necesita estos 3 archivos:
```
proyecto/
├── .env ← local, NO en Git (desarrollo)
├── .env.example ← SÍ en Git (documentación de variables)
├── .dockerignore ← excluye node_modules, .git (NO .env — se copia en contenedor)
└── .gitignore ← excluye .env
```
**⚠️ Pitfall: `NAN_API` no se hereda en el contenedor de NaN**
Las variables de entorno del host (`process.env.NAN_API` en la sesión de Hermes) **NO están disponibles dentro del contenedor Docker de NaN**. El contenedor solo tiene:
1. Variables configuradas en la pestaña **Env** del dashboard NaN
2. Variables copiadas desde archivos del proyecto (`.env` copiado por `COPY . .`)
**Patrón de fallback para tokens API** (implementado en server.js de dieta):
```javascript
function getNanToken() {
// 1) process.env.NAN_API (NaN dashboard Env)
if (process.env.NAN_API) return process.env.NAN_API;
// 2) NTIZAR_API (otro nombre posible)
if (process.env.NTIZAR_API) return process.env.NTIZAR_API;
// 3) Leer .env del proyecto (fallback local)
try {
const envContent = fs.readFileSync(path.join(__dirname, '.env'), 'utf8');
const match = envContent.match(/^NAN_API=(.+)$/m);
if (match) return match[1].trim();
} catch (e) {}
return '';
}
```
**Configuración para deploy:**
- Crear `.env` con el token real en el directorio del proyecto
- Añadir `.env` a `.gitignore` (NUNCA subir a Git)
- **NO** añadir `.env` a `.dockerignore` (se necesita en el contenedor)
- El Dockerfile `COPY . .` lo copiará automáticamente
**.env.example** — template con nombres y placeholders:
```
ESIOS_API_TOKEN=tu_token_aqui
PORT=4000
NAN_API_KEY=tu_clave_aqui
```
**.env** — valores reales (solo local):
```
ESIOS_API_TOKEN=abc123...
PORT=4000
```
### Dónde configurar variables en producción
- **NaN:** pestaña **Env** en la web de NaN → dashboard del espacio
- **NUNCA** en el código, commits, o .env en Git
- Se acceden via `process.env.VAR_NAME` en Node.js
### Validación en código (3 patrones)
**Patrón A — Exit early (recomendado para obligatorias):**
```javascript
// src/config/env.js
const REQUIRED = ['ESIOS_API_TOKEN'];
function loadEnv() {
const missing = REQUIRED.filter(k => !process.env[k]);
if (missing.length > 0) { console.error(`Faltan: ${missing.join(', ')}`); process.exit(1); }
return { ESIOS_TOKEN: process.env.ESIOS_API_TOKEN, ... };
}
```
**Patrón B — Health endpoint con checks:**
```javascript
app.get('/readyz', (req, res) => {
const esiosReady = Boolean(process.env.ESIOS_API_TOKEN);
res.status(esiosReady ? 200 : 503).json({
status: esiosReady ? 'ready' : 'degraded',
checks: { esios_api_token: esiosReady }
});
});
```
**Patrón C — Fallback con generación temporal:**
```javascript
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD
|| crypto.randomBytes(24).toString('base64url'); // Temporal
```
## Dockerfile mínimo para NaN
```dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=4000
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=deps --chown=appuser:appgroup /app/node_modules ./node_modules
COPY package.json ./
COPY server.js ./
COPY src/ ./src/
COPY public/ ./public/
COPY data/ ./data/
USER appuser
EXPOSE 4000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:4000/healthz || exit 1
CMD ["node", "server.js"]
```
## 🚧 Primer deploy: crear el espacio en NaN
Antes de que cualquier URL funcione, **hay que crear el espacio en la web de NaN**:
1. Ir a [cloud.nan.builders](https://cloud.nan.builders) e iniciar sesión
2. Crear un nuevo **Space** apuntando al repositorio GitHub
3. Configurar el puerto (debe coincidir con `EXPOSE` del Dockerfile)
4. El espacio tarda segundos en aprovisionarse
**Síntoma de espacio no creado:** La URL (`<app>-<org>-<project>.apps.nan.builders`) devuelve **404 directamente de Cloudflare** (no 502, no timeout). El DNS resuelve a IPs de Cloudflare pero no hay backend.
> ⚠️ No basta con tener el Dockerfile en el repo. **El espacio debe existir en NaN** para que el auto-deploy por polling funcione. Sin espacio, el push a GitHub no tiene efecto visible.
### ⚠️ CRÍTICO: Renombrar repo en GitHub rompe deploy NaN
Cuando renombras un repositorio en GitHub (`Ntizar/TimeIneco` → `Ntizar/Time`), **el espacio NaN existente deja de funcionar** porque el DNS de NaN apunta al nombre viejo. El espacio NaN sigue vinculado al repo viejo y el polling de GitHub ya no lo detecta.
**Síntomas:**
- URL anterior (`timeineco-ntizar-ntizar.apps.nan.builders`) devuelve 404 o 502
- URL nueva (`time-ntizar-ntizar.apps.nan.builders`) no existe
- Push al repo renombrado no genera redeploy
**Fix:**
1. Ir a [cloud.nan.builders](https://cloud.nan.builders)
2. Crear un **nuevo espacio** apuntando al repo renombrado (`Ntizar/Time`)
3. Configurar variables de entorno (ORS_API_KEY, NAP_API_KEY, PORT)
4. Configurar Container Port (debe coincidir con EXPOSE del Dockerfile)
5. Esperar primer build Kaniko (~2-5 min)
6. Verificar: `curl -s https://time-ntizar-ntizar.apps.nan.builders/healthz`
**NO se puede renombrar un espacio NaN** — hay que crear uno nuevo y (opcionalmente) borrar el viejo tras confirmar que el nuevo funciona.
**Patrón de URL:** `<espacio>-<owner>-<owner>.apps.nan.builders` — el owner aparece duplicado en la URL.
### Patrón de URL
NaN.builders asigna URLs con el patrón:
```
<app-name>-<owner>-<owner>.apps.nan.builders
```
Donde `app-name` es el nombre del espacio en NaN, y `owner` es el usuario/organización de GitHub. El owner aparece **duplicado** en la URL.
### Opciones de hosting alternativas
Si el espacio NaN aún no existe y la URL es urgente:
| Opción | Requisito | Comando |
|--------|-----------|---------|
| **GitHub Pages** | Repo público (o plan pago para privado) | `gh repo edit --visibility public && gh api -X POST repos/:owner/:repo/pages -f source.branch=gh-pages` |
| **Surge.sh** | Login interactivo (email + password) primero | `npm i -g surge && surge ./dist nombre.surge.sh` |
| **Vercel/Netlify** | Login vía CLI | `npx vercel deploy --prebuilt --prod` |
### ⚠️ `.env` en `.dockerignore` mata tokens de API
Si añades `.env` a `.dockerignore`, el archivo NO se copia al contenedor Docker. El servidor arranca pero `process.env.NAN_API` no existe (NaN no hereda env vars del host) y el fallback a `.env` local también falla (no está en el contenedor). **Todos los endpoints de IA (estimación comida, ejercicio, coach) devuelven "Token no configurado".**
**Síntoma:** App funciona, login OK, datos se guardan, pero todo lo que usa IA falla silenciosamente.
**Fix:** Quitar `.env` de `.dockerignore`. El `.env` DEBE estar en el contenedor para que el fallback `fs.readFileSync('.env')` funcione. El `.env` ya está en `.gitignore` (no se sube a GitHub), pero el `COPY . .` del Dockerfile lo copia al contenedor.
```dockerignore
# ❌ MAL — .env no llega al contenedor
node_modules/
.env
# ✅ BIEN — .env se copia al contenedor
node_modules/
.git
.gitignore
```
### ⚠️ CRÍTICO: `"type": "module"` + `require()` = crash silencioso
Si `package.json` tiene `"type": "module"`, Node.js ejecuta TODOS los `.js` como ESM. Si el server usa `require()` (CommonJS), el contenedor **crashea al arrancar** y el pod se queda en `Pending` — **sin error visible en el dashboard de NaN**.
**Síntomas:** Build Kaniko exitoso, `Current Image` se llena, pero URL siempre devuelve 503 o 404.
**Causa típica:** Dockerfile con `RUN echo 'require("http")...' > server.js` (inline CommonJS) en un proyecto Vite que tiene `"type": "module"`.
**Fix:**
1. Crear `server.mjs` como archivo separado en el repo (ESM puro: `import http from "node:http"`)
2. Usar `COPY --chown=appuser:appgroup server.mjs ./` en el Dockerfile
3. Cambiar `CMD ["node", "server.js"]` → `CMD ["node", "server.mjs"]`
4. **NUNCA** hacer `echo 'require(...)' > server.js` en un proyecto con `"type": "module"`
**Verificación:** `grep '"type"' package.json` + `grep 'require(' server.js` — si ambos dan resultado, hay conflicto ESM/CJS.
### Puerto
- El espacio de NaN puede estar configurado en cualquier puerto (3500, 3700, 4000, 4500, 6000, etc.)
- **El Dockerfile EXPOSE debe coincidir con el puerto del espacio**
- **El server.js debe usar `process.env.PORT || <puerto>` como default**
- **Para sitios estáticos con nginx: el EXPOSE y el `listen` en nginx.conf deben coincidir** — no asumir que nginx escucha en 80 por defecto
- Si no coinciden → 502 Bad Gateway
- ⚠️ **NUNCA intentar escuchar en puerto 80 desde un contenedor no-root**. Cuando el Dockerfile usa `USER appuser` (que es obligatorio), el bind a puerto 80 **falla silenciosamente** — Node.js no levanta, el proceso muere, y el pod se queda en Pending. **Solo escuchar en el puerto configurado**: `http.createServer(handler).listen(process.env.PORT || 3700, "0.0.0.0")`
- **HEALTHCHECK debe apuntar al puerto configurado**, no a 80: `CMD wget -qO- http://localhost:3700/healthz || exit 1`
### Usuario no-root (crítico)
- **NaN BLOQUEA contenedores que ejecutan como root** — el pod se queda en "Pending" indefinidamente aunque Kaniko construya la imagen correctamente
- **Siempre crear usuario no-root**: `RUN addgroup -S appgroup && adduser -S appuser -G appgroup`
- **Siempre cambiar antes del CMD**: `USER appuser`
- **Ajustar permisos** de todos los archivos copiados: `RUN chown -R appuser:appgroup /app`
- Es el **mismo patrón** indispensable que usa esios-dashboard
- Síntoma: Kaniko build exitoso (se ve en logs) pero URL devuelve 404 (Cloudflare) → el pod está en Pending → revisar que el Dockerfile use no-root
### NaN auto-polling (semi-automático)
- NaN **NO** usa webhooks de GitHub
- NaN **SÍ** hace polling periódico del repositorio GitHub (cada ~1-5 minutos, depende de carga)
- Cuando detecta un nuevo commit en `main`, **reconstruye y redeploya automáticamente**
- Si la build anterior falló (ej: faltaba Dockerfile, o el contenedor era root), NaN **reintenta con el siguiente commit** — no hace falta ir al dashboard obligatoriamente
- Para forzar inmediatamente: dashboard → [cloud.nan.builders](https://cloud.nan.builders) → **Redeploy**
- `git commit --allow-empty -m "trigger redeploy" && git push` **SÍ funciona** indirectamente: el push a GitHub → NaN detecta cambio en su próximo ciclo de polling → reconstruye
### Variables de entorno
- Se configuran en la web de NaN → pestaña **Env**
- NO se suben por Git
- Se necesitan para: API keys, tokens, URLs
- `.env.example` en el repo sirve como documentación
### Kaniko
- No soporta `--build-arg` para secrets
- Las secrets VAN en variables de entorno, no en el Dockerfile
- El build puede fallar si `package-lock.json` no coincide con `package.json`
- **⚠️ `npm ci` falla si package-lock.json está desincronizado** — si añades nuevas dependencias (bcryptjs, express-session, etc.), el lockfile queda obsoleto. Regenerar con `npm install` antes de commit. Síntoma: build Kaniko exitoso pero contenedor crash al arrancar porque faltan módulos.
- **Error `error resolving dockerfile path: please provide a valid path to a Dockerfile within the build context with --dockerfile`** → No hay Dockerfile en la raíz del repo, o el Dockerfile está mal nombrado (debe llamarse exactamente `Dockerfile`)
- Si el build falla (ej: no existía Dockerfile), NaN reintenta automáticamente con el siguiente commit detectado por polling
### Dockerfile single-stage con build interno + Node.js (Vite static)
Para proyectos Vite que necesitan build y `node:alpine` en una sola etapa (más simple que multi-etapa):
```dockerfile
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3700
# 1. Dependencias
COPY package.json package-lock.json ./
RUN npm ci --include=dev # --include=dev para que Vite esté disponible en build
# 2. Código fuente
COPY index.html vite.config.js ./
COPY public/ ./public/
COPY src/ ./src/
# 3. Build
RUN npx vite build
# 4. Usuario no-root (requisito NaN)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# 5. Servidor HTTP Node.js embebido (SPA + multi-puerto)
RUN echo 'const http = require("http"); \
const fs = require("fs"); \
const path = require("path"); \
const DIST = path.join(__dirname, "dist"); \
const MIME = { \
".html": "text/html", ".css": "text/css", \
".js": "application/javascript", ".json": "application/json", \
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", \
".ico": "image/x-icon", ".woff2": "font/woff2" \
}; \
function handler(req, res) { \
GitHub에서 보기이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기