add docker deployment stack and vps setup docs

This commit is contained in:
Kellan Drucquer 2026-04-03 22:10:35 -04:00
parent d301c7b4f3
commit 1fccb16dba
8 changed files with 544 additions and 0 deletions

16
.dockerignore Normal file
View file

@ -0,0 +1,16 @@
.git
.github
.DS_Store
.bun
.tmp
node_modules
dist
coverage
logs
apps/web/.next
.env
.env.*
session-ses_*.md
token-usage-output.txt
!.env.example
!**/.env.example

View file

@ -0,0 +1,103 @@
NATS_URL=nats://nats:4222
CLICKHOUSE_URL=http://clickhouse:8123
CLICKHOUSE_DATABASE=default
REDIS_URL=redis://redis:6379
API_PORT=4000
REST_DEFAULT_LIMIT=200
NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_NBBO_MAX_AGE_MS=1000
# Options ingest
OPTIONS_INGEST_ADAPTER=synthetic
ALPACA_KEY_ID=
ALPACA_SECRET_KEY=
ALPACA_REST_URL=https://data.alpaca.markets
ALPACA_WS_BASE_URL=wss://stream.data.alpaca.markets/v1beta1
ALPACA_FEED=indicative
ALPACA_UNDERLYINGS=SPY,NVDA,AAPL
ALPACA_STRIKES_PER_SIDE=8
ALPACA_MAX_DTE_DAYS=30
ALPACA_MONEYNESS_PCT=0.06
ALPACA_MONEYNESS_FALLBACK_PCT=0.1
ALPACA_MAX_QUOTES=200
# Databento replay
DATABENTO_API_KEY=
DATABENTO_DATASET=OPRA.PILLAR
DATABENTO_SCHEMA=trades
DATABENTO_NBBO_SCHEMA=tbbo
DATABENTO_START=
DATABENTO_END=
DATABENTO_SYMBOLS=ALL
DATABENTO_STYPE_IN=raw_symbol
DATABENTO_STYPE_OUT=raw_symbol
DATABENTO_LIMIT=0
DATABENTO_PRICE_SCALE=1
DATABENTO_PYTHON_BIN=python3
# IBKR adapter (options)
IBKR_HOST=host.docker.internal
IBKR_PORT=7497
IBKR_CLIENT_ID=0
IBKR_SYMBOL=SPY
IBKR_EXPIRY=20250117
IBKR_STRIKE=450
IBKR_RIGHT=C
IBKR_EXCHANGE=SMART
IBKR_CURRENCY=USD
IBKR_PYTHON_BIN=python3
# Equities ingest
EQUITIES_INGEST_ADAPTER=synthetic
EMIT_INTERVAL_MS=1000
ALPACA_EQUITIES_FEED=iex
# Testing mode
TESTING_MODE=false
TESTING_THROTTLE_MS=200
# Compute and inference
COMPUTE_DELIVER_POLICY=new
COMPUTE_CONSUMER_RESET=false
NBBO_MAX_AGE_MS=1000
ROLLING_WINDOW_SIZE=50
ROLLING_TTL_SEC=86400
EQUITY_QUOTE_MAX_AGE_MS=1000
DARK_INFER_WINDOW_MS=60000
DARK_INFER_COOLDOWN_MS=30000
DARK_INFER_MIN_BLOCK_SIZE=2000
DARK_INFER_MIN_ACCUM_SIZE=3000
DARK_INFER_MIN_ACCUM_COUNT=4
DARK_INFER_MIN_PRINT_SIZE=200
DARK_INFER_MAX_EVIDENCE=20
DARK_INFER_MAX_SPREAD_PCT=0.005
CLASSIFIER_SWEEP_MIN_PREMIUM=40000
CLASSIFIER_SWEEP_MIN_COUNT=3
CLASSIFIER_SWEEP_MIN_PREMIUM_Z=2
CLASSIFIER_SPIKE_MIN_PREMIUM=20000
CLASSIFIER_SPIKE_MIN_SIZE=400
CLASSIFIER_SPIKE_MIN_PREMIUM_Z=2.5
CLASSIFIER_SPIKE_MIN_SIZE_Z=2
CLASSIFIER_Z_MIN_SAMPLES=12
CLASSIFIER_MIN_NBBO_COVERAGE=0.5
CLASSIFIER_MIN_AGGRESSOR_RATIO=0.55
CLASSIFIER_0DTE_MAX_ATM_PCT=0.01
CLASSIFIER_0DTE_MIN_PREMIUM=20000
CLASSIFIER_0DTE_MIN_SIZE=400
# Candles
CANDLE_INTERVALS_MS=60000,300000
CANDLE_MAX_LATE_MS=0
CANDLE_CACHE_LIMIT=2000
CANDLE_DELIVER_POLICY=new
CANDLE_CONSUMER_RESET=false
# Replay profile
REPLAY_STREAMS=options,nbbo,equities,equity-quotes
REPLAY_START_TS=0
REPLAY_END_TS=0
REPLAY_SPEED=1
REPLAY_BATCH_SIZE=200
REPLAY_LOG_EVERY=1000

View file

@ -0,0 +1,15 @@
FROM oven/bun:1.3.11
WORKDIR /app
ENV NODE_ENV=production
COPY . .
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 python3-pip \
&& rm -rf /var/lib/apt/lists/* \
&& pip3 install --no-cache-dir -r services/ingest-options/py/requirements.txt \
&& bun install --frozen-lockfile
ENTRYPOINT ["bun"]

View file

@ -0,0 +1,11 @@
FROM oven/bun:1.3.11
WORKDIR /app
ENV NODE_ENV=production
COPY . .
RUN bun install --frozen-lockfile
ENTRYPOINT ["bun"]

View file

@ -0,0 +1,34 @@
FROM oven/bun:1.3.11 AS build
WORKDIR /app
ARG NEXT_PUBLIC_API_URL=""
ARG NEXT_PUBLIC_NBBO_MAX_AGE_MS=1000
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_NBBO_MAX_AGE_MS=${NEXT_PUBLIC_NBBO_MAX_AGE_MS}
COPY . .
RUN bun install --frozen-lockfile
RUN bun run --cwd apps/web build
FROM oven/bun:1.3.11 AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/bun.lock ./bun.lock
COPY --from=build /app/tsconfig.base.json ./tsconfig.base.json
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/apps/web ./apps/web
COPY --from=build /app/packages ./packages
EXPOSE 3000
CMD ["bun", "run", "--cwd", "apps/web", "start"]

193
deployment/docker/README.md Normal file
View file

@ -0,0 +1,193 @@
# Docker Deployment
This directory contains a VPS-oriented Docker deployment for the full Islandflow stack.
It is separate from the repo-root `docker-compose.yml`, which is still the lightweight local infra stack for development.
## What this stack does
- Runs the core app behind a single public port on `80`.
- Proxies the UI to the Next.js web app.
- Proxies REST and websocket traffic to the API service.
- Runs ClickHouse, Redis, and NATS JetStream with persistent Docker volumes.
- Runs the core runtime services: `ingest-options`, `ingest-equities`, `compute`, `candles`, `api`, and `web`.
- Keeps `replay` opt-in through a Compose profile, because the current replay service starts immediately when the container is enabled.
## Files
- `deployment/docker/docker-compose.yml`: production-style stack for a single VPS
- `deployment/docker/Dockerfile.service`: shared Bun runtime image for most services
- `deployment/docker/Dockerfile.ingest-options`: Bun runtime plus Python dependencies for Databento and IBKR adapters
- `deployment/docker/Dockerfile.web`: multi-stage build for the Next.js web app
- `deployment/docker/nginx.conf`: reverse proxy that routes `/ws/*` and API paths to the API container and everything else to the web container
- `deployment/docker/.env.example`: container-oriented environment template
## Prerequisites
- A Linux VPS with Docker Engine and Docker Compose v2 installed
- Enough RAM for ClickHouse plus the Bun services
- Port `80/tcp` open on the VPS firewall
Optional:
- A DNS record pointed at the VPS
- Alpaca, Databento, or IBKR credentials if you are not using the synthetic adapters
## First deployment
1. Copy the env template:
```bash
cd deployment/docker
cp .env.example .env
```
2. Edit `.env`.
Important defaults:
- `NATS_URL`, `CLICKHOUSE_URL`, and `REDIS_URL` should stay on the internal container hostnames unless you intentionally split infra out.
- `OPTIONS_INGEST_ADAPTER=synthetic` and `EQUITIES_INGEST_ADAPTER=synthetic` are the safest first boot settings.
- Leave `NEXT_PUBLIC_API_URL` blank if you want the browser to use the same public host as the UI. That is the default layout this stack is configured for.
3. Build and start the stack:
```bash
docker compose up -d --build
```
4. Confirm the containers are healthy:
```bash
docker compose ps
docker compose logs -f api web compute candles ingest-options ingest-equities
```
5. Open the app:
- `http://<your-vps-ip>/`
- Health check: `http://<your-vps-ip>/health`
## Replay service
Replay is disabled by default in this stack.
Start it only when you want it:
```bash
docker compose --profile replay up -d replay
```
Stop it again:
```bash
docker compose stop replay
```
## Adapter notes
### Synthetic mode
This is the easiest way to smoke-test the deployment:
- `OPTIONS_INGEST_ADAPTER=synthetic`
- `EQUITIES_INGEST_ADAPTER=synthetic`
### Alpaca mode
Set the adapter values and credentials in `.env`:
- `OPTIONS_INGEST_ADAPTER=alpaca`
- `EQUITIES_INGEST_ADAPTER=alpaca`
- `ALPACA_KEY_ID=...`
- `ALPACA_SECRET_KEY=...`
### Databento mode
The `ingest-options` image in this deployment includes Python plus the repos sidecar dependencies, so Databento can run without a custom image. Set the Databento env vars in `.env`, especially:
- `OPTIONS_INGEST_ADAPTER=databento`
- `DATABENTO_API_KEY=...`
- `DATABENTO_START=...`
### IBKR mode
If TWS or IB Gateway is running on the VPS host, the default `.env.example` already points `IBKR_HOST` at `host.docker.internal`, and the Compose stack adds the required host gateway mapping.
If IBKR is running somewhere else, change:
- `IBKR_HOST`
- `IBKR_PORT`
## Public routing
The reverse proxy sends these requests to the API container:
- `/health`
- `/prints/*`
- `/nbbo/*`
- `/quotes/*`
- `/candles/*`
- `/joins/*`
- `/dark/*`
- `/flow/*`
- `/replay/*`
- `/ws/*`
Everything else is sent to the Next.js web app.
That routing matters because the web client falls back to same-host API requests when `NEXT_PUBLIC_API_URL` is unset.
## Updating the deployment
When you pull new code:
```bash
cd deployment/docker
docker compose up -d --build
```
If you changed only env values for the Bun services:
```bash
docker compose up -d
```
If you changed `NEXT_PUBLIC_API_URL` or `NEXT_PUBLIC_NBBO_MAX_AGE_MS`, rebuild the web image because those are public Next.js build-time values:
```bash
docker compose build web
docker compose up -d web proxy
```
## Backups and persistence
Persistent data lives in Docker volumes:
- `clickhouse-data`
- `redis-data`
- `nats-data`
Before destructive maintenance, back up those volumes or the underlying Docker data directory for the host.
## Shutdown
Stop everything while keeping data:
```bash
docker compose down
```
Stop everything and remove volumes too:
```bash
docker compose down -v
```
Only use `-v` if you intentionally want to wipe ClickHouse, Redis, and JetStream state.
## Known caveats
- The root `.env.example` still contains a `REPLAY_ENABLED` comment, but the current replay service does not read that variable. Use the Compose replay profile instead.
- This stack exposes plain HTTP on port `80`. If you want TLS termination on the box, put Caddy, Nginx, Traefik, or a cloud load balancer in front of it, or replace the bundled Nginx config with your preferred HTTPS setup.
- The stack assumes a single-node VPS deployment. If you later split infra or add external managed services, update the three core connection URLs in `.env`.

View file

@ -0,0 +1,133 @@
name: islandflow-vps
x-service-common: &service-common
build:
context: ../..
dockerfile: deployment/docker/Dockerfile.service
env_file:
- ./.env
restart: unless-stopped
init: true
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- nats
- clickhouse
- redis
services:
proxy:
image: nginx:1.27-alpine
restart: unless-stopped
depends_on:
- web
- api
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
web:
build:
context: ../..
dockerfile: deployment/docker/Dockerfile.web
args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-}
NEXT_PUBLIC_NBBO_MAX_AGE_MS: ${NEXT_PUBLIC_NBBO_MAX_AGE_MS:-1000}
env_file:
- ./.env
restart: unless-stopped
init: true
depends_on:
api:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"bun",
"-e",
"const r=await fetch('http://127.0.0.1:3000/'); if(!r.ok) throw new Error('web healthcheck failed: '+r.status);"
]
interval: 30s
timeout: 10s
retries: 5
start_period: 45s
api:
<<: *service-common
command: ["services/api/src/index.ts"]
healthcheck:
test:
[
"CMD",
"bun",
"-e",
"const r=await fetch('http://127.0.0.1:4000/health'); if(!r.ok) throw new Error('api healthcheck failed: '+r.status);"
]
interval: 30s
timeout: 10s
retries: 5
start_period: 20s
compute:
<<: *service-common
command: ["services/compute/src/index.ts"]
candles:
<<: *service-common
command: ["services/candles/src/index.ts"]
ingest-options:
build:
context: ../..
dockerfile: deployment/docker/Dockerfile.ingest-options
env_file:
- ./.env
restart: unless-stopped
init: true
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- nats
- clickhouse
- redis
command: ["services/ingest-options/src/index.ts"]
ingest-equities:
<<: *service-common
command: ["services/ingest-equities/src/index.ts"]
replay:
<<: *service-common
profiles: ["replay"]
command: ["services/replay/src/index.ts"]
clickhouse:
image: clickhouse/clickhouse-server:23.8
restart: unless-stopped
ulimits:
nofile:
soft: 262144
hard: 262144
volumes:
- clickhouse-data:/var/lib/clickhouse
redis:
image: redis:7.2
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
nats:
image: nats:2.10
restart: unless-stopped
command: ["-js", "-sd", "/data"]
volumes:
- nats-data:/data
volumes:
clickhouse-data:
redis-data:
nats-data:

View file

@ -0,0 +1,39 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name _;
client_max_body_size 16m;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
location = /health {
proxy_pass http://api:4000/health;
}
location ^~ /ws/ {
proxy_pass http://api:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location ~ ^/(prints|nbbo|quotes|candles|joins|dark|flow|replay)/ {
proxy_pass http://api:4000;
proxy_http_version 1.1;
}
location / {
proxy_pass http://web:3000;
proxy_http_version 1.1;
}
}