How to Run a Telegram Bot on a VPS: 24/7 Uptime with Webhook Mode and Python

How to Run a Telegram Bot on a VPS: 24/7 Uptime with Webhook Mode and Python

A Telegram bot running on your laptop only works while your laptop is on. A bot on a VPS runs 24/7, responds instantly via webhook (Telegram pushes updates to your server rather than your bot polling), and handles thousands of messages per hour. This guide builds and deploys a production Telegram bot with Python, webhook mode, Nginx SSL, and systemd for automatic restart.

Polling vs Webhook Mode

  • Polling: Bot asks Telegram “any new messages?” on a schedule. Simple to develop locally, 1–2 second delay, wastes resources, no inbound port needed.
  • Webhook: Telegram pushes each update to your HTTPS endpoint the moment it arrives. Zero latency, no polling overhead. Requires HTTPS. Use this in production.

Step 1: Create Your Bot with BotFather

  1. Open Telegram → search for @BotFather
  2. Send /newbot and follow the prompts
  3. Copy your bot token: 1234567890:ABCDefghijklMNOpqrstuvwxyz

Step 2: Set Up Python Environment on VPS

sudo apt install -y python3.12 python3.12-venv redis-server
sudo systemctl enable --now redis-server

mkdir -p /opt/mybot && cd /opt/mybot
python3.12 -m venv .venv
source .venv/bin/activate
pip install "python-telegram-bot[webhooks]" redis python-dotenv aiohttp

Step 3: Write the Bot

nano /opt/mybot/bot.py
"""Production Telegram bot with webhook mode."""
import logging
import os
from dotenv import load_dotenv
from telegram import Update
from telegram.ext import (
    Application, CommandHandler, MessageHandler,
    filters, ContextTypes,
)

load_dotenv('/opt/mybot/.env')
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.INFO
)
logger = logging.getLogger(__name__)

BOT_TOKEN = os.environ['BOT_TOKEN']
WEBHOOK_URL = os.environ['WEBHOOK_URL']   # e.g. https://bot.yourdomain.com
WEBHOOK_PATH = f'/bot/{BOT_TOKEN}'
PORT = int(os.environ.get('PORT', 8443))


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Handle /start command."""
    user = update.effective_user
    await update.message.reply_html(
        f'Hello {user.mention_html()}! I am your bot.\n'
        'Send /help to see available commands.'
    )


async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Handle /help command."""
    await update.message.reply_text(
        '/start - Welcome message\n'
        '/help - This help message\n'
        'Send any text - I will echo it back'
    )


async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Echo the user message."""
    await update.message.reply_text(f'Echo: {update.message.text}')


async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
    logger.error('Exception while handling an update:', exc_info=context.error)


def main() -> None:
    app = Application.builder().token(BOT_TOKEN).build()

    app.add_handler(CommandHandler('start', start))
    app.add_handler(CommandHandler('help', help_command))
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
    app.add_error_handler(error_handler)

    # Run webhook server
    app.run_webhook(
        listen='127.0.0.1',
        port=PORT,
        url_path=WEBHOOK_PATH,
        webhook_url=f'{WEBHOOK_URL}{WEBHOOK_PATH}',
        allowed_updates=Update.ALL_TYPES,
    )


if __name__ == '__main__':
    main()
nano /opt/mybot/.env
BOT_TOKEN=1234567890:ABCDefghijklMNOpqrstuvwxyz
WEBHOOK_URL=https://bot.yourdomain.com
PORT=8443
chmod 600 /opt/mybot/.env

Step 4: Nginx with SSL (Telegram Requires HTTPS)

sudo nano /etc/nginx/sites-available/telegram-bot
server {
    listen 80;
    server_name bot.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name bot.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/bot.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/bot.yourdomain.com/privkey.pem;

    # Telegram webhook path
    location /bot/ {
        proxy_pass http://127.0.0.1:8443;
        proxy_http_version 1.1;
        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 certbot --nginx -d bot.yourdomain.com
sudo systemctl reload nginx

Step 5: systemd Service

sudo nano /etc/systemd/system/telegram-bot.service
[Unit]
Description=Telegram Bot
After=network.target redis.service

[Service]
User=deploy
WorkingDirectory=/opt/mybot
EnvironmentFile=/opt/mybot/.env
ExecStart=/opt/mybot/.venv/bin/python bot.py
Restart=on-failure
RestartSec=10s
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now telegram-bot
sudo systemctl status telegram-bot
sudo journalctl -u telegram-bot -f

Step 6: Verify Webhook Registration

curl "https://api.telegram.org/bot${BOT_TOKEN}/getWebhookInfo" | python3 -m json.tool

# Expected response:
# {
#   "url": "https://bot.yourdomain.com/bot/YOUR_TOKEN",
#   "has_custom_certificate": false,
#   "pending_update_count": 0,
#   "last_error_message": ""
# }

Conversation State with Redis

from redis import Redis

redis_client = Redis(host='localhost', port=6379, db=0, decode_responses=True)

async def handle_step1(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    redis_client.set(f'user:{user_id}:step', 'awaiting_email', ex=3600)
    await update.message.reply_text('Please enter your email address:')

async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    step = redis_client.get(f'user:{user_id}:step')

    if step == 'awaiting_email':
        email = update.message.text
        redis_client.set(f'user:{user_id}:email', email)
        redis_client.delete(f'user:{user_id}:step')
        await update.message.reply_text(f'Email {email} saved!')
    else:
        await update.message.reply_text(f'Echo: {update.message.text}')

Getting Started

A Telegram bot uses 50–100 MB RAM for a Python bot — even the smallest Ubuntu VPS at VPS.DO can host dozens of bots simultaneously. For bots that frequently call a database or internal API, co-locating the bot on the same VPS as those services eliminates network latency between the bot and its backends.

Conclusion

Deploying a Telegram bot on a VPS with webhook mode eliminates polling delays and local machine dependency — the bot responds instantly, runs 24/7, and survives VPS reboots via systemd. The Nginx SSL proxy handles Telegram’s HTTPS requirement, and Redis provides conversation state that persists across bot restarts. A well-designed Python Telegram bot on a small VPS handles thousands of messages per hour without approaching resource limits.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!