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

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Use Docker Compose to package the bot, its secret, and any supporting services into one repeatable application boundary. For most small bots, python-telegram-bot (PTB) polling is the simplest production path: the container makes outbound connections to Telegram, so you do not need a public inbound endpoint. Choose a webhook only when you have a specific reason to expose an HTTPS endpoint. The deployment below covers both modes, keeps the token out of images and logs, survives host reboots, preserves state, and gives you commands for updates, health checks, and rollback.

What you will deploy

The example is intentionally small: one Python process in a bot container, a Compose-managed secret containing the Telegram token, and an optional named volume for state. Compose defines and runs the application as a unit, so the same commands handle building, starting, stopping, inspecting, and replacing the service. Add a database or queue as another service only when the bot actually needs one.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Image: a pinned Python base image with a pinned PTB dependency.
  • Process: one entry point that constructs an Application, registers handlers, and calls run_polling() or the webhook equivalent.
  • Secret: a Compose secret mounted at /run/secrets/telegram_token, readable only by the bot service.
  • Recovery: a deliberate restart policy and a health check that describes what it can and cannot prove.
  • Durability: a named volume or external datastore for data that must survive container replacement.

Docker’s documentation describes Compose as a tool for defining and running multi-container applications. Treat the Compose project, rather than an individual container, as your deployment boundary: configuration, secrets, dependencies, logs, and lifecycle commands belong there.

Prerequisites and project layout

Install Docker Engine with the Compose v2 plugin on the host where the bot will run. Verify both components before creating files:

docker version
docker compose version

Create a directory such as telegram-bot/. Keep the token file outside version control and use this layout:

telegram-bot/
├── bot.py
├── requirements.txt
├── Dockerfile
├── compose.yaml
├── compose.production.yaml
├── .dockerignore
└── secrets/
    └── telegram_token

Put the token issued by BotFather in secrets/telegram_token with no surrounding quotes and a trailing newline. Restrict the file on a Linux host with chmod 600 secrets/telegram_token. Add both secrets/ and any local .env file to .gitignore; never commit either one.

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.

Build a single-process PTB application

Read the token from the mounted secret

Do not make the token a Dockerfile ARG, bake it into an image layer, or pass it as a normal environment variable. The application reads the file path supplied by Compose:

import os
from pathlib import Path


def read_token() -> str:
    secret_path = Path(os.environ.get('BOT_TOKEN_FILE', '/run/secrets/telegram_token'))
    try:
        token = secret_path.read_text(encoding='utf-8').strip()
    except FileNotFoundError as exc:
        raise RuntimeError(f'Missing Telegram token at {secret_path}') from exc
    if not token:
        raise RuntimeError('Telegram token is empty')
    return token

Compose secrets are mounted under /run/secrets/<secret_name> and are granted per service. Docker warns that ordinary environment variables can be exposed through process inspection or logs, so a mounted secret is the safer default.

Register handlers and choose polling

This complete bot.py responds to /start, /help, and ordinary text. PTB’s ApplicationBuilder creates the application; run_polling() initializes it, starts the getUpdates loop, and performs an orderly shutdown when the process receives a termination signal.

import logging

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

from token_reader import read_token

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


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    if update.effective_message:
        await update.effective_message.reply_text('Bot is running. Send a message to echo it.')


async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    if update.effective_message:
        await update.effective_message.reply_text('Use /start or send plain text.')


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


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


if __name__ == '__main__':
    main()

Save the token reader in token_reader.py beside bot.py, or put the function directly in the same module. Keep one foreground process in the container. A process manager inside the container is unnecessary here; Compose supplies the lifecycle and restart policy.

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

Pin dependencies deliberately

PTB APIs can change between major versions. Resolve and test one version, then commit the generated, exact dependency file. The following illustrates the required shape without claiming that a particular PTB release is current:

python-telegram-bot[webhooks]==<tested-version>

If you will never use webhooks, omit the optional [webhooks] extra. For a polling-only image, python-telegram-bot==<tested-version> is sufficient. Replace the version marker with the exact release you tested, and pin the Python patch version in the image or its digest as well.

Containerize the bot

Dockerfile

This image runs as a non-root user, avoids writing Python bytecode, and starts the bot in the foreground:

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 
    PYTHONUNBUFFERED=1 
    BOT_TOKEN_FILE=/run/secrets/telegram_token

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir --require-hashes -r requirements.txt

COPY token_reader.py bot.py ./

RUN addgroup --system appgroup && 
    adduser --system --ingroup appgroup appuser
USER appuser

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

--require-hashes is appropriate only when your requirements file contains hashes for every resolved package. If you have not generated a hash-locked file, remove that option while you establish and test the lock file; do not pretend that an un-hashed list is fully reproducible. In production, replace the floating python:3.12-slim tag with the exact patch tag or digest approved by your update process.

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

.dockerignore

Prevent local secrets, caches, and development files from entering the build context:

.git
.gitignore
.env
secrets/
__pycache__/
*.pyc
.venv/
compose.production.yaml

The secret is mounted at runtime by Compose, so excluding it from the build context is an additional safeguard rather than the only one.

Define the Compose application

Base compose.yaml

The following file declares the secret, restart behavior, a process-level health check, and an optional state volume. The check verifies that PID 1 is alive; it does not prove that Telegram is reachable or that handlers are functioning.

services:
  bot:
    build:
      context: .
    secrets:
      - telegram_token
    environment:
      BOT_TOKEN_FILE: /run/secrets/telegram_token
      STATE_DIR: /var/lib/telegram-bot
    restart: unless-stopped
    init: true
    read_only: true
    tmpfs:
      - /tmp
    volumes:
      - bot_state:/var/lib/telegram-bot
    healthcheck:
      test: ['CMD-SHELL', 'kill -0 1']
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s

secrets:
  telegram_token:
    file: ./secrets/telegram_token

volumes:
  bot_state:

read_only: true protects the image filesystem. Give the application a writable named volume only for files it intentionally persists; in this example that location is /var/lib/telegram-bot. If the bot is entirely stateless, remove the volume and the STATE_DIR variable.

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

Add a database or queue only when needed

When the bot depends on another service, define a health check for that dependency and gate startup with depends_on. A process that merely starts first is not necessarily ready to accept connections:

services:
  bot:
    depends_on:
      database:
        condition: service_healthy

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

secrets:
  database_password:
    file: ./secrets/database_password

volumes:
  database_data:

The bot should still handle a temporary database outage with retries and clear errors. service_healthy prevents the initial race; it is not a substitute for runtime reconnection logic.

Start and verify the deployment

Build and launch locally

  1. From the project directory, build the image and start the service in the foreground: docker compose up --build.
  2. In another terminal, watch only the bot logs with docker compose logs -f bot.
  3. Inspect state with docker compose ps. Wait for the health status to become healthy if the image declares the check above.
  4. Send /start to the bot in Telegram and confirm a response.
  5. Stop with Ctrl-C and repeat in detached mode when the behavior is understood: docker compose up -d.

Use docker compose config to render and validate the merged configuration before deployment. It is useful for catching indentation errors, missing secret files, and an unexpected override. Do not paste rendered output into a public issue if it contains environment values or paths you consider sensitive.

Understand polling conflicts

Polling calls Telegram’s getUpdates API over an outbound connection. It needs no public inbound port, certificate, or domain, which is why it is the default choice for a small bot or a private host. Telegram will not deliver updates by polling while a webhook remains configured for the same bot. Before switching to polling, remove the old webhook and decide whether pending updates should be retained or discarded. Run only one active consumer for a bot token unless you have intentionally designed a different update-delivery arrangement.

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

Polling or webhook?

Consideration Polling Webhook
Network shape The bot opens outbound connections to Telegram with getUpdates. Telegram sends updates to a public URL that you operate.
Setup effort Lowest; no public inbound endpoint is required. Requires URL registration, routing, TLS termination, and a reachable host.
PTB entry point Application.run_polling() Application.run_webhook() or Updater.start_webhook()
Typical fit Small deployments, development, and private servers. A public production endpoint when avoiding polling has a clear operational reason.
Main security control Protect the token and host; no inbound bot listener is needed. Use a webhook secret token and validate the X-Telegram-Bot-Api-Secret-Token header.

PTB’s webhook guidance describes polling as repeatedly using getUpdates and a webhook as Telegram calling a URL when an update arrives. It advises switching only when there is a good reason. A webhook does not automatically improve reliability; it changes your network and certificate responsibilities.

Deploying with a webhook

Put TLS at a reverse proxy

The bot can listen on plain HTTP inside the Compose network while a reverse proxy terminates HTTPS. This keeps certificate handling at the edge and limits the bot process to the internal network. PTB documents webhook operation without supplying a certificate and key to the bot when another application performs TLS termination.

  • Obtain a DNS name that resolves to the host and an HTTPS certificate.
  • Route one private path from the proxy to the bot’s internal HTTP listener.
  • Allow inbound traffic only to the proxy’s HTTPS port; do not expose the internal listener directly unless necessary.
  • Set a random PTB webhook secret_token, 1–256 characters, and configure the proxy or application to validate the matching X-Telegram-Bot-Api-Secret-Token header.
  • Register the complete HTTPS webhook URL with Telegram and confirm that the proxy forwards the request path and body unchanged.

PTB’s optional webhook support is supplied by the webhooks extra. Add that extra to the pinned requirements file, then replace run_polling() with the webhook startup path appropriate to your PTB version. The Bot API documents max_connections as 1–100, with a default of 40; this is a connection-setting constraint, not a promise of a particular throughput.

Compose shape for the webhook listener

services:
  bot:
    command: ['python', 'bot_webhook.py']
    expose:
      - '8080'
    environment:
      WEBHOOK_LISTEN: 0.0.0.0
      WEBHOOK_PORT: 8080
      WEBHOOK_PATH: /telegram/update
      WEBHOOK_SECRET_FILE: /run/secrets/webhook_secret
    secrets:
      - telegram_token
      - webhook_secret

  reverse-proxy:
    image: your-approved-proxy-image
    depends_on:
      - bot
    ports:
      - '443:443'
    volumes:
      - ./proxy-config:/etc/proxy:ro
      - ./certificates:/etc/certificates:ro

secrets:
  webhook_secret:
    file: ./secrets/webhook_secret

The proxy image, configuration syntax, certificate renewal, and firewall rules depend on the proxy you select, so keep those details in its own documented configuration. The important Compose properties are that the bot is reachable on the private network, only the proxy publishes HTTPS, and both token types are mounted as secrets.

Use a production override

Keep developer conveniences separate from production settings. Docker recommends a production-specific Compose file that removes source-code bind mounts, applies production ports and logging, and sets restart behavior. The base file above has no source mount; if you add one for development, remove it in the override.

services:
  bot:
    restart: unless-stopped
    build:
      context: .
    logging:
      driver: json-file
      options:
        max-size: 10m
        max-file: '5'
    deploy:
      resources:
        limits:
          memory: 512M

  # Publish only the ports needed by your chosen edge service.
  # Do not publish a polling bot because it has no inbound listener.

The deploy.resources values are examples of explicit limits, not universal sizing recommendations. Choose CPU and memory from your bot’s workload, message volume, library behavior, and observed headroom. The Docker Engine may treat some Swarm-oriented deploy keys differently outside Swarm, so verify the effective configuration on your target host.

Start with both files so the production settings are merged:

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.
docker compose -f compose.yaml -f compose.production.yaml up -d --build

Use docker compose -f compose.yaml -f compose.production.yaml config to inspect the merged result. Keep source code, Compose files, and the exact dependency lock file in version control; keep secrets on the host or in a separate secret-management system.

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

Updates, rollback, and reboot recovery

Update changed code

  1. Pull or check out the reviewed code and dependency changes.
  2. Build only the changed service: docker compose build bot.
  3. Recreate it without needlessly restarting dependencies: docker compose up --no-deps -d bot.
  4. Watch startup and handler errors with docker compose logs -f bot, then exercise a real command in Telegram.

Compose replaces the container from the new image while preserving named volumes. If a dependency schema changed, apply migrations as an explicit, reviewed one-off command rather than hiding migration logic in an endless restart loop.

Roll back safely

Tag known-good images in your registry or retain the previous Git revision and lock file. To roll back, check out that revision, rebuild the bot, and run the same up --no-deps -d bot command. If the new container fails before it becomes healthy, inspect docker compose ps and docker compose logs bot before removing anything. Keep the named volume intact unless you have confirmed that a data migration must be reversed; deleting it can destroy state.

Survive a host reboot

restart: unless-stopped tells Docker to restart the bot after a process failure and after the Docker daemon or host returns, unless an operator intentionally stopped the service. Other policies such as always or on-failure may be appropriate for a different lifecycle, but choose one deliberately and test it. Enable Docker at host boot, then verify the actual behavior by restarting the Docker service during a maintenance window and checking docker compose ps afterward.

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

Persist state outside the container layer

Files written only to a container’s writable layer disappear when that container is removed. Docker’s quickstart documentation specifically warns that docker compose down removes containers and their writable-layer data. A named volume survives ordinary container replacement:

docker volume ls
docker compose down
docker compose up -d

After recreation, verify that expected files remain under /var/lib/telegram-bot. Do not use docker compose down -v on a project whose volumes contain data you need; the -v option removes named volumes. For larger, shared, or highly available workloads, put state in an external database rather than relying on a local volume tied to one host.

Operational checks that catch real failures

  • Configuration: run docker compose config and confirm the intended files, service names, secret mounts, and restart policy.
  • Secret presence: verify the token file exists on the host and is readable by Docker, but never print its contents. A missing or empty file should make the process fail clearly.
  • Process health: use docker compose ps and the health status. The sample check proves only that PID 1 exists; add an application-local heartbeat if you need to detect a wedged event loop.
  • Logs: follow docker compose logs -f bot, configure bounded retention, and forward logs to an external aggregator when continuous operation requires centralized search or alerting.
  • Shutdown: run docker compose stop and confirm PTB exits cleanly on SIGTERM. A graceful stop prevents partial writes and makes restart behavior predictable.
  • Persistence: recreate the container deliberately and verify the volume or external database still contains a known test record.
  • Network mode: for polling, outbound HTTPS is sufficient; for webhooks, test DNS, certificate validity, proxy routing, firewall rules, and Telegram’s ability to reach the public URL.

Troubleshooting by symptom

The container exits immediately

Read the first traceback with docker compose logs bot. A missing secret, an empty token, an import error, or a malformed requirements lock file is more likely than a Docker networking problem. Confirm the mounted path inside a temporary diagnostic container without printing the token, and verify that the command points to the module copied into the image.

The bot starts but receives no messages in polling mode

Check that another process is not consuming the same token and that a webhook is not still registered. Confirm outbound DNS and HTTPS from the host or container. Do not expose a port merely to fix polling; polling does not require inbound traffic. If you changed from webhook to polling, remove the webhook according to your Bot API procedure and decide what to do with queued updates.

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

Webhook requests return errors

Work from the outside in: resolve the hostname, inspect the certificate chain, check the proxy access log, verify the forwarded path and HTTP method, then inspect the bot log. A wrong secret header should be rejected; a missing header indicates a proxy or registration configuration problem. Ensure the internal bot port is reachable on the Compose network but not accidentally published to the internet.

The bot loses data after an update

Determine whether the data lived in the container layer, a named volume, or an external database. If it was in the layer, it was never durable. If it was in a named volume, check that the new Compose project name and volume declaration refer to the same volume and that no one ran down -v. For database migrations, restore from a tested backup rather than deleting the volume.

The container is healthy but the bot is stuck

The sample kill -0 1 check cannot detect a blocked event loop or failed Telegram requests. Add a local heartbeat file or a private health endpoint updated by the application, and make the check test that signal’s freshness. Keep that check local unless it genuinely measures the dependency you claim it measures; do not label a process check as proof of Telegram reachability.

Security and maintenance checklist

  • Never commit the Telegram token, .env files, private keys, or certificate material.
  • Prefer Compose secrets and grant each secret only to the service that needs it.
  • Run as a non-root user and keep the application filesystem read-only where the framework permits.
  • Pin the Python image and PTB version you tested; review updates in a controlled rebuild.
  • Keep logs bounded and avoid logging authorization headers, tokens, or full update payloads that contain personal data.
  • Use depends_on: condition: service_healthy for startup ordering, while retaining runtime retry logic.
  • Use a reverse proxy for webhook TLS when the bot should listen on plain HTTP internally.
  • Test SIGTERM shutdown, container replacement, reboot recovery, volume persistence, and rollback before launch.

This design gives a small bot a reproducible Compose lifecycle without pretending that one health check, restart policy, or volume solves every operational problem. Polling is the low-friction default; a webhook is a deliberate network architecture with TLS, routing, and header-validation work. Whichever mode you choose, keep the token out of images, make state durable, and verify the exact behavior you rely on.

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

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.