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
- Containerizing the Application with Docker
- Orchestrating Services with Docker Compose
- Handling Environment Variables and Webhooks
- Production Hardening and Deployment
- Getting Started
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