Skip to content

How to deploy a Telegram bot on a VPS with systemd

A bot started with python bot.py in an SSH session stops as soon as you close the terminal, and it does not come back after a crash or a reboot. The reliable way to run a bot on a Linux server is a systemd service: the system starts it on boot, restarts it when it fails and collects its logs. This guide deploys a Python Telegram bot built with aiogram, with its own user, a virtual environment and the token kept out of the code.

What you need

  • A VPS with Ubuntu 24.04. A typical bot that uses long polling needs very little: a few dozen MB of RAM and almost no CPU while idle. We recommend VPS-1 (1 vCPU, 1 GB RAM, 16 GB NVMe) for one or two bots. Take VPS-2 (2 GB RAM) if you run several bots, a database or heavy libraries next to them. Plans are on the bot hosting page.
  • A bot token from @BotFather. Send /newbot, choose a name and copy the token.
  • SSH access. See how to connect via SSH.

With long polling the bot connects out to Telegram, so you do not need a domain, a certificate or open ports.

Step 1. Connect and install Python tools

bash
ssh root@YOUR_SERVER_IP
apt update && apt upgrade -y
apt install -y python3-venv git

Ubuntu 24.04 ships Python 3.12, which is supported by current aiogram and python-telegram-bot releases. The python3-venv package lets you create isolated environments.

Step 2. Create a dedicated user

Running the bot as root means any bug in it has full control of the server. Create a system user without a login shell that owns only the bot folder:

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

Step 3. Put the code on the server

If your bot is in a Git repository, clone it:

bash
sudo -u mybot git clone https://github.com/YOUR_NAME/YOUR_BOT.git /opt/mybot/app

To follow along without a repository, create a minimal aiogram 3 bot based on the example from the aiogram documentation:

bash
sudo -u mybot mkdir -p /opt/mybot/app
sudo -u mybot nano /opt/mybot/app/bot.py
python
import asyncio
import logging
import sys
from os import getenv

from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.types import Message

dp = Dispatcher()


@dp.message(CommandStart())
async def start(message: Message) -> None:
    await message.answer("Hello! I am running on a VPS.")


@dp.message()
async def echo(message: Message) -> None:
    await message.send_copy(chat_id=message.chat.id)


async def main() -> None:
    bot = Bot(token=getenv("BOT_TOKEN"))
    await dp.start_polling(bot)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, stream=sys.stdout)
    asyncio.run(main())

The token is read from the BOT_TOKEN environment variable, never written in the code.

Step 4. Create the virtual environment

bash
sudo -u mybot python3 -m venv /opt/mybot/venv
sudo -u mybot /opt/mybot/venv/bin/pip install aiogram

For a real project install from its requirements file instead:

bash
sudo -u mybot /opt/mybot/venv/bin/pip install -r /opt/mybot/app/requirements.txt

If you use python-telegram-bot, the package is called python-telegram-bot. Everything else in this guide stays the same.

Step 5. Store the token in an env file

bash
nano /etc/mybot.env
bash
BOT_TOKEN=123456789:YOUR_TOKEN_FROM_BOTFATHER

Make the file readable only by root:

bash
chmod 600 /etc/mybot.env

systemd reads the file as root before it starts the process, so the bot receives the variable while the mybot user cannot read the file itself. The token also stays out of Git.

Step 6. Create the systemd service

bash
nano /etc/systemd/system/mybot.service
ini
[Unit]
Description=My Telegram bot
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
User=mybot
Group=mybot
WorkingDirectory=/opt/mybot/app
EnvironmentFile=/etc/mybot.env
ExecStart=/opt/mybot/venv/bin/python bot.py
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target

What the key lines do:

SettingPurpose
User, GroupRun the bot as the unprivileged mybot user.
EnvironmentFileLoad BOT_TOKEN from the protected file.
ExecStartStart the bot with the Python from the virtualenv, so no activation is needed.
Restart=always, RestartSec=5Restart the bot 5 seconds after any exit, including crashes.
StartLimitIntervalSec=0Never give up restarting, useful when Telegram or the network is down for a while.
NoNewPrivileges, PrivateTmp, ProtectSystemBasic hardening: no privilege escalation, a private /tmp, and system folders read only.

Load the unit, enable it on boot and start it:

bash
systemctl daemon-reload
systemctl enable --now mybot
systemctl status mybot

The status should say active (running). Send /start to your bot in Telegram.

Step 7. Read the logs

Everything the bot prints to stdout and stderr goes to the systemd journal:

bash
journalctl -u mybot -f

Useful variations:

bash
journalctl -u mybot -n 100
journalctl -u mybot --since "1 hour ago"
journalctl -u mybot -b

The first follows new lines live, the second shows the last 100 lines, the third filters by time and the fourth shows the log since the last boot.

Output appears late?

Python buffers output when it is not attached to a terminal. If print statements show up in the journal with a delay, add Environment=PYTHONUNBUFFERED=1 to the [Service] section, or use the logging module as in the example.

Update the bot

Pull the new code, update dependencies and restart:

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

If you changed the unit file, run systemctl daemon-reload before the restart.

The same approach for Node.js and Discord bots

systemd does not care what language the bot is written in. For a Node.js bot, for example with discord.js, install Node.js, put the code in /opt/mybot/app, run npm ci as the mybot user and change one line in the unit:

ini
ExecStart=/usr/bin/node index.js

Keep the Discord token in the same kind of env file. Logs, restarts and updates work exactly as described above. To run several bots, create one user, one env file and one unit per bot, for example shopbot.service and supportbot.service.

Summary

A systemd service makes a bot a proper part of the server: it starts on boot, restarts after crashes, keeps its token in a protected file and writes logs you can read with journalctl. The whole setup fits on the smallest plan and runs around the clock. Pick a plan on the bot hosting page.