Technology Encyclopedia Home >Run a Private Telegram Bot Server — Build and Host Bots That Do Anything

Run a Private Telegram Bot Server — Build and Host Bots That Do Anything

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.


Table of Contents

  1. Polling vs Webhooks — The Right Architecture
  2. What You Need
  3. Part 1: Create Your Bot with BotFather
  4. Part 2: Set Up the Server Environment
  5. Part 3: Build a Feature-Rich Bot
  6. Part 4: Configure Webhooks with Nginx
  7. Part 5: Deploy as a Service
  8. Part 6: Practical Bot Examples
  9. The Thing That Tripped Me Up
  10. Troubleshooting
  11. Summary

  • Key Takeaways
  • Use the appropriate Lighthouse application image to skip manual installation steps where available
  • Lighthouse snapshots provide one-click full-server backup before major changes
  • OrcaTerm browser terminal lets you manage the server from any device
  • CBS cloud disk expansion handles growing storage needs without server migration
  • Console-level firewall + UFW = two independent protection layers

Polling vs Webhooks — The Right Architecture {#polling-vs-webhooks}

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.


What You Need {#prerequisites}

Requirement Details
Server Ubuntu 22.04, 1 GB+ RAM
Domain Required for HTTPS webhook
Telegram account To create the bot
Python 3.10+

Part 1: Create Your Bot with BotFather {#part-1}

1.1 — Create the Bot

  1. Open Telegram and search for @BotFather
  2. Send /newbot
  3. Choose a name: "My Automation Bot"
  4. Choose a username: my_automation_bot (must end in bot)
  5. BotFather gives you a token: 7123456789:AAHxxxx...

Save this token. You'll use it as BOT_TOKEN in your configuration.

1.2 — Configure the Bot

/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: /setjoingroupsDisable to prevent others adding your bot to groups.


Part 2: Set Up the Server Environment {#part-2}

2.1 — Install Python and Dependencies

sudo apt update
sudo apt install -y python3 python3-pip python3-venv

2.2 — Create Project

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

2.3 — Create .env File

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.


Part 3: Build a Feature-Rich Bot {#part-3}

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()

Part 4: Configure Webhooks with Nginx {#part-4}

Telegram requires HTTPS for webhooks. Nginx handles SSL termination.

4.1 — Install Nginx and Certbot

sudo apt install -y nginx certbot python3-certbot-nginx

4.2 — Create Nginx Config

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

Part 5: Deploy as a Service {#part-5}

5.1 — Create systemd Service

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

5.2 — Test the Bot

Open Telegram, find your bot by username, and send /start.

View logs:

sudo journalctl -u telegram-bot -f

Part 6: Practical Bot Examples {#part-6}

Send a Notification When Something Happens (Push Mode)

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)"

File/Document Bot

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))

The Thing That Tripped Me Up {#gotcha}

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.


Troubleshooting {#troubleshooting}

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

Summary {#verdict}

What you built:

  • Telegram bot with webhook-based message delivery (production approach)
  • User restriction to prevent unauthorized access
  • Commands: start, help, time, status, ping, echo
  • Systemd service for 24/7 operation with auto-restart
  • HTTPS endpoint via Nginx for Telegram webhook compatibility
  • Pattern for sending notifications from any server script via cURL

The server status command is genuinely useful — with one Telegram message you see uptime, disk usage, and memory without needing to SSH in.

Frequently Asked Questions {#faq}

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.

How do I monitor task execution history?
Telegram bot typically provides logs via 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