Recommended Free Tools
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.
Need a server to try this on? Antihost offers Linux VPS plans with NVMe storage, full root access and a 14-day free trial.
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.
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
getUpdatesrequests. 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.
#1 Best Overall
| 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.
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.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →__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.
Rank #2
- Create a directory that is ignored by Git:
mkdir -p secrets. - Write only the Bot API token to
secrets/telegram_token.txt, with no explanatory text. - Restrict the host file, for example with
chmod 600 secrets/telegram_token.txt. - Add
secrets/and.envto.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.
Start, inspect, and test locally
- 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. - Build and start the service:
docker compose up --build -d. - Watch the bot logs:
docker compose logs -f bot. A successful startup should not print the token. - Check state and health with
docker compose ps. The service should move from starting to healthy after/tmp/bot-readyis created. - Send
/startto 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.
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.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →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.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteapplication.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.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsUnderstand 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.
Security and reliability checklist
- Never commit the Telegram token, webhook secret,
.envfile, 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
/tmpand a state volume. - Keep healthchecks honest: a file or local command can prove process readiness, but not Telegram API reachability.
- Use
depends_onwithservice_healthyfor 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.
Best Value
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.
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.
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
- Write the bot module with one process entry point and a local readiness signal.
- Select and pin compatible Python and PTB versions.
- Build a small non-root image that never copies secrets.
- Create the token file, protect its permissions, and declare it as a Compose secret.
- Add the bot service, healthcheck, restart policy, logging limits, and only the volume paths that need persistence.
- Start with
docker compose up --build, then inspectdocker compose psanddocker compose logs -f bot. - Use polling unless a public webhook solves a specific requirement; clear any old webhook before polling.
- For webhooks, install the PTB extra, terminate TLS at a proxy, register the HTTPS URL, and enforce the secret header.
- Keep production settings in an override file and remove development bind mounts.
- Update with
docker compose build botfollowed bydocker 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.
Quick Recap
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.




