Docker for Trading Bots: Consistent Deployment Environments
The day I first deployed my bot to a server, I burned three hours. Code that ran fine locally died on the server with ModuleNotFoundError. Different Python version, different package versions, different timezone. The classic "but it works on my machine" problem. Docker attacks it head-on.
What Docker Actually Does for You
Docker bundles your program together with everything it needs to run—OS libraries, Python version, packages—into a single image. Run that image and you get a container.
Think of an image as a frozen meal. You're not sourcing ingredients on arrival; you're shipping a finished dish, frozen whole. Whatever is in the destination kitchen, you reheat and it tastes the same. The image you built locally and the image running on the server are bit-for-bit identical, so "environment difference" stops being a variable at all.
Overkill for a personal project? I'd argue the opposite. The more solo the project, the fewer people exist to remember how the environment was set up—just you. The Dockerfile is that record.
A Minimal Dockerfile for a Bot
Let's use a Python trading bot as the baseline. Put the file at the project root, named Dockerfile.
# 1) Base image — pin the tag exactly
FROM python:3.12-slim
# 2) Timezone (explained again below)
ENV TZ=Asia/Seoul
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 3) Keep logs from getting stuck in the buffer
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# 4) Dependencies first — this is what keeps the cache alive
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 5) Then the source code
COPY . .
CMD ["python", "-u", "bot.py"]
The order of steps 4 and 5 matters. Docker caches the result of every instruction, so putting what doesn't change on top makes rebuilds fast. Change one line of code and pip install won't re-run. That single ordering choice takes a rebuild from two minutes to five seconds.
In FROM python:3.12-slim, always pin the tag. Leave it as python:latest and one day Python bumps a version and your bot quietly breaks. You adopted Docker to freeze the environment—leaving the tag floating defeats the entire purpose.
Keep Config and Logs Outside the Container
The most common beginner mistake is baking a config file containing API keys into the image. Images get copied, shared, and pushed to registries. Bake in a key and you're distributing that key.
The rule is simple: anything that varies gets injected from outside via a volume (bind mount).
- Config file — mount the host's
config.iniread-only - Logs — if they accumulate inside the container they vanish on restart. Push them to a host folder
- Database / fill records — volumes, same reasoning
And always write a .dockerignore. It stops COPY . . from accidentally sweeping up your key files.
.git
__pycache__/
*.log
config.ini
.env
data/
Locking It In with docker compose
Type run options by hand every time and eventually you'll drop one. Nail them down in a single compose.yaml.
services:
trading-bot:
build: .
container_name: trading-bot
restart: unless-stopped
environment:
- TZ=Asia/Seoul
- API_KEY=${API_KEY} # injected from .env, never written into this file
- API_SECRET=${API_SECRET}
volumes:
- ./config.ini:/app/config.ini:ro # ro = read-only
- ./logs:/app/logs
logging:
driver: "json-file"
options:
max-size: "10m" # prevent unbounded log growth
max-file: "3"
Now docker compose up -d brings it up in one line, and docker compose logs -f tails it live. Moving to a new server? Carry this file and your config; that's the whole migration.
restart: unless-stopped pays off noticeably in production. If the bot dies on an exception or the server reboots, Docker brings it back automatically. But treat this as a safety net, not a fix—you still have to find out why it died. Check the logs so you don't end up in an infinite restart loop.
Two Traps You Will Definitely Hit
1. Timezone. Most Linux images run on UTC. Code that decides "market opens at 9 AM Korean time" is off by nine hours inside the container. Don't skip the TZ setting in the Dockerfile above. The safer approach is to handle timezones explicitly in your code in the first place.
2. Log buffering. Python collects output in a buffer when stdout is a pipe and flushes it all at once. So you stare at docker logs and see nothing for a long while, and assume the bot has hung. Turn it off with PYTHONUNBUFFERED=1 or python -u.
When You're Better Off Without It
Honestly, Docker isn't always the answer.
- Bots that need a GUI — programs that put something on screen are awkward in containers
- Anything using Windows-only APIs — a COM-based brokerage API, for instance, cannot move into a Linux container
- A one-day experiment script — just run
python
The test is this: "Will I ever need to run this on another machine?" If yes, Docker earns its keep.
Summary
- Docker freezes the whole runtime environment, killing "but it works on my machine"
- Pin the base image tag exactly; never
latest - COPY
requirements.txtbefore the source to keep the build cache useful - API keys, config, and logs go in via volumes and environment variables—never baked into the image;
.dockerignoreis mandatory - Pin run options in
compose.yamland get auto-recovery fromrestart: unless-stopped - Timezone defaults to UTC and Python logs are buffered—disable both explicitly
Your first Dockerfile takes thirty minutes to write. Those thirty minutes permanently erase the three hours you used to repeat on every server migration. If you plan to run the bot for a long time, it's worth the upfront investment.
댓글
댓글 쓰기