
Your Discord bot works, but only while your PC is on: a Windows update, sleep mode or a closed terminal takes it offline. A VPS keeps it running, but a plain copy of the bot folder breaks in predictable places: a token the bot can't find, packages built for Windows, file names in the wrong case, and two copies answering every command. This guide moves a discord.js or discord.py bot from Windows to Ubuntu 24.04 or Debian 12 in an order that avoids all four. For a bot that has never run anywhere, start with our Discord bot VPS setup guide.
Quick answer: Move the code through a private Git repository, paste the secrets (.env or config.json) into a new file on the VPS, and copy the data (the SQLite file or a database dump) last. On the VPS, install the same major Node.js or Python version, reinstall packages with npm ci or a new virtual environment instead of copying node_modules or the venv, and fix paths and file names that only worked on Windows. Stop the PC copy and its autostart before the data copy, then run the bot under systemd or PM2 from its own folder. The token, intents and slash commands belong to the Discord application and come along; reset the token if an old copy might still run.
What to Bring and What to Leave
| Item | Move it? | How |
|---|---|---|
Code, package.json and package-lock.json, or requirements.txt | Yes | A private Git repository |
.env or config.json with the token | Yes | Paste into a new file on the VPS, never through Git |
| SQLite database and JSON data files | Yes, last | scp, after the PC copy has stopped |
| MySQL, MariaDB or PostgreSQL on your PC | Yes, last | A dump: see our MariaDB and PostgreSQL guides |
| A hosted database such as MongoDB Atlas | Nothing to copy | Add the VPS's IP address to its access list |
node_modules, .venv, __pycache__ | No | Reinstall on the VPS |
Atlas "only allows client connections to the cluster from entries in the project's IP access list", so without the VPS's address there the bot can't connect.
On the PC, note the Node.js version with node --version. For a discord.py bot, run python --version in its activated virtual environment, plus python -m pip freeze > requirements.txt if you have no requirements file. Without a virtual environment, pip freeze lists every package of your global Python, so trim the file to what the bot imports.
Then check, in the bot folder, that Git ignores your secrets and has never stored them:
git check-ignore -v .env config.json
git log --all --oneline -- .env config.json
The first prints the .gitignore rule for each ignored file and nothing for a file Git would commit. The second should print nothing; a commit in its output means the token is in your history, so reset it (Step 7). The discord.js guide's .gitignore lists node_modules, .env and config.json; for Python add .venv/, __pycache__/ and the database file. Then push to a private repository. Without Git, copy the folder with scp -r once node_modules and the venv are out of it, and check the line endings yourself (Step 3).
Step 1: Prepare the VPS
Log in over SSH with your admin account, install the tools, and create a user for the bot so it never runs as root:
sudo apt update
sudo apt install -y git curl nano
sudo useradd -m -s /bin/bash bot
On a Space-Node VPS you get KVM with full root access, and the order form recommends Ubuntu 24.04 LTS, the system these commands are written for.
Node.js. Install the major version your PC printed, if it is still supported. On 30 October 2026, Node.js 26 is the Active LTS line, 24 gets maintenance releases until 30 April 2028 and 22 until 30 April 2027. If your PC runs 18 or 20, which are end of life, use 24 or 26 and test the bot before you switch. Skip the distribution's own nodejs package: it is version 18 (18.19.1 on Ubuntu 24.04, 18.20.4 on Debian 12), which the Node.js project no longer updates. discord.js 14.28.0 needs Node.js 18 or newer, so 22, 24 and 26 all work. From NodeSource:
NODE_MAJOR=24 # your PC's major version: 22, 24 or 26
curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install -y nodejs
node -v
Python. discord.py 2.7.1 requires Python 3.8 or higher. Ubuntu 24.04 ships 3.12.3 and Debian 12 ships 3.11.2, so the system Python works once you add the venv module:
sudo apt install -y python3-venv
Step 2: Clone the Code and Reinstall Packages
Switch to the bot user and create a key that lets the VPS read your repository:
sudo -iu bot
ssh-keygen -t ed25519
cat ~/.ssh/id_ed25519.pub
Press Enter at each prompt, then paste the printed key on GitHub under the repository's Settings > Deploy keys > Add deploy key. A deploy key opens only that repository and is read-only by default. Then clone:
git clone git@github.com:OWNER/REPO.git ~/mybot
cd ~/mybot
Install packages on the VPS itself, never by copying them from Windows. Native add-ons that npm installed on Windows are Windows binaries, and Linux refuses them with ERR_DLOPEN_FAILED and invalid ELF header. Python's documentation says a virtual environment is "not considered as movable or copyable": you recreate it.
For discord.js:
npm ci
npm ci installs exactly what package-lock.json lists, after deleting any existing node_modules. Without a lock file, run npm install once instead. A TypeScript bot also needs its build script, such as npm run build.
For discord.py:
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
Always run pip through the venv like this: a plain pip install on Ubuntu 24.04 or Debian 12 stops with error: externally-managed-environment, because both mark their own Python as managed by apt.
Step 3: Fix What Only Worked on Windows
Letter case. Microsoft documents the difference: Windows treats FOO.txt and foo.txt as one file, Linux as two. require('./commands/Ping.js') finds ping.js on your PC but fails on the VPS with Cannot find module './commands/Ping.js'. Python says ModuleNotFoundError: No module named 'cogs.Music', and discord.py's load_extension says Extension 'cogs.Music' could not be loaded or found. Make every import match its file name exactly.
Windows paths. A path like C:\Users\you\bot\bot.db does not exist on Linux. This lists drive letters in your code:
grep -rnE --exclude-dir=node_modules --exclude-dir=.venv \
--include='*.js' --include='*.ts' --include='*.py' '(^|[^A-Za-z])[A-Za-z]:[/\\]' .
Build such paths from the script's folder: path.join(__dirname, 'data', 'bot.db') in CommonJS, path.join(import.meta.dirname, 'data', 'bot.db') in an ES module (Node.js 20.11 and newer), or Path(__file__).resolve().parent / "data" / "bot.db" in Python.
The working directory. Relative paths such as bot.db resolve from the directory the process started in. Node's --env-file reads its file "relative to the current directory", and dotenv looks in process.cwd() by default. A systemd service without WorkingDirectory= starts in / and finds no token and no database; Step 6 sets it. python-dotenv searches from the script's folder, but your database path still depends on the working directory.
Line endings. Shell scripts saved on Windows can have CRLF line endings, and on Ubuntu 24.04 they fail with cannot execute: required file not found. Run sed -i 's/\r$//' start.sh, and add *.sh text eol=lf to a .gitattributes file so Git checks them out with LF.
Step 4: Put the Secrets on the VPS
Still as the bot user, create the file and paste in the contents of your PC's .env (name it config.json if that is what your bot reads):
nano ~/mybot/.env
chmod 600 ~/mybot/.env
chmod 600 makes the file readable and writable for the bot user only. Copying the text by hand keeps the token out of Git, as Discord asks: "never share your token or check it into any kind of version control."
Step 5: Stop the PC Copy, Then Copy the Data
Do this right before the first start on the VPS. Discord's gateway documentation allows several sessions with the same shard values, so a second copy logs in without any error and gets the same events: prefix commands are answered twice, a slash command gets one answer while the other copy fails with error 40060 ("Interaction has already been acknowledged"), and each copy writes to its own database.
Stop the bot the way you started it (Ctrl+C, or pm2 delete mybot under PM2), then remove whatever starts it with Windows:
- a shortcut in the Startup folder (
shell:startupin the Run dialog); - an entry in Settings > Apps > Startup or on Task Manager's Startup apps tab;
- a Task Scheduler task;
- PM2's saved list: after deleting your last app, run
pm2 save --force, since PM2 won't save an empty list without it.
Then copy the data. In PowerShell on the PC, from the bot folder (203.0.113.10 stands for your VPS's address):
scp .\bot.db admin@203.0.113.10:~/
On the VPS, as your admin user:
sudo install -o bot -g bot -m 600 ~/bot.db /home/bot/mybot/bot.db
rm ~/bot.db
Copy a leftover bot.db-wal or bot.db-journal file the same way: SQLite says these must travel with the database when the last write did not finish, and a clean stop normally deletes the WAL file. JSON data files go the same way; our SQLite and MySQL guide covers a real database for later. scp comes with OpenSSH for Windows, which you can add under Optional features if PowerShell doesn't know the command.
Step 6: Start the Bot and Keep It Running
Start it by hand first, as the bot user, so errors land in your terminal. Run the line for your library, with your own entry file:
sudo -iu bot
cd ~/mybot
node --env-file=.env index.js # discord.js
.venv/bin/python bot.py # discord.py
Leave out --env-file, here and in the service below, if your bot reads config.json or loads .env with dotenv: without a .env file, Node stops with .env: not found. Once the bot is online and answers a command, stop it with Ctrl+C and type exit to return to your admin user; if it fails, see the table below. Then create a service with sudo nano /etc/systemd/system/mybot.service:
[Unit]
Description=My Discord bot
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=600
StartLimitBurst=5
[Service]
User=bot
WorkingDirectory=/home/bot/mybot
ExecStart=/usr/bin/node --env-file=.env index.js
# discord.py: ExecStart=/home/bot/mybot/.venv/bin/python bot.py
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now mybot
sudo journalctl -u mybot -f
NodeSource's package installs /usr/bin/node. systemd calls Restart=on-failure "the recommended choice for long-running services". The StartLimit lines stop the restarts after five starts in ten minutes, which protects your token: after 1,000 IDENTIFY calls in 24 hours, Discord ends all sessions, resets the token and emails the owner. systemd's default limit (five starts in ten seconds) never triggers at a 10 second pace, so a bot that crashes right after logging in would get there within hours. After a fix, run sudo systemctl reset-failed mybot and start it again.
With PM2, start the bot from its folder or set cwd in the ecosystem file; our PM2 guide covers restarts and logs.
Step 7: Check the Token, Intents and Commands
Token. It belongs to the application, not to your PC, so it works on the VPS unchanged. Reset it on the Bot page (under Token, click Reset Token) if git log found it, if the repository was ever public, or if an old copy might still run on a laptop or a free host. The old token then stops working: the discord.js guide lists "not updating the token after resetting it" as a cause of its invalid token error. GitHub's secret scanning also reports Discord bot tokens in public repositories to Discord, which decides whether to revoke them. Put the new token in ~/mybot/.env and run sudo systemctl restart mybot. Our token security guide covers what else to do after a leak.
Intents. GUILD_MEMBERS, GUILD_PRESENCES and MESSAGE_CONTENT are toggles on the Bot page under Privileged Gateway Intents, so they carry over too. If your code requests one that is off, discord.js stops with Used disallowed intents and discord.py raises PrivilegedIntentsRequired, both from gateway close code 4014: switch it on or drop it from the code. Only a new application needs its intents set again and a new invite.
Slash commands. Discord makes global commands "available for every guild that adds your app", and the discord.js guide says they "only need to be registered once, and updated when the definition (description, options etc) is changed." The move changes nothing there. If your bot registers them on every start, use a separate script instead: Discord allows 200 command creates per day, per guild.
If It Still Fails
| What you see | Cause | Fix |
|---|---|---|
DiscordjsError [TokenInvalid]: An invalid token was provided. | discord.js got no token, or an old one | Check WorkingDirectory=, --env-file and the token in .env |
TypeError: expected token to be a str, received NoneType instead | discord.py got no token | Load .env before you read the token; check the variable name |
LoginFailure: Improper token has been passed. | discord.py's token is wrong or was reset | Paste the current token |
Used disallowed intents or PrivilegedIntentsRequired | A privileged intent is off for this application | Switch it on, or remove it from the code |
Cannot find module, ModuleNotFoundError, ExtensionNotFound | Letter case differs from the file name | Match names exactly (Step 3) |
ERR_DLOPEN_FAILED with invalid ELF header | node_modules came from Windows | rm -rf node_modules && npm ci |
error: externally-managed-environment | pip ran outside the venv | Use .venv/bin/python -m pip |
unable to open database file | Wrong path or missing folder | Fix the path (Step 3) |
attempt to write a readonly database | The bot user can't write the file or its folder | sudo chown bot:bot on both |
| Every command answered twice | A second copy still runs | Stop it, or reset the token |
FAQ
Can I keep the bot running on my PC as a backup? Not with the same token at the same time: both copies log in, get every event and act on it. Keep the PC copy stopped and start it only while the VPS copy is down.
Do I have to invite the bot again or register the slash commands again? No. The bot's membership and roles in your servers and its global commands belong to the Discord application, and you moved that application's token. Only a new application starts from zero.
Does the bot need an open port on the VPS? Not a normal bot: it opens a WebSocket connection to Discord's gateway itself. You need an open port only for an Interactions Endpoint URL, where Discord sends interactions to your server over HTTP, or for a web dashboard.
How big a VPS does a bot need?
Measure first: Task Manager shows the memory node.exe or python.exe uses while the bot runs. Add room for the operating system and any database. Space-Node's smallest plans are the Premium VPS-XS (2 vCores, 2 GB RAM) and the Standard VPS-S (3 vCores, 4 GB RAM); our RAM guide shows how to measure on Linux.
Why do my scheduled messages fire at the wrong hour now?
The VPS may use another time zone than your PC. timedatectl shows it, timedatectl list-timezones lists the names and sudo timedatectl set-timezone Europe/Amsterdam changes it. Better still, give your scheduler an explicit time zone in the code.
Sources
All opened on 31 October 2026:
- Discord developer docs: Gateway (privileged intents, sessions with the same shard values, the 1,000 IDENTIFY limit), Opcodes and Status Codes, Getting started (Reset Token), Application commands, Receiving and responding to interactions
- discord.js 14.28.0: README, error messages, error names, Client.js, WebSocketManager.js, @discordjs/ws 1.2.3; discord.js guide: project setup, deploying commands, common errors
- discord.py 2.7.1: PyPI, README, client.py, http.py, errors.py, ext/commands/errors.py
- Node.js: release schedule, release index, CLI options, path, modules, ES modules; NodeSource install guide and package index; npm ci; dotenv
- Python: venv, pathlib, open(), pip freeze, pip 26.2.2 exceptions, PEP 668, Debian 12 release notes, python-dotenv
- Packages: Ubuntu 24.04 nodejs, python3, python3-venv; Debian 12 nodejs, python3, python3-venv
- SQLite: Write-Ahead Logging, How To Corrupt An SQLite Database File, error messages in main.c; MongoDB: Atlas IP access list
- Git and GitHub: git check-ignore, git log, git clone, deploy keys, line endings, secret scanning for partners, supported secret patterns
- Microsoft: case sensitivity, apps that run at startup, OpenSSH for Windows
- systemd: systemd.exec, systemd.service, systemd.unit, systemd.special, systemd-system.conf, systemctl, journalctl, timedatectl, network-online.target
- PM2 7.1.1: Startup.js (
pm2 save --force), process management (pm2 delete), application declaration (cwd) - Man pages: ssh-keygen(1), scp(1), useradd(8), sudo(8), install(1), chmod(1), chown(1), grep(1), sed(1)
- Reproduced on Ubuntu 24.04 (bash 5.2.21, Python 3.12.3): the
invalid ELF header,cannot execute: required file not found,Cannot find moduleandModuleNotFoundErrormessages, and the drive letter grep