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.targetstops the bot starting before the network is up at boot, which otherwise shows up as a connection error and a restart.PYTHONUNBUFFERED=1matters more than it looks. Under systemd, Python's output is not going to a terminal, so it gets buffered, and yourprint()lines appear in the log minutes late or not at all before a crash.Restart=alwayswithRestartSec=5brings it back five seconds after any exit.ProtectSystem=strictmakes the whole filesystem read-only for the bot exceptReadWritePaths. 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
rsyncorrcloneis enough. - Security updates. Turn on
unattended-upgradeson 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.