Ir al contenido

El medio de los artesanos de la web sábado, 5 de septiembre de 2026

MailStudio
DevOps y servidores

Desplegar una aplicación Node.js en producción: systemd y proxy inverso

Ejecutar node app.js sirve para una demostración, no para producción. Un servicio systemd y un proxy inverso Nginx forman un cimiento mínimo y fiable para poner una aplicación Node.js en línea, con reinicio automático, TLS y despliegue sin cortes.

Couverture guide deploiement Node.js en production

Ejecutar node app.js en una terminal basta para mantener viva una aplicación durante una demostración. En producción, ese comando deja el servicio sin vigilancia: el menor fallo lo detiene, un reinicio del servidor lo olvida y nada gestiona el cifrado ni la carga. Pasar de un proceso lanzado a mano a un servicio fiable se reduce a dos piezas probadas, un supervisor del sistema y un proxy inverso. Este es un cimiento mínimo, sin dependencias exóticas, para poner una aplicación Node.js en línea de forma limpia.

Por qué no lanzar Node directamente

Un proceso lanzado desde el teclado hereda la sesión que lo inició. No sobrevive a una desconexión SSH, no se reinicia tras un fallo, no vuelve tras un reinicio de la máquina y a menudo escucha en un puerto alto expuesto tal cual. La pregunta no es si el proceso se va a caer, sino cuándo, y qué ocurre después.

EnfoqueReinicio automáticoPersistencia en el arranqueDependencia
node app.jsNoNoNinguna
Gestor de aplicaciones (PM2)Mediante scriptPaquete npm global
Servicio systemdNativaYa presente en la distribución

systemd viene instalado por defecto en la mayoría de las distribuciones de servidor. Sabe supervisar un proceso, reiniciarlo según una política definida, engancharlo al arranque y centralizar sus registros. Mejor apoyarse en él que apilar una herramienta más, como ya recordaban las notas sobre el autoalojamiento bien gestionado.

Un servicio systemd para supervisar el proceso

Primera regla: la aplicación nunca se ejecuta como root. Un usuario de sistema dedicado, sin shell de inicio de sesión, limita la superficie de ataque en caso de que sea comprometido.

sudo useradd --system --home /srv/monapp --shell /usr/sbin/nologin monapp
sudo chown -R monapp:monapp /srv/monapp

El servicio se describe en un archivo de unidad ubicado en /etc/systemd/system/. Las variables sensibles quedan fuera del repositorio, cargadas desde un archivo de entorno con permisos restringidos.

[Unit]
Description=Aplicacion Node monapp
After=network.target

[Service]
Type=simple
User=monapp
Group=monapp
WorkingDirectory=/srv/monapp
EnvironmentFile=/srv/monapp/.env
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /srv/monapp/server.js
Restart=on-failure
RestartSec=2
# Endurecimiento
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/monapp/tmp

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now monapp
sudo systemctl status monapp
journalctl -u monapp -f

Los registros van a journalctl, con marca de tiempo y rotados por el sistema, sin redirección manual a un archivo. La política Restart=on-failure relanza el servicio ante una salida anómala, y RestartSec evita un bucle de reinicio demasiado agresivo.

Nginx como proxy inverso

La aplicación escucha en local, en 127.0.0.1:3000, y nunca se expone directamente a internet. Nginx se coloca delante: termina el TLS, sirve los archivos estáticos, aplica la compresión y transmite el resto a Node.

server {
    listen 80;
    server_name monapp.example.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;
        # Soporte de WebSockets
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    gzip on;
    gzip_types text/plain text/css application/javascript application/json;
}

El certificado se obtiene y se renueva con Certbot, que cambia la configuración a HTTPS de forma automática.

sudo certbot --nginx -d monapp.example.com

Una vez colocado el proxy, solo el puerto 443 queda abierto al público; el puerto de la aplicación no sale del bucle local. Este aislamiento va de la mano de copias de seguridad periódicas, cuyo principio se detalla en la guía sobre las copias de seguridad automatizadas de un servidor web.

Despliegue sin cortes

Un simple systemctl restart deja el servicio caído unos instantes. Para evitar esa ventana de indisponibilidad, dos instancias corren en paralelo detrás de un upstream y el reinicio se hace por turnos.

upstream monapp {
    server 127.0.0.1:3000 max_fails=1 fail_timeout=5s;
    server 127.0.0.1:3001 max_fails=1 fail_timeout=5s;
}

Aun así, la aplicación debe cerrarse limpiamente: tiene que dejar de aceptar nuevas conexiones, terminar las peticiones en curso y luego salir. Es el papel de un gestor de apagado sobre SIGTERM, la señal que envía systemd.

const server = app.listen(process.env.PORT || 3000);

process.on("SIGTERM", () => {
  server.close(() => {
    // conexiones drenadas, se liberan los recursos
    process.exit(0);
  });
  // red de seguridad si un socket se queda bloqueado
  setTimeout(() => process.exit(1), 10000).unref();
});

Las dos instancias provienen de una unidad plantilla: un archivo [email protected] reutiliza la unidad anterior pero sustituye la línea de entorno por Environment=PORT=%i, donde %i vale el número pasado tras la arroba. El reinicio rotatorio relanza entonces cada instancia una tras otra, dejando que Nginx enrute el tráfico hacia la que sigue disponible.

sudo systemctl enable --now monapp@3000 monapp@3001
# actualizacion sin cortes
sudo systemctl restart monapp@3000
sleep 5
sudo systemctl restart monapp@3001

En producción, la verdadera pregunta no es si el proceso se va a caer, sino qué ocurre en el segundo en que se cae.

Punto de atención. Tres errores se repiten sin cesar: ejecutar la aplicación como root, olvidar NODE_ENV=production y versionar los secretos en el repositorio. El primero abre la puerta a una escalada de privilegios, el segundo desactiva optimizaciones y deja mensajes de error demasiado locuaces, el tercero expone claves en el primer git push. Un archivo de entorno con permisos 600, propiedad del usuario de la aplicación, resuelve el último punto.

Controles antes de la puesta en línea

ControlEsperado
Usuario de ejecuciónCuenta de sistema dedicada, nunca root
Variable de entornoNODE_ENV=production
Política de reinicioRestart=on-failure activa
CifradoTLS con Certbot, renovación automática
Puerto de la aplicaciónLigado a 127.0.0.1, nunca público
Punto de saludUna ruta /health para la supervisión
CortafuegosSolo 80 y 443 abiertos

Lo que hay que recordar

Un despliegue Node.js robusto no exige herramientas pesadas: un servicio systemd para la supervisión, un proxy inverso Nginx para el TLS y el enrutamiento, un usuario dedicado para el aislamiento y un cierre limpio sobre SIGTERM para las actualizaciones sin cortes. Este cimiento se sostiene en cualquier servidor reciente y sigue siendo legible seis meses después, cuando haya que retomarlo. Sirve tanto para una API como para una aplicación renderizada en el servidor, como las construidas sobre las versiones recientes de Node.js.

Usé PM2 durante mucho tiempo, por costumbre, antes de volver a systemd en mis propios servidores. El cambio me ahorró una dependencia que mantener y me dio unos registros por fin unificados con el resto de la máquina. Mi único añadido sistemático hoy es una ruta /health trivial que comprueba el acceso a la base de datos: es la que me avisa de un incidente antes de que lo haga el cliente. — Simon Janvier

Para profundizar

Referencia de las directivas de servicio en la fuente primaria: documentación de systemd.service.

Compartir LinkedIn Bluesky Hacker News E-mail

Leer también