Cloud Servidores Node.js Npm

Desplegar Node.js en un servidor sin improvisar

Guía para desplegar Node.js con npm ci, usuario dedicado, systemd, nginx, TLS, firewall, health checks, logs y rollback verificable.

Carlos Rodríguez
Carlos Rodríguez

Cloud para e-commerce y riesgo cambiario

Evalúa cloud para e-commerce, costes en moneda dura, copias, CDN y proveedores con rutas razonables hacia LatAm.

9 min de lectura

Para desplegar Node.js con seguridad, use una versión LTS compatible, instale dependencias desde el lockfile y ejecute la aplicación con un usuario sin privilegios. systemd debe controlar el proceso; nginx termina TLS y actúa como proxy. Pruebe salud y rollback antes de abrir tráfico.

El resultado que buscamos

Esta guía usa Ubuntu o Debian con systemd y nginx. Los nombres miapp, nodeapp, app.ejemplo.com y las rutas son ejemplos que debe adaptar. Los comandos administrativos requieren una cuenta autorizada; léalos antes de ejecutarlos y mantenga otra sesión SSH abierta cuando cambie red o servicios.

El diseño final separa responsabilidades:

PiezaResponsabilidadNo debe hacerComprobación
Node.jsEjecutar la aplicación en loopbackEscuchar directamente en InternetPetición local a healthz
systemdIdentidad, entorno, inicio y reinicioGuardar secretos dentro del repositorioEstado y journal de la unidad
nginxProxy, cabeceras y terminación TLSOcultar una aplicación que no está sananginx -t y petición HTTPS
FirewallExponer solo los servicios necesariosAbrir el puerto interno de NodeEstado de reglas desde otra sesión
ReleaseCódigo y dependencias reproduciblesCambiar archivos mientras atiende tráficoHash, pruebas y enlace current

Una página que responde desde una terminal no es todavía un despliegue. Debe sobrevivir a un cierre de SSH, un reinicio del servidor, una release fallida y una renovación de certificado.

Elija una rama LTS soportada

Node.js publica ramas Current y LTS. El proyecto recomienda que las aplicaciones de producción usen Active LTS o Maintenance LTS. A 26 de julio de 2026, Node.js 24 es LTS y Node.js 26 sigue en Current.

Por eso el ejemplo fija la rama 24, pero no congela un parche antiguo: instale el parche de seguridad más reciente de esa rama mediante un método oficial o gestionado por su distribución.

Compruebe exactamente qué binarios utilizará systemd:

node --version
npm --version
command -v node
command -v npm

La primera línea debe mostrar v24.x para seguir este ejemplo. Si su aplicación declara otra versión en engines, un archivo de herramienta o su documentación, resuelva esa diferencia antes de publicar. Actualizar el runtime durante el mismo cambio que modifica la aplicación complica el rollback.

No copie un comando curl-to-shell sin revisar su origen. El método de instalación cambia según distribución y política de actualizaciones. Lo estable aquí es el contrato: rama soportada, parche vigente, ruta absoluta conocida y una prueba de la aplicación con esa misma versión.

Cree un usuario que no pueda iniciar sesión

La aplicación no necesita root para escuchar en 127.0.0.1:3000. Cree una identidad de sistema y directorios separados para releases y datos persistentes:

sudo useradd --system --home /srv/miapp --shell /usr/sbin/nologin nodeapp
sudo install -d -o nodeapp -g nodeapp /srv/miapp/releases
sudo install -d -o nodeapp -g nodeapp /srv/miapp/shared

Si nodeapp ya existe, useradd fallará sin modificarlo. Revise su identidad con getent passwd nodeapp en vez de repetir el comando a ciegas.

El código puede ser legible por nodeapp, pero no necesita ser escribible por nginx. Las subidas, cachés o archivos temporales deben ir a una ruta shared concreta, no al directorio de la release. Permisos amplios arreglan el síntoma y crean otro incidente.

Instale una release nueva sin tocar la activa

Prepare el artefacto en integración continua o copie una versión identificada al servidor. No despliegue sobre /srv/miapp/current. Cree un directorio nuevo y ejecute allí la instalación:

export RELEASE=2026-07-29-001
sudo install -d -o nodeapp -g nodeapp "/srv/miapp/releases/$RELEASE"

Copie en esa ruta package.json, package-lock.json y el código o artefacto necesarios. Después ejecute como nodeapp:

Antes de continuar, confirme:

  • que package-lock.json pertenece al mismo commit que package.json;
  • que la release no contiene el archivo de secretos;
  • que el directorio current todavía apunta a la versión activa.
sudo -u nodeapp -- bash -lc \
  "cd /srv/miapp/releases/$RELEASE && npm ci && npm test && npm run build"

npm ci exige un lockfile y falla si no coincide con package.json. También limpia node_modules dentro de ese directorio. Por eso se ejecuta en una release nueva, nunca en la que está atendiendo tráfico. Si el proyecto creó el lockfile con flags especiales, conserve la configuración correspondiente en un .npmrc versionado.

Algunas aplicaciones necesitan dependencias de desarrollo para compilar y luego pueden ejecutar npm prune –omit=dev. Otras entregan un artefacto ya construido. No añada –omit=dev antes del build por costumbre: confirme qué necesita el script de su proyecto.

Mantenga secretos fuera del código y la release

Cree un archivo de entorno fuera del repositorio. El ejemplo separa configuración no secreta y credenciales, pero el archivo completo debe tratarse como sensible:

NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://USUARIO:CONTRASENA@HOST/BASE
SESSION_SECRET=REEMPLAZAR_CON_UN_SECRETO_REAL

Guárdelo como /etc/miapp/miapp.env y limite el acceso:

sudo install -d -o root -g nodeapp -m 0750 /etc/miapp
sudo chown root:nodeapp /etc/miapp/miapp.env
sudo chmod 0640 /etc/miapp/miapp.env

No pegue secretos reales en el historial de shell para crear el archivo. Use el gestor de secretos, automatización o editor aprobado por su equipo. systemd leerá el archivo, pero eso no lo cifra. La frontera útil es quién puede leerlo y cómo se rota.

La aplicación tampoco debe volcar variables completas a los logs. Redacte tokens, cabeceras de autorización, cadenas de base de datos y cookies antes de registrar errores.

Añada un health check pequeño

El proceso necesita una ruta que confirme que puede atender solicitudes. En una aplicación Express, una comprobación básica puede ser:

app.get('/healthz', (request, response) => {
  response.status(200).json({ status: 'ok' });
});

app.listen(process.env.PORT || 3000, '127.0.0.1');

Esta ruta confirma proceso y servidor HTTP. No demuestra por sí sola que pagos, correo o base de datos funcionen. Si crea una readiness check con dependencias, limite el tiempo y evite que una API externa lenta bloquee cada comprobación.

El health check debe fallar por una razón útil, no publicar un inventario interno. No devuelva versiones, rutas, secretos ni trazas.

Deje que systemd controle el proceso

Cree /etc/systemd/system/miapp.service y sustituya /usr/bin/node por la ruta absoluta obtenida con command -v node:

[Unit]
Description=Aplicacion Node.js miapp
After=network-online.target
Wants=network-online.target

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

[Install]
WantedBy=multi-user.target

Restart=on-failure permite recuperar caídas inesperadas sin convertir una parada administrativa en un bucle. Las opciones de aislamiento son un punto de partida; si la aplicación necesita escribir fuera de shared, identifique esa ruta en lugar de quitar todas las restricciones.

Valide la unidad antes de habilitarla:

sudo systemd-analyze verify /etc/systemd/system/miapp.service
sudo systemctl daemon-reload
sudo systemctl enable miapp.service

Todavía no inicie el servicio si current no apunta a una release válida.

Ponga nginx y TLS delante

Node escucha solo en loopback. nginx recibe tráfico público, termina TLS y reenvía la solicitud. Un bloque mínimo, con rutas de certificado que debe reemplazar por las de su mecanismo de emisión, es:

server {
    listen 443 ssl;
    server_name app.ejemplo.com;

    ssl_certificate /etc/ssl/miapp/fullchain.pem;
    ssl_certificate_key /etc/ssl/miapp/privkey.pem;

    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;
    }
}

Mantenga restringida la clave privada y automatice su renovación con el método aprobado para su sistema. Si la aplicación usa WebSocket, cargas grandes o streaming, añada la configuración específica después de verificar el comportamiento; no copie cabeceras y timeouts de otra aplicación.

Compruebe sintaxis antes de recargar:

sudo nginx -t
sudo systemctl reload nginx

TLS termina en nginx, no en la confianza. La aplicación debe validar el origen de cabeceras reenviadas y no aceptar X-Forwarded-For desde clientes que puedan llegar directamente al puerto interno.

Abra el firewall sin perder SSH

En Ubuntu, UFW sirve para una política simple del host. Primero confirme que OpenSSH y el perfil de nginx existen, ensaye las reglas y mantenga la sesión actual abierta:

sudo ufw app list
sudo ufw --dry-run allow OpenSSH
sudo ufw --dry-run allow 'Nginx Full'

Si la salida coincide con su acceso real, aplique y revise:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw status verbose
sudo ufw enable

Abra una segunda conexión SSH antes de cerrar la primera. Un firewall del proveedor puede existir además del firewall del sistema; ambos deben permitir el tráfico necesario. No abra 3000 al exterior: nginx ya llega por loopback.

Active, compruebe y observe

Antes de cambiar current, guarde la release anterior si existe. Después cambie el enlace y arranque:

if test -L /srv/miapp/current; then
  sudo readlink -f /srv/miapp/current | sudo tee /srv/miapp/previous-release
fi
sudo ln -sfn "/srv/miapp/releases/$RELEASE" /srv/miapp/current
sudo systemctl restart miapp.service

Compruebe primero el proceso local y luego el camino público:

sudo systemctl status miapp.service --no-pager
curl --fail --silent --show-error http://127.0.0.1:3000/healthz
curl --fail --silent --show-error https://app.ejemplo.com/healthz
sudo journalctl -u miapp.service --since today --no-pager

Pruebe también una operación real sin efectos irreversibles: login de prueba, lectura de catálogo o conexión controlada a la base. Un 200 en healthz no confirma una venta completa.

systemd envía stdout y stderr al journal por defecto. Use logs estructurados y un identificador de solicitud, pero no dependa de un disco sin política de rotación ni de un panel externo como única copia durante una caída.

Haga que rollback sea una operación corta

Si la salud o la operación de prueba falla, no edite la release nueva en producción. Vuelva al directorio anterior guardado y reinicie:

export PREVIOUS="$(sudo cat /srv/miapp/previous-release)"
sudo test -d "$PREVIOUS"
sudo ln -sfn "$PREVIOUS" /srv/miapp/current
sudo systemctl restart miapp.service
curl --fail --silent --show-error http://127.0.0.1:3000/healthz

El rollback de código no revierte automáticamente una migración de base de datos. Diseñe cambios compatibles hacia adelante y atrás, o separe la migración destructiva en una operación con copia y procedimiento propio. La release anterior solo sirve si sigue entendiendo los datos actuales.

Conserve varias releases según espacio y política, pero elimínelas en una tarea posterior y revisada. No incluya borrados recursivos forzados en el camino crítico del despliegue.

Compare el servidor por operación, no solo por RAM

DigitalOcean, Hetzner, OVHcloud y Vultr ofrecen puntos de partida para comparar VPS. Verifique la imagen del sistema, copias, región, red, consola de emergencia, facturación y soporte del producto exacto.

Para una tienda en España, México o Argentina, mida latencia y camino de pago desde usuarios reales. Una factura en dólares o euros también afecta continuidad si la tarjeta, impuestos o tipo de cambio cambian el coste en moneda local. El servidor más barato no compensa un rollback que nadie sabe ejecutar.

Lista de comprobación

  • Fije una rama LTS soportada y registre las rutas absolutas de node y npm que usará systemd.
  • Construya cada release en un directorio nuevo con lockfile, pruebas y build antes de cambiar current.
  • Ejecute como usuario sin login y guarde secretos fuera del código con permisos mínimos.
  • Valide unidad systemd y configuración nginx antes de iniciar o recargar servicios.
  • Pruebe health local, HTTPS público, logs, firewall y una operación real; vuelva a la release anterior si falla.

Preguntas frecuentes

¿Debo usar la versión más nueva de Node.js?
No necesariamente. Producción debe usar una rama LTS soportada y compatible con la aplicación. Actualice sus parches de seguridad y pruebe el cambio antes de publicar.
¿PM2 es obligatorio para desplegar Node.js?
No. systemd puede ejecutar, reiniciar y registrar un proceso Node.js. Añada otro gestor solo si resuelve una necesidad concreta y entiende quién supervisa a quién.
¿Puedo guardar secretos en EnvironmentFile?
Sí, con acceso restringido, pero el archivo no queda cifrado por systemd. Para requisitos mayores, integre un gestor de secretos y una rotación verificable.
¿Por qué no exponer directamente el puerto 3000?
Porque nginx puede concentrar TLS, nombre de dominio y cabeceras mientras Node queda en loopback. El firewall debe impedir el acceso público directo al proceso.

Preparado por

Carlos Rodríguez
Carlos Rodríguez

Cloud para e-commerce y riesgo cambiario

Evalúa cloud para e-commerce, costes en moneda dura, copias, CDN y proveedores con rutas razonables hacia LatAm.

Datos verificados

HostScout editorial

Artículos relacionados