Kör en Discord-bot på din egen server

En Discord-bot på Debian 13, skriven med discord.js eller discord.py, som körs som en systemd-tjänst under en egen användare, med token utanför koden, automatiska omstarter, härdning och tydliga loggar.

Testad på discord.js 14.27.0 på Node.js 20.19.2 och discord.py 2.7.1 på Python 3.13.5, på Debian 13 (trixie) på en server hos Melonslab Uppdaterad 2 oktober 2026

Rekommenderad server för guiden

VC-S Micro · 2 vCPU · 8 GB Minne · 250 GB Lagring

Månadsvis, ingen bindningstid 7 dagars öppet köp

90 kr/mån

Beställ nu
På den här sidan

Det här sätter du upp

En Discord-bot som är igång dygnet runt på din server i stället för på din egen dator. Exempelboten svarar på ett snedstreckskommando, /ping, och är liten nog att bygga vidare på. Du skriver den i JavaScript med discord.js eller i Python med discord.py: välj nedan, så visar varje steg kommandona för just det.

Körmiljö

Boten körs som en systemd-tjänst under en egen användare, discordbot. systemd startar den när servern startar, startar om den om den kraschar och sparar allt den skriver i journalen. Token, botens lösenord, ligger i en fil som bara root kan läsa.

En sådan bot behöver inga öppna portar. Den ansluter ut till Discords gateway och håller anslutningen öppen, och Discord skickar kommandon och händelser genom den. Brandväggen kan alltså vara som den är, med bara SSH öppen.

discord.js och discord.py är communityprojekt med öppen källkod: discord.js under licensen Apache 2.0, och discord.py under MIT-licensen, startat av utvecklaren som kallas Rapptz. Discord drivs av Discord Inc., ett amerikanskt företag, och varje meddelande och kommando som boten hanterar går via Discords servrar.

Varje steg nedan har körts på en nyinstallerad Melonslab VC-P Alloy (2 vCPU, 8 GB) med Debian 13, en gång med varje körmiljö:

  • Boten loggade in, registrerade /ping på en testserver och svarade på kommandot.
  • Med fel token stannade tjänsten efter ett försök, med Login failed i loggen, i stället för att starta om gång på gång.
  • Boten anslöt igen av sig själv efter att anslutningen till Discord hade varit bruten i två minuter, och startade igen av sig själv efter en omstart av servern.
  • Härdningen i steg 6 lät boten fungera som vanligt, och ingenting lyssnade på någon port.
  • Ett felaktigt server-ID och en privilegierad intent som inte var påslagen gav båda ett tydligt fel i loggen.

I vila på en Discord-server använde boten omkring 50 MB minne med Node.js och 35 MB med Python.

Innan du börjar

Du behöver:

  • en server med Debian 13, säkrad som i steg 1 till 4 i säkerhetsguiden, med ufw påslagen och bara SSH tillåten. Hoppa över reglerna för HTTP och HTTPS: boten behöver dem inte;
  • ett Discord-konto och en Discord-server där du får lägga till botar. Enklast är en egen testserver: i Discord väljer du + i serverlistan.

Stegen hos Discord följer Discords egen kom igång-guide. Vår testbot skapades på det sättet, men Discord ändrar sin Developer Portal då och då, så etiketterna kan skilja sig något.

1. Skapa boten hos Discord

Öppna Developer Portal i webbläsaren och logga in med ditt Discord-konto.

  1. Välj Create App, ge den ett namn och välj Create. Namnet är det som syns i medlemslistan.
  2. Öppna sidan Bot och välj Reset Token. Discord visar token en enda gång: kopiera den och förvara den säkert, till exempel i en lösenordshanterare, fram till steg 5. Den som har token kan agera som din bot, så lägg den aldrig i koden, i en skärmbild eller i ett Git-repo.
  3. Låt de tre Privileged Gateway Intents på samma sida vara avstängda. En bot som bara svarar på snedstreckskommandon behöver ingen av dem. Message Content Intent behövs bara om boten läser texten i vanliga meddelanden, och då måste du också be om den i koden.
  4. Öppna sidan Installation. Under Installation Contexts ser du till att Guild Install är ikryssad. Under Default Install Settings lägger du till omfången applications.commands och bot. När du lägger till bot visas listan Permissions: välj View Channels och Send Messages, och inget mer. Välj sedan Save Changes.
  5. Kopiera Install Link på samma sida, öppna länken, välj Add to server, välj din server och bekräfta. Boten syns i medlemslistan, offline fram till steg 6.

Till sist behöver du serverns ID. Öppna User Settings i Discord med kugghjulet, sedan Advanced, och slå på Developer Mode. Högerklicka på din server i serverlistan och välj Copy Server ID.

2. Installera körmiljön

Som root:

Med Node.js

apt update
apt install -y --no-install-recommends nodejs npm
node --version
v20.19.2

--no-install-recommends håller borta kompilatorer och byggverktyg som npm bara behöver för vissa paket, och sparar omkring 760 MB. discord.js kräver Node.js 18 eller senare, så Debians version 20 fungerar, och uppdateringarna kommer från Debian, via de automatiska uppdateringarna från säkerhetsguiden.

Med Python

Debian 13 har redan Python 3.13. Lägg till modulen som skapar virtuella miljöer:

apt update
apt install -y python3-venv
python3 --version
Python 3.13.5

discord.py installeras med pip i steg 4, i en egen virtuell miljö för boten, så att den inte blandas med Debians Python-paket.

3. Skapa användaren

Boten körs som discordbot, en systemanvändare som inte kan logga in. Dess hemkatalog, /opt/discordbot, innehåller koden:

useradd --system --create-home --home-dir /opt/discordbot --shell /usr/sbin/nologin discordbot

Om någon hittar ett hål i din bot eller i något av dess bibliotek får de den här användaren, inte root, och steg 6 begränsar vad den kan göra.

4. Skriv boten

Med Node.js

Installera discord.js som discordbot:

cd /opt/discordbot
runuser -u discordbot -- npm install discord.js

Skapa /opt/discordbot/bot.js:

const { Client, Events, GatewayIntentBits, SlashCommandBuilder } = require('discord.js');

// Slash commands need no privileged intents. Guilds is enough.
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

const ping = new SlashCommandBuilder()
  .setName('ping')
  .setDescription('Check that the bot answers');

client.once(Events.ClientReady, async (c) => {
  console.log(`Logged in as ${c.user.tag}`);
  // Register the commands for one server. They appear there at once.
  await c.application.commands.set([ping], process.env.GUILD_ID);
  console.log('Registered /ping');
});

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand() || interaction.commandName !== 'ping') return;
  console.log(`/ping from ${interaction.user.username}`);
  // The latency is -1 until the first heartbeat, about 40 seconds after login.
  const ms = client.ws.ping;
  await interaction
    .reply(ms >= 0 ? `Pong! Gateway latency: ${ms} ms` : 'Pong! (latency not measured yet)')
    .catch((err) => console.error(`Reply failed: ${err.message}`));
});

client.login(process.env.DISCORD_TOKEN).catch((err) => {
  console.error(`Login failed: ${err.message}`);
  // 78 tells systemd not to restart: a wrong token stays wrong.
  process.exit(err.code === 'TokenInvalid' ? 78 : 1);
});

Med Python

Skapa den virtuella miljön och installera discord.py i den, som discordbot:

cd /opt/discordbot
runuser -u discordbot -- python3 -m venv venv
runuser -u discordbot -- venv/bin/pip install discord.py

Skapa /opt/discordbot/bot.py:

import math
import os
import sys

import discord
from discord import app_commands

guild = discord.Object(int(os.environ["GUILD_ID"]))


class Bot(discord.Client):
    def __init__(self):
        # Slash commands need no privileged intents. Guilds is enough.
        super().__init__(intents=discord.Intents(guilds=True))
        self.tree = app_commands.CommandTree(self)

    async def setup_hook(self):
        # Register the commands for one server. They appear there at once.
        await self.tree.sync(guild=guild)
        print("Registered /ping", flush=True)

    async def on_ready(self):
        print(f"Logged in as {self.user}", flush=True)


bot = Bot()


@bot.tree.command(guild=guild, description="Check that the bot answers")
async def ping(interaction: discord.Interaction):
    print(f"/ping from {interaction.user.name}", flush=True)
    # The latency is unknown until the first heartbeat, about 40 seconds after login.
    if math.isfinite(bot.latency):
        text = f"Pong! Gateway latency: {round(bot.latency * 1000)} ms"
    else:
        text = "Pong! (latency not measured yet)"
    await interaction.response.send_message(text)


try:
    bot.run(os.environ["DISCORD_TOKEN"])
except (discord.LoginFailure, discord.PrivilegedIntentsRequired) as err:
    print(f"Login failed: {err}", file=sys.stderr)
    # 78 tells systemd not to restart: a wrong token stays wrong.
    sys.exit(78)

Boten läser sin token och din servers ID från miljön, så ingen av dem finns i koden. Den registrerar /ping för bara din server, varje gång den startar. Kommandon för en enskild server syns där direkt, och att registrera samma kommandon igen räknas inte mot Discords gräns på 200 nya kommandon per dygn. Vill du erbjuda kommandot på alla servrar som boten går med i registrerar du det utan server-ID; Discord kallar dem globala kommandon.

De sista raderna gör om fel token till slutkod 78, och steg 6 säger åt systemd att inte starta om boten vid den koden. Alla andra fel, till exempel ett nätverksproblem vid start, ger ett nytt försök.

När anslutningen till Discord bryts ansluter båda biblioteken igen av sig själva. I Node.js-versionen spelar .catch efter reply roll: utan den stoppar ett svar som misslyckas under ett nätverksproblem hela boten. I vårt test startade systemd den igen, men det är bättre att boten fortsätter att köra. discord.py fångar sådana fel i kommandon själv och skriver dem till loggen.

5. Spara token

Skapa filen tom och läsbar bara för root, och öppna den:

install -m 600 /dev/null /etc/discord-bot.env
nano /etc/discord-bot.env

Lägg in token från steg 1 och serverns ID, med dina egna värden:

DISCORD_TOKEN=paste-the-token-here
GUILD_ID=123456789012345678

systemd läser filen som root när boten startar och lämnar över värdena till den, så användaren discordbot kan inte läsa själva filen.

6. Kör den som en tjänst

Med Node.js

Skapa /etc/systemd/system/discord-bot.service:

[Unit]
Description=Discord bot
Wants=network-online.target
After=network-online.target

[Service]
User=discordbot
WorkingDirectory=/opt/discordbot
EnvironmentFile=/etc/discord-bot.env
ExecStart=/usr/bin/node bot.js
Restart=on-failure
RestartSec=5
# Wait longer after each failure in a row, up to 5 minutes.
RestartSteps=6
RestartMaxDelaySec=300
# Exit code 78 means a wrong token: stop instead of retrying.
RestartPreventExitStatus=78

# Hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
ProcSubset=pid
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
CapabilityBoundingSet=
SystemCallArchitectures=native
SystemCallFilter=@system-service @pkey
SystemCallFilter=~@privileged
UMask=0077

[Install]
WantedBy=multi-user.target

Med Python

Skapa /etc/systemd/system/discord-bot.service:

[Unit]
Description=Discord bot
Wants=network-online.target
After=network-online.target

[Service]
User=discordbot
WorkingDirectory=/opt/discordbot
EnvironmentFile=/etc/discord-bot.env
ExecStart=/opt/discordbot/venv/bin/python bot.py
Restart=on-failure
RestartSec=5
# Wait longer after each failure in a row, up to 5 minutes.
RestartSteps=6
RestartMaxDelaySec=300
# Exit code 78 means a wrong token: stop instead of retrying.
RestartPreventExitStatus=78

# Hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
ProcSubset=pid
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
CapabilityBoundingSet=
SystemCallArchitectures=native
SystemCallFilter=@system-service @pkey
SystemCallFilter=~@privileged
UMask=0077

[Install]
WantedBy=multi-user.target

Det här gör raderna:

  • Restart=on-failure startar boten igen när den kraschar. RestartSec, RestartSteps och RestartMaxDelaySec väntar 5 sekunder efter den första kraschen och sedan längre efter varje krasch i rad, upp till 5 minuter, så att en bot som inte kan starta inte överöser Discord med försök. RestartPreventExitStatus=78 stoppar försöken när token är fel.
  • ProtectSystem=strict gör hela filsystemet skrivskyddat för boten, även dess egen kod, och ProtectHome döljer /home och /root. Behöver boten skriva filer, till exempel en liten databas, tillåter du en katalog med StateDirectory=discordbot, som ger den /var/lib/discordbot.
  • RestrictAddressFamilies låter den öppna internetanslutningar och inget annat, CapabilityBoundingSet= tar bort alla root-befogenheter, och SystemCallFilter spärrar systemanrop som en bot inte har någon nytta av. @pkey behövs för Node.js, som använder minnesskyddsnycklar och stoppas utan dem.

Starta den, och låt den starta när servern startar:

systemctl daemon-reload
systemctl enable --now discord-bot
systemctl status discord-bot
● discord-bot.service - Discord bot
     Loaded: loaded (/etc/systemd/system/discord-bot.service; enabled; preset: enabled)
     Active: active (running) since Fri 2026-10-02 19:01:26 CEST; 12s ago

Hur långt härdningen går ser du med systemd-analyze security discord-bot, som betygsätter tjänsten. Vår fick 1,4 på en skala från 0 till 10, vilket den kallar OK. Lägre är bättre.

7. Kontrollera att den svarar

Loggen visar att boten loggar in:

journalctl -u discord-bot -n 20

Med Node.js

Logged in as MyBot#4850
Registered /ping

Med Python

[2026-10-02 19:01:27] [INFO    ] discord.client: logging in using static token
Registered /ping
[2026-10-02 19:01:28] [INFO    ] discord.gateway: Shard ID None has connected to Gateway (Session ID: 3878203fc3aeef7e07dc8f9a19e5dfce).
Logged in as MyBot#4850

I Discord syns boten nu som online. Skriv /ping i en kanal och välj botens kommando: den svarar med Pong! och sin svarstid, och loggen får en rad /ping from med ditt namn.

Från vår server i Sverige var svarstiden omkring 130 ms. Adressen till Discords gateway leder till en Cloudflare-nod bara några millisekunder bort, så det mesta är vägen vidare därifrån till Discords egna gateway-servrar och tillbaka. Värdet mäts en gång per heartbeat, ungefär var 41:a sekund, så pingar tätt efter varandra visar samma värde, och precis efter en start svarar boten att det inte är uppmätt ännu. Det påverkar bara hur snabbt boten ser ett kommando, inte om det fungerar.

Kontrollera att ingenting lyssnar efter anslutningar:

ss -tlnp | grep -E 'node|python'

Kommandot skriver ingenting. Botens anslutning till Discord går ut från servern, så ufw släpper igenom den utan någon regel.

8. Läs loggarna

Allt som boten skriver hamnar i journalen. Så följer du den löpande, och så ser du den senaste timmen:

journalctl -u discord-bot -f
journalctl -u discord-bot --since "1 hour ago"

Journalen sparar gamla loggar tills den når sin storleksgräns, så du behöver inte städa efter boten. Raderna /ping from innehåller användarnamn från Discord: ta bort sådana rader om du inte behöver dem.

9. Uppdatera

Debians automatiska uppdateringar håller Node.js eller Python uppdaterade. Biblioteket uppdaterar du själv. Discord ändrar sitt API då och då, och ett gammalt bibliotek kan sluta fungera, så sök efter uppdateringar ungefär en gång i månaden:

Med Node.js

cd /opt/discordbot
runuser -u discordbot -- npm outdated
runuser -u discordbot -- npm update
systemctl restart discord-bot

npm update håller sig inom discord.js 14. En ny huvudversion kan ändra hur botar skrivs: läs dess uppgraderingsguide innan du byter.

Med Python

cd /opt/discordbot
runuser -u discordbot -- venv/bin/pip list --outdated
runuser -u discordbot -- venv/bin/pip install --upgrade discord.py
systemctl restart discord-bot

När du har ändrat koden startar du om boten på samma sätt. De enda filer du behöver säkerhetskopiera är din kod i /opt/discordbot och /etc/discord-bot.env. Biblioteken går alltid att hämta igen.

Felsökning

Tjänsten är failed med status=78/CONFIG, och loggen visar Login failed: An invalid token was provided. eller Login failed: Improper token has been passed. Token är fel, eller så har Discord återställt den. Discord återställer en token av sig själv när den hittar den publicerad, till exempel i ett publikt Git-repo. Skapa en ny med Reset Token på sidan Bot, lägg in den i /etc/discord-bot.env och kör systemctl restart discord-bot.

Tjänsten stannar med status=31/SYS. Filtret för systemanrop stoppade boten. Det händer om du utelämnar @pkey för Node.js, eller om ett bibliotek behöver ett systemanrop som filtret spärrar. journalctl -k | grep audit visar systemanropets nummer, och systemd-analyze syscall-filter listar vilken grupp det hör till.

Loggen visar Login failed: Used disallowed intents (Node.js) eller Login failed: Shard ID None is requesting privileged intents (Python). Koden ber om en privilegierad intent, till exempel för meddelandeinnehåll, som är avstängd på sidan Bot. Slå på den där, eller ta bort den ur koden, och starta om boten. Python-boten stannar med slutkod 78. discord.js ger inte felet någon egen kod, så Node.js-boten fortsätter att försöka, med upp till 5 minuter mellan försöken.

Boten loggar in och stannar sedan med DiscordAPIError[50001]: Missing Access. Den kunde inte registrera /ping på servern i GUILD_ID. Antingen är ID:t fel, eller så lades boten till utan omfånget applications.commands: kopiera server-ID:t igen, eller öppna Install Link från steg 1 igen, och starta om boten. Tills dess fortsätter den att försöka, med upp till 5 minuter mellan försöken.

Discord svarar "The application did not respond". Boten svarade inte inom tre sekunder. Kontrollera systemctl status discord-bot: boten kan hålla på att starta om, eller ansluta igen efter ett nätverksproblem, vilket i våra test tog mindre än 15 sekunder efter att nätverket var tillbaka. Ett kommando som behöver mer än tre sekunder måste först tala om för Discord att det arbetar, med interaction.deferReply() i discord.js eller interaction.response.defer() i discord.py.

Kör det på din egen server

VC-S Micro

90 kr/mån

vCPU
2
Minne
8 GB
Lagring
250 GB
Trafik
10 TB
Standard
HDD · RAID 10
  • Full root-åtkomst
  • Nativt /64 IPv6
  • RAID-skyddad lagring
  • Malmö, Sverige
  • Månadsvis, ingen bindningstid
  • 7 dagars öppet köp
Alla guider