NutriFlow — Infraestructura Cloud

render.yaml · Dockerfile · Deploy API · Generado por CULTIVA IA · Junio 2026

render.yaml v1.0 IaC
Arquitectura de servicios
WEB · API
nutriflow-api
Fastify + Node.js 20. Auto-deploy desde main. Health check en /api/health
standard 2vCPU 2GB RAM auto-scale 1→4
WEB · STATIC
nutriflow-frontend
React 18 + Vite SPA. Cache inmutable para assets, no-cache para index.html
static CDN global SPA routing
WORKER
pdf-email-worker
Generación de informes PDF y envío de emails. Consume cola Redis
starter background Redis queue
CRON
daily-maintenance
Limpieza de sesiones + agregación de métricas. Ejecuta a las 03:00 UTC
0 3 * * * Node.js
DATABASE
nutriflow-db
PostgreSQL 16 gestionada. IP allowlist vacía = acceso solo desde Render
standard PG 16 backups diarios
REDIS
nutriflow-redis
Cache de sesiones + cola de jobs. Conexión interna entre servicios
starter in-memory
FLUJO DE DEPLOY
GitHub main
Render Build
Docker Multi-stage
Health Check /api/health
api.nutriflow.app
PR abierto
Preview environment
pr-123.nutriflow-api.onrender.com
Y
render.yaml
Infraestructura como código · 6 servicios
# render.yaml — NutriFlow SaaS · Infraestructura completa como código
# CULTIVA IA · deploy en Render.com desde repositorio GitHub

envVarGroups:
  - name: shared-config
    envVars:
      - key: LOG_LEVEL
        value: info
      - key: CORS_ORIGIN
        value: https://app.nutriflow.app
      - key: SENTRY_DSN
        sync: false             # Configurar manualmente en dashboard
      - key: NODE_ENV
        value: production

services:

  # ── API Web Service ──────────────────────────────────────────────
  - type: web
    name: nutriflow-api
    runtime: docker
    dockerfilePath: ./Dockerfile
    region: frankfurt          # EU region para cumplimiento GDPR
    plan: standard             # 2 vCPU · 2 GB RAM
    autoDeploy: true           # Deploy automático en push a main
    branch: main
    healthCheckPath: /api/health
    numInstances: 1
    scaling:
      minInstances: 1
      maxInstances: 4
      targetCPUPercent: 65
      targetMemoryPercent: 70
    customDomains:
      - domain: api.nutriflow.app   # SSL auto via Let's Encrypt
    envVars:
      - fromGroup: shared-config
      - key: DATABASE_URL
        fromDatabase:
          name: nutriflow-db
          property: connectionString
      - key: REDIS_URL
        fromService:
          name: nutriflow-redis
          type: redis
          property: connectionString
      - key: JWT_SECRET
        generateValue: true     # Render genera valor aleatorio seguro
      - key: PORT
        value: "3000"
      - key: PDF_BUCKET_URL
        sync: false             # Configurar con S3/R2 compatible URL

  # ── Background Worker (PDF + Email) ──────────────────────────────
  - type: worker
    name: pdf-email-worker
    runtime: docker
    dockerfilePath: ./Dockerfile.worker
    region: frankfurt
    plan: starter
    envVars:
      - fromGroup: shared-config
      - key: DATABASE_URL
        fromDatabase:
          name: nutriflow-db
          property: connectionString
      - key: REDIS_URL
        fromService:
          name: nutriflow-redis
          type: redis
          property: connectionString
      - key: RESEND_API_KEY
        sync: false
      - key: WORKER_CONCURRENCY
        value: "5"

  # ── Frontend React SPA ───────────────────────────────────────────
  - type: web
    name: nutriflow-frontend
    runtime: static
    buildCommand: cd frontend && npm ci && npm run build
    staticPublishPath: frontend/dist
    branch: main
    customDomains:
      - domain: app.nutriflow.app
    headers:
      - path: /*
        name: Cache-Control
        value: public, max-age=31536000, immutable
      - path: /index.html
        name: Cache-Control
        value: no-cache
      - path: /*
        name: X-Frame-Options
        value: DENY
    routes:
      - type: rewrite
        source: /*
        destination: /index.html   # SPA fallback para React Router

  # ── Cron: Mantenimiento Diario ───────────────────────────────────
  - type: cron
    name: daily-maintenance
    runtime: node
    buildCommand: npm ci
    startCommand: npm run maintenance
    schedule: "0 3 * * *"          # 03:00 UTC cada día
    region: frankfurt
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: nutriflow-db
          property: connectionString
      - key: TASKS
        value: cleanup-sessions,aggregate-metrics,purge-pdfs

databases:
  - name: nutriflow-db
    plan: standard
    databaseName: nutriflow_prod
    postgresMajorVersion: 16
    region: frankfurt
    ipAllowList: []               # Solo acceso desde servicios Render
    user: nutriflow_admin

  - name: nutriflow-redis
    plan: starter
    region: frankfurt
    maxmemoryPolicy: allkeys-lru  # Expira claves más antiguas en pico
D
Dockerfile
Multi-stage · Node.js 20 Alpine · API Fastify
# ── Stage 1: Builder ──────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app

# Copiar solo package.json para cachear dependencias
COPY package*.json ./
RUN npm ci

# Copiar código fuente y compilar TypeScript
COPY . .
RUN npm run build

# ── Stage 2: Runner (imagen mínima de producción) ─────────────────
FROM node:20-alpine AS runner
WORKDIR /app

# Usuario no-root por seguridad
RUN addgroup -g 1001 -S nodejs && adduser -S nutriflow -u 1001

# Solo artefactos de producción
COPY --from=builder --chown=nutriflow:nodejs /app/dist ./dist
COPY --from=builder --chown=nutriflow:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nutriflow:nodejs /app/package.json ./

USER nutriflow
EXPOSE 3000
ENV NODE_ENV=production

# Health check embebido en imagen
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:3000/api/health || exit 1

CMD ["node", "dist/index.js"]
TS
scripts/deploy.ts
Render API · Deploy manual + escalado programático
// scripts/deploy.ts — Control programático de servicios NutriFlow en Render
// Uso: npx ts-node scripts/deploy.ts [--scale 3] [--env staging]

const RENDER_API_KEY = process.env.RENDER_API_KEY!;
const SERVICES: Record<string, string> = {
  api:      "srv-nutriflow-api",
  worker:   "srv-nutriflow-worker",
  frontend: "srv-nutriflow-frontend",
};

/** Dispara un deploy manual en el servicio dado */
async function triggerDeploy(
  serviceKey: keyof typeof SERVICES,
  clearCache = false
): Promise<void> {
  const serviceId = SERVICES[serviceKey];
  const res = await fetch(
    `https://api.render.com/v1/services/${serviceId}/deploys`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${RENDER_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        clearCache: clearCache ? "clear" : "do_not_clear",
      }),
    }
  );
  const deploy = await res.json();
  console.log(`[${serviceKey}] Deploy iniciado: ${deploy.id} → ${deploy.status}`);
}

/** Escala horizontalmente la API */
async function scaleApi(instances: number): Promise<void> {
  if (instances < 1 || instances > 4) {
    throw new Error("Rango válido: 1–4 instancias (plan standard)");
  }
  const res = await fetch(
    `https://api.render.com/v1/services/${SERVICES.api}/scale`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${RENDER_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ numInstances: instances }),
    }
  );
  console.log(`API escalada a ${instances} instancia(s). Status: ${res.status}`);
}

/** Lista deploys recientes */
async function getDeployHistory(limit = 5) {
  const res = await fetch(
    `https://api.render.com/v1/services/${SERVICES.api}/deploys?limit=${limit}`,
    { headers: { Authorization: `Bearer ${RENDER_API_KEY}` } }
  );
  const deploys = await res.json();
  deploys.forEach((d: any) =>
    console.log(`  ${d.createdAt} — ${d.status} (${d.id})`)
  );
}

// CLI entrypoint
const args = process.argv.slice(2);
if (args.includes("--scale")) {
  const n = parseInt(args[args.indexOf("--scale") + 1], 10);
  scaleApi(n).catch(console.error);
} else {
  triggerDeploy("api");
  triggerDeploy("frontend");
}