From a520b26398d64eef72da7dc45d1ac12d5930f594 Mon Sep 17 00:00:00 2001 From: maaz519 Date: Fri, 3 Jul 2026 19:19:13 +0530 Subject: [PATCH] chore(iios): production Dockerfile + migrate entrypoint + env/secrets contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a multi-stage Dockerfile for iios-service (build → pnpm deploy prune → slim non-root runtime), a docker-entrypoint that runs `prisma migrate deploy` then starts the server as PID 1, a .dockerignore, a fully-commented .env.example config contract, and docs/DEPLOYMENT.md (topology, scaling, release strategy, prod-readiness gaps). Moves prisma to dependencies so the CLI ships in the prod bundle. Image builds, migrates, and serves /health 200. Co-Authored-By: Claude Opus 4.8 (1M context) --- .dockerignore | 16 ++++ docs/DEPLOYMENT.md | 100 +++++++++++++++++++++ packages/iios-service/.env.example | 48 ++++++++++ packages/iios-service/Dockerfile | 54 +++++++++++ packages/iios-service/docker-entrypoint.sh | 19 ++++ packages/iios-service/package.json | 2 +- pnpm-lock.yaml | 6 +- 7 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 docs/DEPLOYMENT.md create mode 100644 packages/iios-service/.env.example create mode 100644 packages/iios-service/Dockerfile create mode 100644 packages/iios-service/docker-entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..08915d9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +# Build context hygiene — keep the image small & builds reproducible. +**/node_modules +**/dist +**/.turbo +**/.next +**/coverage +.git +.gitignore +*.log +**/*.tsbuildinfo +.env +.env.* +!.env.example +/tmp +scratchpad +docs/openapi.yaml diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..7e995a9 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,100 @@ +# Deploying IIOS + +IIOS deploys as **one runtime service** (`@insignia/iios-service`) plus **one datastore** +(Postgres). Everything else in the repo is a **library** (SDKs) or a **dev demo**, not a +deployed service. + +| Artifact | Type | How it ships | +|---|---|---| +| `@insignia/iios-service` | **Runtime service** | Docker image (this guide) | +| `iios-contracts`, `iios-adapter-sdk`, `iios-kernel-client`, `iios-testkit` | Library | published to npm; imported by host apps | +| `iios-message-web`, `iios-inbox-web`, `iios-support-web`, `iios-ai-web`, `iios-meeting-web`, `iios-community-web` | Web SDK (library) | published to npm; **bundled into host-app frontends** (optionally one CDN widget) | +| `apps/*` (message-demo, ai-studio, route-admin…) | Dev demo | not deployed | + +The service is a **modular monolith**: a single NestJS process serves the HTTP API, the +WebSocket (socket.io) gateway, the outbox relay, and the projectors together. + +## Build the image + +Build context is the **monorepo root** (the image needs the workspace + lockfile): + +```bash +docker build -f packages/iios-service/Dockerfile -t iios-service:latest . +``` + +The multi-stage build compiles the service + its workspace deps, generates the Prisma +client, and prunes to a self-contained prod bundle (`pnpm deploy --legacy`). The runtime +image runs as the non-root `node` user and starts via `docker-entrypoint.sh`, which runs +`prisma migrate deploy` then `node dist/main.js`. + +## Run it + +```bash +docker run --rm -p 3200:3200 \ + -e DATABASE_URL='postgresql://USER:PASS@HOST:5432/iios?schema=public' \ + -e APP_SECRETS='{"portal-demo":""}' \ + -e IIOS_DEV_TOKENS=0 \ + iios-service:latest +``` + +Health check: `GET /health` → `200`. Metrics/ops: `GET /metrics` (relay lag, projection +cursors, retention snapshot counts). + +## Configuration & secrets contract + +Every knob is an environment variable — see [`packages/iios-service/.env.example`](../packages/iios-service/.env.example) +for the full, commented list. Highlights: + +- **Secrets** (inject from a vault, never bake into the image): `DATABASE_URL`, + `APP_SECRETS` (per-app JWT signing keys), `ADAPTER_SECRETS` (webhook HMAC keys). +- **⚠️ `IIOS_DEV_TOKENS` MUST be `0`/unset in production.** It exposes `/v1/dev/*` + (unauthenticated token minting, webhook injection, chaos, retention sweep). This is the + single most important prod-hardening flag. +- **`IIOS_CELL_ID`** tags which physical cell this instance serves — the hook for splitting + noisy/regulated tenants into isolated cells later, with no code change. +- **Worker timers** (`IIOS_RELAY_INTERVAL_MS`, `IIOS_RETENTION_SWEEP_INTERVAL_MS`) — see + scaling notes below. + +## Topology + +``` + host apps ──HTTPS/WSS──► [ LB / ingress ] ──► iios-service ×N ──► Postgres (pgvector) + (embed SDKs) (WS + sticky) (API+relay+ │ + projectors) (+ Redis, + OPA/CMP/MDM + as external ports — later) +``` + +- **Postgres** — managed (RDS / Cloud SQL / Neon). One logical DB, tenant-isolated by scope. +- **Redis** — **not required for a single instance.** Add it when you run **multiple replicas + with live chat** (socket.io needs its Redis adapter to fan-out across instances) or when + BullMQ queues are introduced. +- **Platform ports** (OPA policy, CMP consent, MDM, CRRE, SAS) — today in-process permissive + stubs (`LocalDevPorts`). For production, point these at real external services; the service + already calls them **fail-closed**. + +## Scaling & release strategy + +**Horizontal scaling is safe** because the event core was hardened for it: + +- The outbox relay claims rows with `FOR UPDATE SKIP LOCKED` → each event is relayed by + exactly one replica; its co-located projectors process it once (idempotent `claim()` + + the projection-cursor + idempotency-command ledgers give exactly-once effects). +- The **DLQ + replay** path means a bad deploy loses nothing — fix and replay. + +**Rolling / blue-green deploys:** + +1. **Migrations:** run `prisma migrate deploy` as a **one-off pre-deploy job**, then set + `IIOS_SKIP_MIGRATE=1` on the replicas so N pods don't race the migration. (For a single + instance, the default boot-time migration is fine.) +2. Use **expand-contract (backward-compatible) schema changes** so old and new pods can run + against the same schema during the roll — this is the prerequisite for zero-downtime. +3. Roll replicas with a `/health` readiness gate; drain WebSocket connections on `SIGTERM`. +4. **WebSocket ingress** needs sticky sessions (and the Redis socket.io adapter once N>1). + +## Not yet built (prod-readiness gaps) + +- **CI/CD pipeline** to build/push this image and run migrations. +- **Redis + socket.io Redis adapter** wiring (needed at N>1 with realtime). +- **Real platform-port services** (OPA/CMP/MDM) — today permissive stubs. +- **Secrets manager** integration (currently plain env). +- A **schema-compatibility gate** in CI to enforce expand-contract migrations. diff --git a/packages/iios-service/.env.example b/packages/iios-service/.env.example new file mode 100644 index 0000000..9ca6fb4 --- /dev/null +++ b/packages/iios-service/.env.example @@ -0,0 +1,48 @@ +# ───────────────────────────────────────────────────────────────────────────── +# iios-service configuration contract. +# Copy to `.env` for local dev; in prod, inject via your secrets manager / orchestrator. +# 🔒 = secret (never commit a real value). ⚠️ = must be set correctly for prod safety. +# ───────────────────────────────────────────────────────────────────────────── + +# ── Core ───────────────────────────────────────────────────────────────────── +DATABASE_URL=postgresql://iios:iios@localhost:5434/iios?schema=public # 🔒 Postgres connection +PORT=3200 +# NODE_ENV=production # set by the Docker image + +# ── Secrets ────────────────────────────────────────────────────────────────── +# JSON map of appId → HS256 signing secret used to verify session JWTs (SessionVerifier). +APP_SECRETS={"portal-demo":"dev-secret"} # 🔒 +# JSON map of channelType → inbound webhook HMAC secret (adapter signature check). +ADAPTER_SECRETS={"WEBHOOK":"dev-adapter-secret"} # 🔒 +# Default scope an unauthenticated adapter webhook ingests into. +ADAPTER_APP=portal-demo +ADAPTER_ORG=org_demo + +# ── ⚠️ Production-safety flags ──────────────────────────────────────────────── +# Enables /v1/dev/* (token mint, webhook inject, chaos, retention sweep). MUST be unset +# or 0 in production — leaving it on exposes unauthenticated token minting. +IIOS_DEV_TOKENS=0 +# Skip `prisma migrate deploy` at boot (set to 1 when a separate migration job runs). +IIOS_SKIP_MIGRATE=0 + +# ── Tenant isolation ───────────────────────────────────────────────────────── +IIOS_CELL_ID=cell-default # physical cell this instance serves (blast-radius partition) + +# ── Background workers ─────────────────────────────────────────────────────── +IIOS_RELAY_INTERVAL_MS=500 # outbox relay tick; 0 disables the timer +IIOS_OUTBOX_MAX_ATTEMPTS=5 # relay dead-letters an event after N failed publishes +IIOS_RETENTION_SWEEP_INTERVAL_MS=0 # retention sweep tick; 0 disables (run via job instead) +IIOS_RETENTION_POLICY_VERSION=v1 +IIOS_RETENTION_ARCHIVE_DAYS=90 # default archive window (per-class override: _INTERNAL/_RESTRICTED/_CONFIDENTIAL/_REGULATED) +IIOS_RETENTION_DELETE_DAYS=365 # default delete (redact) window; same per-class overrides + +# ── Rate limits / quotas / budgets ─────────────────────────────────────────── +IIOS_OUTBOUND_LIMIT=5 # per-(channel,target) sends per window +IIOS_OUTBOUND_WINDOW_MS=60000 +IIOS_TENANT_OUTBOUND_LIMIT=10000 # per-tenant egress cap per window (noisy-neighbor guard) +IIOS_TENANT_OUTBOUND_WINDOW_MS=60000 +IIOS_AI_BUDGET_UNITS=100000 # per-scope AI cost-unit budget (KG-12) + +# ── Capability providers (governed egress targets) ─────────────────────────── +# Per-channel provider endpoint the CapabilityBroker calls, e.g.: +# IIOS_PROVIDER_URL_EMAIL=https://provider.internal/email diff --git a/packages/iios-service/Dockerfile b/packages/iios-service/Dockerfile new file mode 100644 index 0000000..6670f67 --- /dev/null +++ b/packages/iios-service/Dockerfile @@ -0,0 +1,54 @@ +# syntax=docker/dockerfile:1 +# Production image for @insignia/iios-service (the single IIOS backend). +# Build context MUST be the monorepo ROOT (it needs the workspace + lockfile): +# docker build -f packages/iios-service/Dockerfile -t iios-service:latest . +# +# Multi-stage: (1) build the service + its workspace deps and prune to a +# self-contained prod bundle via `pnpm deploy`; (2) a slim runtime that only +# carries that bundle. Migrations run at container start (see docker-entrypoint.sh). + +ARG NODE_VERSION=22-bookworm-slim +ARG PNPM_VERSION=10.6.2 + +# ── build ─────────────────────────────────────────────────────────────────── +FROM node:${NODE_VERSION} AS build +ARG PNPM_VERSION +ENV PNPM_HOME=/pnpm PATH=/pnpm:$PATH +# openssl + ca-certificates are required by Prisma's engine on debian-slim. +RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* +RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate + +WORKDIR /repo +COPY . . + +# Full install (needs devDeps to compile), then build ONLY the service + its +# workspace dependencies, generate the Prisma client, and emit a pruned prod bundle. +RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ + pnpm install --frozen-lockfile +# Generate the Prisma client BEFORE compiling — nest build (tsc) needs its types, +# otherwise Prisma results are `any` and noImplicitAny fails the build. +RUN pnpm --filter @insignia/iios-service exec prisma generate \ + && pnpm --filter "@insignia/iios-service..." build \ + && pnpm --filter @insignia/iios-service --prod --legacy deploy /app + +# ── runtime ────────────────────────────────────────────────────────────────── +FROM node:${NODE_VERSION} AS runtime +ENV NODE_ENV=production +# Prisma engine needs openssl at runtime too. +RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY --from=build /app ./ +# Regenerate the Prisma client against the final filesystem (deterministic). +RUN node_modules/.bin/prisma generate + +# Run as the built-in non-root `node` user. +RUN chmod +x docker-entrypoint.sh && chown -R node:node /app +USER node + +EXPOSE 3200 +ENV PORT=3200 +# migrate deploy → start; see docker-entrypoint.sh. +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/packages/iios-service/docker-entrypoint.sh b/packages/iios-service/docker-entrypoint.sh new file mode 100644 index 0000000..60fd8c6 --- /dev/null +++ b/packages/iios-service/docker-entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# Container entrypoint for iios-service. +# 1) Apply pending DB migrations (idempotent; safe to run on every boot). +# 2) Exec the server as PID 1 so signals (SIGTERM) reach Node for graceful shutdown. +# +# NOTE: at higher replica counts, prefer running `prisma migrate deploy` ONCE as a +# separate pre-deploy job/init-container and set IIOS_SKIP_MIGRATE=1 here, so N +# replicas don't race the migration on rollout. +set -e + +if [ "${IIOS_SKIP_MIGRATE:-0}" != "1" ]; then + echo "[entrypoint] applying migrations (prisma migrate deploy)…" + node_modules/.bin/prisma migrate deploy +else + echo "[entrypoint] IIOS_SKIP_MIGRATE=1 — skipping migrations" +fi + +echo "[entrypoint] starting iios-service on :${PORT:-3200}" +exec node dist/main.js diff --git a/packages/iios-service/package.json b/packages/iios-service/package.json index a5c36fd..819bd61 100644 --- a/packages/iios-service/package.json +++ b/packages/iios-service/package.json @@ -20,6 +20,7 @@ "@nestjs/platform-socket.io": "^11.1.27", "@nestjs/websockets": "^11.1.27", "@prisma/client": "^6.2.1", + "prisma": "^6.2.1", "socket.io": "^4.8.3", "class-transformer": "^0.5.1", "class-validator": "^0.15.1", @@ -35,7 +36,6 @@ "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^26.0.1", - "prisma": "^6.2.1", "socket.io-client": "^4.8.3", "typescript": "^5.7.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a3bc76..5f69263 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -364,6 +364,9 @@ importers: jsonwebtoken: specifier: ^9.0.3 version: 9.0.3 + prisma: + specifier: ^6.2.1 + version: 6.19.3(typescript@5.9.3) reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -392,9 +395,6 @@ importers: '@types/node': specifier: ^26.0.1 version: 26.0.1 - prisma: - specifier: ^6.2.1 - version: 6.19.3(typescript@5.9.3) socket.io-client: specifier: ^4.8.3 version: 4.8.3