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:
sudo apt install python3-venv git1. 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 mybot2. 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
exitThe 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 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
EOFThe 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:
[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.targetWhat 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 mybotenable 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 mybotUpdating 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 mybotWhen 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-pagerBy 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.
Did this page help?
Thanks for the feedback.