# SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 # The three processes of this app, from one image (decision AT-D2, spec ยง13). # # Ordering (decision AT-D14, verified 2026-10-19 against conusai-deployer): the platform starts a # deployment with a single `docker compose up -d --wait`, so `"kind": "job"` in conusai.json does # not make the migrate service run first, and `pre_deploy_command` has no executor at all. What # orders them is here: migrate carries `restart: "no"`, and web and worker wait for it with # `condition: service_completed_successfully`. A failed migration therefore leaves the old # containers running instead of starting a new release against an unmigrated schema. # # No service publishes a host port: the platform decides what is reachable from the # `public` block of conusai.json. No service names a privileged option; the platform overlay adds # `cap_drop: ALL`, `no-new-privileges` and the pid and log limits on top of what is here. services: web: build: context: . dockerfile: Dockerfile target: runtime working_dir: /app/apps/web # argv, never a shell string, and `next` directly rather than through a package manager: a # package manager as PID 1 does not forward SIGTERM to the server it started. command: ["node", "node_modules/next/dist/bin/next", "start"] environment: NODE_ENV: production PORT: "3000" CONUSAI_API_URL: ${CONUSAI_API_URL} CONUSAI_API_TOKEN: ${CONUSAI_API_TOKEN} CONUSAI_PROJECT_ID: ${CONUSAI_PROJECT_ID} CONUSAI_PUBLIC_URL: ${CONUSAI_PUBLIC_URL} CONUSAI_ORG_ID: ${CONUSAI_ORG_ID} CONUSAI_ORG_SLUG: ${CONUSAI_ORG_SLUG:-} CONUSAI_RELEASE: ${CONUSAI_RELEASE} CONUSAI_ENVIRONMENT: ${CONUSAI_ENVIRONMENT:-} CONUSAI_DSN: ${CONUSAI_DSN:-} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} OTEL_EXPORTER_OTLP_PROTOCOL: ${OTEL_EXPORTER_OTLP_PROTOCOL:-} OTEL_EXPORTER_OTLP_HEADERS: ${OTEL_EXPORTER_OTLP_HEADERS:-} OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-} OTEL_SERVICE_VERSION: ${OTEL_SERVICE_VERSION:-} AI_MODEL: ${AI_MODEL:-} LOG_LEVEL: ${LOG_LEVEL:-} # The support channel: the web mints widget sessions for it and the worker's account sweep erases by it. # kit:support CONUSAI_SUPPORT_CHANNEL: ${CONUSAI_SUPPORT_CHANNEL:-} # kit:support POSTGRES_URL: ${POSTGRES_URL} POSTGRES_READER_URL: ${POSTGRES_READER_URL} AUTH_SESSION_SECRET: ${AUTH_SESSION_SECRET} OIDC_ISSUER: ${OIDC_ISSUER:-} OIDC_WEB_CLIENT_ID: ${OIDC_WEB_CLIENT_ID:-} OIDC_WEB_CLIENT_SECRET: ${OIDC_WEB_CLIENT_SECRET:-} OIDC_AUDIENCE: ${OIDC_AUDIENCE:-} OIDC_RESOURCE: ${OIDC_RESOURCE:-} OIDC_INTROSPECTION_CLIENT_ID: ${OIDC_INTROSPECTION_CLIENT_ID:-} OIDC_INTROSPECTION_CLIENT_SECRET: ${OIDC_INTROSPECTION_CLIENT_SECRET:-} OIDC_RESOURCE_STATUS_URL: ${OIDC_RESOURCE_STATUS_URL:-} OIDC_SCOPES: ${OIDC_SCOPES:-} AUTH_PROVIDER_GENERATION: ${AUTH_PROVIDER_GENERATION:-} AUTH_COOKIE_NAME: ${AUTH_COOKIE_NAME:-} AUTH_SESSION_IDLE_SECONDS: ${AUTH_SESSION_IDLE_SECONDS:-} AUTH_SESSION_ABSOLUTE_SECONDS: ${AUTH_SESSION_ABSOLUTE_SECONDS:-} AUTH_PROVIDER: ${AUTH_PROVIDER:-} AUTH_IDENTITY_NAMESPACE: ${AUTH_IDENTITY_NAMESPACE:-} # kit:convex-better-auth:begin -- the Better Auth deployment (AUTH_PROVIDER=convex-better-auth) BETTER_AUTH_CONVEX_URL: ${BETTER_AUTH_CONVEX_URL:-} BETTER_AUTH_CONVEX_SITE_URL: ${BETTER_AUTH_CONVEX_SITE_URL:-} BETTER_AUTH_CONVEX_ADMIN_KEY: ${BETTER_AUTH_CONVEX_ADMIN_KEY:-} BETTER_AUTH_RELAY_INTERVAL_MS: ${BETTER_AUTH_RELAY_INTERVAL_MS:-} # kit:convex-better-auth:end expose: ["3000"] depends_on: migrate: condition: service_completed_successfully healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/ready').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"] interval: 10s timeout: 5s retries: 3 start_period: 20s init: true stop_grace_period: 20s restart: unless-stopped deploy: resources: limits: cpus: "1.0" memory: 768M worker: build: context: . dockerfile: Dockerfile target: runtime working_dir: /app command: ["node", "apps/worker/dist/main.js"] environment: NODE_ENV: production CONUSAI_API_URL: ${CONUSAI_API_URL} CONUSAI_API_TOKEN: ${CONUSAI_API_TOKEN} CONUSAI_PROJECT_ID: ${CONUSAI_PROJECT_ID} CONUSAI_ORG_ID: ${CONUSAI_ORG_ID} CONUSAI_ORG_SLUG: ${CONUSAI_ORG_SLUG:-} CONUSAI_PUBLIC_URL: ${CONUSAI_PUBLIC_URL} CONUSAI_RELEASE: ${CONUSAI_RELEASE} CONUSAI_ENVIRONMENT: ${CONUSAI_ENVIRONMENT:-} CONUSAI_DSN: ${CONUSAI_DSN:-} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} OTEL_EXPORTER_OTLP_PROTOCOL: ${OTEL_EXPORTER_OTLP_PROTOCOL:-} OTEL_EXPORTER_OTLP_HEADERS: ${OTEL_EXPORTER_OTLP_HEADERS:-} OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-} OTEL_SERVICE_VERSION: ${OTEL_SERVICE_VERSION:-} AI_MODEL: ${AI_MODEL:-} LOG_LEVEL: ${LOG_LEVEL:-} DOCUMENTS_EXCLUDED_PAGE_CLASSES: ${DOCUMENTS_EXCLUDED_PAGE_CLASSES:-} CONUSAI_SUPPORT_CHANNEL: ${CONUSAI_SUPPORT_CHANNEL:-} # kit:support # kit:convex-better-auth:begin -- the sweep ends a deleted person's Better Auth account AUTH_PROVIDER: ${AUTH_PROVIDER:-} AUTH_IDENTITY_NAMESPACE: ${AUTH_IDENTITY_NAMESPACE:-} BETTER_AUTH_CONVEX_URL: ${BETTER_AUTH_CONVEX_URL:-} BETTER_AUTH_CONVEX_ADMIN_KEY: ${BETTER_AUTH_CONVEX_ADMIN_KEY:-} # kit:convex-better-auth:end POSTGRES_URL: ${POSTGRES_URL} POSTGRES_READER_URL: ${POSTGRES_READER_URL} WORKER_HEARTBEAT_FILE: /tmp/worker-heartbeat depends_on: migrate: condition: service_completed_successfully healthcheck: # The relay writes the file on every pass; two missed passes is a stuck worker. test: ["CMD", "node", "apps/worker/dist/healthcheck.js"] interval: 15s timeout: 5s retries: 3 start_period: 20s init: true stop_grace_period: 30s restart: unless-stopped deploy: resources: limits: cpus: "1.0" memory: 512M migrate: build: context: . dockerfile: Dockerfile target: runtime working_dir: /app command: ["node", "apps/worker/dist/migrate.js"] environment: NODE_ENV: production CONUSAI_API_URL: ${CONUSAI_API_URL} CONUSAI_API_TOKEN: ${CONUSAI_API_TOKEN} CONUSAI_PROJECT_ID: ${CONUSAI_PROJECT_ID} CONUSAI_RELEASE: ${CONUSAI_RELEASE} CONUSAI_ENVIRONMENT: ${CONUSAI_ENVIRONMENT:-} CONUSAI_DSN: ${CONUSAI_DSN:-} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} OTEL_EXPORTER_OTLP_PROTOCOL: ${OTEL_EXPORTER_OTLP_PROTOCOL:-} OTEL_EXPORTER_OTLP_HEADERS: ${OTEL_EXPORTER_OTLP_HEADERS:-} OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-} OTEL_SERVICE_VERSION: ${OTEL_SERVICE_VERSION:-} POSTGRES_URL: ${POSTGRES_URL} POSTGRES_READER_URL: ${POSTGRES_READER_URL} # The owner's URL. The platform sends it to this service alone, because this is the service # whose conusai.json entry declares postgres.migrate (data-services DS21, decision AT-D20). POSTGRES_MIGRATE_URL: ${POSTGRES_MIGRATE_URL} # kit:convex-better-auth:begin -- the migrate job deploys the Better Auth functions, and alone holds the secret AUTH_PROVIDER: ${AUTH_PROVIDER:-} CONUSAI_PUBLIC_URL: ${CONUSAI_PUBLIC_URL} BETTER_AUTH_CONVEX_URL: ${BETTER_AUTH_CONVEX_URL:-} BETTER_AUTH_CONVEX_ADMIN_KEY: ${BETTER_AUTH_CONVEX_ADMIN_KEY:-} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-} # kit:convex-better-auth:end # A job runs once. Without this, compose restarts it and `service_completed_successfully` # is never reached. restart: "no" init: true deploy: resources: limits: cpus: "1.0" memory: 512M