# Install VorqulBOT, the self-hosted Discord bot

Last reviewed: 2026-10-15
Canonical page: https://vorqul.com/docs/install/vorqulbot

Installing VorqulBOT takes about 10 to 15 minutes. Create an application in the Discord Developer Portal, enable the presence, server members and message content intents, and copy the token and the application ID. Then either install Node.js 22 LTS on a clean Debian or Ubuntu server, copy .env.example to .env, run npm install and npm start and keep it alive with systemd, or import the included egg into Pterodactyl, upload the files and fill in the Startup variables. Storage is JSON files by default or MySQL/MariaDB, chosen with one line in .env.

## Requirements

- RAM: 512 MB minimum, 1 GB advised, 2 GB if you run the dashboard
- CPU: 1 core minimum, 2 advised
- Disk: 500 MB minimum, 1 GB advised
- Node.js: 18 minimum, 22 LTS advised (18 and 20 are past upstream end of life)
- npm: Ships with Node.js, nothing separate to install
- Database: Optional, JSON files need nothing installed

## Part 1: The Discord side

This half is identical for every deployment path, and skipping the intents is the single most common reason a freshly installed bot does nothing.

### Create the application

In the Discord Developer Portal, create a new application, then add a bot to it and reset the token to reveal it.

The token is shown once. Copy it somewhere safe and never commit it, paste it into a support channel or put it in a screenshot. If it leaks, reset it. Discord invalidates the old one immediately.

Also copy the Application ID from General Information. The bot needs it as DISCORD_CLIENT_ID to register its slash commands, so it is not optional even if you never open the dashboard.

### Enable the three privileged intents

Under Bot, in Privileged Gateway Intents, switch on Presence Intent, Server Members Intent and Message Content Intent.

Without Message Content the bot cannot see message text, so AutoMod, leveling, prefix commands and sticky messages all silently do nothing. Without Server Members it cannot react to joins, which takes out welcome cards, join roles, invite tracking and anti-raid.

Note: If the bot connects but ignores everything, come back to this step first.

### Invite the bot to your server

Under OAuth2, in URL Generator, select the bot and applications.commands scopes, choose the permissions, then open the generated URL and authorise it on your server.

The applications.commands scope is what lets the 83 slash commands appear. Leave it out and the bot joins the server without a single command.

### Collect the OAuth credentials for the dashboard

Under OAuth2, in General, reset and copy the Client Secret, then add your callback URL to Redirects.

Only needed if you want the web dashboard. The bot itself runs without the secret and without any redirect.

```Redirect URLs
http://localhost:3000/api/auth/callback/discord
https://your-domain.tld/api/auth/callback/discord
```

## Part 2: Choose your storage

Two real backends, one line in .env, no separate edition. Pick by how big the server is and ignore how the names sound.

### JSON files - nothing to install

One file per table under data/json. This is the default, it needs no service and no setup, and it is what most self-hosted instances run on.

```.env
DATABASE_TYPE=json
DATA_PATH=./data
```

Note: The bot appends /json to DATA_PATH by itself, so DATA_PATH=./data ends up in ./data/json. Do not write ./data/json here or the files land one level too deep.

### MySQL or MariaDB - for a large server

Install the server, create a database and a user with utf8mb4, then point the bot at it. Worth it once the server is big or you want the data alongside other services.

Then set DATABASE_TYPE=mysql together with DB_HOST, DB_PORT, DB_USER, DB_PASSWORD and DB_NAME in .env.

DATABASE_TYPE=sqlite is not a third option. This version has no SQLite driver, so the value logs a warning and quietly uses the JSON files instead. Your data is still saved, but write json so the configuration says what actually happens.

```MariaDB on Debian
sudo apt install -y mariadb-server
sudo mysql

CREATE DATABASE vorqul CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'vorqul'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON vorqul.* TO 'vorqul'@'localhost';
FLUSH PRIVILEGES;
```

Note: utf8mb4 is not optional. Anything less mangles emoji and diacritics in stored messages.

## Part 3a: Deploy on a clean Debian or Ubuntu server

The path to take if you have your own machine. Everything below runs as an ordinary user with sudo, and the bot itself runs as a dedicated user with no sudo of its own.

### Install Node.js 22 and git

Update the system, install the basics, add the NodeSource repository for Node 22 and install it. npm comes with it.

```bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl ca-certificates git unzip
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node --version
npm --version
```

Note: node --version should print v22.x. Anything from v18 up runs the bot, but 18 and 20 no longer get upstream security fixes.

### Create a user for the bot

A separate user keeps the token and the data out of reach of everything else on the machine.

The second command opens a shell as that user. Every command in the next steps runs there, until the systemd step.

```bash
sudo adduser --disabled-password --gecos "" --home /opt/vorqul vorqul
sudo -iu vorqul
```

### Put the files in place

Clone the repository into a directory of its own, or unpack a release archive there. package.json has to sit directly in the working directory.

Prefer a release archive? Upload it, run unzip, and move the contents up so that package.json is not one level down.

```bash
git clone https://github.com/Yamiru/Vorqul-DS-BOT.git app
cd app
```

### Write the configuration

Copy .env.example to .env and fill in the token, the application ID and the storage type. The file holds secrets, so restrict who can read it.

- DISCORD_TOKEN: required, from the Developer Portal
- DISCORD_CLIENT_ID: required, the Application ID, and without it no slash command is registered
- DATABASE_TYPE: json or mysql (see Part 2)
- BOT_OWNER_ID: your Discord user ID, unlocks the owner-only commands
- DASHBOARD_ENABLED: leave false unless you want the dashboard (see below)

```bash
cp .env.example .env
chmod 600 .env
nano .env
```

### Install dependencies and start it

npm install pulls the dependencies, npm start checks the token, compiles the TypeScript and runs the bot in the foreground so you can watch the first boot.

The repository ships an .npmrc with legacy-peer-deps, which stops npm refusing to install because of peer dependency conflicts in the development tools. Do not delete it.

The first start compiles the code and takes a minute or two. Later starts skip the compile until the source changes. Stop it with Ctrl+C once you see it online and the slash commands registered.

```bash
npm install
npm start
```

Note: Watch this first run. Errors about the token, the application ID or the intents show up here and nowhere else. Do not install with --omit=dev or NODE_ENV=production: the compiler is a development dependency and the build would fail.

### Keep it running with systemd

A service restarts the process when it dies and brings it back after a reboot. This is the step people skip and then wonder why the bot went offline overnight.

Do not add Environment=NODE_ENV=production to the unit. The launcher runs npm install itself and would then skip the development dependencies it needs to compile. It sets production mode for the dashboard on its own.

The launcher also writes its own rotated log to logs/current.log inside the bot directory, next to the journal.

Prefer PM2? sudo npm install -g pm2, then pm2 start start.js --name vorqul, pm2 startup and pm2 save do the same job.

```bash (as your normal user, not vorqul)
sudo tee /etc/systemd/system/vorqul.service >/dev/null <<'EOF'
[Unit]
Description=Vorqul DS BOT
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=vorqul
WorkingDirectory=/opt/vorqul/app
ExecStart=/usr/bin/node start.js
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now vorqul
sudo systemctl status vorqul
journalctl -u vorqul -f
```

### Optionally enable the dashboard

The dashboard is not a second thing to install. Switch it on in .env and the same launcher builds it and starts it as a second process on port 3000.

Restart the service and give the first boot a few extra minutes, because the dashboard is built then. That build wants around 2 GB of RAM.

Do not expose port 3000 directly. Put nginx in front of it with a certificate. Leave NEXTAUTH_URL empty and the address is detected automatically. If NEXTAUTH_SECRET is empty the launcher generates one and keeps it in data/secrets, so it survives restarts.

Set ALLOWED_EMAILS to the e-mail of your Discord account, otherwise anyone who can sign in with Discord can reach the dashboard.

```.env
DASHBOARD_ENABLED=true
DASHBOARD_PORT=3000
DISCORD_CLIENT_SECRET=your_client_secret
NEXTAUTH_SECRET=output_of_openssl_rand_-hex_32
DASHBOARD_PASSWORD=
```

## Part 3b: Deploy on Pterodactyl

The path to take if you already run a game hosting panel. No shell needed at any point.

### Import the egg

In the admin area, open Nests, choose Import Egg and upload egg-vorqul-d-s-bot.json from the project.

The egg sets the startup command to npm start and creates the Startup variables for you, including the two required ones: DISCORD_TOKEN and DISCORD_CLIENT_ID. It offers Node.js 20 and Node.js 18 images. Both work, but both are past upstream end of life, so if your panel provides ghcr.io/pterodactyl/yolks:nodejs_22, add it under the egg Docker Images and select it for the server.

### Create the server

Create a new server on that egg with 1 GB of RAM (2 GB if you will run the dashboard), 1 GB of disk and no special CPU limit.

A network allocation only matters for the dashboard. The bot itself connects out to Discord and listens on nothing.

### Upload the files

Upload the release archive with the File Manager or over SFTP, extract it, and move the contents so that package.json sits in the container root.

```Expected layout
/home/container/
  bot.py
  start.js
  package.json
  package-lock.json
  .npmrc
  tsconfig.bot.json
  src/
  plugins/
```

Note: If package.json ended up inside a subfolder, the launcher will not find the source. Move everything up one level.

### Fill in the Startup variables

The token and the storage type go in as environment variables rather than in a .env file.

- DISCORD_TOKEN: your bot token
- DISCORD_CLIENT_ID: the Application ID, required
- DATABASE_TYPE: json (the default) or mysql
- BOT_OWNER_ID: your Discord user ID, recommended
- DASHBOARD_ENABLED, DASHBOARD_PORT, DISCORD_CLIENT_SECRET, NEXTAUTH_URL and NEXTAUTH_SECRET are only needed for the dashboard

Note: Using the Pterodactyl database host: set DB_HOST to the host the panel shows on the Databases tab, and use the prefixed user and database names it generated. For the dashboard, leave DASHBOARD_PORT empty to reuse the allocated port, and make NEXTAUTH_URL http or https exactly as it is really reachable.

### Check the startup command

npm start on the Node.js egg. python3 bot.py exists as an alternative launcher, but it only works in a container that has Node.js and npm installed as well, which a plain Python image does not.

The Python launcher starts the bot only, without the dashboard. Use the Node.js egg unless you have a reason not to.

```Startup command
npm start
```

### When the Pterodactyl panel itself misbehaves

These run over SSH on the machine that hosts Pterodactyl, not inside the bot. If the panel acts strange, restart pteroq and nginx first. If servers will not start, restart wings.

```bash
sudo systemctl restart pteroq
sudo systemctl restart nginx
sudo systemctl restart wings
sudo systemctl restart mariadb
sudo systemctl restart redis-server
sudo systemctl status wings
sudo journalctl -u wings -f
```

### Press Start

The first boot installs dependencies and compiles the code by itself, which takes one to three minutes. After that the bot comes online.

Note: If it exits immediately, read the console output. A missing token or application ID fails here with a clear message. The console also shows if the compile fell back to the slower tsx runtime.

## Part 4: First setup inside Discord

The bot is online. Three settings decide whether it is useful or annoying.

### Turn on the modules you need

Nine of the 44 modules are on out of the box: moderation, leveling, afk, polls, economy, games, giveaways, reminders and prefix commands. Everything else starts off, on purpose.

Enabling AutoMod on a live server before you have configured it is how you punish people for behaviour that was fine yesterday. Turn filters on one at a time.

### Set a staff role and a log channel

Do this before anything else. Without a log channel, moderation actions happen with no visible record, and the first dispute is unanswerable.

Every automated and manual action writes a numbered case regardless, but a log channel is what makes it visible while it is happening.

### Verify the install

Run /info ping and /help. If the bot answers and the commands are listed, the token, the intents and the command registration are all working.

Note: Missing slash commands mean the applications.commands scope was left out of the invite, or DISCORD_CLIENT_ID is empty or wrong. Discord can take a few minutes to show newly registered commands.

## Part 5: Locking it down

Five minutes here decide whether strangers can use your bot.

### Turn off Public Bot

In the Developer Portal open your application, go to Bot and switch off PUBLIC BOT.

With it on, anyone who knows your client ID can add the bot to their server. This switch is the only real protection against that.

### Lock the dashboard

Set ALLOWED_EMAILS or ALLOWED_IDS in .env. Only those accounts, plus people who manage a server the bot is already on, can sign in. Everyone else does not see the Add bot button.

DASHBOARD_PASSWORD adds one more gate in front of the sign-in. Generate NEXTAUTH_SECRET once with openssl rand -hex 32 and keep it, because changing it signs everybody out.

Set BOT_OWNER_ID to your own Discord ID. Owner commands and plugin management belong to that ID only, and several IDs are separated by commas.

### Treat the token like a password

Never send it to anyone and never upload .env to GitHub (it is in .gitignore). If it leaks, press Reset Token in the Developer Portal and paste the new one.

## Part 6: The .env reference

Every variable, what it does and whether you need it.

### Variables

The first two are required. Everything else has a sensible default.

- DISCORD_TOKEN (required): bot token, Developer Portal, Bot, Reset Token
- DISCORD_CLIENT_ID (required): Application ID, OAuth2, General
- DISCORD_CLIENT_SECRET (dashboard only): OAuth2, Reset Secret
- BOT_OWNER_ID (recommended): your Discord ID
- DATABASE_TYPE (required): json, which needs nothing installed, or mysql
- DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME (mysql only): MySQL connection details
- DATA_PATH: data folder, leave at ./data
- DASHBOARD_ENABLED: true switches the dashboard on
- DASHBOARD_PORT: dashboard port, leave empty on Pterodactyl
- NEXTAUTH_URL: leave empty, the address is detected automatically
- NEXTAUTH_SECRET (dashboard): random string that signs the login
- DASHBOARD_PASSWORD: extra password in front of the dashboard
- ALLOWED_EMAILS, ALLOWED_IDS (recommended): who may sign in, comma separated
- BOT_PREFIX: prefix for text commands, ! by default
- BOT_LANGUAGE: en, sk, cs, de, pl, ru, es, ja or tr
- TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET: for Twitch notifications
- DISABLED_COMMANDS: commands to hide from the / menu, because Discord allows 100
- LOG_TO_FILE: true writes logs to data/logs

Note: DATABASE_TYPE only covers data such as economy, warnings and tickets. Server settings (modules, AutoMod, welcome) always live in data/json/guild_settings.json. Switching from json to mysql does not move old data by itself.

## Part 7: First dashboard sign-in

Six steps, and the third one is where most people get stuck.

### Set the variables

DASHBOARD_ENABLED=true, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET and NEXTAUTH_SECRET.

### Add the redirect URL

In the Developer Portal, OAuth2, General, Redirects, add every address you sign in from, domain and IP:port alike.

```Redirect URLs
http://YOUR-IP:PORT/api/auth/callback/discord
https://your-domain.tld/api/auth/callback/discord
```

### Restart and sign in

Restart the bot, open http://YOUR-IP:PORT (the port is DASHBOARD_PORT, or the port allocated to the server on Pterodactyl), press Sign in with Discord, approve access and pick your server.

Note: If you see an authentication processing error, check DISCORD_CLIENT_ID and DISCORD_CLIENT_SECRET, or clear the cookies for the site.

## Part 8: What the dashboard does

A short tour so you know what to look for.

### Features

Everything below works per server, from one login.

- Server overview, Server Health Score and analytics for messages, joins and leaves
- 44 modules to switch on and off: moderation, AutoMod, anti-nuke, anti-raid, levels, economy, tickets, welcome, reaction roles, giveaways and more
- Settings in tabs, plus limiting modules to chosen channels
- Embed builder and a library of saved messages
- Backup and restore in three modes: complete, server only, bot settings only
- Plugins: install from a ZIP, enable, disable and configure
- Role Manager: a chosen role may manage the server in the dashboard, but not plugins
- Bot version with a notice when a newer one is out
- 9 languages, and refresh and clear-cache buttons in every tab

### The pages

Five screens, in the order you meet them.

- Gate: an optional password screen, switched on with DASHBOARD_PASSWORD
- Login: sign in with Discord
- Server picker: lists the servers you manage where the bot is present
- Server home: bot status with online state, ping, uptime, memory and CPU, server stats and a notice when an update is out
- Settings: one page with tabs, listed in the next step

### The settings tabs

Each tab groups related modules, so a feature is never more than one click from its switch.

- General: language, prefix and manager role
- Channels and Channel scopes: restrict where the bot works
- Modules: turn each feature on or off
- Moderation and security: AutoMod, Warnings, Appeals, Verification and Logging
- Members: Welcome and Goodbye, Reaction roles and Birthday
- Engagement: Leveling, Starboard, Suggestions, Reputation, Counting, Confessions and Giveaways
- Economy and games: Economy and Games
- Content: Tickets, Applications, Autoresponse, Sticky messages, Tags, Feeds (RSS, YouTube, Twitch, Reddit), Socials and Reminders
- Tools: Temp channels, Crypto, Backup and Restore, Webhooks, Embed builder, Links and Help
- Admin: Analytics, Audit log, API keys, Plugins (upload a ZIP, owner only) and Premium

### What the bot itself does

The slash commands, and the same commands with the ! prefix, grouped by area.

- Moderation: ban, tempban, softban, unban, kick, mute, unmute, timeout, voicemute, warn, notes, case, jail, shadowmute, clear, cleanup, lock, lockdown, slowmode, nick, role, timedrole, filter, altcheck and permcheck
- Security: AutoMod, anti-nuke, anti-raid lockdown, verification, appeals, message and server logs, role snapshots and Persist, which restores roles and nickname when a member rejoins
- Members: welcome and goodbye messages, join roles, reaction roles, role menus, invite tracking, name history and referrals
- Engagement: XP and levels with level roles, rank and leaderboards for messages and voice, starboard, suggestions, reputation, a counting channel, birthdays, confessions, AFK, polls, streaks, quests and giveaways
- Economy: balance, daily, work, pay, bank, shop, market, lootbox, spin and heist
- Games: 8ball, coinflip, dice, rock-paper-scissors, tictactoe, connect4, trivia, matchmaking and game stats
- Content tools: tickets, applications, autoresponses, sticky messages, tags, feeds and socials, reminders, a digest, and commands that answer what did I miss and why was I banned
- Utility: temp voice channels, crypto prices, Minecraft status, info, help, links, backup, webhook events, API keys with a read-only v1 stats API, and Premium
- Plugins: drop-in plugins such as sample_plugin, minecraft_status and fear_greed, each with settings per server

## Part 9: Updating

The launcher notices what changed, so an update is short.

### Update to a new release

Get the new files while keeping .env and the data directory, then restart. The launcher reinstalls dependencies when package.json changes and recompiles when the source changes.

On Pterodactyl, upload the new files over the old ones without deleting .env, data or plugins, then restart the server. Settings carry over through the schema migrations.

```bash
sudo -iu vorqul
cd app
git pull
exit
sudo systemctl restart vorqul
```

## Troubleshooting

**The bot is online but ignores every message**

Message Content Intent is off. Switch it on in the Developer Portal under Bot, in Privileged Gateway Intents, then restart the process.

**Slash commands do not appear**

Either the invite was generated without the applications.commands scope, or DISCORD_CLIENT_ID is empty. The log then says DISCORD_CLIENT_ID is not set. Fix it and restart. The commands register on every start.

**The log mentions error 30032 or the 100 command limit**

Discord allows at most 100 global slash commands and the bot ships 83 plus any plugin commands. List the ones you do not want, comma-separated, in DISABLED_COMMANDS in .env. They still work with the ! prefix.

**Welcome cards and join roles do nothing**

Server Members Intent is off. It is a separate switch from Message Content and both are needed.

**The log says SQLite is not available and switches to JSON**

DATABASE_TYPE is set to sqlite, which this version does not implement. Nothing is lost, the JSON files are used. Set DATABASE_TYPE=json, or mysql if you want a real SQL backend.

**npm install fails with peer dependency errors**

The .npmrc that ships with the project turns on legacy-peer-deps. If the file is missing, run npm install --legacy-peer-deps once. The launcher retries with that flag by itself.

**TypeScript compilation failed and the bot runs through tsx**

The compile step did not succeed, so the launcher fell back to running the source with tsx. The bot works, but starts slower. Read the compiler output above the warning. A common cause is dependencies installed with --omit=dev or NODE_ENV=production, which leaves out the compiler.

**The Discord login on the dashboard loops back to the start**

The callback URL in the Developer Portal does not match NEXTAUTH_URL exactly, including the scheme. They have to be identical, and http versus https has to match how the dashboard is really served.

**The dashboard says authentication could not be processed**

Check DISCORD_CLIENT_ID and DISCORD_CLIENT_SECRET, make sure the exact address you sign in from is listed under OAuth2 Redirects, or clear the cookies for the site.

**Stored messages show broken characters**

The MySQL database was created without utf8mb4. Recreate it with utf8mb4 and utf8mb4_unicode_ci.

**The bot dies when the SSH session closes**

It was started with npm start in the foreground. Use the systemd service from Part 3a, or PM2, so the process survives the session.

## Questions

**How long does the installation take?**

Around 10 to 15 minutes on either path, most of it spent waiting for dependencies to install and the first compile. The Discord side is about three minutes of clicking.

**Which Node.js version should I use?**

Node.js 22 LTS. The project runs on 18 and newer, but 18 and 20 are past upstream end of life, so a fresh install should not start there.

**Do I need a database server?**

No. JSON files need nothing installed and are the default. MySQL or MariaDB is worth it for a large server and overkill for a first install.

**Is the dashboard required?**

No. Every module can be configured from Discord. The dashboard is a convenience, particularly if you run the bot on several servers.

**Can I run the bot and the dashboard on different machines?**

The launcher starts both as two processes on the same host, sharing the same data directory. Splitting them across machines needs MySQL as the shared storage and is not something the launcher sets up for you.

**How do I update to a new release?**

Replace the files while keeping your .env and data directory and restart. The launcher reinstalls dependencies and recompiles when it detects a change, and settings carry over through the schema migrations.
