Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan Now×
Skip to content
VGSources
Blog

Python Telegram Bot with Docker Compose: A Reproducible Deployment Guide

Deploy a Python Telegram bot reproducibly with Docker Compose. This guide covers polling and webhook trade-offs, secret handling, healthchecks, persistence, production configuration, updates, rollback, and failure recovery.
Length13 min Posted Quest giverVGSources Team
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The simplest reliable deployment for a small Python Telegram bot is one Compose-managed container running Application.run_polling(), with the bot token delivered as a Docker secret, a healthcheck for local readiness, a restart policy, and a named volume only when the application has state to preserve. Move to a webhook when you have a specific need for Telegram to call a public HTTPS endpoint and you are prepared to operate routing, TLS, and webhook authentication.

What Docker Compose gives your bot

Docker Compose is the application boundary for this deployment. It defines the bot image, secrets, volumes, healthcheck, logging policy, and any supporting services in one YAML project. The same interface starts, stops, rebuilds, inspects, and updates the stack, so a reboot or code release does not depend on a collection of undocumented shell commands.

As an Amazon Associate I earn from qualifying purchases.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A production-minded bot has a single process entry point, a pinned Python base image and pinned dependency release, no token in source control, and an explicit recovery policy. The container should be replaceable: code belongs in the image, while durable state belongs in a named volume or an external database.

  • Polling: the bot makes outbound getUpdates requests. It needs no public inbound port and is the lowest-effort option for a small bot or a private host.
  • Webhook: Telegram sends each update to a public URL. You must provide DNS, HTTPS termination, routing, webhook registration, and protection for the webhook endpoint.
  • Operations: Compose healthchecks, dependency conditions, restart policies, bounded logs, and a documented update/rollback procedure turn a container into a service you can operate.

Polling or webhook: choose the network shape first

The python-telegram-bot documentation describes polling as consuming updates through getUpdates, while a webhook is a URL Telegram calls whenever an update arrives. Polling is the default recommendation unless you have a concrete reason to expose an inbound endpoint.

Decision point Polling Webhook
Network direction Outbound connections from the bot to Telegram Telegram sends updates to your public URL
Setup effort Lowest; no public inbound endpoint is required Requires URL, routing, TLS termination, and webhook registration
PTB entry point Application.run_polling() Application.run_webhook() or Updater.start_webhook()
Good fit Small deployments, development, and private hosts Public production endpoints with a clear reason to avoid polling
Security control Protect the bot token and host Add secret_token and validate X-Telegram-Bot-Api-Secret-Token

Telegram will not deliver updates through both mechanisms at once. Before switching to polling, remove an existing webhook deliberately and decide whether pending updates should be retained or dropped. Before switching to a webhook, register the final HTTPS URL and confirm that your reverse proxy forwards the path and request body unchanged.

Create a single-process bot module

Keep startup in one module so the container command, local testing, and healthcheck all exercise the same application. The example below uses a readiness file: the process writes it after PTB has initialized, and Compose checks that file locally. This check proves application startup; it does not prove that Telegram is reachable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from pathlib import Path

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

TOKEN_FILE = Path('/run/secrets/telegram_token')
READY_FILE = Path('/tmp/bot-ready')


def read_token() -> str:
    token = TOKEN_FILE.read_text(encoding='utf-8').strip()
    if not token:
        raise RuntimeError('telegram_token is empty')
    return token


async def post_init(application) -> None:
    READY_FILE.touch()


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    await update.message.reply_text('Bot is running.')


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


def main() -> None:
    application = (
        ApplicationBuilder()
        .token(read_token())
        .post_init(post_init)
        .build()
    )
    application.add_handler(CommandHandler('start', start))
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
    application.run_polling()


if __name__ == '__main__':
    main()

run_polling() initializes the application, starts polling, and performs an orderly shutdown when the process receives a termination signal. Do not put the token in this file, in an image layer, or in a command-line argument.

Pin the runtime and build a small image

Pin both the Python base image and the PTB release that you have tested together. PTB APIs can change between major versions, so record the chosen versions in your dependency file and review them as a deliberate upgrade rather than allowing an unbounded install.

python-telegram-bot==21.10

The number above is an example of an exact dependency pin; use the release your application has actually tested if it differs. A corresponding Dockerfile can run as a non-root user and avoid retaining pip’s cache:

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 
    PYTHONUNBUFFERED=1

WORKDIR /app

RUN addgroup --system bot && adduser --system --ingroup bot bot

COPY requirements.txt .
RUN python -m pip install --no-cache-dir --upgrade pip && 
    python -m pip install --no-cache-dir -r requirements.txt

COPY bot.py .
RUN mkdir -p /var/lib/bot && chown -R bot:bot /app /var/lib/bot

USER bot
CMD ['python', 'bot.py']

Build dependencies and source from the project directory. Keep the secret file outside the build context, or exclude it with .dockerignore; anything copied during a build can remain in an image layer.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
__pycache__/
*.pyc
.git/
secrets/
.env

Store the token as a Compose secret

Docker defines a secret as data such as a password, certificate, or API key that should not be transmitted over a network or stored unencrypted in a Dockerfile or source code. Compose mounts a declared secret at /run/secrets/<secret_name> and grants it only to services listed under that service’s secrets key.

  1. Create a directory that is ignored by Git: mkdir -p secrets.
  2. Write only the Bot API token to secrets/telegram_token.txt, with no explanatory text.
  3. Restrict the host file, for example with chmod 600 secrets/telegram_token.txt.
  4. Add secrets/ and .env to .gitignore.

Environment variables are convenient but can be exposed through process inspection, diagnostics, or logs. A secret file gives the process the same value without putting the token in the container environment. The token should never appear in a Compose command, image history, exception message, or log line.

Compose file for a polling deployment

Save this as compose.yaml. The read-only root filesystem limits accidental writes; /tmp is explicitly writable for the readiness marker, and the named volume is available only for application state that must survive container replacement.

services:
  bot:
    build:
      context: .
    init: true
    secrets:
      - telegram_token
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /tmp
    volumes:
      - bot_state:/var/lib/bot
    healthcheck:
      test: ['CMD-SHELL', 'test -f /tmp/bot-ready']
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '3'

secrets:
  telegram_token:
    file: ./secrets/telegram_token.txt

volumes:
  bot_state:

The readiness check is intentionally local. It does not claim Telegram reachability, and it will not detect an account-level problem such as a revoked token. Add a separate operational check, alert, or log review for outbound API failures rather than disguising that limitation in the health status.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Start, inspect, and test locally

  1. Check the rendered configuration before starting: docker compose config. Confirm that the secret is declared and that no token value appears in the rendered output.
  2. Build and start the service: docker compose up --build -d.
  3. Watch the bot logs: docker compose logs -f bot. A successful startup should not print the token.
  4. Check state and health with docker compose ps. The service should move from starting to healthy after /tmp/bot-ready is created.
  5. Send /start to the bot in Telegram and verify the reply. This confirms the application path and outbound connectivity, unlike the local healthcheck alone.

If the container exits immediately, inspect docker compose logs bot. An empty or unreadable secret, an import error, a PTB version mismatch, and a syntax error are common startup causes. If the process is healthy but no messages arrive, check whether a webhook is still registered and whether another bot instance is consuming updates.

Persist only state that needs to survive

docker compose down removes containers. Data written only to a container’s writable layer disappears when that container is removed, so local state belongs in the named bot_state volume or in an external datastore. The volume is not a backup; copy or back up its contents according to the value of the data.

To verify persistence deliberately, write a harmless marker, recreate the container, and read it back:

docker compose exec bot sh -c 'date > /var/lib/bot/persistence-check'
docker compose up -d --force-recreate bot
docker compose exec bot cat /var/lib/bot/persistence-check

For a larger or shared workload, use an external database instead of putting a growing database file in the container. If you do not store local state at all, remove the volume declaration and mount so the deployment has fewer moving parts.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Add a dependent database or queue safely

Starting containers in order is not the same as waiting for a service to be usable. Compose can gate the bot on a dependency healthcheck with condition: service_healthy. The dependency must define a healthcheck that tests its own readiness.

services:
  bot:
    depends_on:
      database:
        condition: service_healthy

  database:
    image: postgres:16
    environment:
      POSTGRES_DB: bot
      POSTGRES_USER: bot
      POSTGRES_PASSWORD_FILE: /run/secrets/database_password
    secrets:
      - database_password
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U bot -d bot']
      interval: 10s
      timeout: 5s
      retries: 5

secrets:
  database_password:
    file: ./secrets/database_password.txt

Use this pattern only when the bot really depends on the service. A healthcheck prevents a startup race; it does not replace connection retries in application code. The bot should still handle a database restart and reconnect with bounded backoff.

Use a production override instead of editing the base file

Keep development conveniences separate from production settings. A development override may bind-mount the source tree for rapid edits. Production should build an immutable image, remove source-code bind mounts, set the intended restart and logging policies, and expose only the ports that are required.

services:
  bot:
    build:
      context: .
    restart: unless-stopped
    read_only: true
    volumes:
      - bot_state:/var/lib/bot
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '3'

Run a production configuration explicitly, for example docker compose -f compose.yaml -f compose.production.yaml up -d --build. Review the merged result with docker compose -f compose.yaml -f compose.production.yaml config before applying it. Docker’s production guidance also recommends an external log destination or aggregator when continuous operation makes local log rotation insufficient.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Switching to a webhook

A webhook is appropriate when Telegram must push updates to a public endpoint, when your deployment already has a reverse proxy and TLS workflow, or when polling is unsuitable for your network design. It is not automatically faster or more reliable; it adds public routing and certificate operations.

Install the webhook extra

Install PTB with its optional webhook dependencies in the image, using the same exact PTB version you selected for polling. Keep the webhook and polling entry points as separate, intentional modes rather than trying to run both.

python -m pip install 'python-telegram-bot[webhooks]'

Expose an internal HTTP listener

Your bot can listen on plain HTTP inside the Compose network while a reverse proxy terminates TLS. PTB documents webhook operation without a certificate or key when another application handles SSL. The proxy forwards an HTTPS URL such as https://bot.example.net/telegram to the bot’s internal listener.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
application.run_webhook(
    listen='0.0.0.0',
    port=8080,
    url_path='telegram',
    webhook_url='https://bot.example.net/telegram',
    secret_token=read_webhook_secret(),
    max_connections=40,
)

PTB documents max_connections as an API parameter from 1 through 100, with a default of 40. The webhook secret_token is a separate secret from the Bot API token; PTB documents secret-token values from 1 through 256 characters. Configure the reverse proxy to preserve the path, terminate TLS, and pass the X-Telegram-Bot-Api-Secret-Token header. Reject requests that do not carry the expected value.

Compose considerations for the proxy

  • Put the proxy and bot on the same private Compose network.
  • Publish only the proxy’s HTTPS port to the host; keep the bot listener internal when possible.
  • Mount certificates into the proxy, not into the bot, unless the bot itself is responsible for TLS.
  • Store the webhook secret as a Compose secret and read it from /run/secrets.
  • Register the webhook only after DNS and TLS work, then inspect logs for the first delivery.

When returning to polling, remove the webhook first and restart one polling instance. Multiple instances competing for the same bot updates can produce missing or duplicated handling, so make the mode change a controlled deployment.

Updates, restarts, and rollback

Deploy changed code

Build the changed bot image and recreate only that service:

docker compose build bot
docker compose up --no-deps -d bot
docker compose ps
docker compose logs --tail=100 bot

--no-deps avoids restarting a healthy database or queue while the bot image changes. If a dependency schema must change, deploy that migration as a separately reviewed step.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Understand restart behavior

restart: unless-stopped tells Compose to bring the bot back after a process failure and after a host reboot, unless an operator intentionally stopped it. Choose another policy only when your service lifecycle requires it. A restart policy cannot fix a crash loop caused by a bad image or invalid secret; use the logs and health status to find that cause.

Roll back without deleting state

Keep the previous source revision or image tag available. If a release fails, check out the known-good revision, rebuild the bot, and run docker compose up --no-deps -d bot again. Do not run docker compose down -v during a code rollback: the -v option removes named volumes and can destroy the state you were trying to preserve. Verify the healthcheck, send a test command, and inspect logs after the rollback.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Security and reliability checklist

  • Never commit the Telegram token, webhook secret, .env file, private key, or certificate key.
  • Grant each Compose secret only to the service that reads it.
  • Run the process as a non-root user and check ownership of every writable volume.
  • Use a read-only root filesystem where the application supports it, with explicit writable paths such as /tmp and a state volume.
  • Keep healthchecks honest: a file or local command can prove process readiness, but not Telegram API reachability.
  • Use depends_on with service_healthy for real dependencies and retain application-level reconnect logic.
  • Rotate and bound logs; send them to an external destination when the bot operates continuously.
  • Test SIGTERM shutdown, host reboot recovery, secret-read failures, and volume persistence before launch.
  • Run only one polling consumer for a bot unless your architecture explicitly coordinates update ownership.

Troubleshooting by symptom

The container exits with a token error

Check that secrets/telegram_token.txt exists on the host, contains only the token, and is listed under the bot service. Confirm the in-container path with docker compose exec bot ls -l /run/secrets without printing the file contents. Recreate the service after changing the secret.

The service is healthy but receives no updates

Confirm that Telegram is not still configured with a webhook, that only one polling process is running, and that outbound HTTPS is allowed. Review the PTB logs for authorization or network errors. A local readiness file cannot detect these account or network conditions.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The bot races a database on startup

Add a dependency healthcheck and condition: service_healthy, then keep retry logic in the bot. Increasing a generic sleep may hide the race without making recovery reliable.

State disappears after a redeploy

Check that the state path is mounted to bot_state and that the deployment did not use docker compose down -v. If the data matters beyond one host, move it to an external datastore and back it up.

A webhook returns errors

Verify DNS, certificate validity, proxy routing, the exact URL path, and forwarding of the secret header. Confirm that the listener is bound to 0.0.0.0 inside the container and that the proxy can reach the Compose service name and port.

Capacity expectations and testing

Official Compose and PTB documentation does not establish a universal bot capacity, uptime, latency, or hosting cost. Requirements depend on handler complexity, update volume, external APIs, database work, and the reliability of the host and network. Measure your own workload instead of treating a small example container as a capacity guarantee.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Before production, exercise a representative message flow, force a container restart, reboot the host if possible, revoke and restore a test secret, and recreate the container while checking the named volume. Record how long startup takes, how errors appear in logs, and whether pending updates are retained according to your chosen polling or webhook behavior.

Deployment sequence at a glance

  1. Write the bot module with one process entry point and a local readiness signal.
  2. Select and pin compatible Python and PTB versions.
  3. Build a small non-root image that never copies secrets.
  4. Create the token file, protect its permissions, and declare it as a Compose secret.
  5. Add the bot service, healthcheck, restart policy, logging limits, and only the volume paths that need persistence.
  6. Start with docker compose up --build, then inspect docker compose ps and docker compose logs -f bot.
  7. Use polling unless a public webhook solves a specific requirement; clear any old webhook before polling.
  8. For webhooks, install the PTB extra, terminate TLS at a proxy, register the HTTPS URL, and enforce the secret header.
  9. Keep production settings in an override file and remove development bind mounts.
  10. Update with docker compose build bot followed by docker compose up --no-deps -d bot; roll back by rebuilding the previous known-good revision without deleting volumes.

The Bottom Line

For most small Python Telegram bots, use one Compose-managed polling service with a pinned image, a mounted Compose secret, an honest healthcheck, restart: unless-stopped, and durable state outside the container layer. Adopt a webhook only when you are ready to operate public HTTPS routing and its additional security controls.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More quests from Patch Notes

  1. How to Set Up a RedM (RDR2) Server in 2026: License Key, txAdmin and server.cfg, Step by StepBlog11min
  2. Best RedM (RDR2) Server Hosting in 2026: Comparing Four Hosts on Slots, Memory and PriceBlog10min
  3. How to Host a Mindustry Server in 2026: server-release.jar, Port 6567 and the Commands That MatterBlog7min
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.