Saltar al contenido
Programming 2026-09-05 15 min de lectura

VPS para Node.js: requisitos y despliegue recomendado

Una aplicación Node.js pequeña consume pocos recursos, pero el heap, la compilación, las imágenes, los WebSockets y las tareas en segundo plano cambian la capacidad. El VPS debe dimensionarse con pruebas de concurrencia y memoria real, mientras el despliegue debe poder reiniciar la aplicación sin depender de una terminal abierta.

VPS para Node.js: requisitos y despliegue recomendado
#Node.js#VPS#Nginx#JavaScript
T
Equipo Terranode
Editorial

Qué VPS necesita una aplicación Node.js

Para una API ligera, un bot o una aplicación interna, 1 vCPU y 1 o 2 GB de RAM suelen ser un punto de partida. Una aplicación pública con base local, compilación de frontend o varios procesos se beneficia de 2 vCPU y 4 GB. Next.js, Puppeteer, procesamiento multimedia y trabajos intensivos pueden requerir 8 GB o separar tareas.

Tipo de cargaInicio razonablePrincipal riesgo
API REST ligera1 vCPU, 1–2 GBPicos de conexiones y consultas lentas
App web con base local2 vCPU, 4 GBCompetencia entre Node y la base
Next.js con build en servidor2–4 vCPU, 4–8 GBPicos de RAM durante compilación
WebSockets persistentes2 vCPU, 4 GB o másConexiones, buffers y despliegues sin corte
Procesamiento de archivos4 vCPU, 8 GB o workers separadosCPU y memoria por tarea

No conviertas visitas mensuales en RAM mediante una fórmula fija. Diez mil visitas distribuidas pueden ser triviales; doscientas conexiones simultáneas que mantienen WebSockets o generan informes pueden saturar la misma máquina. Mide solicitudes por segundo, concurrencia, tiempo de CPU, RSS y latencia del event loop.

Elegir una versión de Node y fijarla

Usa una versión LTS soportada por tus dependencias y evita instalar “latest” sin control. El mismo proyecto debe construir y ejecutar con la versión probada. Declárala en .nvmrc, package.json o la imagen de contenedor y registra el cambio en el repositorio.

Con NodeSource, el repositorio de la distribución o un gestor de versiones puedes instalar Node en Ubuntu. El método importa menos que la reproducibilidad. Comprueba el binario que usará el servicio, porque systemd no carga necesariamente el mismo PATH de tu shell:

bash
node --version
npm --version
which node
readlink -f "$(which node)"

Evita ejecutar la aplicación con un Node ubicado dentro del directorio personal de root. Un usuario de despliegue y una ruta estable como /usr/bin/node o /usr/local/bin/node simplifican el arranque tras reinicios.

Preparar un usuario y directorios sin privilegios

El proceso no necesita root para escuchar en un puerto interno. Crea un usuario de sistema y separa releases, archivos persistentes y configuración:

bash
sudo useradd --system --create-home --shell /usr/sbin/nologin nodeapp
sudo mkdir -p /srv/nodeapp/releases /srv/nodeapp/shared/uploads
sudo chown -R nodeapp:nodeapp /srv/nodeapp
sudo install -o root -g nodeapp -m 640 /dev/null /etc/nodeapp.env

La aplicación puede escuchar en 127.0.0.1:3000; Nginx publica HTTPS. No concedas CAP_NET_BIND_SERVICE ni ejecutes como root solo para usar el puerto 80. Mantén los archivos de código como solo lectura para el proceso cuando el flujo de despliegue lo permita, y otorga escritura únicamente a uploads, temporales o cachés definidos.

Instalar dependencias de forma reproducible

npm ci instala exactamente el lockfile y falla si no coincide con package.json. Es preferible a npm install en el servidor porque reduce cambios inesperados entre despliegues:

bash
cd /srv/nodeapp/releases/20260905-1500
npm ci
npm test
npm run build
npm prune --omit=dev

Hay proyectos que necesitan dependencias de desarrollo para compilar. En ese caso instala todo durante el build y elimina lo innecesario antes de activar el release, o construye en CI y entrega un artefacto. No uses npm audit fix --force en producción: puede cambiar versiones mayores y romper el proyecto sin pruebas.

Las dependencias nativas deben compilarse para el sistema y la arquitectura donde se ejecutan. Copiar node_modules desde Windows a Linux no funciona de manera fiable. Si construyes fuera del VPS, usa un entorno compatible con producción.

Variables de entorno y secretos

No guardes .env en Git ni pegues secretos dentro del archivo de servicio. Un archivo administrado fuera del release facilita rotaciones:

text
NODE_ENV=production
HOST=127.0.0.1
PORT=3000
DATABASE_URL=postgresql://nodeapp:[email protected]/nodeapp
LOG_LEVEL=info

Protege /etc/nodeapp.env con modo 640, propietario root y grupo de la aplicación. Node y muchas bibliotecas leen variables al arrancar; un cambio no toma efecto hasta reiniciar. Planifica rotaciones con credenciales superpuestas cuando el proveedor lo permita, actualiza la aplicación y revoca la clave anterior después de comprobar tráfico.

Nunca vuelques todo process.env a logs para depurar. También evita incluir tokens en URLs, porque proxies y herramientas de observabilidad pueden registrarlas. En errores, muestra el nombre de la variable faltante, no su valor.

Supervisar Node.js con systemd

systemd ya está disponible en Ubuntu y puede iniciar, reiniciar y registrar la aplicación. Un servicio sencillo:

ini
[Unit]
Description=Aplicacion Node.js
After=network.target

[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/srv/nodeapp/current
EnvironmentFile=/etc/nodeapp.env
ExecStart=/usr/bin/node dist/server.js
Restart=on-failure
RestartSec=3
TimeoutStopSec=30
KillSignal=SIGTERM
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Guárdalo en /etc/systemd/system/nodeapp.service, recarga unidades y comprueba el arranque:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now nodeapp
sudo systemctl status nodeapp --no-pager
sudo journalctl -u nodeapp -n 100 --no-pager

Restart=on-failure recupera cierres inesperados sin ocultar paradas deliberadas. Si el proceso entra en un bucle de reinicio, systemd limitará intentos; investiga la causa en logs en vez de aumentar la frecuencia indefinidamente.

PM2: cuándo elegirlo y cómo evitar duplicar gestores

PM2 ofrece configuración en JavaScript, modo clúster y utilidades específicas de Node. Es útil cuando el equipo ya lo conoce. No ejecutes PM2 dentro de systemd mientras ambos intentan reiniciar los mismos workers sin una estrategia; usa el mecanismo de integración de PM2 o deja que uno sea el supervisor real.

Un ecosistema explícito evita comandos recordados de memoria:

javascript
module.exports = {
  apps: [{
    name: 'nodeapp',
    script: './dist/server.js',
    cwd: '/srv/nodeapp/current',
    instances: 2,
    exec_mode: 'cluster',
    max_memory_restart: '700M',
    kill_timeout: 30000,
    listen_timeout: 10000,
    env: { NODE_ENV: 'production', PORT: 3000 }
  }]
};

max_memory_restart es una red de seguridad, no una solución a una fuga. Si el proceso se reinicia cada pocas horas, toma snapshots del heap y corrige el crecimiento. Rota los logs o envíalos a journald; archivos sin límite llenarán el disco.

Nginx como proxy inverso y terminación TLS

Nginx mantiene el puerto interno fuera de Internet, maneja certificados, aplica límites y sirve archivos estáticos. Un bloque básico conserva la IP y el protocolo originales:

nginx
server {
    listen 80;
    server_name app.ejemplo.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 5s;
        proxy_read_timeout 60s;
    }
}

Valida y prueba el backend antes de recargar:

bash
curl -fsS http://127.0.0.1:3000/health
sudo nginx -t
sudo systemctl reload nginx
curl -I http://127.0.0.1 -H 'Host: app.ejemplo.com'

Configura el framework para confiar únicamente en el proxy previsto. Confiar en cualquier cabecera X-Forwarded-For permite falsificar IPs si el puerto interno se vuelve accesible. UFW debería exponer SSH restringido, 80 y 443; no 3000.

WebSockets y conexiones persistentes

Para WebSockets, Nginx debe reenviar las cabeceras de actualización:

nginx
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

location /socket/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host $host;
    proxy_read_timeout 75s;
}

El cliente debe reconectarse con backoff, porque cualquier despliegue o reinicio corta conexiones. Si ejecutas varias réplicas y el sistema usa salas, pub/sub o presencia, coordina mensajes mediante Redis u otro broker. Guardarlos solo en memoria crea usuarios que ven estados diferentes según el proceso que los atendió.

Manejar SIGTERM y cerrar sin perder solicitudes

Durante un despliegue el gestor envía SIGTERM. La aplicación debe dejar de aceptar conexiones, permitir que terminen solicitudes activas y cerrar pools dentro del límite:

javascript
const server = app.listen(process.env.PORT, process.env.HOST);

process.on('SIGTERM', async () => {
  server.close(async () => {
    await databasePool.end();
    process.exit(0);
  });

  setTimeout(() => process.exit(1), 25000).unref();
});

Sin este manejo, un reinicio puede cortar escrituras a mitad o mantener el proceso vivo hasta que systemd lo mate. Prueba enviando tráfico controlado y reiniciando en staging. El timeout externo debe dar margen al interno, pero no permitir conexiones eternas.

Healthchecks útiles, no decorativos

Separa un endpoint de vida, que confirma que el event loop responde, de uno de readiness, que verifica dependencias indispensables. La ruta debe ser rápida, no escribir datos y no llamar a APIs externas lentas en cada sondeo.

bash
curl -fsS http://127.0.0.1:3000/health
curl -fsS https://app.ejemplo.com/health

La primera prueba aísla Node de Nginx y DNS; la segunda recorre la cadena pública. Si la local funciona y la externa devuelve 502, revisa proxy y firewall. Si ambas tardan, investiga event loop, base o código. La guía de error 502 en Nginx desarrolla ese recorrido.

Heap, RSS y límites de memoria

El heap de V8 es solo una parte del proceso. Buffers, módulos nativos y memoria externa aparecen en RSS, por lo que heapUsed estable no descarta un crecimiento. Expón métricas agregadas sin datos sensibles:

javascript
const memory = process.memoryUsage();
console.log({
  rssMb: Math.round(memory.rss / 1024 / 1024),
  heapUsedMb: Math.round(memory.heapUsed / 1024 / 1024),
  externalMb: Math.round(memory.external / 1024 / 1024)
});

Puedes establecer --max-old-space-size para acotar el heap, pero un límite bajo provoca recolección frecuente y errores; uno cercano a toda la RAM deja sin espacio al sistema. Considera todos los procesos y deja margen. Si el kernel mata Node, compruébalo:

bash
free -h
ps -o pid,rss,%mem,cmd -C node
journalctl -k | grep -i -E 'out of memory|killed process'

La solución puede ser reducir concurrencia, procesar archivos por streaming, limitar colas o ampliar capacidad. Reiniciar periódicamente oculta la fuga y convierte pérdida de memoria en interrupciones programadas.

Event loop y trabajo de CPU

Node ejecuta JavaScript de una instancia principalmente en un hilo. Una función síncrona intensiva bloquea timers y solicitudes aunque haya RAM libre. Mide event-loop lag y duración por ruta. Para cifrado, imágenes o cálculos, utiliza Worker Threads, procesos de trabajo o una cola separada.

Streams evitan cargar archivos enteros en memoria. Define límites de cuerpo en Nginx y en la aplicación; un upload sin restricción puede ocupar RAM o disco y afectar a todos. Procesa contenido no confiable en un entorno limitado y valida tipo y tamaño antes de guardarlo.

Si una ruta tarda minutos, devuelve un identificador de trabajo, procesa de manera asíncrona y permite consultar el estado. Aumentar el timeout del proxy mantiene conexiones abiertas y no corrige el bloqueo del event loop.

Base de datos, pools y presión de conexiones

Cada proceso Node puede crear su propio pool. Cuatro réplicas con veinte conexiones máximas ya intentan ochenta sesiones. Calcula el total junto con workers y herramientas administrativas, y configura timeouts de adquisición. Un pool no acelera una base saturada.

No mantengas transacciones abiertas mientras llamas a una API externa. Adquiere conexión tarde, ejecuta el trabajo de base y libérala pronto. Instrumenta consultas lentas y errores de espera; una API que parece “lenta por Node” a menudo está esperando una conexión o un bloqueo SQL.

Guarda sesiones en cookies firmadas o un almacén compartido si habrá varias réplicas. La memoria de un proceso no se comparte con otro, incluso dentro del mismo VPS.

Releases atómicos y rollback

Construye cada versión en un directorio independiente, valida el healthcheck y cambia un enlace current. Mantén al menos el release anterior:

bash
ln -sfn /srv/nodeapp/releases/20260905-1500 /srv/nodeapp/current.new
mv -Tf /srv/nodeapp/current.new /srv/nodeapp/current
sudo systemctl restart nodeapp
curl -fsS http://127.0.0.1:3000/health

El cambio de enlace es atómico dentro del mismo sistema de archivos. El rollback vuelve a apuntar al release anterior y reinicia. Las migraciones de base deben ser compatibles con ambos releases o se convierten en el punto irreversible.

No despliegues con git pull y npm install sobre el proceso activo. Mientras npm reemplaza dependencias, las solicitudes pueden cargar una mezcla imposible de archivos. El release nuevo se prepara fuera de tráfico.

Logs y métricas que explican incidentes

Registra nivel, hora, identificador de solicitud, ruta normalizada, estado y duración. No guardes contraseñas, tokens, cookies ni cuerpos completos. En producción, JSON estructurado facilita búsquedas y correlación con Nginx.

Observa p50, p95 y p99 de latencia, errores por tipo, solicitudes, event-loop lag, RSS, reinicios, conexiones y profundidad de colas. Configura alertas por tendencias sostenidas, no por picos de segundos que nadie puede accionar.

bash
sudo journalctl -u nodeapp --since '30 minutes ago'
sudo journalctl -u nodeapp -p warning
sudo ss -ltnp | grep 3000

Si los logs crecen en archivos, usa logrotate. Un disco lleno detiene bases, builds y hasta el inicio de sesión; la observabilidad no puede ser la causa del incidente.

Escalar a varios procesos o servidores

Para usar varios núcleos, ejecuta varias instancias y distribuye solicitudes. El modo cluster de PM2 simplifica esto en un VPS; Nginx también puede balancear varios puertos. Comprueba primero que sesiones, archivos y tareas programadas no dependan de un proceso concreto.

Los cron jobs deben tener bloqueo para no duplicarse. Los consumidores de cola pueden escalar de forma independiente y necesitan idempotencia. Los WebSockets requieren coordinación de eventos. Cuando estas piezas están resueltas, mover réplicas a otros VPS es una evolución, no una reescritura de emergencia.

Escala verticalmente si un VPS mayor resuelve el problema con menos complejidad. Separa la base o los workers cuando sus picos dañen la aplicación web o necesiten un objetivo de disponibilidad diferente. Decide con métricas y pruebas de fallo.

Seguridad, backups y verificación final

Actualiza sistema y Node, ejecuta el proceso sin root, limita UFW y revisa dependencias con una decisión informada. Protege el archivo de entorno y no expongas puertos internos. Añade límites de solicitudes y tamaño donde el producto los necesite; un proxy no sustituye validación de entrada.

Respalda datos y uploads fuera de la máquina. El repositorio y lockfile reconstruyen el código, pero no una base ni un archivo que subió un cliente. Prueba restaurar con una versión compatible y registra el tiempo.

Antes de abrir tráfico, reinicia el VPS completo. Confirma que Nginx y Node arrancan, que el healthcheck público funciona, que SIGTERM cierra correctamente y que los logs no contienen secretos. Ejecuta una operación real que toque base y una prueba negativa al puerto 3000 desde Internet.

La guía cómo montar Node.js detrás de Nginx sirve como implementación complementaria. Esta arquitectura define la capacidad y las decisiones que esa instalación debe respetar.

Cluster, worker_threads y colas: tres formas distintas de usar CPU

Node.js ejecuta JavaScript de una solicitud en un hilo principal, aunque la red y varias operaciones del sistema se resuelvan de forma asíncrona. Para aprovechar varios vCPU debes elegir según el tipo de trabajo, no activar paralelismo sin distinguirlo.

El modo cluster —o varias instancias gestionadas por systemd o PM2— crea procesos independientes que reciben solicitudes HTTP. Cada proceso tiene su propio heap, pool de conexiones y memoria de sesión. Mejora capacidad para muchas solicitudes cortas, pero multiplica consumo y exige estado compartido. Si cuatro procesos cargan 250 MB cada uno, la aplicación ya ocupa cerca de 1 GB antes de contar Nginx, base o caché.

worker_threads sirve para cálculo JavaScript intensivo dentro de una aplicación: transformación, compresión, análisis o generación que bloquearía el event loop. No acelera una consulta SQL ni una llamada HTTP que ya es asíncrona. Define un pool limitado; crear un worker por solicitud cambia bloqueo por agotamiento de CPU y memoria.

Una cola externa es preferible cuando el trabajo puede durar, reintentarse o continuar aunque reinicies la API. El proceso web valida y responde con un identificador; workers separados ejecutan la tarea con su propia concurrencia. Así puedes reservar CPU para usuarios y escalar el lote sin añadir réplicas HTTP innecesarias.

Antes de elegir, mide event-loop lag, CPU por proceso y duración de la tarea. Si CPU está baja y la respuesta espera base de datos, más procesos solo añadirán conexiones. Si un endpoint eleva un núcleo al máximo y bloquea todos los demás, muévelo a workers o a una cola. El objetivo no es usar el cien por ciento del VPS, sino mantener latencia predecible durante el pico.

Despliegues sin caída con dos procesos locales

Reiniciar una única instancia crea una ventana breve sin servicio y puede cortar conexiones. En un VPS puedes reducirla ejecutando temporalmente dos versiones en puertos diferentes detrás de Nginx. Arranca el release nuevo en el puerto de espera, prueba su healthcheck y cambia el upstream solo cuando esté listo. Después deja drenar el proceso anterior con SIGTERM.

Este patrón necesita que ambas versiones sean compatibles con la misma base. Aplica primero cambios aditivos —columnas nuevas opcionales, tablas nuevas—, despliega código que entienda ambos esquemas y retira campos viejos en una entrega posterior. Una migración que renombra o borra una columna antes de cambiar todo el tráfico convierte el rollback en una ilusión.

Para WebSockets y solicitudes largas, el drenaje debe impedir conexiones nuevas y esperar las activas hasta un límite. El manejador de SIGTERM deja de aceptar tráfico, termina solicitudes en curso, cierra consumidores y libera pools. Registra si agotó el plazo; matar siempre con SIGKILL oculta trabajos incompletos.

Prueba el procedimiento con tráfico sintético continuo. Cada respuesta debe incluir o registrar el release que la atendió. Durante el cambio no deben aparecer 502, conexiones rechazadas ni respuestas mezcladas. Luego revierte deliberadamente al release anterior: descubrir que el rollback no conoce una variable o dependencia nueva durante el ensayo es mucho menos costoso que descubrirlo durante una incidencia.

Dependencias nativas y memoria durante el build

Algunos paquetes de npm compilan extensiones nativas o descargan binarios para una combinación específica de sistema, arquitectura y versión de Node. Copiar node_modules desde macOS o Windows a Linux puede fallar aunque el JavaScript sea idéntico. Instala con el lockfile en un entorno compatible con producción y conserva las herramientas de compilación solo si realmente se necesitan.

El build de TypeScript o de un frontend puede usar mucha más RAM que la aplicación en reposo. Si compilar en el VPS activa swap y degrada el servicio, construye el artefacto en CI y despliega el resultado, o limita la tarea a una ventana. Separa dependencias de desarrollo y producción mediante el flujo soportado por tu gestor, pero no elimines paquetes antes de terminar cualquier compilación que dependa de ellos.

Verifica el artefacto en un directorio limpio: instalación reproducible, pruebas, arranque, healthcheck y señal de cierre. Esa prueba detecta dependencias que solo existían globalmente en la máquina del desarrollador. El VPS debe recibir un release identificable y reconstruible, no el estado accidental de una carpeta que alguien modificó a mano.

Dimensiona Node.js por RSS, event-loop lag, concurrencia y tareas, no por visitas mensuales. Una API ligera puede empezar con 1 o 2 GB; 4 GB y dos vCPU dan margen para una aplicación comercial con proxy y base pequeña. Los builds y procesos de CPU necesitan capacidad aparte o una ventana controlada.

El despliegue recomendado combina usuario sin privilegios, dependencias bloqueadas, Nginx, systemd o PM2, cierre por SIGTERM, healthchecks y releases atómicos. Cuando métricas y estado compartido están resueltos, añadir procesos o servidores es sencillo; antes de eso, escalar solo multiplica fallos difíciles de reproducir.

¿Listo para llevar tu sitio al siguiente nivel?

Hosting con LiteSpeed, NVMe y soporte 24/7 desde $3/mes.

Ver planes de hosting

Preguntas frecuentes