> ## Content Index
> Fetch the complete content index at: https://sudosecurity.org/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Self-Host a Discord Bot: Building and Running Your Own Bot 24/7
- URL: https://sudosecurity.org/how-to-self-host-a-discord-bot-building-and-running-your-own-bot-24-7/
- Published: 2026-08-01T09:37:50.000Z
- Updated: 2026-08-01T09:37:50.000Z
- Author: Astramew
- Tags: Self Hosting

There's a moment every Discord bot developer remembers, even if nobody makes a big deal of it. You run `node index.js`, switch over to Discord, type a command, and your own code answers back. It's a small thing to get excited about, and it never gets old.

A day or two later you close your laptop, the bot goes offline with it, and the real question arrives: how do people keep these things running all the time?

I've hosted a handful of Discord bots over the years, and the truth is it stops being complicated the moment you've done it once. It's a stack of small pieces that aren't obvious the first time: a bot token, SSH, a process manager, environment variables, a way to ship updates without breaking anything. This is the walkthrough I wish I'd had, going from a bare "hello world" bot to something running 24/7 on a server and staying up on its own.

We'll build the bot, get it working locally, move it onto a VPS, keep it alive with PM2, and set up a clean way to update it later. Along the way I'll flag the traps that cost me hours the first time, so you can walk straight past them.

Worth saying up front: almost none of this is Discord specific. Swap "Discord bot" for "Telegram bot" or "small Node API" and the process barely changes. Discord bots are just a common first deploy because the feedback loop is so tight. You push a change and a few seconds later you see it working, or not, in a real chat window. That immediacy is a big part of why it's worth learning properly instead of following a hosting tutorial on autopilot and hoping it holds.

## What "self-hosting" actually means, and why your laptop isn't it

Self-hosting just means you run the bot on a computer you control, rather than paying a managed platform to run it for you. That computer can be an old laptop in a drawer or a rented server in a data center. The important word is *control*: you decide the runtime, the updates, the uptime, and the security.

Running your bot locally while you build it is normal. It's how it should work. Fast to edit, fast to restart, no reason to complicate things while you're still figuring out what the bot even does.

The problem appears the second other people start relying on it. Say you build a moderation bot for a community you're in. For that bot to be useful, your computer would have to:

- stay powered on, basically forever
- never lose its internet connection
- keep Node running without interruption
- never crash
- never reboot for a random Windows update at 3am

That's not a fair ask of a personal laptop, and it isn't meant to be. It's the job servers exist for. Moving the bot onto something built to stay online is the moment it stops being "a script I wrote" and becomes an actual service other people can depend on.

## A quick mental model of how a bot works

Before we touch servers, it helps to know what actually happens when your bot responds to something. It makes debugging far less mysterious later.

Your bot doesn't talk to Discord directly. It goes through Discord's API, and a library like discord.js handles that conversation for you:

```
Your application → discord.js → Discord API → Discord's servers

```

In practice:

1. Your bot starts up, and discord.js opens a connection to Discord.
2. It authenticates with a bot token.
3. Discord checks the token is valid.
4. Once accepted, your bot starts receiving events and can respond to them.

So when someone types `!hello`, Discord fires that to your bot as an event, your code handles it, and a reply goes back, but only if your process is running at that moment. The second your program stops, the whole chain stops. That's the entire reason hosting matters. No running process, no bot.

### Two channels: the gateway and the REST API

There are two separate channels your bot uses to talk to Discord, and knowing the difference explains a couple of things you'll bump into later.

The **gateway** is the persistent connection, a WebSocket that stays open the whole time your bot runs. It's how Discord pushes events to you in real time: messages sent, members joining, reactions added. This is the connection PM2 is really keeping alive for you. If your host has flaky networking, this is the connection that suffers, and discord.js will spend its life reconnecting.

The **REST API** is what your bot uses to *do* things: send a message, edit a channel, ban a user, register commands. Those are one-off HTTP requests, separate from the persistent connection. The REST API also has rate limits, so if your bot ever starts hammering it (a loop that edits a message every 100ms, say), you'll start seeing `429 Too Many Requests`. discord.js queues around these for you most of the time, but it's good to know the wall is there.

### Intents, the thing that silently breaks bots

The other thing to learn early is **intents**. When your bot connects, it tells Discord which categories of events it wants to receive. Discord doesn't fire every event at every bot by default; you opt in. This keeps the firehose manageable, and it's also a privacy boundary.

Most intents you just list in your client config. Three of them are **privileged**: Presence, Server Members, and Message Content. Those you have to explicitly enable in the Discord Developer Portal, on your application's Bot page, on top of requesting them in code. And once your bot is in more than 100 servers, using a privileged intent requires going through Discord's verification process.

This trips people up constantly. The bot connects fine, logs in fine, then never responds to messages, because the Message Content intent was never turned on in the portal. There's no error. It just sits there. If your bot goes quiet with no errors at all, this is the very first thing to check.

## Step 0: create the bot in the Developer Portal

Before any code runs, your bot needs an identity and a token. This part lives entirely on Discord's side.

1. Go to the Discord Developer Portal and create a new **application**. Give it your bot's name.
2. Open the **Bot** tab. Your application already has a bot user attached. This is where you'll find (and reset) the **token**, and where you toggle those privileged intents.
3. Copy the token once and put it somewhere safe. If you ever lose it or leak it, reset it here, which invalidates the old one immediately.

Treat that token like a password, because it is one. Anyone who has it can run code *as your bot*, in every server it's in. We'll come back to keeping it out of your source code, but the habit starts now: the token never gets pasted into a file you commit.

### Inviting the bot to a server

A freshly created bot isn't in any server yet. Use the OAuth2 URL generator in the portal:

1. Under **OAuth2 → URL Generator**, tick the `bot` scope, and `applications.commands` too if you plan to use slash commands (you should).
2. Pick the permissions the bot actually needs. Resist the urge to check Administrator "to be safe." A moderation bot needs things like Kick, Ban, Manage Messages, and Read Message History, not the keys to the whole server. Least privilege is a real security control, not a formality.
3. Copy the generated URL, open it, choose your server, and authorize.

Now the bot exists, has a token, and is sitting in a server waiting for a process to bring it to life.

## Step 1: get the bot working locally first

Don't skip ahead to server setup. Get it running on your own machine first, so any problem later is clearly about the server and not the bot.

A simple Node.js bot looks roughly like this:

```
discord-bot/
├── index.js
├── package.json
├── package-lock.json
├── .env
├── .gitignore
└── node_modules/

```

What each piece does:

| File              | Purpose                                        |
| ----------------- | ---------------------------------------------- |
| index.js          | Your bot's actual logic                        |
| package.json      | Project info and dependency list               |
| package-lock.json | Pins exact dependency versions                 |
| .env              | Your token and other secrets, never shared     |
| .gitignore        | Keeps .env and node\_modules out of Git        |
| node\_modules/    | Installed packages (don't upload this by hand) |

One prerequisite: current discord.js (v14) needs **Node 18 or newer**. Check with `node -v`. If you're on something older, fix that now, because it's the exact trap that bites people again on the server later.

### Setting up the project

```bash
mkdir discord-bot
cd discord-bot
npm init -y
npm install discord.js dotenv
```

discord.js does the heavy lifting of talking to Discord. dotenv lets you pull secrets like your token out of a `.env` file instead of pasting them into your code (please don't paste them into your code).

Before you write anything else, create a `.gitignore` so you never commit the two things you must not commit:

```
node_modules/
.env

```

### The bot itself

Create `index.js`:

```js
require("dotenv").config();

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

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

client.once("ready", () => {
    console.log(`${client.user.tag} is online!`);
});

client.login(process.env.TOKEN);
```

Nothing fancy, on purpose. `require("dotenv").config()` pulls in your `.env` values, `new Client()` sets up the connection, `ready` fires once Discord accepts your login, and `client.login()` authenticates. Get this bare version online before you add anything else. A working "hello world" is a much better starting point than a half-built feature you can't get to run.

Create your `.env` alongside it:

```
TOKEN=your_bot_token_here

```

Then run it:

```bash
node index.js
```

You should see your bot's tag printed and the bot show as online in Discord.

### Catch errors before they take the bot down

One habit worth building from the very first version: a single unhandled error should not kill your whole bot. Add a safety net near the top of `index.js`:

```js
process.on("unhandledRejection", (error) => {
    console.error("Unhandled promise rejection:", error);
});
```

Later, when you're handling commands, wrap the risky parts in `try/catch` too. The goal is that one broken command replies with "something went wrong" instead of crashing the process and taking every other server's bot offline with it. PM2 will restart a crashed bot, but a restart still means a few seconds of downtime and a dropped interaction, so it's better not to crash in the first place.

## Where to actually run it

You have a few realistic options.

You could run it on hardware you already own: an old laptop, a Raspberry Pi in a drawer, whatever. It's free and you're in full control. But now you own your own uptime. Home internet drops, power goes out, the Pi's SD card dies, and all of that becomes your bot's problem, on top of the networking and security you now have to handle yourself. It's a fine way to learn. It's a rough way to run something people rely on.

Most people land on a **VPS**, a Virtual Private Server. It's a slice of a much bigger machine in a data center, set up so it feels like your own private Linux box. You SSH in, install what you need, and it stays on. That's the whole selling point: someone else owns the power, the cooling, and the network, and you just rent a reliable place to run a process.

You don't need anything powerful for a Discord bot. Even a cheap, small instance handles most bots without effort. The specs matter less than the fact that the thing doesn't randomly disappear.

### So which VPS?

Don't overthink it. A few names come up for a reason: Hetzner, netcup, DigitalOcean, Vultr, and Linode (now Akamai) all offer small instances aimed at exactly this. Hetzner and netcup in particular are popular in the EU and tend to give you noticeably more RAM per euro, which is handy if your bot is going to grow into a database and a dashboard. DigitalOcean and Vultr have a gentler dashboard and lots of tutorials if you're brand new. Any of them will run a Discord bot for somewhere around $4 to $6 a month, on hardware the bot will never come close to maxing out.

A few things worth paying attention to when you pick one:

- **Region.** Choose something close to you or your bot's main user base. It won't make a huge difference for a text bot, but it's a free win. If your community is European, an EU provider like Hetzner or netcup also keeps you on the right side of data-locality expectations.
- **RAM over CPU.** A small Node process is rarely CPU-bound, but running out of memory crashes the bot outright. 512MB to 1GB is plenty to start; go up if you're adding a database on the same box.
- **Bandwidth caps.** A non-issue for most bots, but glance at it if you plan to add a dashboard or serve images.
- **Snapshots and backups.** Usually a cheap add-on, and worth the extra dollar or two once your bot has data worth losing.

Pick one, spin up the cheapest current Ubuntu LTS instance they offer, and move on. You can resize later if you outgrow it, which takes a few minutes on most providers.

## Step 2: getting the server ready

Once the bot logs in locally, move it somewhere that stays up. I'm using a Linux VPS running Ubuntu, since that's what most bot hosting runs on. It's lightweight, extremely well documented, and gives you full control.

### Connecting over SSH

SSH is how you drive the server from your own terminal. Providers give you an IP and a login. On first connect you'll use a password (or a key you uploaded at creation):

```bash
ssh root@123.123.123.123
```

Once you're in, every command runs on the server, not your machine. Sit with that for a second the first time. It looks and feels almost identical to your normal terminal, but you're driving a completely different computer now.

Passwords over SSH are fine for the first login and bad forever after. Set up a key pair instead. On your **local** machine:

```bash
ssh-keygen -t ed25519 -C "you@example.com"
ssh-copy-id root@123.123.123.123
```

Now you can log in without typing a password, and later we'll turn password login off entirely so nobody can brute-force their way in.

### Make a non-root user first

Providers often drop you in as `root`. Don't stay there. A stray command as root can wipe the whole box, and you don't want your bot running with that much power either. Create a normal user and switch to it:

```bash
adduser bot
usermod -aG sudo bot
```

Log out, copy your SSH key to the new user (`ssh-copy-id bot@your-ip`), and from now on connect as `bot@your-ip`, reaching for `sudo` only when you actually need it.

### Update packages

Before installing anything, update the server. Easy habit, and it pulls in security patches you'd rather have:

```bash
sudo apt update
sudo apt upgrade -y
```

### Set up a basic firewall

A fresh server should only accept traffic on the ports you actually use. On Ubuntu, `ufw` makes this a two-minute job:

```bash
sudo ufw allow OpenSSH
sudo ufw enable
```

That allows SSH and blocks the rest. If you add a dashboard later, you'll open its port (or, better, put it behind a reverse proxy on 80/443). Fewer open doors, fewer things that can go wrong.

### Install Node (don't use `apt install nodejs` here)

This is the one that quietly wrecks people's first deploy. Ubuntu's own repositories ship an old Node, often too old for current discord.js. You install it, everything looks fine, then discord.js throws an unsupported-version error the moment it starts, and you waste an afternoon debugging your code when the real problem is the runtime.

Install a current version instead. A version manager is the friendliest route, and it makes bumping Node later trivial:

```bash
# nvm (touches no system packages)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# reopen your shell, then:
nvm install --lts
```

Prefer a real system package? Use NodeSource, which serves current releases:

```bash
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
```

Either way, confirm it worked:

```bash
node -v
npm -v
```

You should get real version numbers back, and Node should be 18 or higher.

## Step 3: getting your code onto the server

A few ways to do this, and it doesn't matter much which you pick starting out.

**Git** is what most people settle on, because updating later is just a `git pull`. Clone your repo:

```bash
git clone your-repository-url
```

If your repo is **private**, you'll need to authenticate. The clean way is a read-only deploy key: generate an SSH key on the server (`ssh-keygen -t ed25519`), add the public half to your repo's deploy keys on GitHub, and clone over SSH. A personal access token works too, but a deploy key scoped to one repo is tidier and safer.

**SCP** copies files straight over SSH if you don't want a repo yet:

```bash
scp -r discord-bot bot@your-ip:/home/bot/
```

**SFTP** is the drag-and-drop option, something like FileZilla, which is probably the easiest start if the terminal still feels unfamiliar.

### Installing dependencies

```bash
cd discord-bot
npm install
```

This reads your `package.json` and pulls everything listed there, which is exactly why you don't upload `node_modules`. It rebuilds from scratch on the server in a few seconds, matched to the server's own OS and architecture.

### Recreating your `.env`

Your `.env` never goes through Git (it's in your `.gitignore`), so make it directly on the server:

```bash
nano .env
```

```
TOKEN=your_bot_token_here

```

Save it, and your bot has what it needs to log in.

### Test before automating anything

```bash
node index.js
```

You should see it log in, and the bot should show as online in Discord. Kill it with `Ctrl + C` and confirm it goes offline. That's expected, and it sets up the next problem nicely: right now the bot only stays alive as long as this terminal session does.

## Step 4: keeping it alive with PM2

You could, in theory, run `node index.js` and leave your SSH session open. Don't. It falls apart fast:

- Close the SSH session and the process usually dies with it.
- Hit an unhandled error and the bot stays down. Nobody restarts it for you.
- Reboot the server and the bot doesn't come back on its own.

This is the gap PM2 fills. It's a process manager for Node apps. Think of it as something sitting behind your bot, watching it, and restarting it the moment something goes wrong instead of leaving it dead until you notice.

### Installing it

```bash
npm install pm2 -g
pm2 -v
```

### Starting the bot

```bash
pm2 start index.js --name discord-bot
```

Check it's running:

```bash
pm2 status
```

```
┌────┬───────────────┬────────┬────────┐
│ id │ name          │ status │ mode   │
├────┼───────────────┼────────┼────────┤
│ 0  │ discord-bot   │ online │ fork   │
└────┴───────────────┴────────┴────────┘

```

You can close your SSH session now and the bot keeps running in the background. The first time you see that work, it feels a little like magic.

### The commands you'll actually use

| Command                 | What it's for                                           |
| ----------------------- | ------------------------------------------------------- |
| pm2 logs discord-bot    | Live logs, the first place you go when something breaks |
| pm2 restart discord-bot | Restart after an update                                 |
| pm2 stop discord-bot    | Stop it without deleting it                             |
| pm2 delete discord-bot  | Remove it from PM2 completely                           |
| pm2 monit               | Live CPU and memory view per process                    |
| pm2 list                | See everything PM2 is managing                          |

### Use a config file instead of long commands

Starting the bot with a flag or two is fine, but the moment you want memory limits, environment settings, or predictable restarts, put it in an `ecosystem.config.js` at the root of your project:

```js
module.exports = {
  apps: [{
    name: "discord-bot",
    script: "index.js",
    instances: 1,
    exec_mode: "fork",
    max_memory_restart: "300M",
    env: {
      NODE_ENV: "production"
    }
  }]
};
```

Then start it with:

```bash
pm2 start ecosystem.config.js
```

Two things in there matter for bots specifically. Keep `instances` at 1 and `exec_mode` as `fork`. PM2's cluster mode is built for web servers, where load balancing across identical workers makes sense. A Discord bot holds a single gateway connection, and running several copies of it would just log in several times and reply to every command two or three times over. Scaling a bot is Discord's job, through **sharding**, which is a separate mechanism we'll touch on later. And `max_memory_restart` is a cheap safety net: if a leak ever pushes the process past 300MB, PM2 restarts it before the server runs out of memory.

### Make it survive a reboot

PM2 doesn't know to bring your bot back after the server itself restarts. You have to tell it:

```bash
pm2 startup
```

It prints a command specific to your system. Copy that exactly and run it. Then lock in your current setup:

```bash
pm2 save
```

Now the whole chain looks like this:

```
Server reboots → PM2 comes back up → your bot starts automatically → bot's online again

```

This is the point where the bot stops being something you babysit and becomes an always-on service.

### One more thing PM2 won't do for you: rotate logs

Left alone, PM2's log files grow forever. On a small VPS they can quietly fill the disk and take the bot down with them, which is a miserable outage to diagnose because nothing in your code is wrong. Install the log rotation module once and forget about it:

```bash
pm2 install pm2-logrotate
```

## Knowing when your bot is down before your users tell you

PM2 keeps the process alive, but it can't tell you when the bot is *up but broken*, or when the whole server has gone dark. Two cheap layers cover most of this.

The first is an external uptime check. A free service like UptimeRobot can ping a tiny health endpoint on your bot every few minutes and email or message you when it stops answering. To use one, add a minimal HTTP route to your bot (a few lines with `express`) that returns `200 OK` only while the bot is connected. If the ping fails, you find out in minutes instead of the next morning.

The second is a crash notification straight into Discord. Post to a private webhook when the process is about to die:

```js
process.on("uncaughtException", async (error) => {
    console.error(error);
    // fire a webhook to a private channel, then let the process exit
    // so PM2 restarts it cleanly
});
```

You don't need both on day one. But the first time your bot goes down at 2am and you only hear about it at noon, you'll wire one of these up, so you may as well know they exist now.

## Updating the bot without breaking production

Once the bot is stable, you'll start adding things: slash commands, a database, moderation tools. At some point, editing files directly on the server every time starts to feel risky, because it is. One typo and the bot's down until you notice.

A workflow that holds up better:

1. Make changes locally.
2. Test them locally.
3. Push to your repo.
4. Pull on the server and restart.

On your own machine:

```bash
git add .
git commit -m "Added moderation commands"
git push
```

On the server:

```bash
git pull
npm install
pm2 restart discord-bot
```

That's the whole update cycle. It's the same pattern real teams use, at a smaller scale. `npm install` is only strictly needed when you've added or changed a dependency, but running it every time is harmless and saves you the one deploy where you forget.

A small caveat worth knowing: `pm2 reload` exists and does zero-downtime reloads for clustered web apps, but for a single-process bot in fork mode it behaves much like `restart`. There's no truly seamless reload for a single gateway connection, so a quick `restart` is the honest, correct tool here. The blip is a second or two.

## As the bot grows

### Moving to slash commands

If your bot still runs on `!hello`\-style prefix commands, switch to slash commands sooner rather than later. Discord has been steering developers this way for a while, and slash commands bring real perks: autocomplete, built-in argument validation, and users don't have to guess your prefix or remember which three other bots it clashes with.

Registering one looks like this:

```js
const { REST, Routes, SlashCommandBuilder } = require("discord.js");

const commands = [
    new SlashCommandBuilder()
        .setName("ping")
        .setDescription("Replies with pong")
].map(command => command.toJSON());

const rest = new REST().setToken(process.env.TOKEN);

(async () => {
    await rest.put(
        Routes.applicationCommands(process.env.CLIENT_ID),
        { body: commands }
    );
    console.log("Slash commands registered.");
})();
```

Run that once whenever you add or change a command. It just talks to Discord's API, so it doesn't matter whether you run it locally or on the server. Then handle the command with an `interactionCreate` listener:

```js
client.on("interactionCreate", async interaction => {
    if (!interaction.isChatInputCommand()) return;

    if (interaction.commandName === "ping") {
        await interaction.reply("Pong!");
    }
});
```

Small detail that catches people out: **global** slash commands can take up to an hour to appear everywhere after you register them. While you're actively developing, register to a single test server instead with `Routes.applicationGuildCommands`. Those update almost instantly and make iterating far less painful. Register globally only once the command is ready for everyone.

### More packages, as you need them

```bash
npm install mongoose   # MongoDB
npm install express    # web dashboards / APIs / health checks
npm install axios      # calling external APIs
```

Everything lands in `package.json` automatically, so setting the bot up somewhere new is always one `npm install` away.

### Giving it a memory

At some point you'll want the bot to remember things: warnings a mod bot has issued, XP for a leveling bot, open tickets, per-server settings. That's a database's job, not something to bolt onto your bot's logic directly.

SQLite is plenty for smaller bots. It's a single file on disk, needs no separate server process, and it's genuinely enough for a lot of communities. If you're running across many servers, or you want a proper query layer and concurrent access, PostgreSQL or MongoDB hold up better. Whichever you choose, open the connection once when the bot starts, not per command, and handle the case where the database is briefly unreachable so a hiccup doesn't crash the whole bot.

The real point is separation. Keep the bot's behavior in one place and its data in another. When the project grows past a single file, that boundary is what keeps it possible to reason about.

One hosting note: if your database lives on the same VPS, it's now part of your backup story. A bot with no database can be rebuilt from Git in minutes. A bot *with* a database holds the only copy of your community's warnings and levels, so back that file up on a schedule and, once in a while, actually test a restore.

### When one process isn't enough: sharding

Here's a limit worth knowing before you hit it. Once your bot is in roughly 2,500 servers, Discord requires it to **shard**: split the gateway connection into multiple connections, each handling a slice of the servers. discord.js has a `ShardingManager` for this, and it changes how you start and think about the process.

You almost certainly won't need it early, and you shouldn't build for it prematurely. But it's the reason "just run more copies with PM2 cluster mode" is the wrong answer for scaling a bot. Cluster mode would give you duplicate logins; sharding is the sanctioned way to spread load. File it away for the day your bot gets popular.

### Logs you'll actually want later

`console.log("command received")` is fine while you're messing around. Once real people depend on the bot, you want logs that tell you what happened when things went wrong: connection events, command usage, errors with real context, external services timing out. A structured logger like `pino` or `winston` gives you levels (info, warn, error) and timestamps, which turns "why is my bot broken" into "here's exactly what happened right before it broke." That's a much better place to start debugging from, and `pm2 logs` will still show all of it live.

## Downtime happens, and that's fine

Even a well-run bot goes down sometimes. The downtime isn't the failure. The failure is not knowing why, or taking forever to notice. It's usually one of:

- **A bug in your own code.** An unhandled error, something `undefined` that shouldn't be.
- **The server itself.** Maintenance windows, network hiccups, provider issues.
- **Discord's API having a bad day**, which happens to everyone occasionally and isn't yours to fix. During a Discord outage your bot will reconnect on its own once the gateway recovers, so the right move is usually to wait, not to keep restarting.

## When something's broken: a quick troubleshooting pass

Keep a short mental checklist for "the bot isn't working," because most of the time it's one of a small handful of things.

**Bot shows offline, no obvious errors.** Check `pm2 status` first. If it says `errored` or `stopped`, the process crashed and PM2 gave up after too many failed restarts. Run `pm2 logs discord-bot --lines 50` and read from the top of the crash, not the bottom.

**Bot logs in fine but never responds.** Almost always intents. Either you didn't request the right `GatewayIntentBits` in code, or you didn't enable the matching privileged toggle (like Message Content) in the Developer Portal. Both have to match, and it's easy to forget since the bot logs in successfully either way.

**"Cannot find module 'discord.js'" on the server.** You forgot `npm install` after pulling new code, or `node_modules` wasn't rebuilt after an update added a dependency. Cheap fix, easy to miss.

**Works locally, breaks on the server.** Nine times out of ten it's an environment variable that lives in your local `.env` but never made it into the server's copy. Check both files actually match.

**PM2 says online, but the bot isn't responding.** This one's sneaky. The process is alive but stuck, often from an unhandled promise rejection or an infinite loop. `pm2 monit` gives you a live view of CPU and memory per process, which is a good way to spot something spinning that shouldn't be.

**Bot restarts every few minutes.** Check memory with `pm2 monit` or `pm2 status`. If it climbs steadily over time, you're likely looking at a memory leak. A listener added repeatedly without being removed is a classic cause, and PM2 is just restarting the process when it hits the limit you set.

**Commands 429 or feel throttled.** You're hitting REST rate limits, usually from editing or sending too fast in a loop. Batch the work or add a small delay. discord.js queues requests, but it can't invent capacity that Discord won't give you.

Getting comfortable reading `pm2 logs` is most of the battle. The error message is almost always telling you exactly what's wrong. The skill is reading the whole thing instead of skimming for the word "Error" and closing the terminal.

## PM2 isn't the only way: systemd and Docker

PM2 is what I'd recommend for most people because it's simple, Node-specific, and fast to get running. It isn't the only way to keep a process alive on Linux, and two alternatives are worth knowing about.

**systemd** is the init system most modern distros already run, and it can supervise your bot through a service file. You write a small `.service` file describing how to start the bot and telling systemd to restart it if it exits, drop it in `/etc/systemd/system/`, then `systemctl enable` and `systemctl start` it. It works well and it's one less tool to install. The trade-off is more verbose syntax and no built-in log viewer or dashboard like PM2's. If you're already managing a fleet of services with systemd, it's a natural fit.

**Docker** goes the other direction: you package the bot and its exact Node version into a container image, so "works on my machine" and "works on the server" become the same environment. It's excellent once you have a database and a dashboard and want them to come up together with one command (`docker compose up`). For a single bot on a small VPS it's more moving parts than you need on day one, but it's the direction to grow toward if the project gets serious about reproducibility.

For a first bot, reach for PM2\. Graduate to systemd or Docker when you have a concrete reason, not because a blog told you to.

## A few security basics worth not skipping

It's easy to get the bot online and never think about security again. A handful of habits go a long way.

**Lock down SSH.** You already set up keys and a non-root user. Now finish the job: in `/etc/ssh/sshd_config`, set `PasswordAuthentication no` and `PermitRootLogin no`, then reload SSH. Confirm you can still log in with your key *before* you close your current session, so you don't lock yourself out.

**Keep secrets out of your code, always.**

```js
// don't do this
client.login("my-secret-token");

// do this instead
client.login(process.env.TOKEN);
```

And never let a `.env` file end up in a public repo. It happens more than you'd think, and a leaked token means someone else can run your bot until you reset it. If it ever does leak, reset the token in the Developer Portal immediately; that invalidates the old one on the spot.

**Least privilege on the invite.** Give the bot only the Discord permissions it needs. An "Administrator to be safe" bot is one bug or one compromise away from being a disaster in every server it's in.

**Keep the firewall tight.** Open only the ports you use. If you add a dashboard, put it behind a reverse proxy with HTTPS rather than exposing a raw Node port.

**Actually back things up.** Source code lives in Git, but your database and your `.env` do not. Back those up on a schedule, and every so often test that a restore works. A backup you've never restored is just a hope.

## Mistakes worth avoiding (I've made most of these)

- **Running everything as root.** Set up a normal user for the bot. Five minutes now, a headache saved later.
- **Uploading `node_modules`.** It's huge and pointless. Upload `package.json` and `package-lock.json`, run `npm install` on the server, done.
- **Installing Node from Ubuntu's default repo.** You get an old version that current discord.js rejects. Use nvm or NodeSource.
- **Forgetting the `.env` on the server.** Works locally, then silently fails because the token isn't there.
- **Running the bot in PM2 cluster mode.** You'll get duplicate replies. A bot is one process; scaling is sharding's job.
- **Restarting the bot over and over without reading logs.** `pm2 logs discord-bot` usually tells you exactly what's wrong in the first few lines. Read it before you start guessing.
- **Committing the token, once, "just to test."** Reset it and move on. Git remembers everything.

## The setup that actually works, in one place

After all the options and caveats, here's the honest version of what holds up in the real world, the stack I'd set up today without a second thought:

- A cheap VPS from Hetzner, netcup, DigitalOcean, or Vultr, running the current Ubuntu LTS. 1GB of RAM is comfortable.
- A non-root user, SSH keys only, password login off, and `ufw` allowing just SSH.
- Node installed through nvm, not the Ubuntu package, so you're on a version discord.js actually supports and can bump later.
- The code deployed with Git, secrets in a `.env` that never leaves the server, `node_modules` rebuilt on the box with `npm install`.
- PM2 running the bot from an `ecosystem.config.js`, one process in fork mode, `max_memory_restart` set, `pm2 startup` and `pm2 save` done, and `pm2-logrotate` installed.
- Updates as `git pull && npm install && pm2 restart`.
- A database file (SQLite to start) backed up on a schedule, with a restore you've tested at least once.
- One uptime ping so you hear about downtime before your users do.

None of that is exotic. It's cheap, it's boring, and it stays up. Boring is the whole goal. Fancier setups exist, and you'll reach for them when you have a concrete reason, but this is the baseline that runs real bots in real communities without drama.

## Where this eventually goes

Bots have a way of growing past "responds to a few commands." Give it enough time and you end up with a database, a web dashboard, background jobs, maybe a small API of its own. At that point it isn't a script anymore. It's a full application that happens to talk to Discord.

But the fundamentals stay the same the whole way: code, dependencies, config, a place to run, a way to keep it alive, and a way to update it safely. Once those click, they carry over to basically any project you build after this one. The VPS you rented for a Discord bot is the same VPS that can run a website, an API, or a game server. The muscle memory transfers completely.

A Discord bot is a genuinely good way to learn all of it, mostly because the feedback loop is so immediate. You push a change, and thirty seconds later you see it working, or not, in a real server. Not many first projects give you that.

The code is the idea. Getting it hosted properly is what turns it into something people can actually use.