Running a VPS and want to get notified the moment your disk fills up, or need to restart a service without SSH-ing into the server? A Telegram Bot is exactly what I’ve been using to solve that problem — lightweight, fast, and no complex web UI needed.
I use Python as my automation tool for most daily tasks, from deploy scripts to monitoring alerts. The Telegram Bot has become my “control panel” — type a command from my phone and the server responds instantly, no laptop needed.
How Does a Telegram Bot Work?
Every Telegram Bot is a special account controlled via an HTTP API. When a user sends a message to your bot, Telegram stores it on their servers. Your bot retrieves messages in one of two ways:
- Long polling: The bot continuously asks the API “any new messages?” — simple, no domain required, great for testing and small VPS setups.
- Webhook: Telegram actively pushes messages to a URL you register — requires HTTPS and a real domain, ideal for high-traffic production environments.
The python-telegram-bot library wraps the entire HTTP API, letting you work in pure Python instead of making raw requests calls. Note: from version 20+, the library is fully asyncio-based — the code looks quite different from v13 that many older tutorials still use.
Setup: Create a Bot and Get Your Token
Before writing any code, you need to create a bot through BotFather — Telegram’s official bot for managing bots.
- Open Telegram and search for
@BotFather - Send the
/newbotcommand - Set a display name (e.g.,
My Server Monitor) - Set a username — must end with
bot(e.g.,myserver_monitor_bot) - BotFather will return a token in the format
1234567890:ABCdef...— keep this private
You’ll also need your Chat ID so the bot knows who to send notifications to. The quickest way: send any message to your bot, then visit this URL in your browser:
https://api.telegram.org/bot<TOKEN>/getUpdates
Find the message.chat.id field in the returned JSON — that’s your Chat ID.
Installation and Project Structure
pip install python-telegram-bot==21.0.1
Create a clean directory structure:
server-bot/
├── bot.py # Main logic
├── config.py # Token and Chat ID
└── notify.py # Send alerts from other scripts
File config.py:
TOKEN = "1234567890:ABCdefGHIjklMNOpqrSTUvwxYZ"
ALLOWED_CHAT_ID = 987654321 # Only allow a single user
Hands-On: Building the Bot Step by Step
Step 1 — Basic Bot with /start Command
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
import config
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"Bot is running. Available commands:\n"
"/status — Check server uptime\n"
"/disk — Check disk usage\n"
"/restart <service> — Restart a service"
)
def main():
app = Application.builder().token(config.TOKEN).build()
app.add_handler(CommandHandler("start", start))
print("Bot is running...")
app.run_polling()
if __name__ == "__main__":
main()
Step 2 — Security: Block Unauthorized Users
The bot runs publicly — anyone who knows the username can send it messages. Add a decorator to check the Chat ID before processing any command:
from functools import wraps
import config
def restricted(func):
@wraps(func)
async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
if update.effective_chat.id != config.ALLOWED_CHAT_ID:
await update.message.reply_text("Access denied.")
return
return await func(update, context, *args, **kwargs)
return wrapped
Apply this decorator to every sensitive command:
@restricted
async def disk_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
...
Step 3 — Server Inspection Commands
import subprocess
import shutil
@restricted
async def disk_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
total, used, free = shutil.disk_usage("/")
gb = 1024 ** 3
msg = (
f"Disk Usage:\n"
f"Total: {total / gb:.1f} GB\n"
f"Used: {used / gb:.1f} GB ({used/total*100:.1f}%)\n"
f"Free: {free / gb:.1f} GB"
)
await update.message.reply_text(msg)
@restricted
async def system_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
uptime = subprocess.run(["uptime", "-p"], capture_output=True, text=True)
load = subprocess.run(["cat", "/proc/loadavg"], capture_output=True, text=True)
msg = f"Uptime: {uptime.stdout.strip()}\nLoad: {load.stdout.strip()}"
await update.message.reply_text(msg)
Step 4 — Restart Service Command with Confirmation
Restarting a service can cause downtime — add a confirmation step via an Inline Keyboard before executing:
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import CallbackQueryHandler
@restricted
async def restart_service(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not context.args:
await update.message.reply_text("Usage: /restart <service_name>")
return
service = context.args[0]
keyboard = [[
InlineKeyboardButton("Confirm", callback_data=f"restart:{service}"),
InlineKeyboardButton("Cancel", callback_data="cancel"),
]]
await update.message.reply_text(
f"Are you sure you want to restart `{service}`?",
reply_markup=InlineKeyboardMarkup(keyboard),
parse_mode="Markdown"
)
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
if query.data == "cancel":
await query.edit_message_text("Cancelled.")
return
_, service = query.data.split(":", 1)
result = subprocess.run(
["sudo", "systemctl", "restart", service],
capture_output=True, text=True
)
if result.returncode == 0:
await query.edit_message_text(f"Successfully restarted `{service}`.")
else:
await query.edit_message_text(f"Error: {result.stderr}")
Register the handlers in main():
app.add_handler(CommandHandler("disk", disk_status))
app.add_handler(CommandHandler("status", system_status))
app.add_handler(CommandHandler("restart", restart_service))
app.add_handler(CallbackQueryHandler(button_handler))
Step 5 — Sending Proactive Alerts from a Cron Job
This is the part I use most often: a monitoring script sends Telegram notifications without the bot running — just call the Telegram HTTP API directly.
# notify.py
import requests
import config
def send_alert(message: str) -> bool:
url = f"https://api.telegram.org/bot{config.TOKEN}/sendMessage"
payload = {
"chat_id": config.ALLOWED_CHAT_ID,
"text": message,
"parse_mode": "HTML"
}
resp = requests.post(url, json=payload, timeout=10)
return resp.ok
Use it in a cron job to monitor disk space — add to /etc/cron.hourly/:
#!/usr/bin/env python3
import shutil, sys
sys.path.insert(0, "/home/ubuntu/server-bot")
from notify import send_alert
_, _, free = shutil.disk_usage("/")
gb = 1024 ** 3
if free / gb < 5:
send_alert(f"<b>Disk Warning</b>: Only {free/gb:.1f} GB free remaining!")
Running the Bot 24/7 with systemd
Create a service file so the bot automatically restarts on crash or VPS reboot:
# /etc/systemd/system/telegram-bot.service
[Unit]
Description=Telegram Server Bot
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/server-bot
ExecStart=/home/ubuntu/server-bot/venv/bin/python bot.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl enable telegram-bot
sudo systemctl start telegram-bot
sudo systemctl status telegram-bot
Conclusion
From a simple bot, you now have the foundation to build a full “remote control” for your server — receive alerts when something goes wrong, check system status, restart services, all from your phone via Telegram without opening a laptop.
I’m currently using a similar setup to monitor 3 VPS instances simultaneously. Whenever something goes wrong, Telegram alerts me immediately instead of me having to watch a dashboard. With python-telegram-bot v20+, adding inline keyboards or multi-step ConversationHandlers isn’t much more complex than what you’ve just built.
Next steps to try: integrate psutil to fetch real-time CPU and RAM metrics, or use ConversationHandler to build multi-step conversation flows when you need more complex user input.

