docker deployment
Prepare Docker deployment for shared NPM routing
This commit is contained in:
commit
52e1c934bd
14 changed files with 592 additions and 29 deletions
|
|
@ -11,6 +11,8 @@ NPM_SHARED_NETWORK=npm-shared
|
|||
# Recommended with NPM on the same Docker network:
|
||||
# app.<domain> -> web:3000
|
||||
# api.<domain> -> api:4000
|
||||
# Leave NEXT_PUBLIC_API_URL empty to use same-origin mode
|
||||
# (app.<domain> serves UI and proxies API paths to api:4000).
|
||||
NEXT_PUBLIC_API_URL=https://api.example.com
|
||||
NEXT_PUBLIC_NBBO_MAX_AGE_MS=1000
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,12 @@ ENV NODE_ENV=production
|
|||
ENV VIRTUAL_ENV=/opt/ingest-options-venv
|
||||
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
|
||||
|
||||
COPY . .
|
||||
COPY --from=workspace package.json ./package.json
|
||||
COPY --from=workspace bun.lock ./bun.lock
|
||||
COPY --from=workspace tsconfig.base.json ./tsconfig.base.json
|
||||
COPY --from=services . ./services
|
||||
COPY --from=packages . ./packages
|
||||
COPY --from=apps . ./apps
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends python3 python3-pip python3-venv \
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ WORKDIR /app
|
|||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY . .
|
||||
COPY --from=workspace package.json ./package.json
|
||||
COPY --from=workspace bun.lock ./bun.lock
|
||||
COPY --from=workspace tsconfig.base.json ./tsconfig.base.json
|
||||
COPY --from=services . ./services
|
||||
COPY --from=packages . ./packages
|
||||
COPY --from=apps . ./apps
|
||||
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ 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 . .
|
||||
COPY --from=workspace package.json ./package.json
|
||||
COPY --from=workspace bun.lock ./bun.lock
|
||||
COPY --from=workspace tsconfig.base.json ./tsconfig.base.json
|
||||
COPY --from=services . ./services
|
||||
COPY --from=packages . ./packages
|
||||
COPY --from=apps . ./apps
|
||||
|
||||
RUN bun install --frozen-lockfile
|
||||
RUN bun run --cwd apps/web build
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ 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.
|
||||
- `NPM_SHARED_NETWORK=npm-shared` is the recommended external Docker network name for NPM and this stack.
|
||||
- `NEXT_PUBLIC_API_URL=https://api.example.com` is the recommended production shape when using NPM with two subdomains.
|
||||
- `NEXT_PUBLIC_API_URL=https://api.example.com` uses a two-subdomain setup (`app` + `api`).
|
||||
- `NEXT_PUBLIC_API_URL=` (empty) uses same-origin mode where the app host also proxies API paths to `api:4000`.
|
||||
|
||||
3. Build and start the stack:
|
||||
|
||||
|
|
@ -98,11 +99,13 @@ If you want to use a different network name, set `NPM_SHARED_NETWORK` in `.env`
|
|||
|
||||
6. Create these NPM proxy hosts:
|
||||
|
||||
- `app.example.com` -> forward to `web`, port `3000`
|
||||
- `api.example.com` -> forward to `api`, port `4000`
|
||||
- `app.example.com` -> forward to `web` (or `islandflow-vps-web-1`), port `3000`
|
||||
- `api.example.com` -> forward to `api` (or `islandflow-vps-api-1`), port `4000`
|
||||
|
||||
For the API host, enable websocket support.
|
||||
|
||||
If NPM is attached to multiple Docker networks and another stack also has an `api` container alias, prefer the explicit container name (`islandflow-vps-api-1`) to avoid DNS collisions.
|
||||
|
||||
7. Open the app:
|
||||
|
||||
- `https://app.example.com/`
|
||||
|
|
@ -161,19 +164,24 @@ If IBKR is running somewhere else, change:
|
|||
|
||||
## NPM routing
|
||||
|
||||
Recommended proxy hosts:
|
||||
The Islandflow stack expects an external NPM instance on the shared Docker network. The dedicated NPM stack now lives in `../npm`.
|
||||
|
||||
- `app.<domain>` -> `web:3000`
|
||||
- `api.<domain>` -> `api:4000`
|
||||
Supported routing modes:
|
||||
|
||||
The web app should be built with `NEXT_PUBLIC_API_URL=https://api.<domain>` so browser REST and websocket traffic goes straight to the API host through NPM.
|
||||
1. Two-subdomain mode
|
||||
- `app.<domain>` -> `web:3000`
|
||||
- `api.<domain>` -> `api:4000`
|
||||
- Build web with `NEXT_PUBLIC_API_URL=https://api.<domain>`.
|
||||
|
||||
The API host needs websocket support enabled because the app uses `/ws/*` endpoints for live streams.
|
||||
2. Same-origin fallback mode
|
||||
- Build web with `NEXT_PUBLIC_API_URL=` (empty).
|
||||
- Keep `app.<domain>` -> web.
|
||||
- Add path-based proxy rules on `app.<domain>` for API routes to `api:4000`:
|
||||
- `/ws/*`, `/replay/*`, `/prints/*`, `/joins/*`, `/nbbo/*`, `/dark/*`, `/flow/*`, `/candles/*`
|
||||
|
||||
Because `web` and `api` are both attached to the shared user-defined network, NPM can target them directly by container DNS name:
|
||||
Use websocket support on whichever host serves `/ws/*`.
|
||||
|
||||
- `web`
|
||||
- `api`
|
||||
If NPM is on multiple networks and names collide (for example another stack also exposes `api`), target explicit container names (`islandflow-vps-api-1`, `islandflow-vps-web-1`) instead of generic aliases.
|
||||
|
||||
## Updating the deployment
|
||||
|
||||
|
|
@ -227,6 +235,7 @@ Only use `-v` if you intentionally want to wipe ClickHouse, Redis, and JetStream
|
|||
|
||||
- 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 does not publish `web` or `api` to host ports. NPM must be able to resolve `web` and `api` over the shared user-defined network from `NPM_SHARED_NETWORK`.
|
||||
- If NPM is attached to more than one application network, generic upstream aliases like `api` can resolve to the wrong stack. Prefer explicit container names in NPM upstream settings.
|
||||
- Some hosts disable IPv6 inside containers; the bundled ClickHouse config pins `listen_host` to `0.0.0.0` so the API can reach ClickHouse reliably over Docker networking.
|
||||
- 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`.
|
||||
|
||||
|
|
@ -235,6 +244,6 @@ Only use `-v` if you intentionally want to wipe ClickHouse, Redis, and JetStream
|
|||
After NPM is wired up:
|
||||
|
||||
- `https://app.<domain>/` should load the UI.
|
||||
- Browser network requests from the UI should target `https://api.<domain>/...`.
|
||||
- Live feeds should connect over `wss://api.<domain>/ws/...`.
|
||||
- In two-subdomain mode, browser requests should target `https://api.<domain>/...` and live feeds should use `wss://api.<domain>/ws/...`.
|
||||
- In same-origin mode, browser requests should target `https://app.<domain>/...` for API paths and live feeds should use `wss://app.<domain>/ws/...`.
|
||||
- `docker compose ps` should show no service publishing host port `80`.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
name: islandflow-vps
|
||||
|
||||
x-build-contexts: &build-contexts
|
||||
context: .
|
||||
additional_contexts:
|
||||
workspace: ./workspace-root
|
||||
apps: ../../apps
|
||||
services: ../../services
|
||||
packages: ../../packages
|
||||
|
||||
x-service-common: &service-common
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deployment/docker/Dockerfile.service
|
||||
<<: *build-contexts
|
||||
dockerfile: Dockerfile.service
|
||||
env_file:
|
||||
- ./.env
|
||||
restart: unless-stopped
|
||||
|
|
@ -21,8 +29,8 @@ x-service-common: &service-common
|
|||
services:
|
||||
web:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deployment/docker/Dockerfile.web
|
||||
<<: *build-contexts
|
||||
dockerfile: Dockerfile.web
|
||||
args:
|
||||
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-}
|
||||
NEXT_PUBLIC_NBBO_MAX_AGE_MS: ${NEXT_PUBLIC_NBBO_MAX_AGE_MS:-1000}
|
||||
|
|
@ -82,8 +90,8 @@ services:
|
|||
|
||||
ingest-options:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deployment/docker/Dockerfile.ingest-options
|
||||
<<: *build-contexts
|
||||
dockerfile: Dockerfile.ingest-options
|
||||
env_file:
|
||||
- ./.env
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
276
deployment/docker/workspace-root/bun.lock
Normal file
276
deployment/docker/workspace-root/bun.lock
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "islandflow",
|
||||
"devDependencies": {
|
||||
"typescript-language-server": "^5.1.3",
|
||||
},
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@islandflow/web",
|
||||
"dependencies": {
|
||||
"@islandflow/types": "workspace:*",
|
||||
"lightweight-charts": "^4.2.0",
|
||||
"next": "^14.2.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^18.3.3",
|
||||
"typescript": "^5.5.4",
|
||||
},
|
||||
},
|
||||
"packages/bus": {
|
||||
"name": "@islandflow/bus",
|
||||
"dependencies": {
|
||||
"nats": "^2.24.0",
|
||||
},
|
||||
},
|
||||
"packages/config": {
|
||||
"name": "@islandflow/config",
|
||||
"dependencies": {
|
||||
"zod": "^3.23.8",
|
||||
},
|
||||
},
|
||||
"packages/observability": {
|
||||
"name": "@islandflow/observability",
|
||||
},
|
||||
"packages/storage": {
|
||||
"name": "@islandflow/storage",
|
||||
"dependencies": {
|
||||
"@clickhouse/client": "^0.2.6",
|
||||
"@islandflow/types": "workspace:*",
|
||||
},
|
||||
},
|
||||
"packages/types": {
|
||||
"name": "@islandflow/types",
|
||||
"dependencies": {
|
||||
"zod": "^3.23.8",
|
||||
},
|
||||
},
|
||||
"services/api": {
|
||||
"name": "@islandflow/api",
|
||||
"dependencies": {
|
||||
"@islandflow/bus": "workspace:*",
|
||||
"@islandflow/config": "workspace:*",
|
||||
"@islandflow/observability": "workspace:*",
|
||||
"@islandflow/storage": "workspace:*",
|
||||
"@islandflow/types": "workspace:*",
|
||||
"redis": "^5.10.0",
|
||||
"zod": "^3.23.8",
|
||||
},
|
||||
},
|
||||
"services/candles": {
|
||||
"name": "@islandflow/candles",
|
||||
"dependencies": {
|
||||
"@islandflow/bus": "workspace:*",
|
||||
"@islandflow/config": "workspace:*",
|
||||
"@islandflow/observability": "workspace:*",
|
||||
"@islandflow/storage": "workspace:*",
|
||||
"@islandflow/types": "workspace:*",
|
||||
"redis": "^5.10.0",
|
||||
"zod": "^3.23.8",
|
||||
},
|
||||
},
|
||||
"services/compute": {
|
||||
"name": "@islandflow/compute",
|
||||
"dependencies": {
|
||||
"@islandflow/bus": "workspace:*",
|
||||
"@islandflow/config": "workspace:*",
|
||||
"@islandflow/observability": "workspace:*",
|
||||
"@islandflow/storage": "workspace:*",
|
||||
"@islandflow/types": "workspace:*",
|
||||
"redis": "^5.10.0",
|
||||
"zod": "^3.23.8",
|
||||
},
|
||||
},
|
||||
"services/eod-enricher": {
|
||||
"name": "@islandflow/eod-enricher",
|
||||
"dependencies": {
|
||||
"@islandflow/config": "workspace:*",
|
||||
"@islandflow/observability": "workspace:*",
|
||||
},
|
||||
},
|
||||
"services/ingest-equities": {
|
||||
"name": "@islandflow/ingest-equities",
|
||||
"dependencies": {
|
||||
"@islandflow/bus": "workspace:*",
|
||||
"@islandflow/config": "workspace:*",
|
||||
"@islandflow/observability": "workspace:*",
|
||||
"@islandflow/storage": "workspace:*",
|
||||
"@islandflow/types": "workspace:*",
|
||||
"ws": "^8.18.3",
|
||||
"zod": "^3.23.8",
|
||||
},
|
||||
},
|
||||
"services/ingest-options": {
|
||||
"name": "@islandflow/ingest-options",
|
||||
"dependencies": {
|
||||
"@islandflow/bus": "workspace:*",
|
||||
"@islandflow/config": "workspace:*",
|
||||
"@islandflow/observability": "workspace:*",
|
||||
"@islandflow/storage": "workspace:*",
|
||||
"@islandflow/types": "workspace:*",
|
||||
"@msgpack/msgpack": "^3.1.3",
|
||||
"ws": "^8.18.3",
|
||||
"zod": "^3.23.8",
|
||||
},
|
||||
},
|
||||
"services/refdata": {
|
||||
"name": "@islandflow/refdata",
|
||||
"dependencies": {
|
||||
"@islandflow/config": "workspace:*",
|
||||
"@islandflow/observability": "workspace:*",
|
||||
},
|
||||
},
|
||||
"services/replay": {
|
||||
"name": "@islandflow/replay",
|
||||
"dependencies": {
|
||||
"@islandflow/bus": "workspace:*",
|
||||
"@islandflow/config": "workspace:*",
|
||||
"@islandflow/observability": "workspace:*",
|
||||
"@islandflow/storage": "workspace:*",
|
||||
"@islandflow/types": "workspace:*",
|
||||
"zod": "^3.23.8",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@clickhouse/client": ["@clickhouse/client@0.2.10", "", { "dependencies": { "@clickhouse/client-common": "0.2.10" } }, "sha512-ZwBgzjEAFN/ogS0ym5KHVbR7Hx/oYCX01qGp2baEyfN2HM73kf/7Vp3GvMHWRy+zUXISONEtFv7UTViOXnmFrg=="],
|
||||
|
||||
"@clickhouse/client-common": ["@clickhouse/client-common@0.2.10", "", {}, "sha512-BvTY0IXS96y9RUeNCpKL4HUzHmY80L0lDcGN0lmUD6zjOqYMn78+xyHYJ/AIAX7JQsc+/KwFt2soZutQTKxoGQ=="],
|
||||
|
||||
"@islandflow/api": ["@islandflow/api@workspace:services/api"],
|
||||
|
||||
"@islandflow/bus": ["@islandflow/bus@workspace:packages/bus"],
|
||||
|
||||
"@islandflow/candles": ["@islandflow/candles@workspace:services/candles"],
|
||||
|
||||
"@islandflow/compute": ["@islandflow/compute@workspace:services/compute"],
|
||||
|
||||
"@islandflow/config": ["@islandflow/config@workspace:packages/config"],
|
||||
|
||||
"@islandflow/eod-enricher": ["@islandflow/eod-enricher@workspace:services/eod-enricher"],
|
||||
|
||||
"@islandflow/ingest-equities": ["@islandflow/ingest-equities@workspace:services/ingest-equities"],
|
||||
|
||||
"@islandflow/ingest-options": ["@islandflow/ingest-options@workspace:services/ingest-options"],
|
||||
|
||||
"@islandflow/observability": ["@islandflow/observability@workspace:packages/observability"],
|
||||
|
||||
"@islandflow/refdata": ["@islandflow/refdata@workspace:services/refdata"],
|
||||
|
||||
"@islandflow/replay": ["@islandflow/replay@workspace:services/replay"],
|
||||
|
||||
"@islandflow/storage": ["@islandflow/storage@workspace:packages/storage"],
|
||||
|
||||
"@islandflow/types": ["@islandflow/types@workspace:packages/types"],
|
||||
|
||||
"@islandflow/web": ["@islandflow/web@workspace:apps/web"],
|
||||
|
||||
"@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="],
|
||||
|
||||
"@next/env": ["@next/env@14.2.35", "", {}, "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ=="],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.33", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA=="],
|
||||
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.33", "", { "os": "darwin", "cpu": "x64" }, "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA=="],
|
||||
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.33", "", { "os": "linux", "cpu": "arm64" }, "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw=="],
|
||||
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.33", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg=="],
|
||||
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.33", "", { "os": "linux", "cpu": "x64" }, "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg=="],
|
||||
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.33", "", { "os": "linux", "cpu": "x64" }, "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA=="],
|
||||
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.33", "", { "os": "win32", "cpu": "arm64" }, "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ=="],
|
||||
|
||||
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.33", "", { "os": "win32", "cpu": "ia32" }, "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q=="],
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.33", "", { "os": "win32", "cpu": "x64" }, "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg=="],
|
||||
|
||||
"@redis/bloom": ["@redis/bloom@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A=="],
|
||||
|
||||
"@redis/client": ["@redis/client@5.10.0", "", { "dependencies": { "cluster-key-slot": "1.1.2" } }, "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA=="],
|
||||
|
||||
"@redis/json": ["@redis/json@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-B2G8XlOmTPUuZtD44EMGbtoepQG34RCDXLZbjrtON1Djet0t5Ri7/YPXvL9aomXqP8lLTreaprtyLKF4tmXEEA=="],
|
||||
|
||||
"@redis/search": ["@redis/search@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-3SVcPswoSfp2HnmWbAGUzlbUPn7fOohVu2weUQ0S+EMiQi8jwjL+aN2p6V3TI65eNfVsJ8vyPvqWklm6H6esmg=="],
|
||||
|
||||
"@redis/time-series": ["@redis/time-series@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-cPkpddXH5kc/SdRhF0YG0qtjL+noqFT0AcHbQ6axhsPsO7iqPi1cjxgdkE9TNeKiBUUdCaU1DbqkR/LzbzPBhg=="],
|
||||
|
||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="],
|
||||
|
||||
"@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
|
||||
|
||||
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
|
||||
|
||||
"@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="],
|
||||
|
||||
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="],
|
||||
|
||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"fancy-canvas": ["fancy-canvas@2.1.0", "", {}, "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"lightweight-charts": ["lightweight-charts@4.2.3", "", { "dependencies": { "fancy-canvas": "2.1.0" } }, "sha512-5kS/2hY3wNYNzhnS8Gb+GAS07DX8GPF2YVDnd2NMC85gJVQ6RLU6YrXNgNJ6eg0AnWPwCnvaGtYmGky3HiLQEw=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"nats": ["nats@2.29.3", "", { "dependencies": { "nkeys.js": "1.1.0" } }, "sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA=="],
|
||||
|
||||
"next": ["next@14.2.35", "", { "dependencies": { "@next/env": "14.2.35", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.33", "@next/swc-darwin-x64": "14.2.33", "@next/swc-linux-arm64-gnu": "14.2.33", "@next/swc-linux-arm64-musl": "14.2.33", "@next/swc-linux-x64-gnu": "14.2.33", "@next/swc-linux-x64-musl": "14.2.33", "@next/swc-win32-arm64-msvc": "14.2.33", "@next/swc-win32-ia32-msvc": "14.2.33", "@next/swc-win32-x64-msvc": "14.2.33" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig=="],
|
||||
|
||||
"nkeys.js": ["nkeys.js@1.1.0", "", { "dependencies": { "tweetnacl": "1.0.3" } }, "sha512-tB/a0shZL5UZWSwsoeyqfTszONTt4k2YS0tuQioMOD180+MbombYVgzDUYHlx+gejYK6rgf08n/2Df99WY0Sxg=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
||||
|
||||
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
|
||||
|
||||
"redis": ["redis@5.10.0", "", { "dependencies": { "@redis/bloom": "5.10.0", "@redis/client": "5.10.0", "@redis/json": "5.10.0", "@redis/search": "5.10.0", "@redis/time-series": "5.10.0" } }, "sha512-0/Y+7IEiTgVGPrLFKy8oAEArSyEJkU0zvgV5xyi9NzNQ+SLZmyFbUsWIbgPcd4UdUh00opXGKlXJwMmsis5Byw=="],
|
||||
|
||||
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
|
||||
|
||||
"styled-jsx": ["styled-jsx@5.1.1", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-language-server": ["typescript-language-server@5.1.3", "", { "bin": { "typescript-language-server": "lib/cli.mjs" } }, "sha512-r+pAcYtWdN8tKlYZPwiiHNA2QPjXnI02NrW5Sf2cVM3TRtuQ3V9EKKwOxqwaQ0krsaEXk/CbN90I5erBuf84Vg=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
}
|
||||
}
|
||||
20
deployment/docker/workspace-root/package.json
Normal file
20
deployment/docker/workspace-root/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "islandflow",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"services/*",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "bun run scripts/dev.ts",
|
||||
"dev:infra": "docker compose up",
|
||||
"dev:infra:down": "docker compose down",
|
||||
"dev:web": "bun --cwd=apps/web run dev",
|
||||
"dev:services": "bun run scripts/dev-services.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript-language-server": "^5.1.3"
|
||||
}
|
||||
}
|
||||
13
deployment/docker/workspace-root/tsconfig.base.json
Normal file
13
deployment/docker/workspace-root/tsconfig.base.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
4
deployment/npm/.env.example
Normal file
4
deployment/npm/.env.example
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
TZ=Etc/UTC
|
||||
NPM_ADMIN_BIND_IP=100.87.130.79
|
||||
NPM_EDGE_NETWORK=nextcloud_edge
|
||||
NPM_SHARED_NETWORK=npm-shared
|
||||
3
deployment/npm/.gitignore
vendored
Normal file
3
deployment/npm/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
data/
|
||||
letsencrypt/
|
||||
.env
|
||||
65
deployment/npm/README.md
Normal file
65
deployment/npm/README.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Nginx Proxy Manager
|
||||
|
||||
This stack runs Nginx Proxy Manager separately from the Nextcloud stack while preserving the existing proxy host database and certificates.
|
||||
|
||||
## Layout
|
||||
|
||||
- `docker-compose.yml` defines the standalone NPM service.
|
||||
- `.env` holds only stack-local settings like `TZ` and the admin bind IP.
|
||||
- Runtime state lives in:
|
||||
- `./data`
|
||||
- `./letsencrypt`
|
||||
|
||||
## Networks
|
||||
|
||||
This stack joins the same external Docker networks that the current proxy hosts depend on:
|
||||
|
||||
- `nextcloud_edge` for `nextcloud-app` and `portainer`
|
||||
- `npm-shared` for Islandflow services like `web` and `api`
|
||||
|
||||
Because the container name stays `nginx-proxy-manager`, the existing `proxy.deltaisland.io -> nginx-proxy-manager:81` host continues to work after migration.
|
||||
|
||||
### Upstream alias collisions
|
||||
|
||||
This NPM instance is attached to multiple Docker networks. If two stacks both expose a generic alias like `api` or `web`, Nginx can resolve the wrong upstream.
|
||||
|
||||
For Islandflow hosts, prefer explicit upstream hostnames in NPM:
|
||||
|
||||
- `islandflow-vps-web-1` on port `3000`
|
||||
- `islandflow-vps-api-1` on port `4000`
|
||||
|
||||
This avoids routing Islandflow traffic to similarly named containers from other stacks.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Copy `.env.example` to `.env` and adjust values if needed.
|
||||
2. Stop the old NPM service from `/home/delta/nextcloud`.
|
||||
3. Copy the existing state directories into this stack:
|
||||
|
||||
```bash
|
||||
cp -rf /home/delta/nextcloud/npm/data /home/delta/islandflow/deployment/npm/
|
||||
cp -rf /home/delta/nextcloud/npm/letsencrypt /home/delta/islandflow/deployment/npm/
|
||||
```
|
||||
|
||||
4. Start the new stack:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. Verify the expected hosts still load:
|
||||
|
||||
- `https://proxy.deltaisland.io`
|
||||
- `https://portainer.deltaisland.io`
|
||||
- `https://cloud.dpdrm.com`
|
||||
|
||||
## Current Live Proxy Hosts
|
||||
|
||||
- `cloud.dpdrm.com` -> `nextcloud-app:80`
|
||||
- `portainer.deltaisland.io` -> `portainer:9000`
|
||||
- `proxy.deltaisland.io` -> `nginx-proxy-manager:81`
|
||||
|
||||
Islandflow-specific host mapping should use explicit upstream container names whenever possible:
|
||||
|
||||
- `flow.deltaisland.io` -> `islandflow-vps-web-1:3000`
|
||||
- `api.flow.deltaisland.io` -> `islandflow-vps-api-1:4000`
|
||||
29
deployment/npm/docker-compose.yml
Normal file
29
deployment/npm/docker-compose.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
name: nginx-proxy-manager
|
||||
|
||||
services:
|
||||
npm:
|
||||
image: jc21/nginx-proxy-manager:2
|
||||
container_name: nginx-proxy-manager
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "${NPM_ADMIN_BIND_IP:-100.87.130.79}:81:81"
|
||||
- "443:443"
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
TZ: ${TZ}
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./letsencrypt:/etc/letsencrypt
|
||||
networks:
|
||||
- edge
|
||||
- shared
|
||||
|
||||
networks:
|
||||
edge:
|
||||
external: true
|
||||
name: ${NPM_EDGE_NETWORK:-nextcloud_edge}
|
||||
shared:
|
||||
external: true
|
||||
name: ${NPM_SHARED_NETWORK:-npm-shared}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { createClient, type ClickHouseClient } from "@clickhouse/client";
|
||||
import {
|
||||
AlertEventSchema,
|
||||
ClassifierHitEventSchema,
|
||||
|
|
@ -87,13 +86,133 @@ export type ClickHouseOptions = {
|
|||
password?: string;
|
||||
};
|
||||
|
||||
export const createClickHouseClient = (options: ClickHouseOptions): ClickHouseClient => {
|
||||
return createClient({
|
||||
url: options.url,
|
||||
database: options.database,
|
||||
username: options.username,
|
||||
password: options.password
|
||||
type ClickHouseQueryFormat = "JSONEachRow";
|
||||
|
||||
type ClickHouseQueryResult = {
|
||||
json<T>(): Promise<T>;
|
||||
};
|
||||
|
||||
export type ClickHouseClient = {
|
||||
exec(params: { query: string }): Promise<void>;
|
||||
insert(params: { table: string; values: unknown[]; format: ClickHouseQueryFormat }): Promise<void>;
|
||||
query(params: { query: string; format: ClickHouseQueryFormat }): Promise<ClickHouseQueryResult>;
|
||||
ping(): Promise<{ success: boolean; error?: Error }>;
|
||||
close(): Promise<void>;
|
||||
};
|
||||
|
||||
const buildBaseUrl = (options: ClickHouseOptions): URL => {
|
||||
const url = new URL(options.url);
|
||||
|
||||
if (options.database) {
|
||||
url.searchParams.set("database", options.database);
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
const buildHeaders = (options: ClickHouseOptions, hasBody: boolean): Headers => {
|
||||
const headers = new Headers();
|
||||
|
||||
if (hasBody) {
|
||||
headers.set("content-type", "text/plain; charset=utf-8");
|
||||
}
|
||||
|
||||
if (options.username || options.password) {
|
||||
const auth = Buffer.from(`${options.username ?? "default"}:${options.password ?? ""}`).toString("base64");
|
||||
headers.set("authorization", `Basic ${auth}`);
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
const executeClickHouse = async (
|
||||
options: ClickHouseOptions,
|
||||
query: string,
|
||||
body?: string
|
||||
): Promise<Response> => {
|
||||
const url = buildBaseUrl(options);
|
||||
url.searchParams.set("query", query);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: buildHeaders(options, body !== undefined),
|
||||
body
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const message = (await response.text()).trim() || `${response.status} ${response.statusText}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const parseJsonEachRow = <T>(text: string): T => {
|
||||
const trimmed = text.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return [] as T;
|
||||
}
|
||||
|
||||
const rows = trimmed
|
||||
.split("\n")
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
return rows as T;
|
||||
};
|
||||
|
||||
export const createClickHouseClient = (options: ClickHouseOptions): ClickHouseClient => {
|
||||
return {
|
||||
async exec({ query }) {
|
||||
await executeClickHouse(options, query);
|
||||
},
|
||||
|
||||
async insert({ table, values, format }) {
|
||||
const rows = values.map((value) => JSON.stringify(value)).join("\n");
|
||||
const body = rows.length > 0 ? `${rows}\n` : "";
|
||||
await executeClickHouse(options, `INSERT INTO ${table} FORMAT ${format}`, body);
|
||||
},
|
||||
|
||||
async query({ query, format }) {
|
||||
const response = await executeClickHouse(options, `${query} FORMAT ${format}`);
|
||||
return {
|
||||
async json<T>() {
|
||||
const text = await response.text();
|
||||
return parseJsonEachRow<T>(text);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
async ping() {
|
||||
try {
|
||||
const url = buildBaseUrl(options);
|
||||
url.pathname = "/ping";
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: buildHeaders(options, false)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const message = (await response.text()).trim() || `${response.status} ${response.statusText}`;
|
||||
return { success: false, error: new Error(message) };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return { success: false, error };
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async close() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const ensureOptionPrintsTable = async (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue