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 carga | Inicio razonable | Principal riesgo |
|---|---|---|
| API REST ligera | 1 vCPU, 1–2 GB | Picos de conexiones y consultas lentas |
| App web con base local | 2 vCPU, 4 GB | Competencia entre Node y la base |
| Next.js con build en servidor | 2–4 vCPU, 4–8 GB | Picos de RAM durante compilación |
| WebSockets persistentes | 2 vCPU, 4 GB o más | Conexiones, buffers y despliegues sin corte |
| Procesamiento de archivos | 4 vCPU, 8 GB o workers separados | CPU 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:
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:
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:
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:
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:
[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:
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:
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:
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:
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:
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:
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.
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:
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:
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:
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.
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.
Hosting con LiteSpeed, NVMe y soporte 24/7 desde $3/mes.