A Discord bot that only runs when your computer is on is barely useful. Members test the bot at 3 AM and it doesn't respond. The solution: host it on a VPS and let it run 24/7. This guide covers both Node.js (discord.js) and Python (discord.py) setups.
Prerequisites
- A Space-Node VPS (smallest plan works for most bots)
- SSH access
- Your bot code repository (GitHub, GitLab, or local zip)
Node.js Bot Deployment
# Install Node.js 20 LTS (Ubuntu)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install nodejs
# Clone your bot
git clone https://github.com/yourusername/your-discord-bot.git /home/bot/
cd /home/bot/
# Install dependencies
npm install
# Test run
node index.js
Python Bot Deployment
# Install Python 3.11+
sudo apt install python3 python3-pip python3-venv
# Create virtual environment
python3 -m venv /home/bot/venv
source /home/bot/venv/bin/activate
# Clone and install
git clone https://github.com/yourusername/your-discord-bot.git /home/bot/code
cd /home/bot/code
pip install -r requirements.txt
# Test run
python3 bot.py
Keeping the Bot Running with PM2
PM2 manages the process lifecycle:
# Install PM2
npm install -g pm2
# Start Node.js bot
pm2 start index.js --name my-discord-bot
# OR Python bot
pm2 start "python3 /home/bot/code/bot.py" --name my-discord-bot
# Save process list and enable on startup
pm2 save
pm2 startup # Follow the output instruction
Managing Environment Variables Securely
Never put your Discord token in your source code. Use environment variables:
# Create .env file (NOT committed to git)
cat > /home/bot/.env << 'EOF'
DISCORD_TOKEN=your_token_here
DATABASE_URL=your_db_url_here
EOF
# Load in Node.js:
require('dotenv').config({ path: '/home/bot/.env' })
const token = process.env.DISCORD_TOKEN
# Load in Python:
from dotenv import load_dotenv
load_dotenv('/home/bot/.env')
token = os.getenv('DISCORD_TOKEN')
Updating Your Bot
cd /home/bot/code/
git pull origin main
pm2 restart my-discord-bot
Set up a GitHub webhook to automate this, or run it manually after each push.
Host your Discord bot 24/7 on Space-Node
When to pick Node.js vs Python for a Discord bot
| Decision | Node.js (discord.js) | Python (discord.py / py-cord) |
|---|---|---|
| Slash command ergonomics | excellent | excellent |
| Voice support | good (with @discordjs/voice + ffmpeg) | good (with PyNaCl + ffmpeg) |
| Music bots at scale | Lavalink + Java is the meta | same Lavalink, less common |
| ML / pandas integration | poor | excellent |
| Type safety | TypeScript | type hints |
| Hosting RAM at idle | 80-120 MB | 60-90 MB |
discord.py was unmaintained for a while in 2021-2022; the active fork most people use today is py-cord (or the resurrected discord.py 2.x). Both are stable in 2026.
VPS sizing
| Bots / users | vCPU | RAM | Disk |
|---|---|---|---|
| 1 bot, < 1k servers | 1 | 1 GB | 20 GB |
| 1 bot, 1-10k servers | 2 | 2 GB | 40 GB |
| Multiple bots, shared host | 2 | 4 GB | 50 GB |
| Music bot with Lavalink | 2-4 | 4 GB | 30 GB |
systemd unit for a Node bot
[Unit]
Description=My Discord Bot
After=network-online.target
[Service]
Type=simple
User=bot
WorkingDirectory=/home/bot/mybot
ExecStart=/usr/bin/node dist/index.js
Restart=always
RestartSec=5
EnvironmentFile=/etc/mybot.env
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Sharding threshold
Discord requires sharding once your bot is in 2500+ servers. Both libraries handle this; on Node, set shardCount: 'auto' in the Client constructor. You don't need to touch shards before that scale.
Things that surprise people
| Surprise | Reason | What to do |
|---|---|---|
| Bot intermittently misses messages | gateway intent not enabled | enable intents in dev portal AND in code |
| "Disallowed intents" error | privileged intents not whitelisted | request approval (required at 100+ servers) |
| Token leaked, can't roll fast | token in git history | use BFG / filter-repo, then regenerate |
| Memory grows over weeks | cache.MessageManager unbounded | configure cache sweepers |
Cache sweeper example (discord.js)
const client = new Client({
sweepers: {
messages: { interval: 3600, lifetime: 1800 },
users: { interval: 3600, filter: () => u => u.bot && u.id !== client.user!.id },
},
});
Without sweepers, message cache will hold every visible message forever and OOM the process eventually.
