Containerizing a Trading Bot with Docker
A practical guide to packaging a Python trading-data service with multi-stage Docker builds, env-based secrets, Redis, healthchecks, and small images.
FactorQX is an engineering and education resource. This article covers software packaging and deployment only. It is not investment advice, a trading signal, or any kind of profit claim. The "bot" here is a generic service that validates and moves data.
A trading "bot" in production is mostly plumbing: it reads data, validates it, runs deterministic logic, and writes results somewhere. The interesting engineering problem is making that process reproducible, observable, and safe to restart at 3 a.m. without a human. Containers solve the reproducibility half. This guide walks through a clean multi-stage Docker setup for a Python service, then wires it to Redis with docker compose.
Why multi-stage builds
A naive image installs build tools, leaves caches around, and ships your .git directory. A multi-stage build compiles dependencies in a fat builder stage, then copies only the artifacts you need into a slim runtime stage. The result is a smaller attack surface and faster pulls.
FROM python:3.12-slim AS builder
ENV PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt
FROM python:3.12-slim AS runtime
RUN useradd --create-home --uid 10001 appuser
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/install/bin:$PATH" \
PYTHONPATH="/install/lib/python3.12/site-packages"
WORKDIR /app
COPY --from=builder /install /install
COPY --chown=appuser:appuser ./src ./src
USER appuser
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -m src.healthcheck || exit 1
CMD ["python", "-m", "src.main"]A few decisions worth calling out:
PYTHONUNBUFFERED=1flushes logs immediately sodocker logsshows output in real time instead of after a buffer fills.- Non-root user. If the process is compromised, it should not own the filesystem. The
--uid 10001avoids colliding with host users. HEALTHCHECKruns a tiny script that confirms the process can reach its dependencies (e.g., ping Redis) and reports ready. Orchestrators use this to decide when to route traffic or restart.
.dockerignore
Keep build context small and avoid leaking files into the image. This speeds up docker build and prevents accidental secret inclusion.
.git
.gitignore
__pycache__/
*.pyc
.venv/
.env
.env.*
tests/
*.md
.pytest_cache/
.mypy_cache/Note .env is ignored. Secrets never belong in an image layer; anyone who pulls the image can extract them with docker history and docker save.
Secrets via environment, not layers
Pass configuration in at runtime. Read it in code with sane failure if something required is missing:
import os
def require(name: str) -> str:
value = os.environ.get(name)
if not value:
raise RuntimeError(f"missing required env var: {name}")
return value
REDIS_URL = require("REDIS_URL")
DATA_API_KEY = require("DATA_API_KEY")For local development, keep values in a git-ignored .env file that docker compose loads. In production, inject them through your platform's secret manager (Kubernetes Secrets, AWS Parameter Store, etc.) rather than committing .env anywhere.
docker compose with Redis
The service depends on Redis for caching and coordination. Compose lets you bring both up together with a private network and a health-gated startup order.
services:
bot:
build: .
restart: unless-stopped
env_file: .env
environment:
REDIS_URL: "redis://redis:6379/0"
depends_on:
redis:
condition: service_healthy
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--save", "", "--appendonly", "no"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
volumes:
- redis-data:/data
volumes:
redis-data:Key points:
restart: unless-stoppedbrings the service back after crashes or host reboots, but respects a manualdocker stop. Useon-failureif you only want restarts on non-zero exit.depends_onwithcondition: service_healthywaits for Redis to pass its healthcheck before starting the bot, avoiding a connection-refused crash loop on cold start.- Log rotation (
max-size/max-file) prevents a chatty service from filling the disk. Logs go to stdout/stderr so the Docker logging driver — and any downstream aggregator — can capture them.
Keeping images small
Beyond multi-stage builds: prefer -slim base images over full ones, pin versions for reproducibility, combine RUN steps to reduce layers, and order your COPY statements so dependencies (which change rarely) are cached above your source (which changes often). Run docker history <image> to see where bytes go, and docker scout quickview to flag known CVEs before shipping.
With this setup you have a deterministic build, no secrets in the image, ordered startup, automatic restarts, rotated logs, and a healthcheck that lets your orchestrator reason about readiness — the boring infrastructure that lets the actual logic run unattended.