Skip to content

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

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

yaml
# 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: local

Named Volumes vs Bind Mounts

ApproachProsCons
Named Volumes (recommended)Better performance, managed by DockerData in Docker's volume directory
Bind Mounts (./db:/app/db)Easy access to filesPermission 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 application

Start Script

The start.sh script is a sophisticated 246-line entrypoint that handles:

  1. Railway/Cloud Environment Detection - Auto-generates .env from environment variables
  2. Directory Setup - Creates required directories with proper permissions
  3. Database Migrations - Runs upgrade/migrate_all.py if present
  4. WebSocket Proxy - Starts in background with PID tracking
  5. Signal Handling - Graceful shutdown on SIGTERM/SIGINT
  6. Gunicorn Startup - Eventlet worker with optimized settings
bash
#!/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:app

Key Differences from Simple Script

FeatureOld (6 lines)Actual (341 lines)
Cloud SupportNoneFull Railway/Render support
.env GenerationNone40+ variables auto-generated
MigrationsNoneAuto-runs on startup
Signal HandlingNoneGraceful shutdown
Timeout120s300s
Graceful TimeoutNone30s
Worker Temp DirDefault/tmp/gunicorn_workers
Control SocketEnabledDisabled with --no-control-socket
Compromised-key preflightNoneBlocks startup on the known leaked APP_KEY/API_KEY_PEPPER when .env is not writable

Build Commands

bash
# 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 tradeboard

Docker Compose Commands

bash
# Start services
docker-compose up -d

# View logs
docker-compose logs -f

# Stop services
docker-compose down

# Rebuild and start
docker-compose up -d --build

Environment Variables for Docker

bash
# .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_pepper

Resource 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:

VariablePurposeDefault
OPENBLAS_NUM_THREADSOpenBLAS thread limit2
OMP_NUM_THREADSOpenMP thread limit2
MKL_NUM_THREADSIntel MKL thread limit2
NUMEXPR_NUM_THREADSNumExpr thread limit2
NUMBA_NUM_THREADSNumba JIT thread limit2

Resource Scaling by Container RAM

Container RAMThread LimitStrategy MemorySHM SizeMax Strategies
2GB1256MB256MB5
4GB2512MB512MB5-8
8GB2-41024MB1GB10+
16GB+41024MB2GB20+

Configuration in docker-compose.yaml

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:

bash
# 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))
fi

Reference: See GitHub Issue #822 for details on the RLIMIT_NPROC fix.

Security Considerations

AspectImplementation
Non-root userRuns as appuser, pinned to UID/GID 1000 so a host .env chowned to 1000 stays writable
.env mountBind-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 directory700 permissions
No build toolsSlim production image
Minimal packagesOnly runtime dependencies

Volume Persistence

VolumePurposeRequired
/app/dbSQLite databasesYes
/app/logApplication logsRecommended
/app/strategiesUser strategiesOptional
/app/keysRemote MCP OAuth signing keysYes when Remote MCP is enabled
/app/tmpNumba, matplotlib and gunicorn scratch spaceYes
/app/.envConfiguration (bind mount, not a named volume)Yes

Key Files Reference

FilePurpose
DockerfileMulti-stage build configuration
docker-compose.yamlService orchestration
start.shContainer entrypoint
.dockerignoreBuild exclusions