VPS Runbook
Runbook · v1.0

Plan de Migración
a VPS Propio

DigitalOcean · Ubuntu 24.04 · Nginx · PM2 · Let's Encrypt

arquitectura-objetivo.txt
Browser
  └── GoDaddy DNS (A records → IP del VPS)
        └── DigitalOcean VPS (Ubuntu 24.04, 512 MB RAM)
              ├── Nginx (reverse proxy + SSL termination)
              │     ├── www.mydomain.com        localhost:3000
              │     ├── budget.mydomain.com     localhost:3001
              │     └── app.mydomain.com        localhost:3002
              └── PM2 (process manager — mantiene las apps corriendo)
ProcesoRAM idle aprox.
OS + sistema base~100 MB
Nginx + fail2ban~20 MB
Portafolio www (Next.js)~120 MB
App adicional (Node.js)~100 MB
Total~340 MB
¿Por qué DigitalOcean sobre AWS?

Los Droplets tienen precio fijo y predecible (no hay sorpresas en la factura). El panel es simple y la documentación está entre las mejores del sector.

AWS o GCP añaden complejidad (IAM, VPC, egress costs) sin beneficio real para proyectos personales. Con 512 MB RAM + 1 GB swap hay margen para 1–2 apps Node.js. Si agregas una tercera, considera el plan de 1 GB ($6/mes).

01

Crear el Droplet en DigitalOcean

Crear cuenta en DigitalOcean → Droplets → Create Droplet con esta configuración:

ParámetroValor
OSUbuntu 24.04 x64 LTS
PlanBasic $4/mes — 1 vCPU / 512 MB RAM
RegiónLa más cercana a tus visitantes
AutenticaciónSSH key — nunca contraseña

Generar SSH key

bash
ssh-keygen -t ed25519 -C "tu@email.com" -f ~/.ssh/id_ed25519_digitalocean
¿Por qué ed25519?

Es el algoritmo más moderno de SSH — más seguro y con claves más cortas que RSA 4096. Todos los sistemas modernos lo soportan.

Nombrar la llave con -f ~/.ssh/id_ed25519_digitalocean permite tener múltiples llaves para distintos servicios (GitHub, otro VPS, etc.) sin que se mezclen.

bash
# Copiar la clave pública para pegarla en DigitalOcean
cat ~/.ssh/id_ed25519_digitalocean.pub

# Verificar conexión
ssh -i ~/.ssh/id_ed25519_digitalocean root@TU_IP_VPS
¿Por qué -i en el comando ssh?

Le indica a SSH qué llave privada usar. Si tienes varias llaves, sin este flag SSH prueba todas en orden y puede conectarse con la llave equivocada o fallar.

SSH config (opcional)

Agrega esto a ~/.ssh/config para no escribir -i cada vez:

config
Host TU_IP_VPS mydomain.com
    User deploy
    IdentityFile ~/.ssh/id_ed25519_digitalocean
Tip Usa root solo en esta primera conexión. Después de aplicar el paso 3, root quedará bloqueado por SSH y todas las conexiones serán como deploy.
02

Apuntar dominio GoDaddy al VPS

En el panel DNS de GoDaddy (My Products → DNS), agrega un registro A por cada subdominio:

TipoNombreValorTTL
A@IP del VPS600
AwwwIP del VPS600
AbudgetIP del VPS600
¿Qué significan @ y TTL?

@ representa el dominio raíz (tudominio.com sin prefijo). Los otros registros como www se convierten en www.tudominio.com.

TTL (Time To Live) es el tiempo en segundos que otros servidores DNS cachean el registro. Con 600 (10 min), los cambios propagan rápido. Una vez estable, puedes subirlo a 3600.

Verificar propagación

bash
dig www.tudominio.com +short
# Debe devolver la IP del VPS
Nota Continúa al Paso 3 sin esperar la propagación completa — la configuración del servidor es independiente del DNS.
03

Configuración inicial del servidor

Conectarse como root la primera vez:

bash
ssh root@TU_IP_VPS
1 Actualizar el sistema
bash
apt update && apt upgrade -y
apt install -y nano git curl wget
¿Por qué estas herramientas?

nano — editor para archivos de config como sshd_config.

git — para clonar repos en el servidor.

curl — peticiones HTTP desde terminal; usado para descargar el instalador de Node.js.

wget — descargar archivos. Algunos scripts lo prefieren sobre curl.

2 Crear usuario deploy
bash
adduser deploy
usermod -aG sudo deploy

# Copiar SSH keys de root al nuevo usuario
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

# Dar permisos a Nginx sobre el home (evita 403)
chmod o+x /home/deploy
¿Por qué no usar root para todo?

Root tiene permisos absolutos. Si una app tiene vulnerabilidad o hay un error de comando, puede borrar archivos o comprometer todo el servidor. Un usuario normal limita el daño potencial.

rsync --chown=deploy:deploy copia las llaves SSH autorizadas para que puedas conectarte como deploy usando la misma llave privada local.

chmod o+x /home/deploy es necesario porque Nginx corre como www-data y necesita poder atravesar el directorio. Sin esto verás 403 Forbidden.

3 Deshabilitar login de root por SSH
bash
nano /etc/ssh/sshd_config

Cambiar o agregar estas líneas en el archivo:

config
PermitRootLogin no
PasswordAuthentication no
bash
systemctl restart ssh
Importante Antes de cerrar esta sesión, abre una terminal nueva y verifica que puedes conectarte con ssh deploy@TU_IP_VPS. Si algo salió mal, aún tienes la sesión de root activa para corregirlo.
4 Configurar firewall (ufw)
bash
ufw allow OpenSSH
ufw allow http
ufw allow https
ufw enable

# Verificar reglas activas
ufw status
¿Por qué el orden importa?

ufw allow OpenSSH debe ejecutarse antes de ufw enable. Si no lo hicieras primero, al activar el firewall bloquearías tu propia conexión SSH y quedarías fuera del servidor.

5 Instalar Nginx
bash
apt install -y nginx
systemctl enable --now nginx
¿Por qué --now?

enable configura Nginx para arrancar automáticamente en cada reboot. --now lo inicia inmediatamente, sin necesidad de correr systemctl start nginx por separado.

6 Instalar Node.js LTS
bash
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
apt install -y nodejs
node -v && npm -v
¿Por qué no apt install nodejs directamente?

Los repos de Ubuntu tienen versiones muy desactualizadas de Node.js (a veces años atrás). NodeSource es el repositorio oficial que siempre tiene la versión LTS actual (Node 20+).

7 Instalar PM2
bash
npm install -g pm2
¿Por qué PM2?

Node.js por sí solo no se mantiene corriendo si el proceso falla o el servidor se reinicia. PM2 mantiene las apps vivas: las reinicia si crashean, las arranca al inicio del sistema, y permite correr múltiples apps en paralelo.

8 Instalar Certbot
bash
apt install -y certbot python3-certbot-nginx
9 Agregar swap (obligatorio con 512 MB RAM)
bash
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

# Hacer permanente
echo '/swapfile none swap sw 0 0' >> /etc/fstab

# Verificar
free -h
¿Por qué swap con 512 MB?

El OS + Nginx consume ~120 MB. Una app Next.js suma otros ~120 MB — quedan menos de 300 MB libres. Con múltiples apps, esa reserva se agota. Cuando la RAM se agota, el kernel de Linux mata procesos (OOM killer) — normalmente mata la app más grande, causando downtime sin aviso.

El swap es espacio en disco que el sistema usa como RAM de emergencia. Mucho más lento, pero evita que las apps mueran abruptamente.

10 Instalar fail2ban
bash
apt install -y fail2ban
systemctl enable --now fail2ban
bash
nano /etc/fail2ban/jail.local
ini
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5

[sshd]
enabled = true
bash
systemctl restart fail2ban
fail2ban-client status sshd

Verificación final

bash
systemctl status nginx
systemctl status ufw
systemctl status fail2ban
node -v
pm2 -v
04

Instalar MariaDB (opcional)

bash
apt install -y mariadb-server
systemctl enable --now mariadb
mysql_secure_installation

El asistente pregunta paso a paso:

PreguntaRespuesta
Switch to unix_socket authentication?n
Change the root password?y (contraseña fuerte)
Remove anonymous users?y
Disallow root login remotely?y
Remove test database?y
Reload privilege tables?y

Config low-memory (512 MB RAM)

bash
nano /etc/mysql/conf.d/memory.cnf
ini
[mysqld]
innodb_buffer_pool_size = 64M
key_buffer_size         = 16M
max_connections         = 20
tmp_table_size          = 16M
max_heap_table_size     = 16M
bash
systemctl restart mariadb

Con esta config MariaDB consume ~80 MB en idle en lugar de los 300–500 MB por defecto.

Crear base de datos por app

bash
mysql -u root -p
sql
CREATE DATABASE budget_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'budget_user'@'localhost' IDENTIFIED BY 'contraseña_segura';
GRANT ALL PRIVILEGES ON budget_db.* TO 'budget_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Referencia rápida MariaDB

AcciónComando
Conectarse como rootmysql -u root -p
Ver bases de datosSHOW DATABASES;
Ver usuariosSELECT user, host FROM mysql.user;
Liberar RAMsudo systemctl stop mariadb
Volver a levantarsudo systemctl start mariadb
5a

Deploy — App Node.js con PM2

Para proyectos con servidor propio: Next.js, Express, Fastify, NestJS. Conectarse como deploy:

bash
ssh deploy@TU_IP_VPS

Clonar y construir

bash
mkdir -p ~/apps
cd ~/apps

git clone https://github.com/tuusuario/tu-app.git www
cd www && npm install && npm run build

Iniciar con PM2

bash
pm2 start node_modules/.bin/next --name "www" -- start -p 3000
curl http://localhost:3000
bash
PORT=3000 pm2 start npm --name "www" -- start
curl http://localhost:3000

Persistir entre reinicios

bash
pm2 save
pm2 startup
# Copia y ejecuta el comando que PM2 imprima con sudo

Estrategias de deploy

yaml — .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm install
      - run: npm run build
      - name: Subir archivos al VPS
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.VPS_HOST }}
          username: deploy
          key: ${{ secrets.VPS_SSH_KEY }}
          source: ".next/,public/,package.json,package-lock.json"
          target: "~/apps/www"
      - name: Instalar y reiniciar
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.VPS_HOST }}
          username: deploy
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd ~/apps/www
            npm install --omit=dev
            pm2 restart www
Secrets necesarios en GitHub → Settings → Secrets:
VPS_HOST — IP del VPS  ·  VPS_SSH_KEY — contenido de la clave privada SSH
bash — deploy.sh
#!/bin/bash
set -e

VPS="deploy@TU_IP_VPS"
APP_DIR="~/apps/www"
APP_NAME="www"

echo "→ Building..."
npm run build

echo "→ Uploading..."
rsync -avz --delete .next/ public/ package.json package-lock.json $VPS:$APP_DIR/

echo "→ Restarting..."
ssh $VPS "cd $APP_DIR && npm install --omit=dev && pm2 restart $APP_NAME"

echo "✓ Done"
bash
chmod +x deploy.sh
./deploy.sh
bash — en el VPS
cd ~/apps/www
git pull
npm install
npm run build
pm2 restart www
Nota El build corre en el VPS — puede ser problemático con 512 MB RAM. Next.js en build puede consumir 500 MB+. Usa rsync o GitHub Actions si tienes problemas de memoria.
5b

Deploy — Sitio Estático (Astro / React)

Para proyectos que generan archivos HTML/CSS/JS estáticos. Nginx los sirve directamente desde disco — sin Node.js, sin PM2.

bash — en tu máquina local
# Setup inicial (una sola vez)
ssh deploy@TU_IP_VPS "mkdir -p ~/apps/www"

# Build y upload
npm run build
rsync -avz --delete dist/ deploy@TU_IP_VPS:~/apps/www/dist/

No hay proceso que iniciar. Nginx sirve desde ~/apps/www/dist/ directamente.

Script deploy.sh

bash — deploy.sh
#!/bin/bash
set -e

VPS="deploy@TU_IP_VPS"
DIST_DIR="~/apps/www/dist"

echo "→ Building..."
npm run build

echo "→ Uploading..."
rsync -avz --delete dist/ $VPS:$DIST_DIR/

echo "✓ Done"
¿Qué hace rsync --delete?

Sincroniza solo los archivos que cambiaron entre tu máquina y el VPS — mucho más rápido que subir todo cada vez. --delete elimina archivos del VPS que ya no existen en tu dist/ local, evitando acumulación de builds viejos.

No se sube código fuente (src/, node_modules/, etc.) — solo la carpeta de salida del build.

06

Configurar Nginx como Reverse Proxy

bash
sudo nano /etc/nginx/sites-available/www
nginx
server {
    listen 80;
    server_name tudominio.com www.tudominio.com;

    root /home/deploy/apps/www/dist;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}
bash
sudo nano /etc/nginx/sites-available/www
nginx
server {
    listen 80;
    server_name tudominio.com www.tudominio.com;

    location / {
        proxy_pass              http://localhost:3000;
        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;
    }
}

Habilitar el sitio

bash
sudo ln -s /etc/nginx/sites-available/www /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Ejemplo: agregar budget.tudominio.com (puerto 3001)

bash
sudo nano /etc/nginx/sites-available/budget
sudo ln -s /etc/nginx/sites-available/budget /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Permisos

Nginx corre como www-data y necesita permiso x en cada directorio del path hasta los archivos del sitio.

bash
# Verificar permisos
ls -la /home/
ls -la /home/deploy/

# Dar permiso de ejecución en el home (si falta)
chmod o+x /home/deploy

# Dar permiso de lectura en los archivos del dist
chmod -R o+r /home/deploy/apps/www/dist

Errores comunes

ErrorCausa probableSolución
403 ForbiddenNginx no puede leer la carpetachmod o+x /home/deploy
404 Not Foundroot apunta a carpeta incorrectaVerificar path con ls
500 Internal ErrorConfig mal formadasudo tail /var/log/nginx/error.log
502 Bad GatewayApp Node.js no está corriendopm2 list + curl localhost:3000
07

SSL con Certbot

Prerequisito Los registros DNS deben estar propagados y apuntando al VPS antes de ejecutar Certbot.

Emitir certificados

bash
sudo certbot --nginx -d mydomain.com -d www.mydomain.com
sudo certbot --nginx -d budget.mydomain.com

Certbot modifica automáticamente los archivos de Nginx para: agregar listen 443 ssl, redirigir HTTP → HTTPS y apuntar a los certificados en /etc/letsencrypt/live/.

Verificar renovación automática

bash
# Probar sin renovar realmente
sudo certbot renew --dry-run

# Ver el timer de systemd (renueva cada 90 días)
systemctl status snap.certbot.renew.timer

Verificación final

bash
sudo nginx -t
sudo systemctl reload nginx

Abre en el navegador: https://www.mydomain.com — debe mostrar candado verde.

08

Agregar Nuevas Apps en el Futuro

El patrón es siempre el mismo. Para cada nuevo proyecto:

1 Desplegar la app en el servidor
bash
cd ~/apps
git clone https://github.com/tuusuario/nueva-app.git nueva-app
cd nueva-app && npm install && npm run build
pm2 start node_modules/.bin/next --name "nueva-app" -- start -p 3002
pm2 save
2 Agregar registro DNS en GoDaddy
TipoNombreValorTTL
Anueva-appIP del VPS600
3 Crear config de Nginx
bash
sudo nano /etc/nginx/sites-available/nueva-app
nginx
server {
    listen 80;
    server_name nueva-app.mydomain.com;

    location / {
        proxy_pass         http://localhost:3002;
        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_cache_bypass $http_upgrade;
    }
}
bash
sudo ln -s /etc/nginx/sites-available/nueva-app /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
4 Emitir certificado SSL
bash
sudo certbot --nginx -d nueva-app.mydomain.com

Checklist antes de cortar Vercel

Pre-launch checklist

09

Referencia Rápida

PM2

AcciónComando
Ver apps corriendopm2 list
Ver logs de una apppm2 logs www
Reiniciar una apppm2 restart www
Detener una app (libera RAM)pm2 stop www
Levantar una app detenidapm2 start www
Monitoreo en tiempo realpm2 monit
Guardar estado actualpm2 save

Nginx

AcciónComando
Verificar configuraciónsudo nginx -t
Recargar configuraciónsudo systemctl reload nginx
Ver logs de erroressudo tail -f /var/log/nginx/error.log
Ver logs de accesosudo tail -f /var/log/nginx/access.log

SSL

AcciónComando
Emitir certificadosudo certbot --nginx -d dominio.com
Renovar manualmentesudo certbot renew
Probar renovaciónsudo certbot renew --dry-run

Firewall (ufw)

AcciónComando
Ver reglas activassudo ufw status
Abrir un puertosudo ufw allow 8080/tcp

Actualizar una app desde Git

bash
cd ~/apps/www
git pull
npm install
npm run build
pm2 restart www

Sistema

AcciónComando
Ver uso de RAMfree -h
Ver uso de discodf -h
Ver logs del sistemasudo journalctl -xe
Revisar logs de arranquesudo journalctl -b