Guide

Running a Discord bot as a systemd service

If you run your own Linux box, systemd is the right way to keep a bot alive. Not screen, not tmux, not nohup. It starts the bot at boot, restarts it when it dies, and keeps its logs. It takes about fifteen minutes to set up.

7 minute read

This works the same on a VPS, a Raspberry Pi running Raspberry Pi OS, or any Debian or Ubuntu machine. Commands assume you can use sudo.

1. A user for the bot

Do not run the bot as root, or as your own account. If a dependency or your code ever has a hole in it, whoever uses it gets that user's permissions and nothing more.

sudo adduser --system --group --home /opt/mybot mybot

2. The code and a virtual environment

sudo -u mybot -H bash
cd /opt/mybot
git clone https://github.com/you/your-bot.git app    # or copy the files in
python3 -m venv venv
venv/bin/pip install -r app/requirements.txt
exit

The virtual environment keeps the bot's packages separate from the system's Python, so an OS update cannot break them and pip does not fight with apt.

3. The token, in a file only the bot can read

sudo tee /opt/mybot/bot.env > /dev/null <<'EOF'
BOT_TOKEN=paste-your-token-here
EOF
sudo chown mybot:mybot /opt/mybot/bot.env
sudo chmod 600 /opt/mybot/bot.env

No quotes around the value; systemd would include them in the token. Your code reads it the usual way, os.environ["BOT_TOKEN"].

4. The unit file

Create /etc/systemd/system/mybot.service:

[Unit]
Description=My Discord bot
After=network-online.target
Wants=network-online.target

[Service]
User=mybot
Group=mybot
WorkingDirectory=/opt/mybot/app
EnvironmentFile=/opt/mybot/bot.env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/mybot/venv/bin/python bot.py
Restart=always
RestartSec=5

# Optional hardening. Remove a line if your bot needs what it takes away.
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/opt/mybot
PrivateTmp=true

[Install]
WantedBy=multi-user.target

What the less obvious lines are for:

  • After=network-online.target stops the bot starting before the network is up at boot, which otherwise shows up as a connection error and a restart.
  • PYTHONUNBUFFERED=1 matters more than it looks. Under systemd, Python's output is not going to a terminal, so it gets buffered, and your print() lines appear in the log minutes late or not at all before a crash.
  • Restart=always with RestartSec=5 brings it back five seconds after any exit.
  • ProtectSystem=strict makes the whole filesystem read-only for the bot except ReadWritePaths. If your bot writes a database or log file, keep it under /opt/mybot.

5. Start it

sudo systemctl daemon-reload
sudo systemctl enable --now mybot
systemctl status mybot

enable starts it at every boot; --now also starts it straight away. Status should say active (running).

Everyday commands

journalctl -u mybot -f            # follow the log live
journalctl -u mybot --since today # today's log
sudo systemctl restart mybot      # after updating the code
sudo systemctl stop mybot

Updating the bot

sudo -u mybot git -C /opt/mybot/app pull
sudo -u mybot /opt/mybot/venv/bin/pip install -r /opt/mybot/app/requirements.txt
sudo systemctl restart mybot

When it keeps restarting

A bot that crashes the moment it starts gets restarted every five seconds forever, filling the log. Look at the first error, not the last:

journalctl -u mybot -n 50 --no-pager

By default systemd gives up on a unit that fails five times within ten seconds. With RestartSec=5 that limit is never reached, which is what you want for a bot, but it means a broken token loops. If you would rather it stopped, add StartLimitIntervalSec=300 and StartLimitBurst=5 under [Unit].

Things this does not do for you

  • Backups. Copy your database somewhere else on a timer. A systemd timer or cron job running rsync or rclone is enough.
  • Security updates. Turn on unattended-upgrades on Debian and Ubuntu, and keep SSH on keys only.
  • Telling you it died. systemd restarts it quietly. If you want to know, an OnFailure= unit can post to a Discord webhook.

That list is the honest case for a bot host: it is the same job, done by somebody else. If you would rather not run the machine yourself, SnowServers bot hosting handles restarts, logs and daily backups from $3 a month. If you enjoy this part, systemd is a fine way to do it.