Despliegue de Aplicaciones Express.js en Linux
Express.js es el framework web más popular para Node.js, utilizado para construir APIs REST, aplicaciones web y servicios de backend. Desplegar Express.js en producción en Linux requiere gestión de procesos con PM2, configuración de Nginx como proxy inverso, manejo de variables de entorno y estrategias de despliegue sin tiempo de inactividad. Esta guía cubre el despliegue completo de aplicaciones Express.js, desde la configuración de PM2 hasta el modo cluster, SSL y actualizaciones zero-downtime.
Requisitos Previos
- Ubuntu 20.04+ o Rocky Linux 8+
- Node.js 18 LTS o superior
- Nginx instalado
- Acceso root o sudo
- Aplicación Express.js funcional
Instalación de Node.js en Linux
# Usando NodeSource (recomendado para versiones LTS actuales)
# Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs # Ubuntu/Debian
# CentOS/Rocky Linux
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
dnf install -y nodejs
# Verificar instalación
node --version
npm --version
# Instalar PM2 globalmente
npm install -g pm2
# Verificar PM2
pm2 --version
Aplicación Express.js de Ejemplo
// app.js - Aplicación Express.js para producción
const express = require('express');
const app = express();
// Middlewares básicos
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Headers de seguridad
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
});
// Rutas
app.get('/', (req, res) => {
res.json({ message: 'Express.js en producción', pid: process.pid });
});
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
// Manejador de errores global
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Error interno del servidor' });
});
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
console.log(`Servidor Express iniciado en puerto ${PORT} (PID: ${process.pid})`);
});
// Apagado gracioso para PM2
process.on('SIGTERM', () => {
console.log('Recibida señal SIGTERM, cerrando servidor...');
server.close(() => {
console.log('Servidor cerrado correctamente');
process.exit(0);
});
});
module.exports = app;
Configuración de PM2
# Crear usuario dedicado para la aplicación
useradd -r -s /bin/bash -d /opt/express-app nodejs
mkdir -p /opt/express-app
chown -R nodejs:nodejs /opt/express-app
# Copiar la aplicación
cp -r /ruta/al/proyecto/* /opt/express-app/
chown -R nodejs:nodejs /opt/express-app
# Instalar dependencias como usuario nodejs
su - nodejs -s /bin/bash
cd /opt/express-app
npm ci --only=production # Solo dependencias de producción
exit
# Iniciar la aplicación con PM2
pm2 start /opt/express-app/app.js \
--name "express-api" \
--user nodejs
# Ver estado de PM2
pm2 status
pm2 show express-api
# Ver logs en tiempo real
pm2 logs express-api
# Configurar PM2 para arrancar al inicio del sistema
pm2 startup systemd -u nodejs --hp /opt/express-app
# Ejecutar el comando que PM2 sugiere, luego:
pm2 save
Ecosistema PM2 y Variables de Entorno
El archivo de ecosistema PM2 es la forma correcta de gestionar la configuración en producción:
# Crear archivo de ecosistema PM2
cat > /opt/express-app/ecosystem.config.js << 'EOF'
module.exports = {
apps: [
{
name: 'express-api',
script: 'app.js',
cwd: '/opt/express-app',
// Variables de entorno para producción
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
env_development: {
NODE_ENV: 'development',
PORT: 3001
},
// Modo cluster (ver sección siguiente)
instances: 'max', // Usar todos los núcleos disponibles
exec_mode: 'cluster',
// Reinicio automático en caso de fallo
autorestart: true,
watch: false, // No monitorear cambios de archivos en producción
max_memory_restart: '1G', // Reiniciar si usa más de 1GB de RAM
// Logs
log_file: '/var/log/express-app/combined.log',
out_file: '/var/log/express-app/out.log',
error_file: '/var/log/express-app/error.log',
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
// Apagado gracioso
kill_timeout: 5000, // ms a esperar para SIGKILL tras SIGTERM
wait_ready: true, // Esperar señal ready del proceso
listen_timeout: 3000, // ms a esperar que el proceso escuche
// Variables de entorno desde archivo
// Cargadas de forma segura sin exponerlas en el ecosistema
}
]
};
EOF
# Crear directorio de logs
mkdir -p /var/log/express-app
chown nodejs:nodejs /var/log/express-app
# Crear archivo .env con variables de entorno (no en el ecosistema)
cat > /opt/express-app/.env << 'EOF'
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://user:pass@localhost:5432/db
JWT_SECRET=cambiar-esto-por-un-secreto-seguro
REDIS_URL=redis://localhost:6379
EOF
chmod 600 /opt/express-app/.env
chown nodejs:nodejs /opt/express-app/.env
# Iniciar con el archivo de ecosistema
pm2 start /opt/express-app/ecosystem.config.js --env production
pm2 save
# Comandos útiles de PM2
pm2 status # Estado de todos los procesos
pm2 logs express-api --lines 50 # Últimas 50 líneas de logs
pm2 monit # Monitor interactivo en tiempo real
pm2 restart express-api # Reiniciar
pm2 reload express-api # Reinicio gracioso (zero-downtime)
pm2 stop express-api # Detener
pm2 delete express-api # Eliminar de PM2
Nginx como Proxy Inverso
# Configuración de Nginx para Express.js
cat > /etc/nginx/sites-available/express-api << 'EOF'
# Rate limiting
limit_req_zone $binary_remote_addr zone=express_limit:10m rate=30r/s;
# Upstream - PM2 en modo cluster usa el mismo puerto para todas las instancias
upstream express_app {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name api.midominio.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.midominio.com;
ssl_certificate /etc/letsencrypt/live/api.midominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.midominio.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Logs con información de tiempo de respuesta
access_log /var/log/nginx/express-access.log;
error_log /var/log/nginx/express-error.log;
# Tamaño máximo del cuerpo de solicitud
client_max_body_size 50M;
location / {
# Rate limiting con burst
limit_req zone=express_limit burst=50 nodelay;
proxy_pass http://express_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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_cache_bypass $http_upgrade;
# Buffer para respuestas de la app
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
}
# Archivos estáticos servidos directamente por Nginx (más rápido)
location /static/ {
alias /opt/express-app/public/;
expires 30d;
add_header Cache-Control "public, max-age=2592000";
gzip_static on;
}
location /health {
proxy_pass http://express_app;
access_log off;
}
}
EOF
ln -s /etc/nginx/sites-available/express-api /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
Modo Cluster para Alta Disponibilidad
# El modo cluster de PM2 replica el proceso en todos los núcleos del CPU
# Node.js es single-threaded, el cluster multiplica la capacidad
# Ver número de CPUs disponibles
nproc
# Configurar el número de instancias en ecosystem.config.js:
# instances: 'max' -> Un proceso por CPU
# instances: 4 -> Exactamente 4 procesos
# instances: -1 -> CPUs - 1 (dejar un núcleo libre)
# Verificar que el cluster está funcionando
pm2 list # Ver todas las instancias
pm2 show express-api # Información detallada
# Reiniciar instancias una a una (zero-downtime)
pm2 reload express-api
# Escalar el cluster
pm2 scale express-api +2 # Añadir 2 instancias
pm2 scale express-api 4 # Establecer exactamente 4 instancias
# Ver distribución de solicitudes entre instancias
pm2 monit
SSL y Seguridad
# Instalar Certbot y obtener certificado SSL
apt install certbot python3-certbot-nginx # Ubuntu
certbot --nginx -d api.midominio.com --email [email protected] --agree-tos --no-eff-email
# Habilitar helmet.js en Express para cabeceras de seguridad
# npm install helmet
# En app.js:
# const helmet = require('helmet');
# app.use(helmet());
# Limitar tasa de solicitudes en Express (complemento al rate limiting de Nginx)
# npm install express-rate-limit
cat >> /opt/express-app/app.js << 'EOF'
// Rate limiting en la capa de Express
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // 100 solicitudes por IP por ventana
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Demasiadas solicitudes, intente más tarde' }
});
app.use('/api/', limiter);
EOF
Despliegues Zero-Downtime
# Script de despliegue sin tiempo de inactividad
cat > /opt/express-app/scripts/deploy.sh << 'EOF'
#!/bin/bash
# Despliegue zero-downtime de Express.js con PM2
set -e
APP_DIR="/opt/express-app"
APP_NAME="express-api"
BACKUP_DIR="/opt/express-app-backup"
echo "=== Iniciando despliegue de Express.js ==="
echo "Fecha: $(date)"
# 1. Crear backup de la versión actual
echo "Creando backup..."
cp -r "$APP_DIR" "$BACKUP_DIR-$(date +%Y%m%d_%H%M%S)"
# 2. Copiar nuevos archivos (ajustar según el método de despliegue)
echo "Copiando nuevos archivos..."
# rsync -av --exclude='node_modules' --exclude='.env' /tmp/nueva-version/ "$APP_DIR"/
# 3. Instalar dependencias
echo "Instalando dependencias..."
cd "$APP_DIR"
npm ci --only=production
# 4. Verificar que la aplicación inicia correctamente
echo "Verificando nueva versión..."
NODE_ENV=production node -e "require('./app.js')" 2>&1 && echo "OK" || {
echo "ERROR: La nueva versión no inicia correctamente"
exit 1
}
# 5. Recarga gracioso con PM2 (sin perder conexiones activas)
echo "Recargando aplicación..."
pm2 reload "$APP_NAME" --update-env
# 6. Verificar que el servicio responde
sleep 3
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health)
if [ "$HTTP_STATUS" = "200" ]; then
echo "Despliegue exitoso. Status: $HTTP_STATUS"
pm2 save
else
echo "ERROR: El servicio no responde correctamente (HTTP $HTTP_STATUS)"
echo "Iniciando rollback..."
# Rollback automático
pm2 reload "$APP_NAME"
exit 1
fi
echo "=== Despliegue completado ==="
EOF
chmod +x /opt/express-app/scripts/deploy.sh
# Configurar rotación de logs de PM2
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true
Solución de Problemas
PM2 no inicia al arrancar el sistema:
# Regenerar el script de inicio
pm2 startup systemd -u nodejs --hp /home/nodejs
# Ejecutar el comando que PM2 sugiere
# Guardar la lista de procesos actual
pm2 save
# Verificar el servicio
systemctl status pm2-nodejs
Error "EADDRINUSE: address already in use":
# Ver qué proceso usa el puerto
ss -tlnp | grep 3000
# O con lsof
lsof -i :3000
# Detener el proceso conflictivo
pm2 stop express-api
kill -9 $(lsof -ti:3000)
pm2 start ecosystem.config.js --env production
Los workers del cluster fallan aleatoriamente:
# Ver errores por instancia
pm2 logs express-api --err --lines 100
# Verificar fugas de memoria
pm2 monit # Ver uso de memoria por instancia
# Reducir el límite max_memory_restart
pm2 reload ecosystem.config.js
Nginx devuelve 502 al proxy:
# Verificar que PM2 está corriendo
pm2 status
# Verificar que la app escucha en el puerto correcto
ss -tlnp | grep 3000
# Ver logs de Nginx
tail -50 /var/log/nginx/express-error.log
Conclusión
PM2 es el gestor de procesos estándar para aplicaciones Node.js/Express.js en producción, ofreciendo modo cluster, reinicio automático, cero tiempo de inactividad en deployments y monitoreo integrado. La combinación de PM2 + Nginx como proxy inverso + systemd para el arranque automático proporciona una plataforma de producción robusta. Para aplicaciones de alta carga, considera añadir Redis para sesiones compartidas entre instancias del cluster y configurar logging estructurado para facilitar el diagnóstico en producción.


