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
sudoprivileges. - 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.
- In Telegram, open a chat with
@BotFather(check for the blue verification mark). - Send
/newbot. - Enter a display name, for example
My Server Bot. - Enter a username. It must be unique and end in
bot, for examplemy_server_example_bot.
BotFather replies with a token that looks like this:
123456789:AAH4k1Xy-your_bot_token_here
ImportantThe token gives full control of your bot. Do not commit it to Git or paste it in public places. If it leaks, send
/revoketo 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:
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:
sudo useradd --system --home-dir /opt/telegram-bot --shell /usr/sbin/nologin telegrambot
Create the application directory:
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:
curl -s https://api.telegram.org/botyour_bot_token/getMe
{"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:
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:
sudo -u telegrambot nano /opt/telegram-bot/requirements.txt
python-telegram-bot>=22,<23
Install them:
sudo -u telegrambot /opt/telegram-bot/venv/bin/pip install -r /opt/telegram-bot/requirements.txt
Verify the installed version:
sudo -u telegrambot /opt/telegram-bot/venv/bin/python -c "import telegram; print(telegram.__version__)"
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:
sudo -u telegrambot nano /opt/telegram-bot/.env
TELEGRAM_BOT_TOKEN=your_bot_token
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:
sudo -u telegrambot nano /opt/telegram-bot/bot.py
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
CommandHandlermaps a command such as/startto anasyncfunction. - The
MessageHandlerwithfilters.TEXT & ~filters.COMMANDcatches plain text messages that are not commands. run_polling()keeps a long-polling connection open to Telegram and stops cleanly onSIGTERM, 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:
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'
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:
sudo nano /etc/systemd/system/telegram-bot.service
[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:
EnvironmentFileloadsTELEGRAM_BOT_TOKENfrom the.envfile.Restart=on-failurewithRestartSec=5restarts the bot five seconds after a crash, for example when Telegram is briefly unreachable.ProtectSystem=strictmounts the whole file system read-only for the bot, except/opt/telegram-botlisted inReadWritePaths, where the bot can store data files such as a SQLite database.ProtectHome=truehides users' home directories.
Load the new unit and start it, enabling it at boot:
sudo systemctl daemon-reload
sudo systemctl enable --now telegram-bot
Check its status:
sudo systemctl status telegram-bot --no-pager
● 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:
sudo journalctl -u telegram-bot -f
Show only errors from the last day:
sudo journalctl -u telegram-bot -p err --since "24 hours ago"
When you change bot.py, restart the service to load the new code:
sudo systemctl restart telegram-bot
To update the library within the pinned major version:
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:
sudo systemctl kill --signal=SIGKILL telegram-bot
sleep 6
systemctl is-active telegram-bot
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.
