Skip to content

Despliegue ​

Tu API funciona en tu computadora. Ahora vas a ponerla en internet para que tu cliente y su frontend puedan usarla. Verás tres caminos: plataformas administradas (Render y Railway), y tu propio VPS con PM2 y Nginx. Para más detalle de servidores revisa Servidores y despliegue.

Concepto ​

OpciónVentajasDesventajasIdeal para
Render / Railway (PaaS)Despliegue con git push, HTTPS automático, sin administrar servidoresCosto crece con el uso; disco efímero; "duermen" en planes gratisMVPs, clientes pequeños y medianos
VPS (DigitalOcean, Hetzner, Linode, Contabo)Control total, precio fijo bajo, varias apps en un servidorTú administras seguridad, actualizaciones y respaldosVarios proyectos, más control
Contenedores (Docker en VPS, Fly.io)Mismo entorno en todas partesCurva de aprendizajeEquipos y proyectos grandes

Preparar la app para producción ​

Antes de desplegar, revisa:

  1. Puerto desde variable de entorno: las plataformas asignan el puerto.
js
const PORT = process.env.PORT ?? 3000
app.listen(PORT, '0.0.0.0', () => console.log(`Escuchando en ${PORT}`))
  1. Scripts en package.json:
json
{
  "scripts": {
    "start": "node src/index.js",
    "build": "prisma generate",
    "db:deploy": "prisma migrate deploy"
  },
  "engines": { "node": ">=22" }
}
  1. Ruta de salud para que la plataforma sepa si la app está viva:
js
app.get('/api/salud', (req, res) => res.json({ ok: true, uptime: process.uptime() }))
  1. Cierre ordenado (graceful shutdown): al reiniciar, termina las peticiones en curso y cierra la base de datos.
js
// src/index.js
import app from './app.js'
import { prisma } from './db/prisma.js'

const PORT = process.env.PORT ?? 3000
const servidor = app.listen(PORT, () => console.log(`API en puerto ${PORT}`))

async function apagar(senal) {
  console.log(`${senal} recibido, cerrando...`)
  servidor.close(async () => {
    await prisma.$disconnect()
    process.exit(0)
  })
  // Si tarda demasiado, forzar
  setTimeout(() => process.exit(1), 10_000).unref()
}

process.on('SIGTERM', apagar)
process.on('SIGINT', apagar)

// Errores no capturados: registrar y reiniciar limpio (PM2/plataforma lo levanta)
process.on('unhandledRejection', (err) => {
  console.error('Promesa no manejada:', err)
  apagar('unhandledRejection')
})
  1. NODE_ENV=production, trust proxy, helmet, CORS con el dominio real del frontend.

Opción 1: Render ​

  1. Sube tu código a GitHub (sin .env).
  2. En render.com → New → Web Service → conecta el repositorio.
  3. Configura:
    • Runtime: Node
    • Build Command: npm ci && npm run build
    • Start Command: npm run db:deploy && npm start
    • Health Check Path: /api/salud
  4. En Environment agrega: NODE_ENV=production, DATABASE_URL, JWT_SECRET, CORS_ORIGIN...
  5. Base de datos: New → PostgreSQL en Render y copia la Internal Database URL a DATABASE_URL.

Cada git push a la rama principal despliega automáticamente.

También puedes declararlo en código con un archivo render.yaml (Blueprint):

yaml
# render.yaml
services:
  - type: web
    name: mi-api
    runtime: node
    plan: starter
    buildCommand: npm ci && npm run build
    startCommand: npm run db:deploy && npm start
    healthCheckPath: /api/salud
    envVars:
      - key: NODE_ENV
        value: production
      - key: JWT_SECRET
        generateValue: true
      - key: DATABASE_URL
        fromDatabase:
          name: mi-db
          property: connectionString
databases:
  - name: mi-db
    plan: basic-256mb

Plan gratuito

Los servicios gratis de Render se "duermen" tras 15 minutos sin tráfico y tardan ~1 minuto en despertar. Para un cliente real usa un plan de pago.

Opción 2: Railway ​

  1. En railway.com → New Project → Deploy from GitHub repo.
  2. Agrega una base de datos: + New → Database → PostgreSQL / MySQL / MongoDB.
  3. En las variables de tu servicio, crea DATABASE_URL haciendo referencia a la variable de la base de datos (se escribe así en el panel de Railway):
txt
DATABASE_URL=${{Postgres.DATABASE_URL}}
  1. En Settings: Start Command npm run db:deploy && npm start, Healthcheck Path /api/salud.
  2. Settings → Networking → Generate Domain para obtener una URL pública, o conecta tu dominio.

Con la CLI:

bash
npm i -g @railway/cli
railway login
railway link
railway up          # despliega la carpeta actual
railway logs

Opción 3: VPS con PM2 y Nginx ​

Arquitectura:

txt
Internet ──► Nginx (:80/:443, HTTPS) ──► Node con PM2 (:3000) ──► PostgreSQL/MySQL

1. Preparar el servidor (Ubuntu 24.04) ​

bash
# Conéctate
ssh root@IP_DEL_SERVIDOR

# Usuario sin privilegios para la app
adduser deploy
usermod -aG sudo deploy
su - deploy

# Actualizar e instalar herramientas
sudo apt update && sudo apt upgrade -y
sudo apt install -y git nginx ufw

# Firewall: sólo SSH y web
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

# Node con nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.bashrc
nvm install 22
npm i -g pm2

2. Subir la app ​

bash
cd ~
git clone https://github.com/tu-usuario/mi-api.git
cd mi-api
npm ci --omit=dev
nano .env        # variables de producción (aquí sí se crea el archivo, sólo en el servidor)
npx prisma migrate deploy

3. PM2: mantener la app viva ​

PM2 reinicia la app si se cae, la arranca al reiniciar el servidor, guarda logs y puede usar todos los núcleos.

js
// ecosystem.config.cjs
module.exports = {
  apps: [
    {
      name: 'mi-api',
      script: 'src/index.js',
      instances: 'max',          // un proceso por núcleo (modo clúster)
      exec_mode: 'cluster',
      node_args: '--env-file=.env',
      env: { NODE_ENV: 'production', PORT: 3000 },
      max_memory_restart: '300M',
      out_file: './logs/out.log',
      error_file: './logs/error.log',
      time: true
    }
  ]
}
bash
pm2 start ecosystem.config.cjs
pm2 status              # ver procesos
pm2 logs mi-api         # ver logs en vivo
pm2 monit               # CPU y memoria
pm2 reload mi-api       # reinicio sin caída (zero downtime)

# Arrancar automáticamente al reiniciar el servidor
pm2 startup             # copia y ejecuta el comando que imprime
pm2 save

4. Nginx como proxy inverso ​

nginx
# /etc/nginx/sites-available/mi-api
server {
    listen 80;
    server_name api.midominio.com;

    client_max_body_size 10M;   # para subidas de archivos

    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;
        # WebSockets
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
bash
sudo ln -s /etc/nginx/sites-available/mi-api /etc/nginx/sites-enabled/
sudo nginx -t                # verifica la sintaxis
sudo systemctl reload nginx

En tu proveedor de DNS crea un registro A: api.midominio.com → IP_DEL_SERVIDOR.

5. HTTPS gratis con Let's Encrypt ​

bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d api.midominio.com
# Renovación automática ya queda configurada; pruébala:
sudo certbot renew --dry-run

6. Script de actualización ​

bash
#!/usr/bin/env bash
# deploy.sh — ejecútalo en el servidor para publicar la última versión
set -e
cd ~/mi-api
git pull origin main
npm ci --omit=dev
npx prisma migrate deploy
pm2 reload ecosystem.config.cjs
echo "✅ Desplegado $(git rev-parse --short HEAD)"
bash
chmod +x deploy.sh
./deploy.sh

Puedes automatizarlo con GitHub Actions conectándote por SSH (por ejemplo con la acción appleboy/ssh-action).

Docker (opcional) ​

dockerfile
# Dockerfile
FROM node:22-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npx prisma generate
ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["sh", "-c", "npx prisma migrate deploy && node src/index.js"]
bash
docker build -t mi-api .
docker run -d -p 3000:3000 --env-file .env --name mi-api mi-api

Después de desplegar ​

  • Logs y errores: Sentry o Better Stack para recibir alertas de errores.
  • Monitoreo de disponibilidad: UptimeRobot o Better Stack revisando /api/salud cada minuto.
  • Respaldos diarios de la base de datos (los servicios administrados lo ofrecen; en VPS con pg_dump/mysqldump + cron).

Buena práctica

Mantén un entorno de staging (copia de producción con otra base de datos) para probar antes de publicar cambios importantes al cliente.

Error común

Escuchar en localhost dentro de Docker o en algunas plataformas: la app no es accesible desde fuera. Escucha en 0.0.0.0 (o deja que Express use su valor por defecto).

Seguridad en VPS

Desactiva el login de root por SSH y usa llaves en vez de contraseñas (PermitRootLogin no y PasswordAuthentication no en /etc/ssh/sshd_config). Nunca expongas el puerto de la base de datos a internet.

Ejercicios ​

  1. Escribe el ecosystem.config.cjs para dos apps en el mismo VPS: api-tienda (puerto 3000, 2 instancias) y api-blog (puerto 3001, 1 instancia).
  2. Escribe la configuración de Nginx para servir ambas en api.tienda.com y api.blog.com.
Ver solución
js
// ecosystem.config.cjs
module.exports = {
  apps: [
    {
      name: 'api-tienda',
      cwd: '/home/deploy/api-tienda',
      script: 'src/index.js',
      instances: 2,
      exec_mode: 'cluster',
      node_args: '--env-file=.env',
      env: { NODE_ENV: 'production', PORT: 3000 }
    },
    {
      name: 'api-blog',
      cwd: '/home/deploy/api-blog',
      script: 'src/index.js',
      instances: 1,
      node_args: '--env-file=.env',
      env: { NODE_ENV: 'production', PORT: 3001 }
    }
  ]
}
nginx
server {
    listen 80;
    server_name api.tienda.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
    server_name api.blog.com;
    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Después: sudo nginx -t && sudo systemctl reload nginx y sudo certbot --nginx -d api.tienda.com -d api.blog.com.

Resumen ​

  • Prepara la app: PORT de entorno, ruta de salud, cierre ordenado y npm start.
  • Render/Railway: conectas GitHub, configuras variables y cada push despliega.
  • VPS: Node con nvm + PM2 (reinicios, clúster, logs) + Nginx (proxy inverso) + Certbot (HTTPS).
  • Monitorea errores y disponibilidad, y respalda la base de datos.

Academia Web — aprende, copia y construye.