Running python bot.py in an SSH session works until you log out. To keep a Python program running 24/7 — a Telegram or Discord bot, a scraper, a worker, a Flask/FastAPI app — create a systemd service. It starts at boot, restarts on crashes and logs everything to the journal.

1. User and virtual environment

sudo useradd --system --create-home --home-dir /srv/bot --shell /usr/sbin/nologin bot
sudo -u bot git clone https://github.com/you/bot.git /srv/bot/app
sudo -u bot python3 -m venv /srv/bot/venv
sudo -u bot /srv/bot/venv/bin/pip install -r /srv/bot/app/requirements.txt

See Python virtual environments.

2. Secrets in an environment file

/etc/bot/env (mode 640, group bot):

BOT_TOKEN=123456:ABC…
DATABASE_URL=sqlite:////srv/bot/data/bot.db
sudo mkdir -p /etc/bot && sudo chown root:bot /etc/bot/env && sudo chmod 640 /etc/bot/env

Read them in Python with os.environ["BOT_TOKEN"].

3. The unit file

/etc/systemd/system/bot.service:

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

[Service]
Type=simple
User=bot
Group=bot
WorkingDirectory=/srv/bot/app
EnvironmentFile=/etc/bot/env
Environment=PYTHONUNBUFFERED=1
ExecStart=/srv/bot/venv/bin/python -m bot
Restart=always
RestartSec=5

NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/srv/bot/data

[Install]
WantedBy=multi-user.target

The two details that trip people up:

  • Use the venv’s Python by absolute path (/srv/bot/venv/bin/python). Activating a venv in a unit file isn’t how it works; the interpreter path is enough.
  • PYTHONUNBUFFERED=1 — otherwise print() output is buffered and appears in the logs late or not at all.

For a web app, ExecStart points to Gunicorn or Uvicorn in the venv instead — see Flask with Gunicorn and FastAPI with Uvicorn.

4. Start and check

sudo mkdir -p /srv/bot/data && sudo chown bot: /srv/bot/data
sudo systemctl daemon-reload
sudo systemctl enable --now bot
systemctl status bot --no-pager
journalctl -u bot -f

5. Restart behaviour

  • Restart=always — restart whenever the process exits (crash or not). For a bot that should never stop, that’s right.
  • Restart=on-failure — only on non-zero exit codes or signals.
  • If a program crashes in a tight loop, systemd stops trying after 5 starts in 10 seconds (StartLimitBurst/StartLimitIntervalSec). Raise them in [Unit] if needed, and fix the crash. sudo systemctl reset-failed bot clears the state.

6. Graceful shutdown

systemd sends SIGTERM, waits (TimeoutStopSec, default 90 s), then sends SIGKILL. Handle SIGTERM to close connections and save state:

import signal, sys

def shutdown(signum, frame):
    # close DB connections, finish current task…
    sys.exit(0)

signal.signal(signal.SIGTERM, shutdown)

Async frameworks (aiogram, python-telegram-bot, discord.py) usually handle this for you.

7. Scheduled scripts: use a timer

For a script that runs periodically (every 15 minutes, nightly) rather than continuously, pair a Type=oneshot service with a timer instead of cron:

# /etc/systemd/system/report.service
[Service]
Type=oneshot
User=bot
EnvironmentFile=/etc/bot/env
ExecStart=/srv/bot/venv/bin/python /srv/bot/app/report.py

# /etc/systemd/system/report.timer
[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true

[Install]
WantedBy=timers.target

sudo systemctl enable --now report.timer. Persistent=true runs a missed job after downtime. See systemd timers.

8. Updating

cd /srv/bot/app && sudo -u bot git pull --ff-only
sudo -u bot /srv/bot/venv/bin/pip install -r requirements.txt
sudo systemctl restart bot

Troubleshooting

  • status=203/EXEC — ExecStart path wrong or not executable.
  • ModuleNotFoundError — not using the venv’s Python, or wrong WorkingDirectory.
  • PermissionError — the service user can’t write somewhere; check ownership and ReadWritePaths.
  • No log output — missing PYTHONUNBUFFERED=1.

Related: Telegram bot on a VPS, systemd unit files and Supervisor. Bots and scripts run happily on the smallest VPS plans.

  • Python
  • systemd
  • Linux