--- title: "How to Deploy a Python Telegram Bot on Ubuntu 24.04" description: "Create a Telegram bot with BotFather, write it in Python with python-telegram-bot and run it on an Ubuntu 24.04 VPS as a hardened systemd service with logs." category: "Use Cases" language: en source: https://cubepath.com/docs/use-cases/telegram-bot-on-vps-server updated: 2026-09-25 --- # How to Deploy a Python Telegram Bot on Ubuntu 24.04 Telegram bots are programs that talk to users through the Telegram Bot API. Running one on your own server keeps it online around the clock, independent of your laptop. In this tutorial you will create a bot with BotFather, write it in Python using the `python-telegram-bot` library, and run it on Ubuntu 24.04 as a systemd service under its own unprivileged user, so it starts on boot, restarts after crashes and logs to the journal. The bot uses **long polling**: it asks Telegram for new messages over an outgoing HTTPS connection. You do not need a domain, a TLS certificate or any open inbound port. ## Prerequisites To follow this tutorial you need: - A server running Ubuntu 24.04 LTS, for example a CubePath VPS. 512 MB of RAM is enough for a small bot. - A non-root user with `sudo` privileges. - A Telegram account and the Telegram app on your phone or desktop. - Basic familiarity with Python. ## Step 1 - Creating the bot with BotFather Every bot is registered through Telegram's official **@BotFather** account, which gives you the token your code uses to authenticate. 1. In Telegram, open a chat with `@BotFather` (check for the blue verification mark). 2. Send `/newbot`. 3. Enter a display name, for example `My Server Bot`. 4. Enter a username. It must be unique and end in `bot`, for example `my_server_example_bot`. BotFather replies with a token that looks like this: ```text 123456789:AAH4k1Xy-your_bot_token_here ``` > Important: The token gives full control of your bot. Do not commit it to Git or paste it in public places. If it leaks, send `/revoke` to BotFather to generate a new one. ## Step 2 - Preparing the server Install Python's virtual environment support. Ubuntu 24.04 ships Python 3.12 but does not let you install packages into the system Python with `pip`, so the bot gets its own virtual environment: ```bash sudo apt update sudo apt install python3-venv ``` Create a system user for the bot. It has no login shell and no password, so a bug in the bot cannot be used to log in to the server: ```bash sudo useradd --system --home-dir /opt/telegram-bot --shell /usr/sbin/nologin telegrambot ``` Create the application directory: ```bash sudo mkdir -p /opt/telegram-bot sudo chown telegrambot:telegrambot /opt/telegram-bot ``` Before writing any code, check that the server can reach the Bot API and that the token is valid. Replace **`your_bot_token`** with the token from BotFather: ```bash curl -s https://api.telegram.org/botyour_bot_token/getMe ``` ```output {"ok":true,"result":{"id":123456789,"is_bot":true,"first_name":"My Server Bot","username":"my_server_example_bot",...}} ``` ## Step 3 - Installing python-telegram-bot Create the virtual environment as the bot user: ```bash sudo -u telegrambot python3 -m venv /opt/telegram-bot/venv ``` List the dependencies in a `requirements.txt` file. Pinning the major version avoids surprises when a new release changes the API: ```bash sudo -u telegrambot nano /opt/telegram-bot/requirements.txt ``` ```text python-telegram-bot>=22,<23 ``` Install them: ```bash sudo -u telegrambot /opt/telegram-bot/venv/bin/pip install -r /opt/telegram-bot/requirements.txt ``` Verify the installed version: ```bash sudo -u telegrambot /opt/telegram-bot/venv/bin/python -c "import telegram; print(telegram.__version__)" ``` ```output 22.x ``` ## Step 4 - Storing the token in an environment file Keep the token out of the source code in an environment file that only the bot user can read: ```bash sudo -u telegrambot nano /opt/telegram-bot/.env ``` ```ini TELEGRAM_BOT_TOKEN=your_bot_token ``` ```bash sudo chmod 600 /opt/telegram-bot/.env ``` systemd will load this file into the bot's environment in Step 6. ## Step 5 - Writing the bot The example bot answers `/start` and `/help`, echoes any other text message back, and logs errors instead of crashing. Create the file: ```bash sudo -u telegrambot nano /opt/telegram-bot/bot.py ``` ```python import logging import os from telegram import Update from telegram.ext import ( Application, CommandHandler, ContextTypes, MessageHandler, filters, ) logging.basicConfig( format="%(levelname)s %(name)s: %(message)s", level=logging.INFO, ) # The HTTP client logs every request at INFO level, which is too noisy logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: user = update.effective_user await update.message.reply_text( f"Hi {user.first_name}! Send me any text and I will repeat it." ) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: await update.message.reply_text("Commands:\n/start - greeting\n/help - this message") async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: await update.message.reply_text(update.message.text) async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None: logger.error("Error while handling an update", exc_info=context.error) def main() -> None: token = os.environ["TELEGRAM_BOT_TOKEN"] application = Application.builder().token(token).build() application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("help", help_command)) application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo)) application.add_error_handler(error_handler) logger.info("Bot started, polling for updates") application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() ``` How it works: - `Application.builder().token(token).build()` creates the bot client. The token comes from the environment, never from the code. - Each `CommandHandler` maps a command such as `/start` to an `async` function. - The `MessageHandler` with `filters.TEXT & ~filters.COMMAND` catches plain text messages that are not commands. - `run_polling()` keeps a long-polling connection open to Telegram and stops cleanly on `SIGTERM`, which is what systemd sends when you stop the service. - The log format has no timestamp because the systemd journal adds one. Run the bot once in the foreground to test it. The command loads the `.env` file into a shell running as the bot user: ```bash sudo -u telegrambot bash -c 'set -a; . /opt/telegram-bot/.env; set +a; exec /opt/telegram-bot/venv/bin/python /opt/telegram-bot/bot.py' ``` ```output INFO __main__: Bot started, polling for updates INFO telegram.ext.Application: Application started ``` Open your bot in Telegram (search for its username), send `/start` and then any message. The bot should greet you and echo the text. Press `Ctrl+C` in the terminal to stop it. ## Step 6 - Running the bot as a systemd service A systemd unit keeps the bot running after you log out, starts it at boot and restarts it if it crashes. Create the unit file: ```bash sudo nano /etc/systemd/system/telegram-bot.service ``` ```ini [Unit] Description=Telegram bot Wants=network-online.target After=network-online.target [Service] Type=simple User=telegrambot Group=telegrambot WorkingDirectory=/opt/telegram-bot EnvironmentFile=/opt/telegram-bot/.env Environment=PYTHONUNBUFFERED=1 ExecStart=/opt/telegram-bot/venv/bin/python /opt/telegram-bot/bot.py Restart=on-failure RestartSec=5 # Hardening NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/opt/telegram-bot [Install] WantedBy=multi-user.target ``` The key settings are: - `EnvironmentFile` loads `TELEGRAM_BOT_TOKEN` from the `.env` file. - `Restart=on-failure` with `RestartSec=5` restarts the bot five seconds after a crash, for example when Telegram is briefly unreachable. - `ProtectSystem=strict` mounts the whole file system read-only for the bot, except `/opt/telegram-bot` listed in `ReadWritePaths`, where the bot can store data files such as a SQLite database. `ProtectHome=true` hides users' home directories. Load the new unit and start it, enabling it at boot: ```bash sudo systemctl daemon-reload sudo systemctl enable --now telegram-bot ``` Check its status: ```bash sudo systemctl status telegram-bot --no-pager ``` ```output ● telegram-bot.service - Telegram bot Loaded: loaded (/etc/systemd/system/telegram-bot.service; enabled; preset: enabled) Active: active (running) since Fri 2026-09-25 10:00:00 UTC; 5s ago Main PID: 2345 (python) ``` Send the bot another message in Telegram to confirm it still answers. ## Step 7 - Reading logs and updating the bot Everything the bot logs goes to the systemd journal. Follow it live: ```bash sudo journalctl -u telegram-bot -f ``` Show only errors from the last day: ```bash sudo journalctl -u telegram-bot -p err --since "24 hours ago" ``` When you change `bot.py`, restart the service to load the new code: ```bash sudo systemctl restart telegram-bot ``` To update the library within the pinned major version: ```bash sudo -u telegrambot /opt/telegram-bot/venv/bin/pip install --upgrade -r /opt/telegram-bot/requirements.txt sudo systemctl restart telegram-bot ``` Test that it restarts correctly after a crash by killing the process; systemd brings it back after five seconds: ```bash sudo systemctl kill --signal=SIGKILL telegram-bot sleep 6 systemctl is-active telegram-bot ``` ```output active ``` For real projects, keep `bot.py` and `requirements.txt` in a Git repository and deploy with `git pull` into `/opt/telegram-bot`. Add `.env` and `venv/` to `.gitignore`. ## Troubleshooting **`telegram.error.Conflict: terminated by other getUpdates request`.** Two copies of the bot are polling with the same token, for example the service and a test run in your terminal, or the bot on your laptop. Stop all other copies; only one process can poll per token. **`telegram.error.InvalidToken` or `Unauthorized`.** The token in `.env` is wrong or was revoked. Test it with the `getMe` request from Step 2, fix the file and run `sudo systemctl restart telegram-bot`. **The service fails with `KeyError: 'TELEGRAM_BOT_TOKEN'`.** systemd could not load the variable. Check that the line in `.env` is `TELEGRAM_BOT_TOKEN=...` with no spaces or quotes around `=`, and that the `EnvironmentFile` path in the unit is correct. **The bot works in private chats but ignores group messages.** By default bots in groups only receive commands and replies to them. Disable privacy mode for the bot with `/setprivacy` in BotFather, then remove the bot from the group and add it again. **`PermissionError` when the bot writes a file.** With `ProtectSystem=strict`, the bot can only write inside `/opt/telegram-bot` (and `/tmp`). Store data there or add the path to `ReadWritePaths`. ## Conclusion Your Telegram bot now runs on your Ubuntu 24.04 server as an unprivileged, sandboxed systemd service that survives crashes and reboots, with its logs in the journal. From here you can store user data in SQLite inside `/opt/telegram-bot`, schedule periodic messages with the library's `JobQueue` (install the `python-telegram-bot[job-queue]` extra), or switch from long polling to webhooks behind Nginx if the bot has to handle high message volumes.