Docker Development
This guide covers running Tradeboard in Docker on your own machine. Tradeboard ships its own Dockerfile, docker-compose.yaml and .dockerignore in the repository root, so you do not write any of them yourself.
Prerequisites
- Docker Engine
- Docker Compose v2 (the
docker composesubcommand, not the standalonedocker-composebinary) - Git
Two ways to run it
| Approach | Command | When to use |
|---|---|---|
| Pre-built image | install/docker-run.sh (or install\docker-run.bat on Windows) | You just want Tradeboard running. Pulls wesoftcorp/tradeboard-docs:latest, no build step. |
| Build from source | docker compose up -d --build | You are changing the code and want your changes in the image. |
The rest of this page covers the build-from-source path.
Essential .env Changes for Docker
Start from the template, then change only what Docker needs:
cp .sample.env .env1. WebSocket host
# The WebSocket proxy must bind all interfaces inside the container
# so the published port can reach it from the host
WEBSOCKET_HOST='0.0.0.0'
WEBSOCKET_PORT='8765'
WEBSOCKET_URL='ws://127.0.0.1:8765'WEBSOCKET_URL is the address the browser uses, so it stays on the host's loopback address, not the container's.
2. Flask host
FLASK_HOST_IP='0.0.0.0'
FLASK_PORT='5000'The container entrypoint binds Gunicorn to 0.0.0.0:5000 regardless, but install-docker.sh sets FLASK_HOST_IP too so the value in .env matches what is actually happening.
3. Leave ZeroMQ on loopback
ZMQ_HOST='127.0.0.1'
ZMQ_PORT='5555'DANGER
Do not set ZMQ_HOST to 0.0.0.0. ZeroMQ is the internal, unauthenticated bus between the broker WebSocket adapters and the WebSocket proxy. Both run inside the same container, so loopback is sufficient. Binding it to all interfaces publishes the raw tick feed to anything that can reach the port. The official install-docker.sh deliberately rewrites WEBSOCKET_HOST and FLASK_HOST_IP and deliberately leaves ZMQ_HOST alone.
Summary of changes
| Key | Local development | Docker |
|---|---|---|
FLASK_HOST_IP | 127.0.0.1 | 0.0.0.0 |
WEBSOCKET_HOST | 127.0.0.1 | 0.0.0.0 |
ZMQ_HOST | 127.0.0.1 | 127.0.0.1 (unchanged) |
Everything else, including broker credentials and database URLs, stays as it is.
What the shipped Dockerfile does
You do not need to create it, but it helps to know what is in the image:
- Python builder stage on
python:3.12-bullseye: creates/app/.venvwithuv syncfrompyproject.toml, then addsgunicorn>=25.0,<26andeventlet. - Frontend builder stage on
node:22-bullseye-slim: runsnpm ciandnpm run buildinfrontend/. - Production stage on
python:3.12-slim-bullseye: copies the virtual environment and the builtfrontend/dist, installs Chromium (needed by Kaleido for Telegram chart rendering), creates theappuseraccount pinned to UID/GID 1000, and runs/app/start.sh.
start.sh runs the database migrations, starts the WebSocket proxy on port 8765, then execs Gunicorn with the eventlet worker on port 5000.
Quick Start
# 1. Configure
cp .sample.env .env
# edit .env: broker credentials, plus the Docker host changes above
# 2. Build and start
docker compose up -d --build
# 3. Watch startup logs
docker compose logs -fTradeboard is then reachable at http://127.0.0.1:5000. Create your account at http://127.0.0.1:5000/setup.
What the compose file gives you
docker-compose.yaml defines a single service named tradeboard (container name tradeboard-web):
- Publishes
${FLASK_PORT:-5000}:5000and${WEBSOCKET_PORT:-8765}:8765 - Named volumes for
db,log,strategies,keysandtmp, so data survives a rebuild - Bind-mounts your host
./.envto/app/.env - Sets
TZ=Asia/Kolkataand caps the BLAS/OpenMP thread counts at 2 to avoidRLIMIT_NPROCexhaustion - A healthcheck against
http://127.0.0.1:5000/auth/check-setup restart: unless-stopped
Common Commands
# Start (detached)
docker compose up -d
# Start and rebuild after a code change
docker compose up -d --build
# View logs
docker compose logs -f
# Stop containers
docker compose down
# Rebuild ignoring the layer cache (after a dependency change)
docker compose build --no-cache && docker compose up -d
# Shell inside the container
docker compose exec tradeboard bash
# Container status and health
docker compose ps
docker inspect tradeboard-web --format='{{.State.Health.Status}}'INFO
docker compose down removes the containers but not the named volumes, so your database survives. docker compose down -v deletes the volumes and everything in them.
Adjusting resources
Three settings in .env are read by docker-compose.yaml and matter on small containers:
STRATEGY_MEMORY_LIMIT_MB = '1024' # per Python strategy subprocess
SHM_SIZE = '512m' # /dev/shm, roughly 25% of container RAM
OPENBLAS_NUM_THREADS = '2' # also OMP / MKL / NUMEXPR / NUMBAThey are commented out in .sample.env; uncomment them to override the defaults. For a 2 GB container use 256, 256m and 1.
Directory Structure
tradeboard/
├── Dockerfile # multi-stage: python builder, node builder, production
├── docker-compose.yaml
├── .dockerignore
├── start.sh # container entrypoint
├── .env # yours, bind-mounted into the container
├── app.py
├── pyproject.toml
├── frontend/
│ └── dist/ # pre-built React bundle, also rebuilt in the image
└── db/ # named volume in DockerDevelopment Tips
- Code changes need a rebuild. The production image copies the source in rather than mounting it, so run
docker compose up -d --buildafter editing Python code. For a tight edit-reload loop, run Tradeboard directly withuv run app.pyinstead of in Docker. - Database access. The SQLite databases live in the
tradeboard_dbnamed volume. Copy them out withdocker compose cp tradeboard:/app/db ./db-backup. - Debugging. Logs go to the console and to the
tradeboard_logvolume.log/errors.jsonlcarries structured errors with full tracebacks. - Dependencies. Add packages to
pyproject.toml, then rebuild withdocker compose build --no-cache.
Troubleshooting
Port already in use
bash# See what is holding port 5000 or 8765 sudo lsof -i :5000 sudo lsof -i :8765 docker compose down docker compose up -dYou can also move the published ports by setting
FLASK_PORTandWEBSOCKET_PORTin.env.Permission errors on the mounted
.envThe container runs as UID 1000. If the file is owned by another user the first-run secret rotation cannot write to it and the worker restarts in a loop:
bashsudo chown 1000:1000 .env sudo chmod 600 .env docker compose up -dContainer will not start
bashdocker compose logs --tail=100 docker compose down docker compose up -d --buildPackage installation issues
bashdocker compose build --no-cache docker compose up -ddocker-compose: command not foundCompose v1 is end of life. Use the
docker composesubcommand that ships with Docker Engine.
Note
The shipped configuration is production-oriented: Gunicorn, a healthcheck and restart: unless-stopped. For a public deployment with a custom domain and SSL, use install/install-docker.sh instead, which adds nginx and a Let's Encrypt certificate in front of this container.
