Deploying Telegram Bots Using Docker Compose

Deploying Telegram Bots Using Docker Compose

What You’ll Need

  • A virtual private server such as a Hetzner VPS, Contabo VPS, or DigitalOcean instance running Ubuntu 22.04 LTS or higher.
  • A domain name registered via Namecheap if you plan to route secure incoming webhooks to your bot container.
  • Docker Engine and Docker Compose installed on your local development machine or remote server.
  • A Telegram API token generated by messaging BotFather on Telegram.

Table of Contents


Architecture and Bot Setup

When I build Telegram bots meant for continuous operation, relying on simple background scripts or plain process managers like PM2 quickly shows limitations. Network drops happen, memory leaks occur, and dependency drift between local development and production servers can bring down your service. Containerizing your Telegram bot with Docker ensures that your environment remains completely reproducible regardless of where you deploy it.

In this architecture, we will build an asynchronous Telegram bot in Python using the python-telegram-bot framework. We will connect it to a Redis instance for managing state and caching data. If you are building automated pipelines or background data gathering services, you can expand this stack by referencing my guide on building resilient scrapers with Playwright and Redis.

To kick off our project, I will create a project directory structure containing our bot application, dependencies, environment configuration, Dockerfile, and Compose specification:

telegram-bot-docker/
├── app/
│   ├── __init__.py
│   ├── bot.py
│   └── database.py
├── .env.example
├── Dockerfile
├── docker-compose.yml
└── requirements.txt

Below is the complete code for requirements.txt. Every dependency is explicitly pinned to avoid unexpected breaking updates during automated builds:

python-telegram-bot==20.7
redis==5.0.1
hiredis==2.3.2
python-dotenv==1.0.0

Next, let us write the application code inside app/database.py to handle connections to our Redis caching container:

import os
import redis.asyncio as redis

REDIS_HOST = os.getenv("REDIS_HOST", "redis")
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
REDIS_DB = int(os.getenv("REDIS_DB", 0))

redis_client = redis.Redis(
    host=REDIS_HOST,
    port=REDIS_PORT,
    db=REDIS_DB,
    decode_responses=True
)

async def set_user_state(user_id: int, key: str, value: str) -> None:
    redis_key = f"user:{user_id}:{key}"
    await redis_client.set(redis_key, value)

async def get_user_state(user_id: int, key: str) -> str | None:
    redis_key = f"user:{user_id}:{key}"
    return await redis_client.get(redis_key)

Now, let us write the complete implementation of app/bot.py. This script handles command routing, state updates in Redis, graceful shutdowns, and error processing:

import os
import logging
from telegram import Update
from telegram.ext import (
    Application,
    CommandHandler,
    ContextTypes,
    MessageHandler,
    filters,
)
from app.database import set_user_state, get_user_state

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

async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user = update.effective_user
    if user is None or update.message is None:
        return
    
    await set_user_state(user.id, "last_action", "start")
    welcome_text = f"Hello {user.first_name}! Your bot is running smoothly inside Docker."
    await update.message.reply_text(welcome_text)

async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user = update.effective_user
    if user is None or update.message is None:
        return
    
    last_action = await get_user_state(user.id, "last_action")
    status_text = f"System: Operational\nLast Recorded Action: {last_action}"
    await update.message.reply_text(status_text)

async def echo_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    user = update.effective_user
    if user is None or update.message is None or update.message.text is None:
        return
    
    text = update.message.text
    await set_user_state(user.id, "last_action", "echo")
    await update.message.reply_text(f"Echo: {text}")

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

def main() -> None:
    token = os.getenv("TELEGRAM_BOT_TOKEN")
    if not token:
        raise ValueError("TELEGRAM_BOT_TOKEN is missing from environment variables.")

    application = Application.builder().token(token).build()

    application.add_handler(CommandHandler("start", start_command))
    application.add_handler(CommandHandler("status", status_command))
    application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo_handler))
    application.add_error_handler(error_handler)

    logger.info("Starting Telegram Bot long polling cycle...")
    application.run_polling(allowed_updates=Update.ALL_TYPES)

if __name__ == "__main__":
    main()

Containerizing the Application with Docker

To deploy this script reliably on a host like a Hetzner VPS, I write explicit Dockerfiles using multi-stage builds or slim base images. Using python:3.11-slim provides a minimal runtime surface while drastically reducing image size compared to full Linux distribution images.

We must ensure that Python output is unbuffered so that log entries stream immediately to the Docker log collector. If you want to analyze and centralize these output streams, inspect my walkthrough on building structured log pipelines with Fluent Bit.

Here is the production Dockerfile placed in the root directory:

FROM python:3.11-slim AS base

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=off \
    PIP_DISABLE_PIP_VERSION_CHECK=on \
    PYTHONPATH=/app

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY app/ ./app/

RUN useradd -m -u 1000 botuser && chown -R botuser:botuser /app
USER botuser

CMD ["python", "-m", "app.bot"]

This setup drops privileges from root to botuser at the end of the container build process, adhering strictly to container security best practices.


Orchestrating Services with Docker Compose

💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.

Docker Compose allows us to bind our application service and cache layers into a coherent multi-container network. We define container health checks, dependency ordering, environment variable files, and resource limits inside a single docker-compose.yml file.

Below is the complete docker-compose.yml configuration:

version: '3.8'

services:
  bot:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: telegram_bot_app
    restart: unless-stopped
    env_file:
      - .env
    environment:
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    depends_on:
      redis:
        condition: service_healthy
    networks:
      - bot_network
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  redis:
    image: redis:7.2-alpine
    container_name: telegram_bot_redis
    restart: unless-stopped
    command: redis-server --save 60 1 --loglevel notice
    volumes:
      - redis_data:/data
    ports:
      - "127.0.0.1:6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - bot_network

networks:
  bot_network:
    driver: bridge

volumes:
  redis_data:
    driver: local

The configuration enforces a strict dependency using condition: service_healthy. The Telegram bot container will not initiate execution until Redis passes internal ping tests. Log outputs are clamped at 10 megabytes across three rotations to prevent disk exhaustion on smaller server instances.


Handling Environment Variables and Webhooks

For small to medium workloads, long polling (application.run_polling()) works exceptionally well. However, high-throughput bots require an incoming HTTP webhook system. To route encrypted traffic safely into Docker containers, you need a reverse proxy handling automatic SSL certificates. You can set this up easily by reading my complete tutorial on configuring Traefik reverse proxy for container deployments.

Whether using webhooks or long polling, secrets must never be hardcoded in your repository. I use .env files locally and secure host environment mounts in production.

Here is the full example .env.example file required for this build:

TELEGRAM_BOT_TOKEN=1234567890:ABCdefGhIJKlmNoPQRsTUVwxyZ
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DB=0

Copy this template to .env on your target server and replace the placeholder token with your active credential from BotFather:

cp .env.example .env

Production Hardening and Deployment

To deploy your containerized Telegram bot on your cloud provider, connect to your server via SSH and run through the system initialization process.

First, clone your application source code and navigate into the root directory:

git clone https://github.com/yourusername/telegram-bot-docker.git
cd telegram-bot-docker

Create your production .env file containing your valid bot credentials:

cat <<EOF > .env
TELEGRAM_BOT_TOKEN=6819203948:AAExmP1a2b3c4d5e6f7g8h9i0j
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DB=0
EOF

Start the container stack in detached mode using Docker Compose:

docker compose up -d --build

You can verify that all services are healthy and running using the following command:

docker compose ps

To view live container logs in real time, use the command below:

docker compose logs -f bot

If you need to make changes to your bot code, pull the updates and re-deploy without experiencing extended downtime:

git pull origin main
docker compose up -d --build --no-deps bot

To ensure that Docker starts automatically after system reboots on your server, enable the host Docker service:

sudo systemctl enable docker
sudo systemctl enable containerd

This production setup guarantees that if your process experiences unexpected unhandled exceptions, Docker will automatically restart the service using the specified unless-stopped restart policy.


Getting Started

Deploying production workloads requires reliable server platforms and smooth network access. If you are preparing your deployment environment, choose an provider such as a Hetzner VPS, Contabo VPS, or DigitalOcean droplet. If you are routing webhooks over custom domain names, configure your DNS records using Namecheap.

Outsource Your Automation

Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.

Want to automate this yourself?

Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.

Want this engine running on your own VPS?

This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.

Get System 3
system online