← Blog

How to host a Telegram bot that's always online

Railyard

September 14, 2026 · 7 min read

Follow this on your own server. Start free, no card needed

A Telegram bot that only responds while your laptop happens to be open isn’t a bot yet, it’s a script. The gap between the two is the same gap as any other always-on service: a process that keeps running, restarts if it crashes, and either keeps asking Telegram for updates or has somewhere public for Telegram to send them. This guide builds a real bot with python-telegram-bot, walks through the one decision that actually matters for hosting it (long polling versus a webhook), and covers the Dockerfile, restart policy and deploy path for keeping it online.

What you’ll need

  • Python 3.10 or newer installed locally (python-telegram-bot 22.x requires it).
  • A Telegram account and @BotFather to create the bot and get a token.
  • Docker installed locally, to build and test the image.
  • A server to run the container on, or a Railyard account for the shortcut in the last section.

Step 1: Create the bot and get a token

Open a chat with @BotFather on Telegram, send /newbot, give it a name and a username ending in bot. BotFather replies with a token that looks like 123456789:AAFkj2-examplenotrealtoken. That token is the full credential for controlling the bot; it goes in an environment variable, never committed to a repository.

Step 2: Write the bot

python-telegram-bot 22.8 is the current release on PyPI. Install it:

python -m venv venv
source venv/bin/activate
pip install "python-telegram-bot==22.8"

bot.py:

import logging
import os

from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters

logging.basicConfig(format="%(asctime)s %(name)s %(levelname)s %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    await update.message.reply_text("Hi. Send me anything and I'll echo it back.")


async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    await update.message.reply_text(update.message.text)


def main() -> None:
    token = os.environ["TELEGRAM_BOT_TOKEN"]
    application = Application.builder().token(token).build()

    application.add_handler(CommandHandler("start", start))
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))

    application.run_polling(allowed_updates=Update.ALL_TYPES)


if __name__ == "__main__":
    main()

Test it locally:

TELEGRAM_BOT_TOKEN=your-token-here python bot.py

Message the bot on Telegram; /start and any plain text should both get a reply within a second or two.

Step 3: Long polling versus a webhook, and why it matters for hosting

application.run_polling() above is long polling: your bot repeatedly asks Telegram’s servers “anything new for me?” over an HTTP connection that Telegram holds open and responds to as soon as an update exists, then the bot immediately asks again. Nothing about your bot needs to be reachable from the internet; it only ever makes outbound requests to Telegram, the same shape as the Discord gateway connection in hosting a Discord bot 24/7. That’s why long polling is the right default for “always online”: it needs exactly one thing, a process that keeps running and keeps asking, with nothing to expose, no domain, and no certificate.

The alternative, a webhook, flips the direction: you register a public HTTPS URL with Telegram (application.run_webhook(...), listening on a port, plus a call to set the webhook URL on Telegram’s side), and Telegram pushes each update to that URL as an HTTP POST the moment it happens, instead of your bot asking. The genuinely honest tradeoff: a webhook removes the constant round-trip of polling and can be marginally lower-latency, but it isn’t actually simpler to host, it’s a different set of requirements. It needs a real domain with a valid TLS certificate (Telegram requires HTTPS for webhooks, self-signed only with extra steps most bots skip), and critically, it still needs an always-running process to receive it: a webhook is HTTP-triggered, not request-triggered-and-torn-down, because if nothing is listening at the registered URL when Telegram tries to deliver an update, that update is simply dropped. “HTTP-based” doesn’t mean “works on a scale-to-zero platform” here, since the bot still has to be up at the exact, unpredictable moment Telegram pushes.

For a bot you’re hosting yourself without already having a domain and certificate set up, long polling is the simpler, equally “always online” choice, and the one this guide builds around. Switch to a webhook later if request volume genuinely justifies it; the handler functions (start, echo) don’t change either way, only main() does.

Step 4: Write the Dockerfile

FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --create-home appuser
USER appuser
CMD ["python", "bot.py"]

requirements.txt:

python-telegram-bot==22.8

As with the Discord bot’s Dockerfile, there’s no EXPOSE here: long polling only makes outbound connections, so nothing needs to accept an inbound one. The image runs as a created, non-root appuser rather than root, the Python equivalent of the node user story for a Node image.

Step 5: Restart policy and logging

docker build -t my-telegram-bot .
docker run -d --name my-telegram-bot --restart unless-stopped \
  -e TELEGRAM_BOT_TOKEN=your-token-here \
  my-telegram-bot

--restart unless-stopped restarts the container after a crash or a host reboot, without anyone SSHing in. The logging.basicConfig call in Step 2 sends everything to stdout, which Docker already captures:

docker logs -f my-telegram-bot

You should see the INFO lines python-telegram-bot itself emits on startup (confirming it connected to Telegram’s API) and one for each /start or echoed message handled.

The shortcut: do it with Railyard

Point Railyard at the same repository. A Dockerfile that builds a self-contained image (not one expecting a bind-mounted source directory, which Railyard’s build detection skips automatically) builds and runs directly; see build methods. As with the Discord bot, this is a worker-only app with no web process, which Railyard runs the same as any other app, just with no port attached; see Workers and scheduled jobs.

Add TELEGRAM_BOT_TOKEN under the app’s Variables tab; it’s encrypted and applied on the next deploy, replacing the -e flag from Step 5. See variables and secrets. Leave Sleep when idle (under Settings → General) off for this process: a quiet chat can look CPU-idle for long stretches while the bot is mid-long-poll, waiting on Telegram’s servers to respond, and that wait is exactly what shouldn’t be interrupted.

The restart policy from Step 5 isn’t a setting to configure separately, it’s how every app’s container runs on Railyard by default, the same unless-explicitly-stopped behavior. Logs are the Logs tab on the app page, or railyard logs my-bot --follow from the CLI.

If you’d rather use the webhook approach from Step 3 instead of long polling, Railyard’s automatic HTTPS (every app gets a certificate immediately, a custom domain adds one DNS record and an ownership check; see domains and HTTPS) covers the one piece that otherwise makes webhooks the more complicated option to self-host; the app would then need a small web process alongside the bot logic to receive Telegram’s POST requests, which is a real architecture change from the worker-only shape this guide builds, not just a settings toggle.

Troubleshooting

The bot doesn’t respond, but the container is running. Check docker logs for an authorization error first; an invalid token fails at Application.builder().token(token).build() or shortly after run_polling() starts, and that error is almost always the first thing in the log.

“Conflict: terminated by other getUpdates request.” Telegram allows only one long-polling connection per bot token at a time. This means two processes are running with the same token simultaneously, commonly a local test run left going while the same bot is also deployed; stop one of them.

The container restarts in a loop. Check the first lines of docker logs right after a restart, not the tail. A missing TELEGRAM_BOT_TOKEN environment variable (a KeyError from os.environ["TELEGRAM_BOT_TOKEN"]) is the most common immediate crash for a freshly deployed bot.

Messages sent while the bot was offline never arrive. Telegram does queue updates for a bot that’s briefly unreachable and delivers them once polling resumes, for a limited time, but a bot that’s down for an extended period will still miss what falls outside that window; this is a reason to treat “always online” as the actual goal rather than “usually online,” not a safety net to rely on.

FAQ

Long polling or webhook: which should I actually use? Long polling, for most bots run by one person or a small team without an existing domain and certificate already set up. It needs nothing public-facing at all. Switch to a webhook only once request volume or latency genuinely justifies the added domain, certificate and inbound-port requirements.

Does python-telegram-bot or node-telegram-bot-api matter for hosting? Not for anything in this guide. node-telegram-bot-api’s current major version (2.x) is a from-scratch redesign of its API compared to older versions, but the hosting shape is identical either way: an always-on process, a Dockerfile with no exposed port for the long-polling case, and a restart policy. The Python and Node code differ; what the process needs to stay alive doesn’t.

Can I run the bot for free? The library is free; an always-on process needs always-on compute, which a serverless free tier generally isn’t built for, since most scale to zero between requests. A small VPS at a few dollars a month is the realistic floor for genuinely continuous uptime.

What happens if two instances of the bot run at the same time with the same token? Telegram’s long-polling API explicitly rejects a second concurrent getUpdates connection for the same bot token with the “Conflict” error from the troubleshooting section above; only one instance can hold the polling connection at once, by design, which also means you can’t horizontally scale a long-polling bot the way you would a web app.

Do I need webhook mode to run many bots or a bot with lots of users? Not necessarily just for user count; long polling scales with however many messages arrive, not with how many users are following the bot. Webhook mode becomes more attractive at genuinely high message volume where the polling round-trip itself adds meaningful overhead, which is a higher bar than most bots reach.

Staying online, for a Telegram bot, comes down to the same two things it does for any other continuously running service: a process that doesn’t stop on its own, and one that comes back when something stops it anyway. Everything else, including which of polling or webhooks you pick, is a detail on top of that.

Why people pick Railyard for this

  • Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
  • One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
  • One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
  • Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
  • Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.

Skip the server setup

Deploy your Telegram bot's Dockerfile to Railyard as a worker process, with a restart policy and logs already wired up.

Free plan, no card, and 14 days of Pro for every new team. One flat price per team.