BotNetwork

Deploying a Python bot with Docker and Docker Compose

A Docker image turns "it works on my machine" into "it works on any machine with Docker". For a bot, a dozen lines of configuration are enough.

Published 4 min read

Why containerize a bot

  • Reproducible builds. The same Python version and pinned dependencies everywhere.
  • Portable hosting. The image runs on a VPS, on platforms that build from a Dockerfile (Railway, Fly.io, Render) or on a home server.
  • Restarts for free. Docker's restart policy brings the bot back after a crash or a reboot.

If you have not decided where to run it yet, see PaaS vs VPS for bots and our overview deploying a Python bot.

Project layout

mybot/
  bot/
    __init__.py
    main.py
  requirements.txt
  Dockerfile
  .dockerignore
  compose.yaml
  .env            # never committed

Pin dependencies in requirements.txt (for example python-telegram-bot==22.* or discord.py==2.*) so a rebuild next month installs the same major version.

The Dockerfile

FROM python:3.12-slim

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

# Dependencies first, so code changes do not reinstall them
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY bot/ bot/

# Run as an unprivileged user
RUN useradd --create-home --uid 10001 app
USER app

# Exec form: Python is PID 1 and receives SIGTERM directly
CMD ["python", "-m", "bot.main"]

Details that matter:

  • PYTHONUNBUFFERED=1 makes print and logging appear immediately in docker logs. Without it, output can sit in a buffer and you see nothing when the bot crashes.
  • Copying requirements.txt before the code keeps the dependency layer cached.
  • Exec form (["python", ...]) instead of shell form (python -m bot.main). In shell form /bin/sh becomes PID 1 and may not forward signals, so docker stop waits for its grace period and then kills the bot. See graceful shutdown for bots.
  • The -slim image is a good default. Alpine images use musl, which can make some Python wheels slower to install or require compilers.

.dockerignore

.env
.git
__pycache__/
*.pyc
.venv/
tests/

Excluding .env matters: without it, COPY . . would bake your token into an image layer, and anyone with the image could extract it.

compose.yaml

services:
  bot:
    build: .
    restart: unless-stopped
    env_file: .env
    stop_grace_period: 20s
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
  • restart: unless-stopped restarts the bot after crashes and after the Docker daemon starts on boot, unless you stopped it manually.
  • env_file injects TELEGRAM_TOKEN, DISCORD_TOKEN and friends at runtime. Validate the file with our .env validator.
  • The default json-file log driver keeps growing unless you cap it; max-size and max-file stop a chatty bot from filling the disk.

Start it and follow the logs:

docker compose up -d --build
docker compose logs -f bot

Webhook bots: expose a port

A bot that receives webhooks (WhatsApp Cloud API, Telegram in webhook mode) must listen on a port and sit behind HTTPS:

    ports:
      - "127.0.0.1:8080:8080"
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3)"]
      interval: 30s
      timeout: 5s
      retries: 3

Binding to 127.0.0.1 keeps the port private; put a reverse proxy such as Caddy or nginx in front for TLS. On a PaaS, read the port from the PORT environment variable instead. Polling and gateway bots (Discord) need no open port at all.

Running it in production

  • VPS: install Docker Engine, copy the project (or pull the image from a registry), create .env with chmod 600, then docker compose up -d. Our Vultr VPS tutorial covers the server hardening part.
  • PaaS: platforms like Railway detect the Dockerfile and build it; set variables in the dashboard instead of an .env file. See deploy a bot on Railway.
  • Updates: git pull && docker compose up -d --build rebuilds and replaces the container. Run only one replica of a polling or gateway bot, or it will receive every event twice (Telegram returns a 409 Conflict to the second poller).

Frequently asked questions

Do I need Docker to deploy a Python bot?

No. A virtualenv plus a systemd unit works well on a VPS. Docker helps when you want the same setup on several hosts or a platform that builds images.

Why is my container restarting in a loop?

The bot exits on start, usually because a required environment variable is missing or the token is invalid. Read docker compose logs bot; with PYTHONUNBUFFERED=1 the traceback is there.

How do I keep data across rebuilds?

Use a named volume for files such as a SQLite database (volumes: ["botdata:/app/data"]), or a managed database. Anything written inside the container is lost when it is recreated.

Sources