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.