# How to deploy a Telegram bot on a VPS with systemd

> Run a Python Telegram bot 24/7 as a systemd service with a virtualenv, a dedicated user and the token in an env file. Logs, restarts and updates included.

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](https://datapasa.com/en/bot-hosting) page.
- **A bot token** from [@BotFather](https://t.me/BotFather). Send `/newbot`, choose a name and copy the token.
- **SSH access.** See [how to connect via SSH](https://datapasa.com/wiki/server-management/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:

| 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 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. Registering the application, intents and the invite link are covered in [how to host a Discord bot on a VPS](https://datapasa.com/wiki/guides/host-discord-bot-on-vps). 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](https://datapasa.com/en/bot-hosting) page.
