11 - Docker Configuration
Overview
Tradeboard provides Docker support for containerized deployment with 3-stage builds (Python builder, Frontend builder, Production), IST timezone configuration, and proper security isolation. The Docker setup uses Python 3.12, Gunicorn with Eventlet workers, and runs as a non-root user. It includes Railway/cloud deployment support with automatic .env generation.
Architecture Diagram
┌───────────────────────────────────────────────────────────────────────────────┐
│ Docker Architecture (3-Stage Build) │
└───────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────┐
│ Stage 1: Python Builder │
│ (python:3.12-bullseye) │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ 1. Install build dependencies (curl, build-essential) │ │
│ │ 2. Copy pyproject.toml │ │
│ │ 3. Create virtual environment with uv │ │
│ │ 4. Install dependencies: uv sync │ │
│ │ 5. Add gunicorn>=25.0,<26 and eventlet │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────┐
│ Stage 2: Frontend Builder │
│ (node:22-bullseye-slim) │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ 1. Copy frontend/package*.json │ │
│ │ 2. npm ci │ │
│ │ 3. Copy frontend source │ │
│ │ 4. npm run build (React production build) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────┐
│ Stage 3: Production │
│ (python:3.12-slim-bullseye) │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ 1. Set timezone to IST (Asia/Kolkata) │ │
│ │ 2. Install runtime deps (curl, libopenblas0, libgomp1, │ │
│ │ libgfortran5, chromium, fonts-liberation) │ │
│ │ 3. Create non-root user appuser pinned to UID/GID 1000 │ │
│ │ 4. Copy venv from python-builder │ │
│ │ 5. Copy application source │ │
│ │ 6. Copy frontend/dist from frontend-builder │ │
│ │ 7. Create directories (log, db, strategies, keys, tmp, numba_cache) │ │
│ │ 8. Set permissions (keys: 700, others: 755) │ │
│ │ 9. Run as appuser │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────┐
│ Container Runtime (start.sh) │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ 1. Railway/Cloud Detection & .env Generation │ │
│ │ - Detects HOST_SERVER environment variable │ │
│ │ - Auto-generates .env with all required variables │ │
│ │ - Supports 40+ configuration options │ │
│ │ 2. Directory Setup │ │
│ │ 3. Database Migrations (if /app/upgrade/migrate_all.py exists) │ │
│ │ 4. WebSocket Proxy (background, PID tracked) │ │
│ │ 5. Signal Handling (SIGTERM, SIGINT cleanup) │ │
│ │ 6. Gunicorn with Eventlet │ │
│ │ - Single worker (-w 1) for WebSocket compatibility │ │
│ │ - Timeout: 300s, Graceful timeout: 30s │ │
│ │ - Worker temp dir: /tmp/gunicorn_workers │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ Exposed Ports: │
│ - 5000: Flask application (or PORT env var for Railway) │
│ - 8765: WebSocket proxy │
│ - 5555: ZeroMQ message bus (internal) │
└───────────────────────────────────────────────────────────────────────────────┘Dockerfile
# ------------------------------ Python Builder Stage ----------------------- #
FROM python:3.12-bullseye AS python-builder
RUN apt-get update && apt-get install -y --no-install-recommends curl build-essential && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY pyproject.toml .
# Isolated virtual-env with uv, then gunicorn and eventlet on top
RUN pip install --no-cache-dir uv && uv venv .venv && uv pip install --upgrade pip && uv sync && uv pip install "gunicorn>=25.0,<26" eventlet && rm -rf /root/.cache
# ------------------------------ Frontend Builder Stage --------------------- #
FROM node:22-bullseye-slim AS frontend-builder
WORKDIR /app
COPY frontend/package*.json ./frontend/
RUN cd frontend && npm ci
COPY frontend/ ./frontend/
RUN cd frontend && npm run build
# ------------------------------ Production Stage --------------------------- #
FROM python:3.12-slim-bullseye AS production
# Timezone plus runtime deps. chromium and fonts-liberation are required by
# Kaleido 1.x (plotly static image export), which drives a real headless
# Chromium. Without them the Telegram bot's /chart silently fails in Docker.
RUN apt-get update && apt-get install -y --no-install-recommends tzdata curl libopenblas0 libgomp1 libgfortran5 chromium fonts-liberation && ln -fs /usr/share/zoneinfo/Asia/Kolkata /etc/localtime && dpkg-reconfigure -f noninteractive tzdata && apt-get clean && rm -rf /var/lib/apt/lists/*
# Pin appuser to UID/GID 1000. The install scripts chown the host .env to
# UID 1000 before bind-mounting it, so a different UID makes .env unwritable.
RUN groupadd --gid 1000 appuser && useradd --create-home --uid 1000 --gid 1000 appuser
WORKDIR /app
COPY --from=python-builder --chown=appuser:appuser /app/.venv /app/.venv
COPY --chown=appuser:appuser . .
COPY --from=frontend-builder --chown=appuser:appuser /app/frontend/dist /app/frontend/dist
# chown /app itself, not just its contents. WORKDIR creates /app as root:root
# 755 and that ownership survives COPY --chown, which would stop appuser
# creating temp files in /app (for example the atomic .env rewrite).
RUN mkdir -p /app/log /app/log/strategies /app/db /app/tmp /app/tmp/numba_cache /app/tmp/matplotlib /app/strategies /app/strategies/scripts /app/strategies/examples /app/keys && chown appuser:appuser /app && chown -R appuser:appuser /app/log /app/db /app/tmp /app/strategies /app/keys && chmod -R 755 /app/strategies /app/log /app/tmp && chmod 700 /app/keys && touch /app/.env && chown appuser:appuser /app/.env && chmod 666 /app/.env
COPY --chown=appuser:appuser start.sh /app/start.sh
RUN sed -i 's/
$//' /app/start.sh && chmod +x /app/start.sh
# ---- RUNTIME ENVS --------------------------------------------------------- #
# Thread caps prevent RLIMIT_NPROC exhaustion in containers (issue #822)
ENV PATH="/app/.venv/bin:$PATH" PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 TZ=Asia/Kolkata APP_MODE=standalone TMPDIR=/app/tmp NUMBA_CACHE_DIR=/app/tmp/numba_cache LLVMLITE_TMPDIR=/app/tmp MPLCONFIGDIR=/app/tmp/matplotlib OPENBLAS_NUM_THREADS=2 OMP_NUM_THREADS=2 MKL_NUM_THREADS=2 NUMEXPR_NUM_THREADS=2 NUMBA_NUM_THREADS=2 BROWSER_PATH=/usr/bin/chromium CHROME_BIN=/usr/bin/chromium
USER appuser
EXPOSE 5000
CMD ["/app/start.sh"]The block above reproduces the directives of the real Dockerfile with its longer rationale comments condensed. Read Dockerfile itself for the full commentary.
Docker Compose
# docker-compose.yaml (note the .yaml extension, not .yml)
services:
tradeboard:
image: tradeboard:latest
build:
context: .
dockerfile: Dockerfile
container_name: tradeboard-web
ports:
- "${FLASK_PORT:-5000}:5000"
- "${WEBSOCKET_PORT:-8765}:8765"
# persistent DB, strategies, logs + mount the host .env read-only so dotenv can read it
volumes:
- tradeboard_db:/app/db
- tradeboard_log:/app/log # Application logs (named volume)
- tradeboard_strategies:/app/strategies # Python strategies (named volume)
- tradeboard_keys:/app/keys # API keys/certificates (named volume)
- tradeboard_tmp:/app/tmp # Temporary directory for numba/scipy (named volume)
- ./.env:/app/.env
# (optional) extra env-vars that are NOT in .env
environment:
- FLASK_ENV=${FLASK_ENV:-production}
- FLASK_DEBUG=${FLASK_DEBUG:-0}
- TZ=Asia/Kolkata
# Limit OpenBLAS/NumPy threads to prevent RLIMIT_NPROC exhaustion
# See: https://github.com/wesoftcorp/tradeboard-docs/issues/822
- OPENBLAS_NUM_THREADS=${OPENBLAS_NUM_THREADS:-2}
- OMP_NUM_THREADS=${OMP_NUM_THREADS:-2}
- MKL_NUM_THREADS=${MKL_NUM_THREADS:-2}
- NUMEXPR_NUM_THREADS=${NUMEXPR_NUM_THREADS:-2}
# Numba JIT compiler settings
- NUMBA_NUM_THREADS=${NUMBA_NUM_THREADS:-2}
# Strategy memory limit (MB) - reduce for low-memory containers
# 2GB container with 5 strategies: set to 256
- STRATEGY_MEMORY_LIMIT_MB=${STRATEGY_MEMORY_LIMIT_MB:-1024}
# Shared memory for scipy/numba operations
# Recommended: 25% of container RAM (min 128m, max 2g)
# 2GB container: 256m | 4GB: 512m | 8GB: 1g | 16GB+: 2g
shm_size: ${SHM_SIZE:-512m}
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:5000/auth/check-setup"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
# Define named volumes for persistence
volumes:
tradeboard_db:
driver: local
tradeboard_log:
driver: local
tradeboard_strategies:
driver: local
tradeboard_keys:
driver: local
tradeboard_tmp:
driver: localNamed Volumes vs Bind Mounts
| Approach | Pros | Cons |
|---|---|---|
| Named Volumes (recommended) | Better performance, managed by Docker | Data in Docker's volume directory |
Bind Mounts (./db:/app/db) | Easy access to files | Permission issues possible |
Directory Structure
Container /app/
├── .venv/ # Python virtual environment
├── frontend/
│ └── dist/ # Built React frontend (from frontend-builder stage)
├── db/ # SQLite databases (mounted volume)
│ ├── tradeboard.db
│ ├── logs.db
│ ├── latency.db
│ ├── sandbox.db
│ └── historify.duckdb
├── log/ # Log files (mounted volume)
│ └── strategies/
├── strategies/ # User strategies (mounted volume)
│ ├── scripts/
│ └── examples/
├── tmp/ # Temporary files (internal volume)
│ ├── numba_cache/ # Numba JIT cache
│ └── matplotlib/ # Matplotlib config
├── keys/ # Encryption keys (700 permissions)
├── .env # Environment configuration (666 for Railway)
├── start.sh # Entrypoint script (246 lines)
├── upgrade/
│ └── migrate_all.py # Database migrations (run on startup)
└── app.py # Main applicationStart Script
The start.sh script is a sophisticated 246-line entrypoint that handles:
- Railway/Cloud Environment Detection - Auto-generates
.envfrom environment variables - Directory Setup - Creates required directories with proper permissions
- Database Migrations - Runs
upgrade/migrate_all.pyif present - WebSocket Proxy - Starts in background with PID tracking
- Signal Handling - Graceful shutdown on SIGTERM/SIGINT
- Gunicorn Startup - Eventlet worker with optimized settings
#!/bin/bash
# start.sh (simplified overview - actual script is 246 lines)
echo "[Tradeboard] Starting up..."
# ============================================
# RAILWAY/CLOUD ENVIRONMENT DETECTION
# ============================================
# If HOST_SERVER is set and no .env exists, auto-generate .env
# with 40+ configuration variables including:
# - Broker configuration
# - Database URLs
# - CORS, CSP, CSRF settings
# - Rate limiting
# - WebSocket/ZeroMQ configuration
# ============================================
# DIRECTORY SETUP
# ============================================
for dir in db log log/strategies strategies strategies/scripts keys; do
mkdir -p "$dir" 2>/dev/null || true
done
# ============================================
# DATABASE MIGRATIONS
# ============================================
if [ -f "/app/upgrade/migrate_all.py" ]; then
/app/.venv/bin/python /app/upgrade/migrate_all.py
fi
# ============================================
# WEBSOCKET PROXY SERVER
# ============================================
/app/.venv/bin/python -m websocket_proxy.server &
WEBSOCKET_PID=$!
# ============================================
# SIGNAL HANDLING
# ============================================
cleanup() {
echo "[Tradeboard] Shutting down..."
kill $WEBSOCKET_PID 2>/dev/null
exit 0
}
trap cleanup SIGTERM SIGINT
# ============================================
# GUNICORN STARTUP
# ============================================
APP_PORT="${PORT:-5000}" # Railway uses PORT env var
mkdir -p /tmp/gunicorn_workers
exec /app/.venv/bin/gunicorn \
--worker-class eventlet \
--workers 1 \
--bind 0.0.0.0:${APP_PORT} \
--timeout 300 \
--graceful-timeout 30 \
--worker-tmp-dir /tmp/gunicorn_workers \
--log-level warning \
app:appKey Differences from Simple Script
| Feature | Old (6 lines) | Actual (341 lines) |
|---|---|---|
| Cloud Support | None | Full Railway/Render support |
| .env Generation | None | 40+ variables auto-generated |
| Migrations | None | Auto-runs on startup |
| Signal Handling | None | Graceful shutdown |
| Timeout | 120s | 300s |
| Graceful Timeout | None | 30s |
| Worker Temp Dir | Default | /tmp/gunicorn_workers |
| Control Socket | Enabled | Disabled with --no-control-socket |
| Compromised-key preflight | None | Blocks startup on the known leaked APP_KEY/API_KEY_PEPPER when .env is not writable |
Build Commands
# Build image
docker build -t tradeboard .
# Run container
docker run -d \
--name tradeboard \
-p 5000:5000 \
-p 8765:8765 \
-v $(pwd)/db:/app/db \
-v $(pwd)/log:/app/log \
-v $(pwd)/.env:/app/.env:ro \
tradeboard
# View logs
docker logs -f tradeboard
# Stop container
docker stop tradeboard
# Remove container
docker rm tradeboardDocker Compose Commands
# Start services
docker-compose up -d
# View logs
docker-compose logs -f
# Stop services
docker-compose down
# Rebuild and start
docker-compose up -d --buildEnvironment Variables for Docker
# .env for Docker deployment
FLASK_HOST_IP=0.0.0.0 # Listen on all interfaces
FLASK_PORT=5000
FLASK_DEBUG=False
FLASK_ENV=production
WEBSOCKET_HOST=0.0.0.0
WEBSOCKET_PORT=8765
WEBSOCKET_URL=ws://localhost:8765
HOST_SERVER=http://your-domain.com # External URL
DATABASE_URL=sqlite:///db/tradeboard.db
# Security (generate unique values)
APP_KEY=your_32_byte_hex_key
API_KEY_PEPPER=your_32_byte_hex_pepperResource Configuration for Python Strategies
Running Python strategies with numerical libraries (NumPy, SciPy, Numba) in Docker requires careful resource configuration to prevent RLIMIT_NPROC exhaustion errors.
Thread Limiting Environment Variables
OpenBLAS, NumPy, and other numerical libraries spawn threads by default. In containers with limited process/thread limits, this causes crashes. The Dockerfile and docker-compose.yaml include these limits:
| Variable | Purpose | Default |
|---|---|---|
OPENBLAS_NUM_THREADS | OpenBLAS thread limit | 2 |
OMP_NUM_THREADS | OpenMP thread limit | 2 |
MKL_NUM_THREADS | Intel MKL thread limit | 2 |
NUMEXPR_NUM_THREADS | NumExpr thread limit | 2 |
NUMBA_NUM_THREADS | Numba JIT thread limit | 2 |
Resource Scaling by Container RAM
| Container RAM | Thread Limit | Strategy Memory | SHM Size | Max Strategies |
|---|---|---|---|---|
| 2GB | 1 | 256MB | 256MB | 5 |
| 4GB | 2 | 512MB | 512MB | 5-8 |
| 8GB | 2-4 | 1024MB | 1GB | 10+ |
| 16GB+ | 4 | 1024MB | 2GB | 20+ |
Configuration in docker-compose.yaml
services:
tradeboard:
environment:
# Thread limits (adjust based on container RAM)
- OPENBLAS_NUM_THREADS=${OPENBLAS_NUM_THREADS:-2}
- OMP_NUM_THREADS=${OMP_NUM_THREADS:-2}
- MKL_NUM_THREADS=${MKL_NUM_THREADS:-2}
- NUMEXPR_NUM_THREADS=${NUMEXPR_NUM_THREADS:-2}
- NUMBA_NUM_THREADS=${NUMBA_NUM_THREADS:-2}
# Strategy memory limit (MB)
- STRATEGY_MEMORY_LIMIT_MB=${STRATEGY_MEMORY_LIMIT_MB:-1024}
# Shared memory for scipy/numba (25% of container RAM)
shm_size: ${SHM_SIZE:-512m}Install Script Dynamic Calculation
The install/install-docker.sh script automatically calculates optimal values:
# Thread limits based on RAM
# <3GB: 1 thread | 3-6GB: 2 threads | 6GB+: min(4, cores)
if [ $TOTAL_RAM_MB -lt 3000 ]; then
THREAD_LIMIT=1
elif [ $TOTAL_RAM_MB -lt 6000 ]; then
THREAD_LIMIT=2
else
THREAD_LIMIT=$((CPU_CORES < 4 ? CPU_CORES : 4))
fiReference: See GitHub Issue #822 for details on the RLIMIT_NPROC fix.
Security Considerations
| Aspect | Implementation |
|---|---|
| Non-root user | Runs as appuser, pinned to UID/GID 1000 so a host .env chowned to 1000 stays writable |
| .env mount | Bind-mounted read-write as ./.env:/app/.env. It is deliberately not :ro, because utils/env_check.py rotates placeholder secrets in place on first run |
| Keys directory | 700 permissions |
| No build tools | Slim production image |
| Minimal packages | Only runtime dependencies |
Volume Persistence
| Volume | Purpose | Required |
|---|---|---|
/app/db | SQLite databases | Yes |
/app/log | Application logs | Recommended |
/app/strategies | User strategies | Optional |
/app/keys | Remote MCP OAuth signing keys | Yes when Remote MCP is enabled |
/app/tmp | Numba, matplotlib and gunicorn scratch space | Yes |
/app/.env | Configuration (bind mount, not a named volume) | Yes |
Key Files Reference
| File | Purpose |
|---|---|
Dockerfile | Multi-stage build configuration |
docker-compose.yaml | Service orchestration |
start.sh | Container entrypoint |
.dockerignore | Build exclusions |
