Appearance
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 gitUbuntu 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 mybotStep 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/appTo 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.pypython
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 aiogramFor a real project install from its requirements file instead:
bash
sudo -u mybot /opt/mybot/venv/bin/pip install -r /opt/mybot/app/requirements.txtIf 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.envbash
BOT_TOKEN=123456789:YOUR_TOKEN_FROM_BOTFATHERMake the file readable only by root:
bash
chmod 600 /etc/mybot.envsystemd 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.serviceini
[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.targetWhat the key lines do:
| Setting | Purpose |
|---|---|
User, Group | Run the bot as the unprivileged mybot user. |
EnvironmentFile | Load BOT_TOKEN from the protected file. |
ExecStart | Start the bot with the Python from the virtualenv, so no activation is needed. |
Restart=always, RestartSec=5 | Restart the bot 5 seconds after any exit, including crashes. |
StartLimitIntervalSec=0 | Never give up restarting, useful when Telegram or the network is down for a while. |
NoNewPrivileges, PrivateTmp, ProtectSystem | Basic 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 mybotThe 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 -fUseful variations:
bash
journalctl -u mybot -n 100
journalctl -u mybot --since "1 hour ago"
journalctl -u mybot -bThe 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 mybotIf 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.jsKeep 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.