Docker Deployment
SkopaqTrader ships as one Docker image for every service: API, Telegram bot, scheduler,
daemon, chatbot, MCP server and any skopaq CLI command. The same image is used by
docker-compose.yml, Fly (fly.toml, fly-telegram.toml) and Railway (railway.toml,
railway-daemon.toml).
Running 24/7
For an always-on host (a Mac mini M4 or any Linux box), follow the Mac mini runbook: host setup, verification, daily operations.
Quick Start
Build the image (native arm64 on Apple Silicon, amd64 elsewhere), then run the API
server on loopback only, the interactive chatbot, or any skopaq CLI command:
docker build -t skopaqtrader .
docker run -d --env-file .env -p 127.0.0.1:8000:8000 skopaqtrader api
docker run -it --env-file .env skopaqtrader chat
docker run --rm --env-file .env skopaqtrader status
Shell blocks on this page have no # comments, so they paste into macOS's default zsh.
The Image
Built from the root Dockerfile:
- single stage on
python:3.14-slim-trixie; no apt packages and no compiler (every dependency ships CPython 3.14 wheels for linux/arm64 and linux/amd64); - dependencies from
pip install -e ".[deploy]"(Telegram bot with its job queue, Kite Connect, Ollama, psycopg2, quantstats, langchain-community); - runs as the non-root user
skopaq(uid 1000) withWORKDIR /home/skopaq, so relative paths (results/,.cache/) are writable;/app(the code) is read-only for the app; TZ=Asia/Kolkata,PYTHONUNBUFFERED=1;- no image-level
HEALTHCHECK: each compose service defines its own (below), and Fly and Railway use their own checks.
Available Services
docker/entrypoint.sh picks the service from the first argument and passes the rest
through:
| Service | Command | Description | Port |
|---|---|---|---|
api |
docker run ... api |
FastAPI backend (honours $PORT) |
8000 |
telegram |
docker run ... telegram |
Telegram bot | none |
scheduler |
docker run ... scheduler |
One daemon session per NSE trading day | none |
chat |
docker run -it ... chat |
Interactive chatbot | none |
mcp |
docker run -i ... mcp |
MCP server (stdio; stdout carries only JSON-RPC) | none |
daemon |
docker run ... daemon [--dry-run] |
Paper trading session, now | none |
daemon-live |
docker run ... daemon-live |
Live trading session, now | none |
monitor |
docker run ... monitor |
Position monitor | none |
scan |
docker run ... scan |
One-shot market scan | none |
status |
docker run ... status |
System health check | none |
shell |
docker run -it ... shell |
Bash shell | none |
any skopaq command |
docker run ... halt "reason", resume --yes, token set <tok>, settle, report |
Passed to the CLI | none |
help |
docker run ... help |
Lists the services and CLI commands | none |
python ..., bash, sh and absolute paths are executed directly (so Railway's
python -m ... start commands work).
Docker Compose
docker compose up -d --build starts the default services api, telegram and
scheduler. The others are behind profiles and are meant for docker compose run:
| Command | What it does |
|---|---|
docker compose up -d --build |
api + telegram + scheduler |
docker compose ps |
all three turn "healthy" |
docker compose run --rm chat |
interactive chatbot (profile: interactive) |
docker compose run --rm scan |
one-shot market scan (profile: tools) |
docker compose run --rm daemon --dry-run |
scan-only daemon session now (profile: trading) |
docker compose run --rm daemon |
a full paper session now |
docker compose run --rm -T mcp |
MCP server over stdio (profile: mcp); never up -d |
docker compose exec api skopaq halt "reason" |
any skopaq command in a running container |
Every service shares the same image, .env, volumes, TZ, init: true (signal
forwarding) and log rotation (5 x 10 MB). The API is published on 127.0.0.1:8000
only; expose it publicly only through a Cloudflare Tunnel with Access.
Volumes and State
| Volume | Path | Holds |
|---|---|---|
skopaq-home |
/home/skopaq/.skopaq/ |
INDstocks token (token.enc, token.key), kill-switch HALT file, per-symbol SELL locks (locks/), live order journal (orders/) |
skopaq-home |
/home/skopaq/.tradingagents/ |
decision log |
skopaq-home |
/home/skopaq/results/, .cache/ |
analysis reports, data cache |
skopaq-home |
/home/skopaq/scheduler/, logs/daemon/ |
scheduler markers, one log per session |
skopaq-data |
/data/ |
Kite access token (shared by api and telegram) |
New named volumes start out owned by skopaq (the image creates the directories).
If a volume was created by an older image and is root-owned:
Health Checks
| Service | Check |
|---|---|
api |
python -m skopaq.healthcheck api (GET /health on $PORT, status ok) |
telegram |
python -m skopaq.healthcheck heartbeat /tmp/skopaq-telegram.heartbeat 180 |
scheduler |
python -m skopaq.healthcheck heartbeat /tmp/skopaq-scheduler.heartbeat 180 |
The bot and the scheduler touch their heartbeat file (SKOPAQ_HEARTBEAT_FILE) every
minute or less.
Environment Variables
Then fill it in. Compose treats $ in a value as a variable: write a literal $ as $$.
Essentials: an LLM key (SKOPAQ_GOOGLE_API_KEY), Supabase, SKOPAQ_TELEGRAM_BOT_TOKEN
and SKOPAQ_TELEGRAM_CHAT_ID, SKOPAQ_TRADING_MODE=paper. Settings added for the
always-on stack:
| Variable | Default | Purpose |
|---|---|---|
SKOPAQ_SCHEDULER_ENABLED |
true |
false keeps the scheduler up but idle |
SKOPAQ_SCHEDULER_MODE |
paper |
live also needs SKOPAQ_SCHEDULER_CONFIRM_LIVE=true |
SKOPAQ_SCHEDULER_START / _LAST_START / _DEADLINE / _SETTLE_AT |
09:15 / 11:30 / 15:45 / 18:30 |
IST times |
SKOPAQ_SCHEDULER_PREFLIGHT |
08:45 |
IST; alert if the INDstocks token is missing or expires before the session ends |
SKOPAQ_SCHEDULER_PING_URL |
empty | dead-man's switch |
SKOPAQ_NSE_HOLIDAYS |
empty | extra NSE closures (YYYY-MM-DD,...) |
SKOPAQ_DOCKER_OLLAMA_BASE_URL |
http://host.docker.internal:11434 |
Ollama on the host |
SKOPAQ_PUBLIC_BASE_URL |
empty | public HTTPS URL of the API (Kite login links) |
SKOPAQ_API_BASE_URL |
empty | where a process without the /data volume (another machine, the native Mac MCP server) fetches the Kite token; never set it on api |
SKOPAQ_API_TOKEN |
empty | Bearer token for /api/chat/* and /api/kite/token |
SKOPAQ_CORS_ORIGINS |
* |
browser origins allowed to call the API |
Never commit .env
The .env file contains secrets and is gitignored and excluded from the image
build context (.dockerignore). Pass it at runtime with env_file / --env-file.
File Reference
| File | Purpose |
|---|---|
Dockerfile |
The one image (compose, Fly, Railway) |
docker-compose.yml |
Always-on stack plus profile tools |
docker/entrypoint.sh |
Service routing and CLI passthrough |
skopaq/healthcheck.py |
Per-service health checks |
scripts/macmini/verify.sh |
Host, config and stack readiness checks |
.env.example |
Environment variable template |