Skip to content
CitexNode
All guides

Python, venv and services

Python 3 with a virtual environment and pip, and a bot or script as a systemd service.

4 min read

Applies to Debian 12/13, Ubuntu 22.04/24.04, AlmaLinux 9 and Rocky Linux 9. In the end your Python program runs in its own environment as a service, with logs in the journal.

1. Install Python

Python 3 is already on every system. Usually only venv and pip are missing.

Debian and Ubuntu:

bash
apt install -y python3 python3-venv python3-pip

AlmaLinux and Rocky Linux:

bash
dnf install -y python3 python3-pip

Which version you have:

Systempython3
Debian 123.11
Debian 133.13
Ubuntu 22.043.10
Ubuntu 24.043.12
AlmaLinux/Rocky 93.9

If your program needs a newer version on AlmaLinux or Rocky, install it alongside. python3 stays 3.9 because the system relies on it:

bash
dnf install -y python3.12 python3.12-pip

Then use python3.12 instead of python3 everywhere below.

bash
python3 --version

2. User and folder

bash
useradd --system --create-home --home-dir /opt/pybot --shell /usr/sbin/nologin pybot

Put your code in /opt/pybot/app, via Git or SFTP, and give it to the user:

bash
chown -R pybot:pybot /opt/pybot

3. Virtual environment and pip

A virtual environment (venv) is a folder with its own Python and packages just for this program. Programs don't get in each other's way, and you don't need root for pip.

bash
sudo -u pybot python3 -m venv /opt/pybot/venv
sudo -u pybot /opt/pybot/venv/bin/pip install --upgrade pip
sudo -u pybot /opt/pybot/venv/bin/pip install -r /opt/pybot/app/requirements.txt

Without a requirements.txt, install packages one by one, e.g. …/pip install discord.py.

To work by hand, activate the environment:

bash
source /opt/pybot/venv/bin/activate
python --version
deactivate

4. Secrets in an env file

Tokens and passwords don't belong in your code:

bash
install -d -m 0750 /etc/pybot
nano /etc/pybot/pybot.env
ini
DISCORD_TOKEN=your-token
bash
chmod 600 /etc/pybot/pybot.env

In your code:

python
import os
token = os.environ["DISCORD_TOKEN"]

5. systemd service

bash
nano /etc/systemd/system/pybot.service
ini
[Unit]
Description=Python bot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=pybot
WorkingDirectory=/opt/pybot/app
EnvironmentFile=/etc/pybot/pybot.env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/pybot/venv/bin/python main.py
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true

[Install]
WantedBy=multi-user.target

Replace main.py with your entry file. PYTHONUNBUFFERED=1 makes print() output reach the journal right away instead of later.

bash
systemctl daemon-reload
systemctl enable --now pybot

Run a script on a schedule

For a script that doesn't run permanently but, say, hourly, use a timer instead of Restart. In the service, set Type=oneshot and drop the Restart lines and the [Install] section, then:

bash
nano /etc/systemd/system/pybot.timer
ini
[Unit]
Description=Python script hourly

[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target
bash
systemctl daemon-reload
systemctl enable --now pybot.timer
systemctl list-timers pybot.timer

Check that it runs

bash
systemctl status pybot
journalctl -u pybot -f

active (running) and your output in the journal. Ctrl+C only stops the display.

Updating

bash
cd /opt/pybot/app
sudo -u pybot git pull
sudo -u pybot /opt/pybot/venv/bin/pip install -r requirements.txt
systemctl restart pybot

After upgrading the distribution to a new Python version (e.g. Debian 12 to 13), recreate the venv: delete the folder and repeat step 3.

Common errors

MessageCause and fix
error: externally-managed-environmentpip install outside a venv. The system protects its own packages. Use the venv from step 3.
The virtual environment was not created successfully because ensurepip is not availablepython3-venv is missing (Debian/Ubuntu): apt install python3-venv.
ModuleNotFoundError in the service, works by handThe service uses a different Python. ExecStart must point to /opt/pybot/venv/bin/python.
status=203/EXECWrong path in ExecStart or the venv is missing.
KeyError: 'DISCORD_TOKEN'The variable is missing from the env file or EnvironmentFile is misspelt.
No output in the journalPYTHONUNBUFFERED=1 is missing.

What next