🚀 Guía Completa de Despliegue en VPS (Next.js + PM2 + Redis + Nginx + SSL + GitHub Actions)
Esta guía documenta paso a paso el proceso de despliegue en producción de tu aplicación, las decisiones de arquitectura tomadas, comandos útiles, precauciones y solución de errores comunes.
🏗️ Arquitectura del Despliegue
La aplicación se compone de:
- Frontend y API Route Handlers (Next.js) corriendo localmente en el puerto
<PUERTO_APP>(ej.3000). - Worker en segundo plano (BullMQ + tsx) encargado de procesar tareas asíncronas y de IA.
- Servidor Redis (Docker) escuchando en el puerto local
<PUERTO_REDIS>(ej.6379). - Nginx actuando como proxy inverso, redirigiendo el tráfico seguro del puerto
80/443a la aplicación en el puerto<PUERTO_APP>. - Certbot (Let's Encrypt) para gestionar el certificado SSL HTTPS de forma gratuita y automática.
- CI/CD (GitHub Actions) para automatizar la integración y despliegue continuo cada vez que se hace push a la rama principal (ej.
main).
📋 Paso a Paso del Despliegue
Paso 1: Configurar DNS
Antes de configurar el servidor, debes indicarle al dominio a dónde apuntar.
- Entra a tu proveedor de dominio (ej. Cloudflare) y ve a la sección DNS.
- Añade un nuevo registro tipo A:
- Type:
A - Name:
<subdominio>(ej.meetingspara crear el subdominiomeetings.tu-dominio.comu@para el dominio raíz). - IPv4 Address: La IP pública de tu VPS (ej.
<IP_PUBLICA_VPS>). - Proxy Status: Activo (o solo DNS, según tus necesidades).
- Type:
Paso 2: Preparación del VPS y Permisos del Usuario
Es una mala práctica correr tus aplicaciones Node.js o el pipeline de despliegue continuo como usuario root. Por ello se utiliza un usuario con privilegios limitados (ej. <usuario_deploy>).
- Crear la carpeta del proyecto en el servidor:
mkdir -p /var/www/<nombre-proyecto> - Clonar el repositorio dentro de la carpeta:
cd /var/www/ git clone https://github.com/<tu-usuario>/<tu-repositorio>.git - Mover la propiedad al usuario
<usuario_deploy>: Si clonaste comoroot, debes transferir la propiedad al usuario que usará GitHub Actions:chown -R <usuario_deploy>:<usuario_deploy> /var/www/<nombre-proyecto>
Paso 3: Configurar las Variables de Entorno (.env)
Crea el archivo .env en la raíz de tu proyecto /var/www/<nombre-proyecto>/ con tus credenciales de producción:
# Configuración del servidor y base de datos
PORT=<PUERTO_APP>
NEXT_PUBLIC_SUPABASE_URL=https://<tu-proyecto>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=tu_service_role_key
DATABASE_URL="postgresql://<usuario_db>:<password_db>@<host_db>:<puerto_db>/<nombre_db>"
# Configuración de Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=<PUERTO_REDIS>
REDIS_PASSWORD=
REDIS_DB=1 # Se recomienda usar una base de datos específica para no colisionar con otros proyectos
💡 Nota: Al dejar
REDIS_PASSWORDvacío, se asume que tu Redis local no tiene contraseña. Al especificar una DB diferente, aislamos los datos de este proyecto de otros que usen el mismo contenedor.
Paso 4: Proxy Inverso con Nginx
Nginx recibe las conexiones en los puertos web estándar y las delega internamente al puerto <PUERTO_APP>.
- Crear archivo de configuración:
sudo nano /etc/nginx/sites-available/<dominio_o_subdominio> - Pegar configuración básica de proxy inverso:
server { listen 80; server_name <dominio_o_subdominio>; location / { proxy_pass http://127.0.0.1:<PUERTO_APP>; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; 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; } } - Habilitar el sitio y reiniciar Nginx:
sudo ln -s /etc/nginx/sites-available/<dominio_o_subdominio> /etc/nginx/sites-enabled/ sudo nginx -t # Verificar que la sintaxis esté correcta sudo systemctl restart nginx
Paso 5: Habilitar HTTPS con Certbot (SSL)
Consigue que la conexión sea cifrada con un certificado SSL válido.
sudo certbot --nginx -d <dominio_o_subdominio>
Sigue las instrucciones en pantalla, selecciona la opción para redirigir todo el tráfico HTTP a HTTPS de manera automática.
Paso 6: Configurar e iniciar PM2
PM2 mantiene los procesos vivos en segundo plano.
- Iniciar la aplicación Next.js:
pm2 start npm --name "<nombre-app>" -- run start -- -p <PUERTO_APP> - Iniciar el Worker utilizando el ejecutable o script correspondiente:
pm2 start pnpm --name "<nombre-worker>" -- exec tsx <ruta_al_worker>.ts - Guardar el listado actual en PM2:
(Esto asegura que si el VPS se reinicia, ambos procesos se levanten solos).
pm2 save
🛠️ GitHub Actions (CI/CD Automático)
El archivo de configuración de GitHub Actions en tu repositorio se ubica en .github/workflows/<archivo_despliegue>.yml.
Secretos requeridos en el repositorio de GitHub:
Ve a Settings -> Secrets and variables -> Actions -> New repository secret:
SSH_PRIVATE_KEY: Tu clave SSH privada.SERVER_IP: La IP de tu servidor (<IP_PUBLICA_VPS>).SERVER_USER: El usuario del servidor configurado (<usuario_deploy>).
⚠️ Precauciones y Errores Comunes
1. El Puerto ya está en uso (EADDRINUSE)
- Causa: Hay un proceso de Node.js huérfano (zombie) corriendo fuera de PM2 que está ocupando el puerto.
- Solución: Fuerza la terminación del proceso con:
kill -9 $(lsof -t -i:<PUERTO_APP>) 2>/dev/null || fuser -k <PUERTO_APP>/tcp 2>/dev/null || true
2. Conflicto de Permisos (EACCES: permission denied, unlink ...)
- Causa: Se ejecutaron comandos como
rootdentro de la carpeta del proyecto, haciendo que el usuario<usuario_deploy>pierda la facultad de escribir o borrar archivos. - Solución: Ejecuta esto en tu VPS como
rootpara limpiar y devolver la propiedad a<usuario_deploy>:
Regla de Oro: Nunca ejecutes comandos de instalación comorm -rf /var/www/<nombre-proyecto>/node_modules chown -R <usuario_deploy>:<usuario_deploy> /var/www/<nombre-proyecto>rooten la carpeta del proyecto.
3. El comando tsx no es encontrado (Command "tsx" not found)
- Causa:
tsxno está instalado globalmente en el VPS.- O bien la instalación se ejecutó en modo producción omitiendo
devDependenciesdonde residetsx.
- Solución:
- Asegurar la instalación de dependencias requeridas en el despliegue con:
pnpm install --frozen-lockfile --prod=false - Arrancar el worker usando
pnpm exec tsxo la ruta local al ejecutable:pm2 start pnpm --name "<nombre-worker>" -- exec tsx <ruta_al_worker>.ts
- Asegurar la instalación de dependencias requeridas en el despliegue con:
4. Lockfile desactualizado (ERR_PNPM_OUTDATED_LOCKFILE)
- Causa: Se modificó manualmente el archivo
package.jsonen local pero no se actualizó el lockfile correspondiente antes de hacer el push. - Solución: Corre la instalación de dependencias en local, haz commit del archivo lockfile generado y súbelo.
🔍 Comandos de Diagnóstico Útiles
- Ver estado general de tus apps:
pm2 status - Ver logs en vivo de una app específica:
pm2 logs <nombre-app>opm2 logs <nombre-worker> - Reiniciar una app:
pm2 restart <nombre-app> - Monitorear recursos (CPU/RAM):
pm2 monit - Ver qué puertos están escuchando en el VPS:
ss -tulnp | grep LISTEN - Verificar que Redis esté corriendo:
docker ps | grep redis - Acceder a la consola de Redis en Docker o CLI:
docker exec -it <nombre_contenedor_redis> redis-clioredis-cli