Para alojar un bot de Discord 24/7, mueve tu proyecto Node.js a un VPS, ejecútalo con un usuario sin privilegios y deja que PM2 supervise el proceso. Después debes guardar la lista de procesos y registrar el servicio de arranque que PM2 genere: así el bot continúa cuando cierras SSH, se recupera de un fallo de proceso y vuelve tras un reinicio del servidor. Eso no equivale a un 100 % de disponibilidad: una caída del VPS, de la red, de Discord o un error lógico todavía puede dejarlo fuera de servicio.
Este runbook parte de un bot discord.js que ya funciona en local y lo despliega en Ubuntu 24.04 LTS. Si todavía estás eligiendo tecnología, revisa primero nuestra comparación entre discord.js y discord.py; esta guía se centra en operar un proyecto Node.js ya funcional.
Qué mantiene realmente online al bot
Un bot conectado al Gateway de Discord conserva un WebSocket persistente, envía heartbeats y debe reconectar o reanudar la sesión cuando corresponda. discord.js resuelve ese protocolo; PM2 cubre otra capa: conserva el proceso Node.js fuera de tu terminal, lo reinicia si termina y puede restaurarlo durante el arranque del sistema.
Son responsabilidades distintas: PM2 cubre cierre de SSH, crash y reboot; la biblioteca cubre la reconexión del Gateway. Que PM2 muestre “online” solo prueba que existe un proceso, por eso más adelante añadiremos una comprobación de disponibilidad del cliente.
Prerrequisitos y recursos orientativos
Necesitas acceso SSH con una cuenta que pueda usar sudo, un bot registrado como aplicación, un token vigente y un repositorio o copia del proyecto con package-lock.json. Usa un usuario bot real: Discord indica que automatizar una cuenta de usuario normal —un self-bot— no está permitido en su documentación de bots y OAuth2.
| Elemento | Punto de partida | Cuándo aumentar |
|---|---|---|
| CPU | Una vCPU puede servir para evaluar un bot de texto pequeño. | Comandos intensivos, cifrado, imágenes, navegador o voz elevan la carga. |
| Memoria | Alrededor de 1 GB deja más margen operativo que una instancia mínima. | Muchos servidores, cachés, base de datos local, voz o procesos auxiliares requieren medir y ampliar. |
| Disco | Reserva espacio para el sistema, dos o más releases, dependencias y logs. | Archivos generados, SQLite, audio y retención larga de logs cambian el cálculo. |
| Red | Salida HTTPS/WSS estable hacia Discord y los servicios que consuma el bot. | Webhooks entrantes, panel web o callbacks OAuth2 requieren puertos, TLS y proxy inverso. |
Estas referencias no son requisitos universales. Mide CPU, memoria, disco y latencia con tu carga real antes de comprometer capacidad.
1. Prepara Ubuntu y crea el usuario del bot
Entra con tu cuenta administrativa. Actualiza el índice de paquetes, instala utilidades básicas y crea una identidad dedicada. No ejecutes el bot ni el daemon de PM2 como root.
sudo apt update
sudo apt install --yes ca-certificates curl git nano rsync ufw xz-utils
sudo adduser --system --group --home /home/discordbot --shell /bin/bash discordbot
sudo install -d -o discordbot -g discordbot -m 0750 /opt/discord-bot
sudo -u discordbot install -d -m 0750 /opt/discord-bot/releases /opt/discord-bot/shared
Si el usuario ya existe, no vuelvas a crearlo: comprueba su home, grupo y shell. Para un bot que solo inicia una conexión saliente al Gateway no hace falta abrir un puerto entrante. Antes de activar UFW, sustituye la regla SSH si tu servidor usa un puerto distinto y confirma que el panel del proveedor ofrece una consola de recuperación.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enable
sudo ufw status verbose
Un dashboard, un receptor de webhooks o un callback OAuth2 sí cambia este modelo: expón únicamente el puerto del proxy HTTPS necesario, no el proceso Node.js directamente.
2. Instala una rama LTS soportada de Node.js
A fecha de corte del 31 de julio de 2026, Node.js 24 está en Active LTS y Node.js 22 en Maintenance LTS. El security release del 29 de julio publicó 24.18.1 y 22.23.2. Para producción, el proyecto Node recomienda una rama Active LTS o Maintenance LTS en su calendario de releases.
El siguiente bloque instala el binario oficial 24.18.1 para AMD64 o ARM64 y verifica su checksum antes de extraerlo. Si una dependencia exige Node 22, cambia NODE_VERSION a 22.23.2. En una revisión futura, sustituye la versión por el parche LTS vigente tras leer sus notas de seguridad.
(
set -euo pipefail
ARCH="$(dpkg --print-architecture)"
case "$ARCH" in
amd64) NODE_ARCH="x64" ;;
arm64) NODE_ARCH="arm64" ;;
*) printf 'Arquitectura no cubierta: %s\n' "$ARCH" >&2; exit 1 ;;
esac
NODE_VERSION="24.18.1"
NODE_DIST="node-v${NODE_VERSION}-linux-${NODE_ARCH}"
DOWNLOAD_DIR="$(mktemp -d)"
trap 'rm -rf "$DOWNLOAD_DIR"' EXIT
cd "$DOWNLOAD_DIR"
curl --fail --silent --show-error --location --remote-name \
"https://nodejs.org/dist/v${NODE_VERSION}/${NODE_DIST}.tar.xz"
curl --fail --silent --show-error --location --remote-name \
"https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt"
grep " ${NODE_DIST}.tar.xz$" SHASUMS256.txt | sha256sum --check --strict
sudo install -d -m 0755 /usr/local/lib/nodejs
sudo tar -xJf "${NODE_DIST}.tar.xz" -C /usr/local/lib/nodejs
for binary in node npm npx corepack; do
sudo ln -sfn "/usr/local/lib/nodejs/${NODE_DIST}/bin/${binary}" "/usr/local/bin/${binary}"
done
node --version
npm --version
)
Los dos últimos comandos son validaciones que debes ejecutar: revisa que indiquen la rama instalada; este tutorial no presupone su salida.
3. Publica una release reproducible
La estructura releases/ más el enlace current permite cambiar de versión y volver atrás sin sobrescribir la anterior. Prefiere un tag inmutable o un commit revisado, no la punta cambiante de main.
Opción A: clonar con Git
Reemplaza la URL de ejemplo y el tag. En un repositorio privado usa una deploy key de solo lectura; nunca pongas un token personal dentro de la URL.
sudo -iu discordbot
set -euo pipefail
export RELEASE_ID="initial-v1.0.0"
export RELEASE_TAG="v1.0.0"
export REPOSITORY_URL="https://example.invalid/OWNER/REPOSITORY.git"
git clone --branch "$RELEASE_TAG" --depth 1 "$REPOSITORY_URL" "/opt/discord-bot/releases/$RELEASE_ID"
ln -sfnT "/opt/discord-bot/releases/$RELEASE_ID" /opt/discord-bot/current
cd /opt/discord-bot/current
git rev-parse HEAD
exit
Guarda el hash que obtengas como parte de tu registro de despliegue; no asumimos cuál será.
Opción B: copiar el proyecto desde tu equipo
Desde tu máquina local, copia el código sin .env, node_modules ni logs:
rsync -az \
--include='.env.example' \
--exclude='.env*' \
--exclude='.git' \
--exclude='node_modules' \
--exclude='*.log' \
./ VPS_ADMIN@VPS_HOST:/tmp/discord-bot-upload/
Después, en el VPS, publícalo con propietario y permisos correctos:
sudo install -d -o discordbot -g discordbot -m 0750 /opt/discord-bot/releases/initial-copy
sudo rsync -a /tmp/discord-bot-upload/ /opt/discord-bot/releases/initial-copy/
sudo chown -R discordbot:discordbot /opt/discord-bot/releases/initial-copy
sudo -iu discordbot ln -sfnT /opt/discord-bot/releases/initial-copy /opt/discord-bot/current
4. Saca el token de Git e instala dependencias
El secreto vivirá en /opt/discord-bot/shared/.env, fuera de cada release. Ábrelo en un editor interactivo para que el token no quede en el historial del shell.
sudo -iu discordbot
umask 077
nano /opt/discord-bot/shared/.env
El archivo tendrá esta forma; sustituye el marcador dentro del editor, nunca en un comando compartido:
dotenv
DISCORD_TOKEN=REEMPLAZAR_DENTRO_DEL_EDITOR
Aplica permisos y compruébalos. La salida debe mostrar modo 600 y propietario discordbot.
chmod 600 /opt/discord-bot/shared/.env
stat -c '%a %U:%G %n' /opt/discord-bot/shared/.env
Mantén además esta política en el .gitignore del proyecto y verifica todos los nombres habituales con git ls-files -- '.env' '.env.*'. Solo .env.example puede aparecer, y únicamente si no contiene secretos:
gitignore
.env
.env.*
!.env.example
node_modules/
*.log
cd /opt/discord-bot/current
git ls-files -- '.env' '.env.*'
npm ci --omit=dev
npm ci exige un lockfile y reproduce sus versiones. Si el proyecto compila TypeScript u otros assets con dependencias de desarrollo, genera y prueba el artefacto en CI o instala temporalmente todas las dependencias, ejecuta el build y termina con npm prune --omit=dev. No improvises un npm install en producción que cambie el lockfile.
Antes de PM2 puedes ejecutar una prueba breve en primer plano, sustituyendo src/index.js por tu entrada real. Hazlo solo mientras la instancia local está apagada para no abrir dos sesiones con el mismo bot; detén la prueba con Ctrl+C después de observar el evento Ready.
cd /opt/discord-bot/current
NODE_ENV=production /usr/local/bin/node --env-file=/opt/discord-bot/shared/.env ./src/index.js
Si el token apareció en Git, logs, una captura o un comando visible, considera que está comprometido: regénéralo en el Developer Portal, actualiza el archivo y reinicia el proceso. Borrarlo del último commit no invalida la copia expuesta.
Declara solo los Gateway intents y permisos que necesita cada función. La Developer Policy de Discord limita el uso de datos de API a lo necesario para la funcionalidad declarada; pedir más eventos también aumenta caché, superficie de fallo y exposición de datos.
5. Configura PM2 bajo el mismo usuario
Instala PM2 en el prefijo del usuario discordbot. Todos los comandos posteriores de PM2 deben ejecutarse con esa identidad; PM2 mantiene un estado separado por usuario, y mezclar root con discordbot produce listas y daemons distintos.
npm config set prefix "$HOME/.local"
npm install --global pm2
grep -qxF 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.profile" || printf '%s\n' 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.profile"
export PATH="$HOME/.local/bin:$PATH"
pm2 --version
Crea /opt/discord-bot/ecosystem.config.cjs. El sufijo .cjs conserva module.exports como CommonJS incluso si el proyecto declara "type": "module". Cambia script por la entrada de tu bot.
nano /opt/discord-bot/ecosystem.config.cjs
Mantén tu bot de Discord online sin depender de tu PC
Ejecuta tu bot en una infraestructura preparada para procesos persistentes, reinicios y operación continua.


module.exports = {
apps: [
{
name: 'discord-bot',
cwd: '/opt/discord-bot/current',
script: './src/index.js',
interpreter: '/usr/local/bin/node',
node_args: '--env-file=/opt/discord-bot/shared/.env',
exec_mode: 'fork',
instances: 1,
autorestart: true,
watch: false,
min_uptime: '10s',
max_restarts: 10,
restart_delay: 5000,
max_memory_restart: '512M',
kill_timeout: 5000,
time: true,
env: {
NODE_ENV: 'production',
HEALTH_PORT: '3001'
}
}
]
};
El ecosystem file de PM2 centraliza el directorio, el script y las políticas de reinicio sin guardar el token. Para un bot pequeño se usa una instancia en modo fork: duplicar procesos sin diseñar sharding, locks y trabajos idempotentes puede duplicar respuestas o tareas programadas.
restart_delay, min_uptime y max_restarts contienen un crash loop; no corrigen la causa. El umbral de max_memory_restart es solo un ejemplo que debes ajustar por debajo de la capacidad del VPS dejando margen al sistema. Reiniciar por memoria puede limitar el impacto inmediato, pero no sustituye perfilar y reparar una fuga.
6. Añade una comprobación de disponibilidad
PM2 conoce el estado del proceso, no el del Gateway. Integra una ruta local en el módulo donde ya existe client. Este ejemplo para discord.js 14.x devuelve 200 solo cuando client.isReady() es verdadero y escucha en loopback, por lo que no abre una superficie pública. Comprueba la API en la documentación de Client de la versión instalada.
import http from 'node:http';
const healthPort = Number(process.env.HEALTH_PORT || 3001);
const healthServer = http.createServer((request, response) => {
if (request.url !== '/healthz') {
response.writeHead(404);
response.end();
return;
}
const ready = client.isReady();
const body = JSON.stringify({
ready,
gatewayPingMs: ready ? client.ws.ping : null
});
response.writeHead(ready ? 200 : 503, {
'content-type': 'application/json',
'cache-control': 'no-store'
});
response.end(body);
});
healthServer.listen(healthPort, '127.0.0.1');
Adapta el import si tu proyecto es CommonJS, confirma que DISCORD_TOKEN coincida con el nombre que consume tu aplicación y cierra el servidor en tu manejo de señales si ya tienes apagado ordenado. Luego valida sintaxis, inicia la declaración y revisa estado y logs:
node --check /opt/discord-bot/ecosystem.config.cjs
pm2 start /opt/discord-bot/ecosystem.config.cjs
pm2 status
pm2 describe discord-bot
pm2 logs discord-bot --lines 100
Salir de la vista de logs con Ctrl+C no detiene el bot. Cuando los logs indiquen Ready, ejecuta la prueba local:
curl --fail --silent --show-error http://127.0.0.1:3001/healthz
La validación completa tiene tres capas: PM2 debe mostrar el proceso online; /healthz debe responder con un estado listo; y un comando slash seguro en un servidor de prueba debe recibir respuesta. No automatices esa prueba iniciando sesión como un usuario normal. Para monitorización externa, haz que el bot emita una señal saliente o publica la ruta solo detrás de HTTPS y autenticación.
7. Restaura el bot después de un reboot
La documentación de startup de PM2 separa dos acciones: registrar el servicio de inicio y guardar la lista que ese servicio restaurará. Como discordbot, ejecuta:
pm2 startup
exit
PM2 imprimirá un comando específico para tu sistema, usuario, home y PATH. El exit del bloque vuelve a la cuenta administrativa; desde ella, copia y ejecuta exactamente ese comando generado. No construyas uno a mano ni reutilices el de otro servidor. Después vuelve al usuario del bot y guarda el estado actual:
sudo -iu discordbot
export PATH="$HOME/.local/bin:$PATH"
pm2 save
Programa una ventana de prueba, reinicia el VPS y vuelve a conectar:
exit
sudo reboot
Tras recuperar SSH, comprueba el mismo usuario, el servicio, PM2 y la disponibilidad real:
sudo systemctl status pm2-discordbot --no-pager
sudo -iu discordbot
export PATH="$HOME/.local/bin:$PATH"
pm2 status
pm2 describe discord-bot
curl --fail --silent --show-error http://127.0.0.1:3001/healthz
Si el comando generado creó otro nombre de servicio, úsalo en lugar de pm2-discordbot. No marques el despliegue como terminado hasta superar este reboot test. Si actualizas la instalación de Node.js, PM2 recomienda regenerar su startup script para que apunte al runtime vigente; vuelve a ejecutar el flujo de pm2 unstartup/pm2 startup siguiendo los comandos que genere tu host y guarda de nuevo la lista.
8. Controla logs sin llenar el disco
PM2 guarda por defecto los logs en $HOME/.pm2/logs y permite filtrarlos por proceso, como documenta su guía de gestión de logs. Consulta una ventana finita para diagnósticos y vigila también el espacio libre.
pm2 logs discord-bot --lines 200 --nostream
pm2 monit
df -h
Instala la rotación con el mismo usuario. Los valores siguientes son una política inicial, no universal: ajústalos al volumen, espacio y obligaciones de retención de tu bot.
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true
pm2 save
No registres el token, cabeceras de autorización, datos privados ni cargas completas innecesarias. Conserva mensajes útiles: arranque, Ready, desconexión con código, reanudación, error con contexto y apagado.
9. Actualiza por tag y conserva un rollback
Prueba cada tag en CI y, si es posible, en un entorno de staging. En el VPS crea una release nueva sin tocar la activa, instala dependencias y guarda la ruta anterior antes del cambio atómico:
export PATH="$HOME/.local/bin:$PATH"
set -euo pipefail
export RELEASE_TAG="v1.0.1"
export RELEASE_ID="$(date -u +%Y%m%dT%H%M%SZ)-${RELEASE_TAG}"
export REPOSITORY_URL="https://example.invalid/OWNER/REPOSITORY.git"
git clone --branch "$RELEASE_TAG" --depth 1 "$REPOSITORY_URL" "/opt/discord-bot/releases/$RELEASE_ID"
cd "/opt/discord-bot/releases/$RELEASE_ID"
npm ci --omit=dev
node --check ./src/index.js
CURRENT_RELEASE="$(readlink -f /opt/discord-bot/current)"
printf '%s\n' "$CURRENT_RELEASE" > /opt/discord-bot/shared/previous-release.tmp
mv -f /opt/discord-bot/shared/previous-release.tmp /opt/discord-bot/shared/previous-release
NEXT_LINK="/opt/discord-bot/.current-next"
ln -sfnT "/opt/discord-bot/releases/$RELEASE_ID" "$NEXT_LINK"
mv -Tf "$NEXT_LINK" /opt/discord-bot/current
pm2 restart /opt/discord-bot/ecosystem.config.cjs --only discord-bot --update-env
Adapta la comprobación de sintaxis a tu entrada y a tu pipeline; no sustituye tests funcionales. Valida la release antes de guardar el estado:
pm2 status
pm2 logs discord-bot --lines 100 --nostream
curl --fail --silent --show-error http://127.0.0.1:3001/healthz
pm2 save
Si falla, vuelve al enlace anterior. Este rollback cambia código y dependencias, pero no deshace migraciones ni datos:
set -euo pipefail
PREVIOUS_RELEASE="$(cat /opt/discord-bot/shared/previous-release)"
case "$PREVIOUS_RELEASE" in
/opt/discord-bot/releases/*) ;;
*) printf 'Ruta de release anterior no válida\n' >&2; exit 1 ;;
esac
test -d "$PREVIOUS_RELEASE"
NEXT_LINK="/opt/discord-bot/.current-next"
ln -sfnT "$PREVIOUS_RELEASE" "$NEXT_LINK"
mv -Tf "$NEXT_LINK" /opt/discord-bot/current
pm2 restart /opt/discord-bot/ecosystem.config.cjs --only discord-bot --update-env
pm2 logs discord-bot --lines 100 --nostream
curl --fail --silent --show-error http://127.0.0.1:3001/healthz
pm2 save
Para SQLite o cualquier estado mutable, diseña migraciones compatibles hacia atrás o un procedimiento de restauración probado. Mantén al menos una release anterior conocida como buena hasta confirmar la nueva.
10. Copias de seguridad y mantenimiento
- Código: conserva tags/commits inmutables y
package-lock.json. Un repositorio remoto no respalda datos creados en producción. - Secretos: guarda el token en un gestor de secretos o copia cifrada con acceso restringido; nunca en Git ni en un tar sin cifrar.
- Datos: respalda SQLite, archivos subidos y configuraciones persistentes con un método consistente para la aplicación. Una copia de una base activa puede ser irrecuperable si no usa su mecanismo de backup.
- Configuración: conserva una copia revisada de
ecosystem.config.cjs—sin secretos— y registra la versión de Node, PM2 y el commit desplegado. - Restauración: prueba periódicamente que puedes recuperar datos, publicar una release y ejecutar
pm2 resurrecta partir de la lista guardada.
Si usas SQLite, su CLI oficial ofrece el comando .backup para copiar la base abierta. Ajusta ambas rutas, guarda el archivo fuera del VPS mediante un canal cifrado y prueba la restauración. Este ejemplo no incluye .env:
exit
sudo apt install --yes sqlite3
sudo -iu discordbot
BACKUP_ID="$(date -u +%Y%m%dT%H%M%SZ)"
install -d -m 0700 /opt/discord-bot/backups
sqlite3 /opt/discord-bot/shared/data/bot.sqlite ".backup '/opt/discord-bot/backups/bot-${BACKUP_ID}.sqlite'"
sha256sum "/opt/discord-bot/backups/bot-${BACKUP_ID}.sqlite" > "/opt/discord-bot/backups/bot-${BACKUP_ID}.sqlite.sha256"
Como rutina, revisa alertas de Node y dependencias, aplica parches del sistema en una ventana, comprueba reinicios de PM2, latencia del Gateway, crecimiento de memoria y disco, y prueba un reboot tras cambios en Node, PM2 o systemd.
Diagnóstico: síntomas frecuentes
| Síntoma | Causa probable | Comprobación | Acción |
|---|---|---|---|
| Online en local, offline al cerrar SSH | Se inició con node dentro de la sesión o PM2 se ejecutó con otro usuario. | sudo -iu discordbot y pm2 status. | Inicia el ecosystem como discordbot; salir de pm2 logs no detiene el proceso. |
| Online hasta el reboot | Falta el servicio startup o la lista no se guardó. | systemctl status pm2-discordbot y pm2 status. | Repite pm2 startup, ejecuta exactamente su comando generado y después pm2 save. |
| Token inválido | Token revocado, error al editar .env, archivo ilegible o secreto expuesto. | stat del archivo y logs, sin imprimir el token. | Regenera el token si hay duda, edita el archivo, aplica 600 y reinicia. |
| Faltan eventos o contenido | Intents ausentes en el código o, si son privilegiados, en el Developer Portal. | Compara los intents declarados con la función real del bot. | Activa solo los necesarios en ambos lugares; evita recopilar datos sin necesidad. |
| Reconexiones o 429 repetidos | Red inestable, bucle de login, reinicios demasiado rápidos o llamadas REST sin backoff. | Busca códigos de desconexión, Ready/Resume y 429 en logs. | Deja que la biblioteca gestione Gateway; respeta Retry-After/retry_after según los límites oficiales de Discord y corrige el bucle. |
| Crash loop | Excepción al arrancar, dependencia ausente, configuración incorrecta o versión incompatible. | pm2 describe discord-bot y logs finitos. | Detén la cascada, corrige o ejecuta el rollback conocido como bueno. |
| Memoria creciente | Caché sin límite, listeners duplicados, collectors o fuga. | pm2 monit, métricas del sistema y perfilado. | Reduce estado e intents, elimina listeners y perfila. Ajusta max_memory_restart solo como contención. |
Con estas comprobaciones, “24/7” deja de significar una terminal abierta y pasa a ser una operación reproducible: proceso supervisado, arranque persistente, señal de salud, datos recuperables y una versión anterior lista para volver.








