Wie man einen Discord-Bot 24/7 hostet
Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig
Der Bot funktioniert. node index.js auf deinem Laptop loggt ihn in deinen Testserver ein, !ping bekommt ein pong zurück, alles ist gut — bis du das Terminal schließt oder den Laptop schlafen legst, und der Bot zusammen damit offline geht. Was fehlt, ist nicht mehr Code. Es ist ein Prozess, der weiterläuft, nachdem du weg bist — ein anderes Problem als das, das du beim Schreiben des Bots selbst gelöst hast.
Diese Anleitung baut einen echten discord.js-Bot, packt ihn in ein Dockerfile und erklärt die eine Sache, über die fast jeder beim ersten Hosten eines Discord-Bots stolpert: warum er einen Prozess braucht, der nie aufhört, statt eine Function, die bei Bedarf startet. Danach geht es um Restart-Policy, Logging und den One-Click-Weg, um dasselbe auf Railyard laufen zu lassen.
Was du brauchst
- Node.js 22 oder 24 lokal installiert (discord.js 14 braucht Node 18 oder neuer).
- Einen Discord-Account und einen Server (Guild), in dem du testen kannst, idealerweise einen, den du selbst kontrollierst.
- Docker lokal installiert, um das Image zu bauen und zu testen, bevor es irgendwohin anders geht.
- Einen Server, um den Container darauf laufen zu lassen, oder einen Railyard-Account für die Abkürzung im letzten Abschnitt.
Schritt 1: Die Bot-Anwendung erstellen und einen Token holen
Geh zum Discord Developer Portal und klick auf New Application. Gib ihr einen Namen, öffne dann links den Tab Bot. Klick auf Reset Token, um einen Token zu erzeugen, und kopiere ihn an einen sicheren Ort. Das ist die Credential, mit der dein Code sich als Bot einloggt; jeder mit diesem Token kann deinen Bot kontrollieren, also kommt er in eine Umgebungsvariable, niemals in eine Datei, die du committest.
Im gleichen Bot-Tab, unter Privileged Gateway Intents, schalte Message Content Intent ein, falls dein Bot den Text von Nachrichten lesen muss (ein Moderations-Bot oder ein Command-Bot, der nicht rein auf Slash-Commands basiert, braucht das; ein Bot, der nur auf Slash-Commands und Button-Klicks reagiert, nicht).
Öffne OAuth2 → URL Generator, hake den bot-Scope an, hake die Berechtigungen an, die dein Bot tatsächlich braucht (starte mit Send Messages und Read Message History für einen einfachen Bot), kopiere die generierte URL und öffne sie im Browser, um den Bot zu deinem Testserver einzuladen.
Schritt 2: Den Bot schreiben
discord.js 14.27.0 ist das aktuelle npm-Release. Initialisiere das Projekt und installiere es:
npm init -y
npm install discord.js@^14.27.0
Füge "type": "module" zur package.json hinzu, damit du import nutzen kannst, plus ein start-Script:
{
"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);
Jeder GatewayIntentBits-Eintrag deklariert eine Kategorie von Events, die Discord deinem Bot tatsächlich schickt; GuildMessages und MessageContent zusammen sind das, was MessageCreate mit lesbarem message.content für einen einfachen Textbefehl wie !ping auslösen lässt. Wenn du MessageContent im Code anforderst, ohne das Message Content Intent im Developer Portal aus Schritt 1 einzuschalten, bekommst du beim Login einen „disallowed intents”-Fehler — der häufigste erste Fehler bei einem neuen Bot.
Teste lokal:
DISCORD_TOKEN=your-token-here node index.js
Du solltest Logged in as yourbot#0000 im Terminal sehen, und !ping in deinem Testserver sollte eine pong-Antwort bekommen.
Schritt 3: Das Dockerfile schreiben
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"]
Das ist dieselbe zweistufige Form, die du für jede Node-App nutzen würdest: Dependencies werden in ihrer eigenen Stage installiert, damit sie getrennt vom restlichen Source-Code gecacht werden, und das finale Image läuft als der nicht-root User node statt als root. Der einzige echte Unterschied zum Dockerfile einer Web-App ist, was fehlt: kein EXPOSE, weil dieser Prozess nie auf einem Port lauscht. Er öffnet eine ausgehende Verbindung zu Discords Gateway; nichts verbindet sich herein.
Schritt 4: Warum das ein always-on Prozess sein muss, keine Function
Eine serverless Function (eine Vercel Function, ein AWS Lambda) startet, wenn ein HTTP-Request eintrifft, und fährt kurz nach der Antwort wieder herunter. Dieses Modell passt zu einer Workload aus einzelnen, unabhängigen Requests. Ein Discord-Bot ist keine solche: Nachdem client.login() erfolgreich war, öffnet discord.js eine WebSocket-Verbindung zu Discords Gateway und hält sie unbegrenzt offen. Jede Nachricht, Reaktion und Slash-Command-Interaktion, die dein Bot sieht, kommt als Event über diese bereits offene Socket-Verbindung, nicht als eingehender HTTP-Request, den deine Plattform an einen Handler routet.
Es gibt überhaupt keinen Request, der einen Cold Start auslösen würde, und selbst wenn eine Plattform deinen Code nach einem anderen Zeitplan aufrufen würde, müsste die Gateway-Verbindung selbst trotzdem durchgehend zwischen den Aufrufen existieren, um überhaupt irgendetwas empfangen zu können; eine Verbindung, die nur existiert, während eine Function zufällig gerade läuft, ist keine Verbindung, die ein Event empfangen kann, das zwischen zwei Aufrufen eintrifft. Das ist die eigentliche Unterscheidung, um die eine „Discord-Bot hosten”-Suche kreist: nicht Performance, sondern ob in dem Moment, in dem Discord beschließt, etwas zu senden, überhaupt etwas zuhört.
(Discord unterstützt speziell für Slash-Commands auch einen HTTP-basierten Interactions-Endpoint, bei dem Discord deinem Bot pro Interaktion einen HTTP-POST schickt, statt ein Gateway-Event. Dieses Modell kann auf einer serverless Function laufen, deckt aber nur Slash-Commands und Components ab, nicht gewöhnliche Nachrichten, Presence oder alles andere, was das Gateway transportiert — und es ist eine andere Architektur als der Client-plus-Gateway-Ansatz dieser Anleitung.)
Was ein Bot wie dieser hier tatsächlich braucht, ist unglamourös: ein Prozess, der startet, am Laufen bleibt und sich selbst neu verbindet, falls die Verbindung abbricht — was discord.js intern bei gewöhnlichen Netzwerk-Hickups bereits selbst erledigt. Deine Aufgabe ist sicherzustellen, dass der Prozess selbst weiterexistiert, was eine Restart-Policy ist, kein Framework.
Schritt 5: Restart-Policy und Logging
Lass das Image lokal mit einer Restart-Policy laufen, damit Docker es nach einem Crash oder einem Host-Neustart zurückholt:
docker build -t my-discord-bot .
docker run -d --name my-discord-bot --restart unless-stopped \
-e DISCORD_TOKEN=your-token-here \
my-discord-bot
--restart unless-stopped sagt dem Docker-Daemon, den Container automatisch neu zu starten, außer du hast ihn explizit selbst gestoppt — das deckt sowohl eine unbehandelte Exception, die den Prozess crashen lässt, als auch einen Neustart der Host-Maschine ab.
Für Logs füge noch keine Logging-Library hinzu; console.log und console.error nach stdout/stderr reichen für einen Bot dieser Größe, und Docker erfasst beide bereits:
docker logs -f my-discord-bot
Du solltest die Zeile Logged in as... aus Schritt 2 sehen, sowie jede console.error-Ausgabe, falls der Client auf einen Fehler trifft. Logge weiterhin die Discord-spezifischen Events, die für das Debuggen eines still gewordenen Bots wichtig sind, insbesondere Reconnects, damit ein Dashboard oder Terminal, das du später checkst, dir sagt, dass etwas passiert ist, statt einfach still zu werden:
client.on(Events.ShardDisconnect, () => console.warn("Shard disconnected, reconnecting..."));
client.on(Events.ShardReconnecting, () => console.log("Shard reconnecting..."));
Schritt 6: Auf einem echten Server laufen lassen
Kopiere das Image (oder das Repository, falls du auf dem Server baust) auf einen VPS mit installiertem Docker, und führe dort denselben docker run-Befehl aus Schritt 5 aus, mit einer echten .env-Datei statt eines Inline-Tokens:
docker run -d --name my-discord-bot --restart unless-stopped --env-file .env my-discord-bot
Dieses eine Flag, --restart unless-stopped, ist der Großteil dessen, was „einen Bot 24/7 hosten” in der Praxis tatsächlich bedeutet: ein Prozess, der von sich aus zurückkommt, nach allem außer dir, der ihn explizit stoppt.
Die Abkürzung: mit Railyard
Zeig Railyard auf dasselbe Repository. Da das Repo ein Dockerfile hat, das die App tatsächlich ins Image kopiert (kein Development-Image, das ein Bind-Mount erwartet, was Railyards Build-Erkennung automatisch überspringt), baut und läuft dieses Dockerfile direkt, ohne Railpack- oder Buildpack-Raten; siehe Build-Methoden.
Dieser Bot hat keinen Web-Prozess, und das ist in Ordnung: Railyard betreibt reine Worker-Apps genauso wie jede andere, als Prozesstyp ohne angehängten Port, behandelt unter Worker und geplante Jobs. Füge DISCORD_TOKEN im Variables-Tab der App hinzu (siehe Variablen und Secrets); es wird verschlüsselt und beim nächsten Deploy angewendet — das Railyard-seitige Äquivalent zum --env-file .env aus Schritt 6.
Eine Einstellung, die du unangetastet lassen solltest: Sleep when idle, unter Settings → General, stoppt einen Prozess, sobald seine gemeldete CPU-Auslastung lange genug im Leerlauf war. Ein Discord-Bot, der in einem ruhigen Server sitzt, kann nach diesem Maßstab wie im Leerlauf aussehen, während seine Gateway-Verbindung durchaus noch lebt und gleich eine Nachricht empfangen könnte; lass diese Einstellung für diesen Prozess aus, damit die Verbindung nie das ist, was in den Schlaf geschickt wird.
Die Restart-Policy aus Schritt 5 ist hier auch nicht etwas, das du konfigurierst — es ist schlicht, wie Railyard die Container jeder App betreibt: Ein Crash startet den Prozess neu, ohne dass du dich per SSH einloggst, um docker start zu tippen. Logs findest du im Logs-Tab der App-Seite, oder via railyard logs my-bot --follow über die CLI — dieselbe docker logs -f-Gewohnheit aus Schritt 5, nur ohne dass du ein Terminal auf dem Server selbst offen haben musst.
Um ohne jedes Mal eine Repository-URL einzugeben zu deployen, füge einen Deploy-Button zur README des Bots hinzu:
[](https://app.railyard.run/deploy?repo=https://github.com/you/your-discord-bot)
Siehe die Deploy-Button-Doku für das vollständige Link-Format, und Quickstart, falls du von null startest: Anmeldung, ein Server und eine laufende App in etwa zehn Minuten.
Troubleshooting
Der Bot zeigt in Discord als offline, auch wenn der Container läuft. Prüfe zuerst docker logs (oder den Logs-Tab) auf einen Login-Fehler; ein ungültiger oder widerrufener Token schlägt sofort fehl. Wenn der Login gelingt, der Bot aber trotzdem nicht reagiert, bestätige, dass die Einlade-URL aus Schritt 1 tatsächlich den bot-Scope und die Berechtigungen enthielt, die deine Befehle brauchen.
„Used disallowed intents” beim Start. Du forderst GatewayIntentBits.MessageContent (oder ein anderes privilegiertes Intent) im Code an, ohne es im Developer Portal einzuschalten. Geh zurück zum Bot-Tab aus Schritt 1 und aktiviere das passende Privileged Gateway Intent.
Der Container startet immer wieder in einer Schleife neu. Das ist --restart unless-stopped, das genau seinen Job gegen einen Prozess macht, der sofort crasht, meist ein Startup-Fehler, kein vorübergehender. Prüfe die ersten paar Zeilen von docker logs direkt nach einem Neustart, nicht das Ende; der Crash-Grund steht fast immer dort, häufig ein fehlendes DISCORD_TOKEN oder ein Tippfehler in einem Dependency-Import.
Der Bot wird für eine Weile still, kommt dann zurück. Das ist meist ein Gateway-Reconnect, den discord.js selbst behandelt; das ShardDisconnect/ShardReconnecting-Logging aus Schritt 5 macht aus „der Bot schien down zu sein” eine Log-Zeile, die du tatsächlich mit Discords eigener Statusseite abgleichen kannst, falls es öfter passiert.
FAQ
Kann ich einen Discord-Bot kostenlos hosten? Das Bot-Framework selbst ist in jedem Fall kostenlos; was Geld kostet, ist das always-on Compute, auf dem er läuft. Ein kleiner VPS kostet ein paar Dollar im Monat, was der echte Boden für „24/7” ist, da nichts Kostenloses einen Prozess unbegrenzt am Laufen hält, ohne dass du ihn babysittest.
Brauche ich einen Webserver für meinen Discord-Bot? Nein, nicht für den Gateway-basierten Ansatz dieser Anleitung. Der Bot macht nur ausgehende Verbindungen (zu Discords Gateway und REST-API); er muss nie eingehende HTTP-Requests annehmen, also gibt es keinen Port zum Freigeben und keinen Reverse-Proxy oder TLS-Zertifikat für den Bot selbst zu verwalten.
Was passiert mit Nachrichten, die gesendet werden, während mein Bot offline ist? Sie sind aus Sicht des Bots verloren. Discords Gateway queued keine Events für einen getrennten Client, um sie später nachzuliefern; war der Bot offline, als eine Nachricht gesendet wurde, sieht er dieses MessageCreate-Event nie — der praktische Grund, warum „24/7” für einen Bot mehr zählt als für die meisten Web-Apps.
discord.js oder discord.py: spielt das für das Hosting eine Rolle? Für nichts in dieser Anleitung. Ein discord.py-Bot (Python) braucht dieselben Dinge: einen always-on Prozess statt eine Function; eine Restart-Policy; und intents.message_content = True im Code gesetzt plus das Message Content Intent im Developer Portal aktiviert — das Python-Äquivalent zum Intent-Setup aus Schritt 2. Das Dockerfile tauscht node:24-alpine gegen python:3.13-slim und bot.run(TOKEN) gegen client.login(...), aber die Architektur ist identisch.
Warum nicht einfach ein Terminal mit laufendem node index.js offen lassen? Das funktioniert, bis das Terminal schließt, der Laptop schläft, oder die SSH-Session abbricht — dann stirbt der Prozess damit, und nichts bringt ihn zurück. --restart unless-stopped (oder Railyard, das den Container standardmäßig genauso betreibt) ist der Unterschied zwischen einem Bot, der läuft, weil du deinen Laptop noch nicht zugeklappt hast, und einem, der tatsächlich läuft.
Ein Discord-Bot ist ein kleines Stück Code mit einer großen, kontinuierlichen Aufgabe: verbunden zu bleiben. Sobald der Prozess selbst von sich aus neu startet und dort loggt, wo du es sehen kannst, hört das Online-Sein des Bots auf, etwas zu sein, über das du überhaupt nachdenken musst.
Warum Leute sich dafür Railyard entscheiden
- Dein eigenes Cloud-Konto. Deploye auf DigitalOcean, Hetzner, Vultr, AWS, OVHcloud oder jedem Linux-Server per SSH, direkt von deinem Anbieter abgerechnet, ohne Aufschlag.
- Ein fester Preis pro Team. Free ist für immer kostenlos: ein Server plus ein Datenbankserver, drei Apps, keine Karte. Pro kostet 19 $ im Monat für bis zu fünf Server und unbegrenzte Apps. Team kostet 49 $ im Monat mit unbegrenzten Servern. Keine Kosten pro App, pro Deployment, pro Sitz oder pro Gigabyte.
- Templates mit einem Klick. Deploye Open-Source-Apps, wie Datenbanken, Analytics und Dev-Tools, mit einem Klick auf deinem eigenen Server.
- Verwaltete Datenbanken. Postgres, MySQL und Redis mit geplanten, wiederherstellungsgeprüften Backups und Point-in-Time-Recovery.
- Push-to-Deploy. Deploye von GitHub, GitLab oder der CLI, mit automatischem HTTPS und Releases ohne Ausfallzeit.