Cómo alojar un bot de Discord las 24 horas
Sigue esto en tu propio servidor. Empieza gratis, sin tarjeta
El bot funciona. node index.js en tu computadora lo conecta a tu servidor de pruebas, !ping responde pong, todo bien, hasta que cierras la terminal o pones la computadora a dormir, y el bot se desconecta con ella. Lo que falta no es más código. Es un proceso que siga corriendo después de que te vas, lo cual es un problema distinto al que resolviste escribiendo el bot en sí.
Esta guía construye un bot real con discord.js, lo empaqueta en un Dockerfile, y explica lo que confunde a casi todos la primera vez que alojan un bot de Discord: por qué necesita un proceso que nunca se detenga, no una función que arranca bajo demanda. Luego cubre la política de reinicio, el logging, y el camino de un clic para tener lo mismo corriendo en Railyard.
Lo que necesitas
- Node.js 22 o 24 instalado localmente (discord.js 14 requiere Node 18 o más reciente).
- Una cuenta de Discord y un servidor (guild) donde probar, idealmente uno que controles.
- Docker instalado localmente, para construir y probar la imagen antes de llevarla a otro lado.
- Un servidor donde correr el contenedor, o una cuenta de Railyard para el atajo de la última sección.
Paso 1: crea la aplicación del bot y obtén un token
Ve al portal de desarrolladores de Discord y haz clic en New Application. Dale un nombre, luego abre la pestaña Bot a la izquierda. Haz clic en Reset Token para generar un token, y cópialo en un lugar seguro. Esta es la credencial que tu código usa para iniciar sesión como el bot; cualquiera que la tenga puede controlar tu bot, así que va en una variable de entorno, nunca en un archivo que subas a un repositorio.
En la misma pestaña Bot, bajo Privileged Gateway Intents, activa Message Content Intent si tu bot necesita leer el texto de los mensajes (un bot de moderación o uno que responde a comandos de texto lo necesita; uno que solo responde a slash commands y botones no).
Abre OAuth2 → URL Generator, marca el scope bot, marca los permisos que tu bot realmente necesita (empieza con Send Messages y Read Message History para un bot simple), copia la URL generada, y ábrela en un navegador para invitar al bot a tu servidor de pruebas.
Paso 2: escribe el bot
discord.js 14.27.0 es la versión actual en npm. Inicializa el proyecto e instálalo:
npm init -y
npm install discord.js@^14.27.0
Agrega "type": "module" a package.json para poder usar import, y un script start:
{
"name": "my-discord-bot",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"discord.js": "^14.27.0"
}
}
index.js:
import { Client, GatewayIntentBits, Events } from "discord.js";
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
client.once(Events.ClientReady, (readyClient) => {
console.log(`Logged in as ${readyClient.user.tag}`);
});
client.on(Events.MessageCreate, async (message) => {
if (message.author.bot) return;
if (message.content === "!ping") {
await message.reply("pong");
}
});
client.on(Events.Error, (error) => {
console.error("Discord client error:", error);
});
client.login(process.env.DISCORD_TOKEN);
Cada entrada de GatewayIntentBits declara una categoría de eventos que Discord realmente le enviará a tu bot; GuildMessages y MessageContent juntos son lo que permite que MessageCreate se dispare con un message.content legible para un comando de texto simple como !ping. Pedir MessageContent en el código sin activar el Message Content Intent en el portal de desarrolladores del Paso 1 te da un error de “disallowed intents” al iniciar sesión, el error más común con diferencia para un bot nuevo.
Pruébalo localmente:
DISCORD_TOKEN=tu-token-aqui node index.js
Deberías ver Logged in as yourbot#0000 en la terminal, y !ping en tu servidor de pruebas debería recibir una respuesta pong.
Paso 3: escribe el Dockerfile
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER node
CMD ["node", "index.js"]
Esta es la misma forma de dos etapas que usarías para cualquier aplicación Node: las dependencias se instalan en su propia etapa para que queden en caché por separado del resto del código fuente, y la imagen final corre como el usuario no root node en vez de root. La diferencia real con el Dockerfile de una app web es lo que falta: no hay EXPOSE, porque este proceso nunca escucha en un puerto. Abre una conexión saliente hacia el gateway de Discord; nada se conecta hacia él.
Paso 4: por qué esto necesita ser un proceso siempre activo, no una función
Una función serverless (una Vercel Function, un AWS Lambda) arranca cuando llega una solicitud HTTP y se apaga poco después de responder. Ese modelo le queda bien a una carga de trabajo hecha de solicitudes discretas e independientes. Un bot de Discord no lo es: después de que client.login() tiene éxito, discord.js abre una conexión WebSocket al gateway de Discord y la mantiene abierta indefinidamente. Cada mensaje, reacción e interacción de slash command que tu bot ve llega como un evento empujado por ese socket ya abierto, no como una solicitud HTTP entrante que tu plataforma enruta a un handler.
No hay ninguna solicitud que dispare un cold start en primer lugar, y aunque una plataforma invocara tu código en algún otro horario, la conexión al gateway misma tendría que existir continuamente entre invocaciones para recibir algo; una conexión que solo existe mientras una función está corriendo no es una conexión capaz de recibir un evento que llega entre dos invocaciones. Esa es la distinción real detrás de una búsqueda como “alojar un bot de Discord”: no el rendimiento, sino si algo está escuchando en el momento en que Discord decide enviar algo.
(Discord también ofrece un endpoint HTTP basado en interacciones específicamente para slash commands, donde Discord le envía a tu bot un POST HTTP por cada interacción en vez de un evento de gateway. Ese modelo sí puede correr en una función serverless, pero solo cubre slash commands y componentes, no mensajes normales, presencia, ni todo lo demás que lleva el gateway, y es una arquitectura distinta al enfoque de Client más gateway que usa esta guía.)
Lo que un bot como este realmente necesita no tiene nada de glamoroso: un proceso que arranca, se mantiene corriendo, y se reconecta por su cuenta si la conexión se cae, algo que discord.js ya hace internamente ante interrupciones de red comunes. Tu trabajo es asegurarte de que el proceso mismo siga existiendo, lo cual es una política de reinicio, no un framework.
Paso 5: política de reinicio y logging
Corre la imagen localmente con una política de reinicio para que Docker la traiga de vuelta tras un fallo o un reinicio de la máquina:
docker build -t my-discord-bot .
docker run -d --name my-discord-bot --restart unless-stopped \
-e DISCORD_TOKEN=tu-token-aqui \
my-discord-bot
--restart unless-stopped le dice al daemon de Docker que reinicie el contenedor automáticamente a menos que tú lo hayas detenido explícitamente, lo que cubre tanto una excepción no manejada que tumba el proceso como un reinicio de la máquina host.
Para los logs, no agregues todavía una librería de logging; console.log y console.error hacia stdout/stderr bastan para un bot de este tamaño, y Docker ya captura ambos:
docker logs -f my-discord-bot
Deberías ver la línea Logged in as... del Paso 2, y cualquier salida de console.error si el cliente tiene un error. Sigue registrando los eventos específicos de Discord que importan para depurar un bot que se quedó callado, en particular las reconexiones, para que un panel o terminal que revises después te diga que algo pasó en vez de simplemente quedarse en silencio:
client.on(Events.ShardDisconnect, () => console.warn("Shard disconnected, reconnecting..."));
client.on(Events.ShardReconnecting, () => console.log("Shard reconnecting..."));
Paso 6: hazlo correr en un servidor real
Copia la imagen (o el repositorio, si construyes en el servidor) a un VPS con Docker instalado, y corre ahí el mismo comando docker run del Paso 5, con un archivo .env real en vez de un token en línea:
docker run -d --name my-discord-bot --restart unless-stopped --env-file .env my-discord-bot
Esa sola bandera, --restart unless-stopped, es la mayor parte de lo que “alojar un bot las 24 horas” significa en la práctica: un proceso que vuelve por su cuenta después de cualquier cosa que no sea que tú lo detengas explícitamente.
El atajo: hazlo con Railyard
Apunta Railyard al mismo repositorio. Como el repositorio tiene un Dockerfile que realmente copia la aplicación dentro de la imagen (no una imagen de desarrollo que espera un bind mount, algo que la detección de build de Railyard omite automáticamente), ese Dockerfile se construye y corre directamente, sin que Railpack o un buildpack tengan que adivinar nada; consulta los métodos de build.
Este bot no tiene proceso web, y está bien así: Railyard corre las apps que solo tienen worker igual que cualquier otra, como un tipo de proceso sin puerto asociado, cubierto en Workers y tareas programadas. Agrega DISCORD_TOKEN en la pestaña Variables de la app (consulta Variables y secretos); se cifra y se aplica en el siguiente despliegue, el equivalente en Railyard del --env-file .env del Paso 6.
Un ajuste que conviene dejar tranquilo: Sleep when idle, bajo Settings → General, detiene un proceso una vez que su CPU reportado ha estado inactivo el tiempo suficiente. Un bot sentado en un servidor tranquilo puede parecer inactivo según esa medida aunque su conexión al gateway esté bien viva y a punto de recibir un mensaje; deja ese ajuste apagado para este proceso, para que la conexión nunca sea lo que se pone a dormir.
La política de reinicio del Paso 5 tampoco es algo que configures aquí, es simplemente cómo Railyard corre los contenedores de cada app: un fallo reinicia el proceso sin que tengas que entrar por SSH a escribir docker start. Los logs están en la pestaña Logs de la página de la app, o con railyard logs my-bot --follow desde la CLI, el mismo hábito de docker logs -f del Paso 5 sin necesitar una terminal abierta en el servidor mismo.
Para desplegar sin siquiera pegar una URL de repositorio cada vez, agrega un botón de despliegue al README del bot:
[](https://app.railyard.run/deploy?repo=https://github.com/you/your-discord-bot)
Consulta la documentación del botón de despliegue para el formato completo del enlace, y el inicio rápido si partes de cero: registro, un servidor, y una app corriendo en unos diez minutos.
Solución de problemas
El bot aparece desconectado en Discord aunque el contenedor esté corriendo. Revisa primero docker logs (o la pestaña Logs) por un error de inicio de sesión; un token inválido o revocado falla de inmediato. Si el inicio de sesión funciona pero el bot sigue sin responder, confirma que la URL de invitación del Paso 1 realmente incluía el scope bot y los permisos que necesitan tus comandos.
“Used disallowed intents” al arrancar. Estás pidiendo GatewayIntentBits.MessageContent (u otro intent privilegiado) en el código sin haberlo activado en el portal de desarrolladores. Vuelve a la pestaña Bot del Paso 1 y activa el Privileged Gateway Intent correspondiente.
El contenedor se reinicia en bucle. Eso es --restart unless-stopped haciendo exactamente su trabajo frente a un proceso que falla de inmediato, generalmente un error de arranque, no uno transitorio. Revisa las primeras líneas de docker logs justo después de un reinicio, no el final; la razón del fallo casi siempre está ahí, comúnmente un DISCORD_TOKEN faltante o un error de tipeo en un import de dependencia.
El bot se queda callado un rato, luego vuelve. Esto suele ser una reconexión al gateway, que discord.js maneja por su cuenta; el logging de ShardDisconnect/ShardReconnecting del Paso 5 convierte “el bot parecía caído” en una línea de log que realmente puedes verificar contra la página de estado de Discord si se repite.
Preguntas frecuentes
¿Puedo alojar un bot de Discord gratis? El framework del bot en sí es gratis de todas formas; lo que cuesta dinero es el cómputo siempre activo sobre el que corre. Un VPS pequeño cuesta unos pocos dólares al mes, que es el piso real para “24/7”, ya que nada gratuito mantiene un proceso vivo indefinidamente sin que tengas que cuidarlo.
¿Necesito un servidor web para mi bot de Discord? No, no para el enfoque basado en gateway que usa esta guía. El bot solo hace conexiones salientes (al gateway y a la API REST de Discord); nunca necesita aceptar solicitudes HTTP entrantes, así que no hay ningún puerto que exponer ni proxy inverso o certificado TLS que gestionar para el bot en sí.
¿Qué pasa con los mensajes enviados mientras mi bot está desconectado? Se pierden desde el punto de vista del bot. El gateway de Discord no pone en cola los eventos para que un cliente desconectado los reproduzca después; si el bot estaba desconectado cuando se envió un mensaje, nunca ve ese evento MessageCreate, lo cual es la razón práctica por la que “24/7” importa más para un bot que para la mayoría de las aplicaciones web.
¿discord.js o discord.py: importa para el hosting? No para nada de lo que cubre esta guía. Un bot de discord.py (Python) necesita las mismas cosas: un proceso siempre activo, no una función; una política de reinicio; y intents.message_content = True definido en el código además del Message Content Intent activado en el portal de desarrolladores, el equivalente en Python de la configuración de intents del Paso 2. El Dockerfile cambia node:24-alpine por python:3.13-slim y bot.run(TOKEN) reemplaza a client.login(...), pero la arquitectura es idéntica.
¿Por qué no simplemente dejar una terminal abierta corriendo node index.js? Funciona hasta que cierras la terminal, la computadora se duerme, o la sesión SSH se cae, momento en el que el proceso muere con ella y nada lo trae de vuelta. --restart unless-stopped (o Railyard corriendo el contenedor de la misma forma por defecto) es la diferencia entre un bot que está arriba porque todavía no has cerrado tu laptop, y uno que de verdad está arriba.
Un bot de Discord es un pedazo pequeño de código haciendo un trabajo grande y continuo: mantenerse conectado. Una vez que el proceso mismo es algo que se reinicia solo y registra lo que puedes ver, que el bot esté siempre en línea deja de ser algo en lo que tienes que pensar.
Por qué eligen Railyard para esto
- Tu propia cuenta en la nube. Despliega en DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, o cualquier servidor Linux por SSH, facturado directamente por tu proveedor, sin márgenes adicionales.
- Un precio fijo por equipo. El plan Free es gratis para siempre: un servidor más un servidor de base de datos, tres apps, sin tarjeta. Pro cuesta $19 al mes por hasta cinco servidores y apps ilimitadas. Team cuesta $49 al mes con servidores ilimitados. Sin cargos por app, por despliegue, por asiento ni por gigabyte.
- Plantillas de un clic. Despliega apps de código abierto, como bases de datos, analítica y herramientas de desarrollo, en tu propio servidor con un clic.
- Bases de datos administradas. Postgres, MySQL y Redis, con copias de seguridad programadas y verificadas por restauración, y recuperación a un punto en el tiempo.
- Despliegue con git push. Despliega desde GitHub, GitLab o la CLI, con HTTPS automático y lanzamientos sin tiempo de inactividad.