Desplegar una aplicación de Node.js en un VPS significa que tu código deja de correr solo en tu computadora y pasa a estar disponible en internet, con un dominio, HTTPS y un proceso que se reinicia si se cae. No hace falta Docker ni Kubernetes para lograrlo: con systemd, Nginx y Certbot tenés un despliegue sólido y entendible en una tarde.
Esta guía recorre los nueve pasos, desde la compra del servidor hasta la actualización sin cortar el servicio. Incluye un ejemplo trabajado de una API con Express y PostgreSQL, los errores que suelen costar una noche entera y las limitaciones para saber cuándo conviene otra estrategia.
Qué necesitas antes de empezar
- Un VPS con Ubuntu o Debian, 1 o 2 GB de RAM para empezar y una IP pública.
- Un dominio o subdominio apuntando a esa IP con un registro DNS tipo A.
- Acceso SSH con clave, no con contraseña.
- Node.js LTS instalado en el servidor.
- Tu aplicación en un repositorio de Git accesible desde el servidor.
Si te falta el dominio, igual podés practicar con la IP y agregar HTTPS después; certificar requiere un nombre de dominio real.
Paso 1: preparar el servidor y el usuario
Trabajar como root todo el tiempo es una mala práctica. Creá un usuario para la aplicación y dale permisos limitados:
sudo adduser --disabled-password --gecos "" apps
sudo usermod -aG sudo apps
sudo mkdir -p /srv/apps
sudo chown apps:apps /srv/apps
Después configurá el firewall permitiendo solo lo necesario:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
Este es el momento de verificar que podés entrar con la clave SSH del usuario nuevo antes de cerrar la sesión de root.
Paso 2: instalar Node.js LTS y Git
Instalá Node desde el repositorio oficial de NodeSource o con el gestor de versiones que prefieras, pero fijá una versión LTS y anotala. Mezclar versiones entre tu máquina y el servidor es una fuente clásica de errores difíciles de ver.
node -v
npm -v
git --version
Paso 3: subir el código y las variables de entorno
Cloná el repositorio en una carpeta de releases y dejá la configuración fuera del código:
sudo -u apps git clone https://github.com/tu-usuario/tu-api.git /srv/apps/tu-api
cd /srv/apps/tu-api
sudo -u apps npm ci --omit=dev
Las variables de entorno van en un archivo con permisos restringidos, nunca dentro del repositorio:
sudo -u apps tee /srv/apps/tu-api/.env > /dev/null <<'ENV'
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://usuario:clave@127.0.0.1:5432/miapp
ENV
sudo chmod 600 /srv/apps/tu-api/.env
Nunca subas claves a Git. Si ya lo hiciste, rotalas: borrar el commit no alcanza.
Paso 4: ejecutar la aplicación de forma persistente
Si lanzás el proceso con node index.js y cerrás la terminal, la aplicación muere. systemd se encarga de mantenerla viva, reiniciarla y capturar sus registros:
# /etc/systemd/system/tu-api.service
[Unit]
Description=API Node.js de ejemplo
After=network.target postgresql.service
[Service]
Type=simple
User=apps
Group=apps
WorkingDirectory=/srv/apps/tu-api
EnvironmentFile=/srv/apps/tu-api/.env
ExecStart=/usr/bin/node /srv/apps/tu-api/src/index.js
Restart=on-failure
RestartSec=3
TimeoutStopSec=20
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Activá y arrancá el servicio:
sudo systemctl daemon-reload
sudo systemctl enable --now tu-api
sudo systemctl status tu-api
curl -s http://127.0.0.1:3000/api/health
Ese endpoint de salud es tu mejor inversión: devuelve el estado de la aplicación y de sus dependencias (base de datos, caché) y te permite automatizar verificaciones.
Paso 5: Nginx como proxy inverso
La aplicación escucha en un puerto local; Nginx recibe el tráfico público y lo deriva. Ventajas: gestiona HTTPS, comprime, limita tamaños y sirve archivos estáticos sin ocupar el proceso de Node.
server {
listen 80;
server_name api.tudominio.com;
client_max_body_size 10m;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "";
proxy_read_timeout 120s;
}
}
Verificá la configuración antes de recargar:
sudo nginx -t
sudo systemctl reload nginx
Si la configuración tiene un error y recargás sin probar, podés dejar el sitio caído. nginx -t es obligatorio, no opcional.
Paso 6: HTTPS con Certbot
Con el dominio apuntando al servidor, emitir el certificado es un comando:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d api.tudominio.com
Certbot edita la configuración, instala el certificado y programa la renovación automática. Verificala con sudo certbot renew --dry-run: una renovación que falla en silencio te deja sin sitio a los 90 días.
Paso 7: actualizar sin cortar el servicio
El ciclo de despliegue más simple y confiable para un solo servidor:
cd /srv/apps/tu-api
sudo -u apps git pull --ff-only
sudo -u apps npm ci --omit=dev
sudo systemctl restart tu-api
curl -s http://127.0.0.1:3000/api/health
Si querés evitar incluso esos segundos de corte, el patrón es publicar en un directorio nuevo y cambiar un enlace simbólico, con dos servicios en puertos distintos: el proxy apunta al que está sano y la verificación se hace antes de cambiar. Empieza simple y adoptá ese esquema cuando el tráfico lo justifique.
Paso 8: registros, respaldo y monitoreo mínimo
- Logs de la aplicación:
journalctl -u tu-api -fpara ver en vivo y--since "1 hour ago"para revisar. - Rotación: systemd ya rota el journal; limitá el tamaño con
JournalSizeMaxsi el disco es chico. - Respaldo de la base: una copia diaria con
pg_dumpcomprimido, guardada fuera del servidor. - Monitoreo externo: un chequeo cada minuto a
/api/healthque te avise por correo o mensaje. Sin esto, te enterás de las caídas por un usuario enojado. - Alertas de disco: el disco lleno es la causa silenciosa de la mitad de las caídas.
Ejemplo trabajado: una API Express con PostgreSQL
Resumen del recorrido completo, con las decisiones explícitas:
| Decisión | Elección | Por qué |
|---|---|---|
| Gestor de procesos | systemd | Ya viene en el sistema, reinicia solo y centraliza logs |
| Proxy inverso | Nginx | HTTPS, compresión y estáticos sin tocar Node |
| Variables | Archivo con permisos 600 | Evita credenciales en el repositorio |
| Salud | Endpoint /api/health | Permite verificación automática antes y después de cada despliegue |
| Actualización | git pull más restart | Simple y suficiente al inicio |
| HTTPS | Certbot con renovación probada | Un certificado vencido es una caída evitable |
Con la aplicación arriba, la verificación de cierre es esta secuencia:
curl -s -o /dev/null -w "%{http_code}\n" https://api.tudominio.com/api/health
sudo systemctl is-active tu-api
sudo journalctl -u tu-api --since "10 min ago" | tail -20
Si los tres comandos devuelven lo esperado, el despliegue está completo. Si algo falla, tenés los registros y el estado del servicio para diagnosticar sin adivinar.
Errores que te van a costar una noche
- Olvidar abrir los puertos 80 y 443 y creer que Nginx está roto.
- Configurar el DNS sin esperar la propagación y culpar a Certbot.
- Dejar el proceso con
nodeen primer plano y perderlo al cerrar SSH. - Usar
npm installen vez denpm ciy desplegar dependencias distintas a las probadas. - Guardar el
.envdentro del repositorio. - No fijar la versión de Node entre desarrollo y producción.
- Recargar Nginx sin
nginx -t. - No probar la renovación del certificado.
- Exponer la base de datos a internet en lugar de dejarla en
127.0.0.1. - No limitar el tamaño de subida y recibir un archivo que consume toda la memoria.
- No respaldar antes de actualizar y descubrir el problema con la base ya migrada.
Limitaciones: cuándo un VPS no es la mejor opción
- Sin experiencia en sistemas, el mantenimiento es real. Parches de seguridad, actualizaciones del sistema operativo y monitoreo son tareas continuas.
- Un solo servidor es un punto único de falla. Si la máquina cae, el sitio cae. Para alta disponibilidad necesitás varias instancias y balanceo.
- Escalar requiere trabajo. Mientras una plataforma administrada escala con un botón, un VPS pide réplicas, sesiones compartidas y almacenamiento externo.
- Costos ocultos de tiempo. El VPS puede ser más barato en dinero y más caro en horas de mantenimiento.
- Alternativas válidas. Para prototipos y proyectos sin tráfico constante, una plataforma con despliegue desde Git suele ser mejor decisión: pagás por uso y no administrás nada.
- Seguridad. Un servidor expuesto recibe intentos de acceso constante. Claves SSH, actualizaciones al día y firewall mínimo no son opcionales.
Preguntas frecuentes
¿Necesito Docker para desplegar en un VPS? No. Docker ayuda a reproducir entornos, pero agrega una capa que hay que aprender. Con systemd y Nginx llegás lejos y entiendes cada pieza.
¿Cuánta RAM necesito? Para una API pequeña con Node y PostgreSQL, 1 o 2 GB alcanzan para empezar. Monitoreá el consumo antes de escalar.
¿Cómo despliego varios proyectos en el mismo servidor? Un servicio por aplicación en puertos distintos y un bloque de Nginx por dominio. Aislá usuarios y carpetas por proyecto.
¿Qué hago si el proceso se reinicia en bucle? Mirá los logs con journalctl -u tu-api -n 100. Casi siempre es una variable de entorno faltante, un puerto ocupado o un error de sintaxis en el arranque.
¿Sirve un subdominio gratuito para practicar? Sirve para probar, pero para HTTPS y correo profesional necesitás un dominio propio.
¿Cómo sé si me están atacando? Revisá intentos de acceso SSH fallidos, picos de tráfico y errores 4xx inusuales en los logs de Nginx. Un firewall y autenticación por clave cubren la mayoría de los casos comunes.
Siguiente paso
Si querés construir la aplicación antes de desplegarla, la guía de Express desde cero te da la base del backend y desarrollo web: curso completo cubre el frontend. Para incorporar pruebas antes de cada despliegue, revisá testing con pytest —los conceptos de verificación aplican igual en JavaScript— y el flujo de trabajo con ramas en Git y GitHub en equipo. Si preferís una ruta guiada de proyecto a producción, mirá los cursos de Cursalo y la categoría Proyectos y casos reales.
