RDP Monster

Discord Bot Hosting on a VPS: Keep Your Bot Online 24/7

Discord Bot Hosting on a VPS: Keep Your Bot Online 24/7

Why a VPS is where Discord bots actually stay online

A Discord bot is not a web page. Once logged in, it holds a persistent WebSocket connection to Discord's gateway and must answer heartbeats continuously; when the process stops, the bot shows offline and every command fails. That one requirement, a process that never sleeps, rules out most "free" hosting:

  • Heroku removed its free dynos in November 2022.
  • Replit no longer keeps free projects running; always-on moved to paid deployments.
  • Glitch ended its app hosting in 2025.
  • Free web tiers that sleep after inactivity cut the gateway connection every time they idle; to users, that bot is simply down.

Discord bot hosting on a VPS fixes this at the root: a Linux machine with root access that runs around the clock, where the bot is just another service managed by the init system. This guide builds that on Ubuntu 24.04 (also valid for 22.04 and Debian 12) with discord.py and discord.js, then covers token handling, restarts, logs, updates and memory.

Prepare the VPS: a dedicated user and a runtime

Secure the front door first: enable SSH on Ubuntu and harden it with key authentication, no root login and ufw. A bot dials out to Discord and needs no inbound ports, so the firewall can stay closed to everything except SSH.

Step 1: create an unprivileged account for the bot

sudo useradd -r -m -d /opt/mybot -s /usr/sbin/nologin bot

A dedicated account means a buggy bot or a compromised npm or PyPI dependency cannot read your SSH keys or touch other services. Everything lives in /opt/mybot, owned by bot.

Step 2 (Python): install discord.py in a virtual environment

sudo apt update && sudo apt install -y python3 python3-venv python3-pip
sudo -u bot python3 -m venv /opt/mybot/.venv
sudo -u bot /opt/mybot/.venv/bin/pip install -U discord.py python-dotenv

discord.py 2.x requires Python 3.8 or newer; Ubuntu 24.04 ships 3.12 and 22.04 ships 3.10, so the distribution interpreter is fine. Add discord.py[voice] and ffmpeg only for voice channels.

Step 3 (Node.js): install a current LTS and discord.js

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
sudo -u bot bash -c 'cd /opt/mybot && npm init -y && npm install discord.js dotenv'

Ubuntu's own nodejs package lags well behind upstream and discord.js v14 needs a modern runtime, so use NodeSource (above) or nvm for a current LTS. With nvm, run which node and use that absolute path in the service unit.

Keep the token out of the code: the .env file

The bot token is a password with full control over the bot: never paste it into bot.py or index.js, never commit it. Put it in /opt/mybot/.env, readable only by the bot account, using an editor rather than echo, since a token typed on the command line lands in your shell history:

sudo -u bot nano /opt/mybot/.env        # one line: DISCORD_TOKEN=your-token-here
sudo chmod 600 /opt/mybot/.env
echo ".env" | sudo -u bot tee -a /opt/mybot/.gitignore

The format is one KEY=value per line, no quotes, no export. python-dotenv and the dotenv npm package read it, and systemd can inject the same file with EnvironmentFile= (next section); combining both is harmless, since neither library overrides a variable that already exists. If the token ever leaks, reset it immediately in the Developer Portal under Bot → Reset Token.

A minimal bot in discord.py and discord.js

Both examples read the token from the environment, enable the Message Content intent and answer !ping. In the Developer Portal, create an application, add a bot user, copy the token (shown once), enable Message Content Intent under Bot → Privileged Gateway Intents, and invite the bot to a test server via the OAuth2 URL generator with the bot scope.

discord.py: /opt/mybot/bot.py

import os
import discord
from dotenv import load_dotenv

load_dotenv()

intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)

@client.event
async def on_ready():
    print(f"Logged in as {client.user}")

@client.event
async def on_message(message):
    if message.author == client.user:
        return
    if message.content == "!ping":
        await message.channel.send("pong")

client.run(os.environ["DISCORD_TOKEN"])

discord.js: /opt/mybot/index.js

require("dotenv").config();
const { Client, GatewayIntentBits, Events } = require("discord.js");

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

client.once(Events.ClientReady, (c) => {
  console.log(`Logged in as ${c.user.tag}`);
});

client.on(Events.MessageCreate, (message) => {
  if (message.author.bot) return;
  if (message.content === "!ping") message.reply("pong");
});

client.login(process.env.DISCORD_TOKEN);

Run it in the foreground once: sudo -u bot /opt/mybot/.venv/bin/python /opt/mybot/bot.py or sudo -u bot node /opt/mybot/index.js, wait for Logged in as, send !ping, then Ctrl-C. Right now the bot lives only as long as your SSH session: closing the terminal sends SIGHUP to its child processes and the bot dies.

Run it under systemd with Restart=always

systemd is already Ubuntu's init system, so the cleanest way to keep the bot online after logout, crash and reboot is a service unit: no extra daemon, and logs land in the journal.

Step 1: write the unit

sudo nano /etc/systemd/system/mybot.service
[Unit]
Description=Discord bot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=bot
Group=bot
WorkingDirectory=/opt/mybot
EnvironmentFile=/opt/mybot/.env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/mybot/.venv/bin/python /opt/mybot/bot.py
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/mybot

[Install]
WantedBy=multi-user.target

For discord.js, set ExecStart=/usr/bin/node /opt/mybot/index.js and drop the PYTHONUNBUFFERED line. The directives that matter:

  • Restart=always restarts the process on any exit, crash or clean sys.exit() alike; RestartSec=5 keeps a bot that dies instantly (bad token, syntax error) from spinning in a tight loop.
  • Wants=/After=network-online.target delay the start until the network is up, so the first login after a reboot does not fail on DNS.
  • PYTHONUNBUFFERED=1: without a terminal Python block-buffers stdout, and print() lines reach the journal minutes late or only at exit.
  • The Protect*, PrivateTmp and NoNewPrivileges lines make the rest of the filesystem read-only or invisible to the bot; add any directory it must write to ReadWritePaths=.

Step 2: enable, start and verify

sudo systemctl daemon-reload
sudo systemctl enable --now mybot
systemctl status mybot
sudo systemctl kill --signal=KILL mybot   # simulate a crash; status is active again after 5 s

enable creates the boot symlink and --now starts the unit. The kill line is the real test: if systemctl status mybot shows active (running) a few seconds later, the restart policy works, and a sudo reboot proves the boot path. To see it alongside every other service, list running services with systemctl; it appears as mybot.service, and systemctl is-enabled mybot confirms it will return after a restart.

Alternative: pm2, and how it compares to screen, nohup and Docker

Every way of keeping a bot alive answers three questions: does it survive SSH logout, restart on crash, and start on reboot?

MethodSurvives SSH logoutRestarts on crashStarts on rebootVerdict
nohup / &YesNoNoQuick tests only.
screen / tmuxYesNoNo, unless scriptedDebugging, not production.
pm2YesYesYes, after pm2 startup + pm2 saveGood for Node.js teams; one CLI for logs and restarts.
systemd unitYesYes (Restart=)Yes (enable)Recommended; no extra software, hardening built in.
Docker, restart: unless-stoppedYesYesYes, with the Docker daemonGood when the bot already ships as an image.

pm2 is the popular choice in the Node.js world and handles Python too. Run it as the account that owns the bot:

sudo npm install -g pm2
cd /opt/mybot
pm2 start index.js --name mybot
pm2 start bot.py --name mybot --interpreter /opt/mybot/.venv/bin/python   # Python variant
pm2 save
pm2 startup                    # prints a sudo command to run once
pm2 logs mybot
pm2 install pm2-logrotate      # ~/.pm2/logs/ otherwise grows forever

pm2 startup generates a pm2-<user> systemd unit that resurrects your saved process list at boot, so pm2 sits on top of systemd rather than replacing it: the honest argument for using systemd directly for a single bot. pm2 earns its place with several Node processes or a team that already knows it.

If you prefer containers, the equivalent is restart: unless-stopped in a Compose file; the walkthrough to install Docker on an Ubuntu VPS covers the engine, Compose and boot behaviour. Its ufw caveat does not affect a bot, which publishes no ports.

Logs, updates and memory sizing

Logs

journalctl -u mybot -f                    # follow live
journalctl -u mybot --since "1 hour ago"
journalctl -u mybot -p err -b             # errors since boot
sudo journalctl --vacuum-size=200M        # cap journal size

journald rotates and compresses on its own. If the bot writes its own log file, use logging.handlers.RotatingFileHandler in Python or a rotating transport with pino or winston in Node, so a chatty handler cannot fill the disk.

Updates

cd /opt/mybot
sudo -u bot git pull
sudo -u bot .venv/bin/pip install -U -r requirements.txt   # Node: sudo -u bot npm ci
sudo systemctl restart mybot

A restart drops the gateway connection for a few seconds; the library reconnects and users rarely notice. Pin library versions in requirements.txt or package-lock.json and read the changelog before a major bump. Keep the OS current with sudo apt update && sudo apt upgrade or unattended-upgrades.

How much RAM does the bot need?

Do not size from generic numbers; measure. systemctl status mybot prints a Memory: line and pm2 list shows the same. A command-only discord.py bot typically idles in the tens of megabytes; a discord.js bot carries the Node runtime on top. Memory grows with state: member and message caches on large servers, voice and music (an ffmpeg process per stream), image generation or a database. In practice 1 GB of RAM comfortably runs the OS plus a small or medium bot; voice-heavy bots or several bots on one box justify 2 GB or more, and above 2,500 servers Discord requires sharding, which multiplies the footprint. Two cheap safety nets: MemoryMax=512M in the unit, so a leak restarts the bot instead of taking the machine down, and a small swap file.

All of this assumes a Linux box with root access that stays up around the clock. If you still need that machine, rdp.monster's Discord bot VPS is online about 10 seconds after payment confirms, with dedicated CPU and RAM, full root access, unlimited (fair-use) bandwidth, no KYC, crypto accepted, from $8.99/month, so the systemctl enable --now mybot step above can be the last thing you type before the bot shows green.

Frequently Asked Questions

Can I host a Discord bot for free 24/7?

Not reliably anymore: the free tiers that kept bots awake have been removed or now sleep on inactivity.
Heroku dropped free dynos in November 2022, Replit moved always-on to paid deployments, and Glitch ended app hosting in 2025. Remaining free web tiers sleep after inactivity, which closes the bot's gateway connection. Running it on your own PC works until the PC reboots or sleeps. The cheapest dependable option is a small VPS with about 1 GB of RAM, where systemd restarts the bot on crash and reboot and nobody suspends it for idling.

Does a Discord bot need a static IP or open ports on the VPS?

No: the bot connects outbound to Discord, so no inbound port has to be open and the IP does not matter.
A gateway bot opens an outbound WebSocket to Discord and receives events over that same connection, so the firewall on the VPS can stay closed to everything except SSH, and the server's IP never appears to users. You only need an open port and a public hostname if you switch to HTTP interaction endpoints, which Discord calls via a URL you register, or if you add a web dashboard. In that case put it behind HTTPS with a reverse proxy.

Can I run several Discord bots on one VPS?

Yes, one systemd unit (or pm2 process) per bot, ideally each under its own user and directory.
Nothing limits you to one bot per machine. Give each bot its own directory such as /opt/bot-a and /opt/bot-b, its own .env with its own token, and its own unit file, then systemctl enable --now each one. Separate system users keep a bug in one bot from reading the other's token. The only shared budget is RAM and CPU: watch systemctl status per unit, and move to a larger plan when the total nears the limit.

Should I host a Discord bot on a Windows or a Linux VPS?

Linux is the default for bots: cheaper, lighter, and the tooling (systemd, pm2, Docker) is built for it.
Both discord.py and discord.js run on Windows, and a Windows VPS is the right pick if the bot depends on a Windows-only application or you simply prefer a desktop. For a plain bot, Linux wins: the OS uses a fraction of the RAM, a minimal server has almost no attack surface, and keeping a process alive is a ten-line systemd unit. On Windows you would use Task Scheduler or NSSM to get the same restart-on-boot behaviour with more clicks.

Adrien Roche, Infrastructure & Hosting Editor

Systems engineer with 10+ years operating Windows Server and Linux fleets. Adrien runs the rdp.monster infrastructure documentation and writes our guides on RDP, VPS hosting, server administration, networking and privacy tooling.

Register to our reseller program

Your information

If you have any question, contact us by clicking here !
Name(Required)
Enter your email address, you must have an account on manager.rdp.monster !

Your company

Enter your website address if you have one
Quickly explain how you're going to sell services to your customers. For example, talk to people on forums.

Even monsters love cookies!

We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic. By clicking "Accept", you consent to our use of cookies.