Skip to main content

Architecture: Deployment Infrastructure

Status: Active Last updated: 2026-05-02 Audience: AI agents and engineers shipping the platform to a new environment, building Docker images, debugging Kubernetes manifests, or extending the deploy pipeline.


1. Purpose​

The platform ships as four separate runtime artifacts:

  • ever-works-api — the NestJS REST API
  • ever-works-web — the Next.js dashboard
  • ever-works-mcp — the MCP server
  • ever-works-docs — the Docusaurus static documentation site

A fifth runtime — the Trigger.dev worker — is hosted by Trigger.dev itself and deployed via their SDK; it doesn't ship as a container image (see trigger-worker).

This spec covers the .deploy/ layout, the per-app Dockerfile strategy (multi-stage with turbo prune), the Kubernetes manifest set (six manifests across dev / stage / prod × core / mcp), the Compose stack for self-hosted deploys, and the environment variable contract every runtime expects.

2. The .deploy/ Layout​

.deploy/
├── docker/ # Per-app Dockerfile + helpers
│ ├── api/
│ │ ├── Dockerfile
│ │ └── entrypoint.sh
│ ├── web/
│ │ └── Dockerfile
│ ├── mcp/
│ │ └── Dockerfile
│ └── docs/
│ ├── Dockerfile
│ ├── docker-compose.yml # Standalone docs stack
│ └── nginx.conf
└── k8s/ # Kubernetes manifests
├── k8s-manifest.dev.yaml
├── k8s-manifest.stage.yaml
├── k8s-manifest.prod.yaml
├── k8s-manifest.mcp.dev.yaml
├── k8s-manifest.mcp.stage.yaml
└── k8s-manifest.mcp.prod.yaml

Plus the monorepo-level compose.yaml at the repo root for running the full stack locally as containers.

3. Dockerfile Strategy: turbo prune Multi-Stage​

Every app's Dockerfile follows the same pattern, established by apps/web/Dockerfile and replicated for api, mcp, docs:

# Stage 1: pruner — produces a lean build context for one app
FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@10 --activate && npm install -g turbo@latest

FROM base AS builder
USER node
WORKDIR /app
COPY . .
RUN turbo prune --scope=ever-works-<app> --docker

# Stage 2: installer — installs only the deps the app needs
FROM base AS installer
USER root
WORKDIR /app
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/.gitignore .gitignore
RUN cd apps/<app> && pnpm install --frozen-lockfile
COPY --from=builder /app/out/full/ .
RUN pnpm build --filter=ever-works-<app>...

# Stage 3: runner — the actual production image
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=installer --chown=node:node /app/apps/<app>/dist ./dist
# ... (per-app runtime files)
USER node
EXPOSE <port>
CMD ["node", "dist/main.js"]

The key win is turbo prune --scope=<app> --docker:

StageOutput
out/json/Every package.json + lockfile, not source — for layer cache
out/full/All source files for the app + its workspace dependencies

The out/json copy goes into the cached layer first; only when a package.json changes does the install layer rebuild. Source-only changes invalidate just the build layer. Result: typical incremental rebuilds finish in 30–90 seconds vs 5+ minutes for a naive COPY ..

4. Per-App Image Specifics​

4.1 api​

  • Base image: node:22-alpine
  • Entrypoint: entrypoint.sh runs migration:run on first boot, then starts the NestJS server.
  • Exposes: 3100.
  • Special: requires DATABASE_URL, JWT_SECRET, PLUGIN_SECRETS_ENCRYPTION_KEY. See §8.

4.2 web​

  • Base image: node:22-alpine
  • Built with next build standalone output (NEXT_BUILD_OUTPUT=standalone) so the final image only contains node_modules + .next/standalone
    • .next/static + public.
  • Exposes: 3000.
  • Special: requires python3, pkgconfig, pixman-dev, cairo-dev, pango-dev, jpeg-dev, giflib-dev for the canvas + sharp native deps.

4.3 mcp​

  • Base image: node:22-alpine
  • Two entry-point scripts:
    • start:stdio for Claude Desktop / Code subprocess mode.
    • start:http for remote / containerised mode (most relevant in Docker).
  • Exposes: 3200 (HTTP mode only).
  • Special: requires EVER_WORKS_API_KEY, EVER_WORKS_API_URL. See mcp-server-internals.

4.4 docs​

  • Base image: nginx:1.27-alpine (multi-stage builds the static site from Docusaurus, then serves it via nginx).
  • Custom nginx.conf in .deploy/docker/docs/.
  • Exposes: 80.
  • Special: stateless; no DB, no env-var requirements beyond the static site's contents.

5. The Compose Stack (compose.yaml)​

compose.yaml at the repo root composes all four services for self-hosted deploys:

services:
ever-works-api:
build: .
dockerfile: .deploy/docker/api/Dockerfile
env_file: .env.compose
ports: ['3100:3100']
depends_on: [postgres]
ever-works-web:
build: .
dockerfile: .deploy/docker/web/Dockerfile
env_file: .env.compose
ports: ['3000:3000']
depends_on: [ever-works-api]
ever-works-mcp:
build: .
dockerfile: .deploy/docker/mcp/Dockerfile
env_file: .env.compose
ports: ['3200:3200']
depends_on: [ever-works-api]
ever-works-docs:
build: .
dockerfile: .deploy/docker/docs/Dockerfile
ports: ['8080:80']
postgres:
image: postgres:16-alpine
env_file: .env.compose
volumes: [postgres-data:/var/lib/postgresql/data]
volumes:
postgres-data:

.env.compose is the single env-var source for every service. Operators copy .env.compose.example to .env.compose, fill in secrets, and docker compose up. The example file is checked in; the real file is .gitignore'd.

6. Kubernetes Manifests​

.deploy/k8s/ ships six manifests:

ManifestEnvironmentApps included
k8s-manifest.dev.yamldevapi, web
k8s-manifest.stage.yamlstageapi, web
k8s-manifest.prod.yamlprodapi, web
k8s-manifest.mcp.dev.yamldevmcp (separate so it can be enabled per-environment)
k8s-manifest.mcp.stage.yamlstagemcp
k8s-manifest.mcp.prod.yamlprodmcp

Each manifest contains:

  • Service definitions (ClusterIP, port mapping)
  • Deployment definitions with replicas, rolling-update strategy, health probes
  • Ingress rules (HTTPS termination at the cluster edge)
  • Secret references (manifests reference Secret names; the actual values come from cluster-side secret management)

6.1 Replicas + Rollout​

Production deployment defaults:

ServiceReplicasStrategyMax surgeMax unavailable
ever-works-api2RollingUpdate10
ever-works-web2RollingUpdate10
ever-works-mcp1Recreate——

maxUnavailable: 0 means rollouts always over-provision rather than under-provision — no period of degraded capacity during deploys.

6.2 Health Probes​

All long-running services expose /health (see monitoring §10):

livenessProbe:
httpGet: { path: /api/health, port: 3100 }
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet: { path: /api/health, port: 3100 }
initialDelaySeconds: 10
periodSeconds: 5

The MCP server uses its own /health endpoint with shorter delays (stateless, fast cold start).

6.3 Resource Requests + Limits​

Production-tier pods:

ServiceCPU requestCPU limitMemory requestMemory limit
ever-works-api500m21 Gi2 Gi
ever-works-web250m1512 Mi1 Gi
ever-works-mcp100m500m256 Mi512 Mi

Tunable per-deploy via the manifest. dev / stage halve the requests.

7. Build Pipeline​

CI runs on every push to develop (staging) and main (production):

  1. pnpm install --frozen-lockfile — single root install for the whole monorepo.
  2. Type-check + lint + test via turbo — caches across runs.
  3. Build all docker images in parallel — each .deploy/docker/<app>/Dockerfile is built as ghcr.io/ever-works/ever-works-<app>:<sha>.
  4. Push to ghcr.io with both :<sha> and :latest tags (latest only on main).
  5. Deploy — kubectl apply -k overlays/prod/ (kustomize) updates image tags in the production manifest.
  6. Trigger.dev deploy — pnpm deploy:trigger deploys the worker code to Trigger.dev's hosted runtime.
  7. Smoke tests against the deployed environment.
  8. Sentry release — tag the deploy with the git SHA so error traces resolve to the right source-map.

A failed step at any point halts the pipeline and the previous image stays in production.

8. Environment Variables​

Every runtime reads its config from env vars. The contract is documented in docs/environment-variables.md; the categories are:

CategoryExamples
DatabaseDATABASE_TYPE, DATABASE_URL, DATABASE_POOL_MAX
Auth secretsJWT_SECRET, JWT_REFRESH_SECRET
EncryptionPLUGIN_SECRETS_ENCRYPTION_KEY
OAuth providersGITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GOOGLE_*
AI provider keys (when default)PLUGIN_OPENROUTER_API_KEY
MailerMAILER_PROVIDER, SMTP_* / POSTMARK_API_TOKEN / etc.
SubscriptionsSTRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
MonitoringSENTRY_DSN, POSTHOG_PROJECT_KEY, POSTHOG_HOST
Trigger.devTRIGGER_API_URL, TRIGGER_SECRET_KEY
Public clientNEXT_PUBLIC_API_URL, NEXT_PUBLIC_SENTRY_DSN

Plugin-specific env vars follow the convention PLUGIN_<plugin-id-upper>_<setting> and are consumed via the x-envVar schema extension (see settings-system §8).

Every required var is validated at startup; missing values fail boot with a clear log line listing each one. NEXT_PUBLIC_* vars are inlined at Next.js build time, not at runtime.

9. Secrets in Production​

LayerMechanism
KubernetesSecret objects, mounted as env vars
Cloud KMSCluster secrets sourced from cloud provider's secret manager
OAuth tokensEncrypted in DB via PLUGIN_SECRETS_ENCRYPTION_KEY (see settings system)
Plugin secretsSame — the secrets column on plugin_settings / user_plugins / work_plugins is AES-256-GCM

Operators rotate PLUGIN_SECRETS_ENCRYPTION_KEY via the registered-keys mechanism documented in settings-system §5.

10. Multi-Region & Scale-Out​

Today the production deploy is single-region. Multi-region is on the roadmap but not implemented:

  • The DB is a single primary with read replicas in the same region.
  • The API + web are stateless — they scale horizontally cleanly.
  • The Trigger.dev worker scales independently via Trigger.dev's own concurrency settings.
  • The MCP server is also stateless but typically runs replicas: 1 because each instance holds an in-memory OpenAPI cache (see mcp-server-internals).

Sticky sessions are not required — JWT auth is stateless; WebSocket connections balance per-pod via Sec-WebSocket-Protocol headers.

11. Observability of Deploys​

Every deploy emits:

  • Sentry release event (with git SHA, environment, source-maps).
  • PostHog deploy_completed event with environment + duration.
  • Kubernetes deployment events surfaced via cluster logging.
  • A dashboard banner if a deploy is in Progressing for >5 min (alerting hook).

The release Sentry tag (see monitoring §4) is the cross-pivot key — every error in a deploy window correlates to the active release.

12. Constitution Reconciliation​

PrincipleHow deployment respects it
I — Plugin-firstPlugins ship as workspace packages; no per-plugin container.
II — Capability-drivenCapability resolution is runtime; deploy doesn't bake choices in.
III — Source-of-truth reposThe platform never deploys user repos — only its own code.
IV — Trigger.devThe worker deploys separately via the Trigger.dev SDK, not as a container.
V — Forward-only migrationsentrypoint.sh runs migration:run before serving — guaranteed forward direction.
VI — TestsCI runs the full test suite before any image build.
VII — Secret hygieneSecrets via cluster Secret objects; encryption keys rotate via the registered-keys story.
VIII — Plugin countsPlugin counts are runtime queries against the deployed registry.
IX — Behaviour-firstThis spec describes observable deploy behaviour.
X — Backwards-compatForward-only migrations + image-tag-versioned rollouts allow safe rollback.

13. References​