Telegram bots are one of the most versatile automation tools available. They can send notifications, respond to commands, process files, integrate with APIs, forward messages between channels, act as a personal assistant, or serve as an interface to your self-hosted services.
The key to a production-quality bot is hosting it on a server rather than running it on your laptop. A server-hosted bot is always online, responds instantly, and you can add logging, monitoring, and multiple bots to the same instance.
This guide covers building and deploying a Telegram bot with Python (using the python-telegram-bot library) on a cloud server with webhooks — the production approach that's more efficient than polling.
I host my Telegram bots on Tencent Cloud Lighthouse. The entry-level plan handles several bots simultaneously — they're lightweight processes. Two things Lighthouse provides that are specifically important for Telegram bots: a static public IP (Telegram's webhook system needs a consistent HTTPS endpoint to deliver messages to), and the OrcaTerm terminal for checking bot logs and restarting processes from any browser when something needs attention. The server runs continuously, so bots respond to messages even when your laptop is off.
- Key Takeaways
| Approach | How It Works | Best For |
|---|---|---|
| Polling | Bot repeatedly asks Telegram "any new messages?" | Development, testing |
| Webhooks | Telegram pushes updates to your server URL | Production deployment |
For a server deployment, webhooks are the right choice: Telegram sends updates to your HTTPS endpoint instantly, no polling loop consuming resources.
| Requirement | Details |
|---|---|
| Server | Ubuntu 22.04, 1 GB+ RAM |
| Domain | Required for HTTPS webhook |
| Telegram account | To create the bot |
| Python | 3.10+ |
/newbotmy_automation_bot (must end in bot)7123456789:AAHxxxx...Save this token. You'll use it as BOT_TOKEN in your configuration.
/setdescription - Set what the bot does
/setcommands - Define command menu (shown as / shortcuts)
/setprivacy - Control what messages the bot sees in groups
For a private bot: /setjoingroups → Disable to prevent others adding your bot to groups.
sudo apt update
sudo apt install -y python3 python3-pip python3-venv
mkdir -p /opt/telegram-bot
cd /opt/telegram-bot
python3 -m venv venv
source venv/bin/activate
pip install python-telegram-bot[webhooks] aiohttp python-dotenv
nano /opt/telegram-bot/.env
BOT_TOKEN=7123456789:AAHxxxx...
WEBHOOK_URL=https://bot.yourdomain.com/webhook
PORT=8443
ALLOWED_USER_IDS=123456789,987654321
Get your user ID by messaging @userinfobot on Telegram.
Create /opt/telegram-bot/bot.py:
#!/usr/bin/env python3
"""
A feature-rich Telegram bot with webhook support.
"""
import os
import logging
import asyncio
from datetime import datetime
from dotenv import load_dotenv
from telegram import Update, BotCommand
from telegram.ext import (
Application, CommandHandler, MessageHandler,
filters, ContextTypes
)
load_dotenv()
# Configuration
BOT_TOKEN = os.getenv("BOT_TOKEN")
WEBHOOK_URL = os.getenv("WEBHOOK_URL")
PORT = int(os.getenv("PORT", 8443))
# Allowed users (empty list = allow everyone)
ALLOWED_USERS_RAW = os.getenv("ALLOWED_USER_IDS", "")
ALLOWED_USERS = [int(uid) for uid in ALLOWED_USERS_RAW.split(",") if uid.strip()]
# Logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO
)
logger = logging.getLogger(__name__)
def restricted(func):
"""Decorator to restrict commands to allowed users."""
async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
if ALLOWED_USERS and user_id not in ALLOWED_USERS:
await update.message.reply_text("🚫 Access denied.")
logger.warning(f"Unauthorized access attempt by user {user_id}")
return
return await func(update, context)
return wrapped
@restricted
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Send a welcome message."""
user = update.effective_user
await update.message.reply_text(
f"Hello, {user.first_name}! 👋\n\n"
f"I'm your personal bot. Here's what I can do:\n"
f"/help - Show all commands\n"
f"/time - Current server time\n"
f"/echo [text] - Echo your message\n"
f"/status - Server status"
)
@restricted
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Show help message."""
help_text = """
*Available Commands:*
/start - Welcome message
/help - This help message
/time - Current server time
/echo [text] - Echo text back
/status - Check server status
/ping - Test bot response
*Direct Messages:*
Send any text and I'll echo it back.
"""
await update.message.reply_text(help_text, parse_mode="Markdown")
@restricted
async def time_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Show current server time."""
now = datetime.now()
await update.message.reply_text(
f"🕐 Server time: `{now.strftime('%Y-%m-%d %H:%M:%S')}`",
parse_mode="Markdown"
)
@restricted
async def echo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Echo the user's message."""
if context.args:
text = " ".join(context.args)
await update.message.reply_text(f"📢 {text}")
else:
await update.message.reply_text("Usage: /echo [your message]")
@restricted
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Show basic server status."""
import subprocess
try:
# Get uptime
uptime = subprocess.check_output("uptime -p", shell=True).decode().strip()
# Get disk usage
disk = subprocess.check_output("df -h / | tail -1 | awk '{print $3\"/\"$2\" (\"$5\" used)\"}'",
shell=True).decode().strip()
# Get memory
mem = subprocess.check_output("free -h | grep Mem | awk '{print $3\"/\"$2}'",
shell=True).decode().strip()
status_text = (
f"*Server Status* ✅\n\n"
f"⏱ Uptime: `{uptime}`\n"
f"💾 Disk: `{disk}`\n"
f"🧠 Memory: `{mem}`"
)
await update.message.reply_text(status_text, parse_mode="Markdown")
except Exception as e:
await update.message.reply_text(f"Error getting status: {e}")
@restricted
async def ping(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Simple ping response."""
await update.message.reply_text("🏓 Pong!")
@restricted
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle non-command messages."""
text = update.message.text
await update.message.reply_text(f"You said: {text}")
async def post_init(application: Application) -> None:
"""Set bot commands menu."""
commands = [
BotCommand("start", "Welcome message"),
BotCommand("help", "Show all commands"),
BotCommand("time", "Current server time"),
BotCommand("status", "Server status"),
BotCommand("ping", "Test response"),
BotCommand("echo", "Echo a message"),
]
await application.bot.set_my_commands(commands)
logger.info("Bot commands registered")
def main() -> None:
"""Start the bot with webhook."""
application = (
Application.builder()
.token(BOT_TOKEN)
.post_init(post_init)
.build()
)
# Register handlers
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("time", time_command))
application.add_handler(CommandHandler("echo", echo_command))
application.add_handler(CommandHandler("status", status_command))
application.add_handler(CommandHandler("ping", ping))
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
# Start webhook
logger.info(f"Starting webhook on port {PORT}")
application.run_webhook(
listen="0.0.0.0",
port=PORT,
url_path="/webhook",
webhook_url=WEBHOOK_URL,
)
if __name__ == "__main__":
main()
Telegram requires HTTPS for webhooks. Nginx handles SSL termination.
sudo apt install -y nginx certbot python3-certbot-nginx
sudo nano /etc/nginx/sites-available/telegram-bot
server {
listen 80;
server_name bot.yourdomain.com;
location /webhook {
proxy_pass http://localhost:8443/webhook;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
sudo ln -s /etc/nginx/sites-available/telegram-bot /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d bot.yourdomain.com
sudo nano /etc/systemd/system/telegram-bot.service
[Unit]
Description=Telegram Bot
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/telegram-bot
EnvironmentFile=/opt/telegram-bot/.env
ExecStart=/opt/telegram-bot/venv/bin/python3 bot.py
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable telegram-bot
sudo systemctl start telegram-bot
sudo systemctl status telegram-bot
Open Telegram, find your bot by username, and send /start.
View logs:
sudo journalctl -u telegram-bot -f
From any script on your server:
import asyncio
from telegram import Bot
BOT_TOKEN = "your-token"
CHAT_ID = 123456789 # Your Telegram user ID
async def notify(message: str):
bot = Bot(token=BOT_TOKEN)
await bot.send_message(chat_id=CHAT_ID, text=message)
# Usage in any Python script
asyncio.run(notify("Backup completed successfully ✅"))
Or via cURL (works from any language/script):
curl -s -X POST \
"https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "text=Server backup completed at $(date)"
Add a handler to receive and process files:
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
file = await update.message.document.get_file()
file_path = f"/tmp/{update.message.document.file_name}"
await file.download_to_drive(file_path)
await update.message.reply_text(f"File received: {update.message.document.file_name}")
application.add_handler(MessageHandler(filters.Document.ALL, handle_document))
My bot was running but Telegram wasn't delivering messages — every message was being silently dropped.
The issue: the webhook URL wasn't registered with Telegram, so Telegram didn't know where to send updates. When I started the bot with run_webhook(), python-telegram-bot registers the webhook automatically — but only if the URL is reachable from the internet at startup time.
My Nginx config wasn't fully applied yet when the bot started, so Telegram tried to verify the webhook URL and got a connection error.
How to manually verify the webhook is registered:
curl "https://api.telegram.org/bot${BOT_TOKEN}/getWebhookInfo"
Look for "url" in the response. If it's empty, the webhook isn't set.
Register it manually:
curl -X POST "https://api.telegram.org/bot${BOT_TOKEN}/setWebhook" \
-H "Content-Type: application/json" \
-d '{"url": "https://bot.yourdomain.com/webhook"}'
Response: {"ok":true,"result":true,"description":"Webhook was set"}
After this, messages should be delivered immediately.
| Issue | Likely Cause | Fix |
|---|---|---|
| Bot not responding | Webhook not registered | Run setWebhook manually (see above) |
| "Unauthorized" error | Wrong bot token | Double-check token from BotFather |
| Webhook rejected | No HTTPS | Telegram requires valid SSL certificate |
| Messages from unknown users | No user restriction | Add ALLOWED_USER_IDS to .env |
| Bot crashes on start | Missing dependencies | Check pip install python-telegram-bot[webhooks] |
| High latency | Polling instead of webhook | Use run_webhook() not run_polling() for production |
| 409 Conflict error | Two bot instances running | Stop the other instance; can't have two running simultaneously |
✅ What you built:
The server status command is genuinely useful — with one Telegram message you see uptime, disk usage, and memory without needing to SSH in.
What's the difference between Telegram bot and built-in Linux cron?
Linux cron runs commands on a schedule. Telegram bot provides additional capabilities like visual management, dependency handling, error notifications, and often Docker-native integration.
How do I debug a failing Telegram bot task?
Check the execution logs first. Verify the command works when run manually with the same user/environment. Common issues: incorrect paths, missing environment variables, permission problems.
How do I make Telegram bot tasks resilient to failures?
Implement retry logic, alert on failures (email/Slack notification), and log output to a file. For critical tasks, consider writing a simple success/failure status to a monitoring endpoint.
What happens if the server restarts — do scheduled tasks continue?
If configured as a systemd service (as shown in this guide), Telegram bot restarts automatically on server reboot and resumes its schedule. Configure Restart=on-failure for crash recovery.
systemctl or its own web interface. For important tasks, write output to a log file and use a monitoring tool to check for failures.👉 Get started with Tencent Cloud Lighthouse
👉 View current pricing and launch promotions
👉 Explore all active deals and offers