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 min readBeginnerUpdated

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.

You need Python's virtual environment support and Git on the machine first:

Terminal
sudo apt install python3-venv git

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.

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

2. The code and a virtual environment

Terminal
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

Terminal
sudo install -m 600 -o mybot -g mybot /dev/null /opt/mybot/bot.env
sudo tee /opt/mybot/bot.env > /dev/null <<'EOF'
BOT_TOKEN=paste-your-token-here
EOF

The first line makes the file empty, owned by the bot's user and readable only by it, before the token is written into it, so the token is never in a file anyone else can read.

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:

Config
[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

Terminal
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

Terminal
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

Terminal
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:

Terminal
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.

Press / or Ctrl K on any page to search.