25 KiB
Deployment
Docker Baseline Health
The current baseline is enforced by static Docker, workflow, health, and artifact checks. Historical Docker Baseline evidence is archived in milestone records; this page lists the commands agents should use for the current repository state.
Baseline Commands
The table below is the current command contract and expected result for agents/operators. It is not live-environment proof unless a dated CI or deployment run is attached separately.
| Command | Expected exit code | Expected result | Failure fingerprint |
|---|---|---|---|
node scripts/verify-docker-baseline.mjs |
0 | Pass | Enforces web, API, and worker Docker target shape, runtime start commands, healthcheck paths, and runtime tsx exclusions. |
pnpm run lint |
0 | Pass | ESLint runs with --max-warnings=0; warnings are failures. |
pnpm run test |
0 | Pass | Runs the repository non-watch Vitest/script suite configured in package.json. |
pnpm typecheck |
0 | Pass | Prisma client generation, React Router typegen, and tsc -b complete successfully. |
pnpm run verify:production-seed-artifact |
0 | Pass | Verifies the Coolify production seed artifact build contract and, after build, checks for forbidden bare @toolctrl/* runtime imports. |
Use focused Vitest files for narrow route, API, worker, and verifier changes; keep browser/E2E checks manual unless explicitly approved for a focused run.
Current Dockerfile Targets and Runtime Commands
base:node:20-bookworm-slimonlinux/amd64, production-oriented environment variables, OpenSSL, Corepack, andpnpm@10.33.0.deps: installs all workspace dependencies frompackage.json,pnpm-lock.yaml,pnpm-workspace.yaml,prisma, and each workspacepackage.jsonmanifest withpnpm install --frozen-lockfile --prod=false --prefer-offlinebehind a BuildKit/pnpm/storecache mount.production-deps: copies thedepsfilesystem and runspnpm prune --prodfor the final runtime images.build: generates Prisma client, runs the rootpnpm buildcommand, and runspnpm --filter @toolctrl/api buildpluspnpm --filter @toolctrl/worker build; the root build produces the React Router build and web server runtime artifact atbuild/server-runtime/server.js, while the API and worker builds produceapps/api/dist/server.jsandapps/worker/dist.web: copies production node_modules,build,build/server-runtime,apps/web/build,apps/web/public,package.json,start.sh, workspace package runtime dependencies, andprisma; runsENTRYPOINT ["./start.sh"].worker: copies production node_modules, workspace manifests,apps/worker/package.json,apps/worker/dist, queue/mail/db packages, generated Prisma client artifacts, andprisma; setsWORKER_ENABLED=true; usesCMD ["node", "apps/worker/dist/index.js"]; healthcheck runsnode apps/worker/dist/health.js.api: copies production node_modules, workspace manifests,apps/api/package.json,apps/api/dist,packages/config,packages/queue,packages/db,packages/db/generated/prisma/client, andprisma; setsPORT=8080; usesCMD ["node", "apps/api/dist/server.js"]withouttsx.
Runtime start command observations:
start.shdelegates to rootpnpm start.- Root
package.jsondefinesstartaspnpm --filter @toolctrl/web start;apps/web/package.jsonowns the concretenode ../../build/server-runtime/server.jsruntime command, so the web Docker target starts the built Production-Runtime artifact instead oftsx. apps/api/package.jsondefinesbuildasnode scripts/build.mjsandstartasnode dist/server.js; the Dockerapitarget depends on that builtapps/api/distartifact and on the package-owned Prisma client copied frompackages/db/generated/prisma/client.node scripts/verify-docker-baseline.mjsenforces the web Production-Runtime contract and the S03 API Docker contract, including the explicit Dockerfileapitarget, required API/package/Prisma copy paths, BuildKit pnpm-store cache usage in thedepstarget, and absence oftsxfrom the API runtime start command.apps/worker/package.jsondefines productionstartasnode dist/index.jsandhealthasnode dist/health.js; the worker Docker target depends on these compiled runtime artifacts and must not rely ontsxat container runtime.
Registry Push Baseline Gitea Package Registry Push Contract
Registry Push Baseline starts from the existing Gitea CI runner baseline and hardens the S03 deploy workflow contract at .gitea/workflows/deploy.yml. The active workflow builds and pushes versioned web, api, and worker Docker image targets to the Gitea Package Registry on the self-hosted toolctrl-ci runner, then triggers configured Coolify resources. Web and API resources must pass redacted /healthz service-and-revision checks where those resources expose HTTP health URLs; worker resources are separate Coolify applications for WORKER_QUEUE_GROUP=general, mail, import, webhook, and plugin, and each worker rollout must pass Coolify execute-based node apps/worker/dist/health.js readiness with matching revision, enabled worker state, expected group, and expected queue. Image alias/tag checks prove only non-HTTP resource configuration; they are not a pass-open worker health fallback. These static and workflow checks are deployment contract proof; they are still not browser-flow or manual production acceptance evidence.
Auto-Mode-safe verification commands:
| Command | Expected result |
|---|---|
node scripts/verify-gitea-ci-workflow.mjs |
Passes with every .gitea workflow job using runs-on: toolctrl-ci and with forbidden browser/E2E deploy commands absent. |
node scripts/verify-gitea-deploy-workflow.mjs |
Emits deterministic PASS/FAIL lines for the .gitea/workflows/deploy.yml static contract, including runner, registry-login, image-target, tag-policy, and forbidden-command checks. |
pnpm run verify:deploy-readiness |
Confirms the package scripts, CI workflow, deploy workflow, deployment docs, worker matrix docs, and worker-group provisioner stay consistent while distinguishing the lightweight deploy preflight from CI/readiness coverage for staging/promotion, health, i18n, hot-swap, Docker, seed, and migration gates. |
pnpm exec vitest run scripts/verify-gitea-deploy-workflow.test.mjs |
Exercises verifier fixtures for missing deploy contract pieces, forbidden literal secret exposure, forbidden browser/E2E commands, required Coolify trigger coverage, and required image tag behavior. |
grep -R "ubuntu-latest" .gitea |
Returns no matches; any match means the self-hosted runner baseline has regressed. |
grep -n "verify:deploy-workflow" docs/deployment.md && grep -n "main" docs/deployment.md |
Confirms this deployment document keeps the deploy verifier command and tag-policy language visible. |
The package script verify:deploy-workflow is the supported shorthand for the deploy workflow verifier. The deploy workflow follows a stage-and-promote contract on the self-hosted toolctrl-ci runner. Every deploy job depends on deploy-preflight, which intentionally runs only lightweight static deploy wiring checks before any image is published or promoted: the Gitea deploy workflow verifier, deploy-readiness docs/worker/provisioner consistency verifier, Docker baseline, Docker non-root runtime verifier, Coolify helper syntax checks, worker-group provisioner syntax checks, and pre-pull script syntax checks. Broader gates such as API inventory, OpenAPI security, env drift, health baseline, i18n, Phase C boundaries, Host Aware URL baseline, Feature Manifest plugin-removal gates, hot-swap, UI visual, build, and production seed artifact verification belong to the CI/readiness contract rather than the deploy preflight job. Because .gitea/workflows/ci.yml intentionally does not run on push, production readiness requires Gitea main branch protection: direct pushes to main must be disabled and merges to main must require the pull-request CI workflow to pass. Without that repository setting, a direct main push can stage after only the lightweight deploy preflight. Pushes to main then build web, api, and worker, push immutable full-SHA tags plus the staging alias, trigger the staging web resource first, wait for web health/revision proof, then trigger dependent API plus worker-general, worker-mail, worker-import, worker-webhook, and worker-plugin resources. Production promotion is a separate workflow_dispatch action named promote; operators must provide the full staged commit SHA as staged_sha and set backup_confirmed=true only after production backup or restore-point readiness is confirmed. The workflow verifies that full-SHA image set against staging, moves the production main alias without rebuilding, configures the production web pre-deployment migration command, deploys the production web resource first, waits for web health/revision proof, verifies production migration status with the database URL scoped to that step, and only then deploys dependent API plus all five worker-group resources. These names identify required CI variable or secret slots only; documentation must not include real deploy endpoint structures, UUIDs, tokens, passwords, credentials, or secret-shaped examples.
The deploy image tag contract is static and versioned. Every staging run must publish full-SHA immutable tags for each image target and update the staging alias. Promotion must move the main alias to the exact full-SHA image set verified by that workflow run; it must not rebuild from source. Images are recorded by safe target labels such as web, api, and worker; do not paste registry URLs, project IDs, credentials, or token-shaped examples into docs or tickets. The mutable latest alias is not part of the deployment contract.
Coolify deployment is runner-side and follows successful image publication. The .gitea/workflows/deploy.yml stage path runs on pushes to main; the promote path runs only through workflow_dispatch with action=promote, staged_sha=<full 40-character staged SHA>, and backup_confirmed=true. Staging Coolify must be called only after web, api, and worker staging images publish successfully: the workflow patches the staging web resource so pre_deployment_command runs Prisma migrations and post_deployment_command runs the guarded seed, triggers the staging web resource first, waits for web health/revision proof, then triggers and waits on dependent API plus worker-general, worker-mail, worker-import, worker-webhook, and worker-plugin resources. Production follows the same web-first/dependent-resource order after the runner verifies the requested full-SHA image set and moves the main aliases; migration status is confirmed after the production web resource has run its pre-deployment migration command and post-deployment seed command and before dependent API/worker deploys start. The trigger must read endpoint and resource information from configured CI variables and secrets and must not expose deploy URLs, UUIDs, bearer tokens, passwords, or registry credentials in docs, logs, tickets, or verification summaries.
If the self-hosted runner needs an internal hosts override for the Gitea registry, provide TOOLCTRL_GITEA_HOST_IP as a CI variable. The workflows leave /etc/hosts untouched when that variable is absent.
The web application uses separate Coolify command slots. pre_deployment_command must run only reviewed Prisma migrations from the final image: cd /myapp && pnpm exec prisma migrate deploy --config prisma.config.ts. post_deployment_command must run the guarded production seed artifact from the final image: cd /myapp && NODE_ENV=production node packages/db/dist/production-seed.server.js. Docker healthchecks use /healthz; /readyz remains the stricter readiness endpoint for manual/runtime probes after dependencies are available. That production seed path creates only the configured bootstrap admin plus roles, permissions, and entitlements, and requires SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD in the Coolify environment.
Staging may opt into the CRM SaaS demo seed through the guarded command SEED_DEMO_DATA=true ALLOW_STAGING_DEMO_SEED=true NODE_ENV=production node packages/db/dist/production-seed.server.js. That staging-only path adds demo Starter, Pro, and Business tenants, users, plan assignments, product blueprint configuration, tenant-owned roles, KB fixtures, and support tickets. The staging web runtime must also keep SEED_DEMO_DATA=true and ALLOW_STAGING_DEMO_SEED=true so those seeded demo users can log in while NODE_ENV=production. Do not set SEED_DEMO_DATA=true or ALLOW_STAGING_DEMO_SEED=true on production resources.
Set WORKER_ENABLED=true only on worker Coolify resources. Leave it unset on web/API resources and avoid sharing a production env group that forces WORKER_ENABLED=false into worker containers. The deploy resource list must include separate worker applications with WORKER_QUEUE_GROUP=general, mail, import, webhook, and plugin; the deploy workflow identifies them by the group env marker or by resource names such as worker-general, worker-mail, worker-import, worker-webhook, and worker-plugin. worker-import is allowed to be a healthy idle topology placeholder with an empty queue list until IMPORT_QUEUE_ENABLED=true and IMPORT_QUEUE_RUNTIME_READY=true are backed by wired import, export, and scan services. Treat this deployment paragraph as a summary: docs/ops/worker-queues.md is the canonical worker topology and queue ownership reference.
The plugin worker belongs to the topology even while production plugin
execution is gated: with PLUGIN_RUNTIME_PRODUCTION_ENABLED unset or false,
worker-plugin is an idle service, not evidence that tenant plugin execution is
approved.
If COOLIFY_STAGING_PROVISION_WORKER_GROUPS=true or COOLIFY_PROVISION_WORKER_GROUPS=true is set with the matching project, server, environment, and destination UUID variables, the deploy workflow creates any missing worker-group applications through the Coolify Docker Image API before patching image tags. The provisioner copies runtime env from an existing worker source app when it creates a group resource, then overwrites the group-specific worker env, including distinct health and admin ports, so operators should keep an env-source worker app configured until all group resources have their own Coolify env.
B-012 is not a repository code blocker. The deploy workflow and Docker
verifiers cover the repository contract, while the permanent fix for runner
Docker network exhaustion is operator-owned host hardening: enlarge Docker
default-address-pools on the self-hosted runner and add scheduled cleanup for
orphaned Gitea Actions networks.
Deploy secrets, registry credentials, Coolify URLs, UUIDs, personal access tokens, webhook tokens, and other deploy-only values must be supplied through CI secrets or a secret manager. Documentation, workflow logs, tickets, and verification summaries must use placeholder configuration only and must not commit, echo, or paste real URLs, UUIDs, tokens, passwords, registry credentials, or secret-shaped examples. Auto-Mode-safe proof is limited to repository/static checks and the configured CI workflow contract such as pnpm run verify:deploy-workflow, node scripts/verify-gitea-deploy-workflow.mjs, and focused verifier tests. Browser flows, Playwright, Cypress, and E2E checks remain manual exclusions unless a human explicitly approves the exact focused run.
Production Docs Production Deployment Runbook
Production Docs/S04 treats deployment proof as a layered contract. Auto-Mode may run pnpm run verify:production-docs to verify the production documentation set, plus existing static deploy verifiers such as pnpm run verify:deploy-workflow, node scripts/verify-gitea-deploy-workflow.mjs, node scripts/verify-gitea-ci-workflow.mjs, and focused verifier tests. These checks prove repository shape, documentation coverage, image-target policy, placeholder usage, and forbidden automatic browser/E2E command exclusions; they do not prove that a live Docker build, registry push, Coolify execution, or browser runtime succeeded.
The production image set remains the web, api, and worker Docker targets; staging uses the same target set with isolated runtime configuration in Coolify. Published image references must stay placeholder-only in docs and tickets, using safe target labels and tag labels rather than real registry hosts, namespaces, project IDs, UUIDs, credentials, or token-shaped examples. Every deployable image needs an immutable full-SHA tag; the staging alias identifies the currently staged image set, and the main alias is updated only by promotion. Operators should inspect workflow output for redacted target names, tag lists, and PASS/FAIL verifier lines only, never raw secrets.
Coolify execution is part of the main-branch stage path and the workflow_dispatch promote path. The workflow may trigger Coolify only after image publication or promotion prerequisites pass, and it deploys the migration-bearing web application first, verifies web health/revision, then deploys dependent API/worker resources by reading endpoint/resource data from configured CI variables and secrets. Documentation must not include Coolify URLs, webhook paths, UUIDs, bearer tokens, passwords, registry credentials, or derived endpoint examples. Pull requests and schedules must not invoke Coolify.
Ad hoc live Docker builds, registry pushes, Coolify runs, browser flows, Playwright, Cypress, and E2E checks outside the configured Gitea deploy workflow are manual runtime activities unless a human explicitly approves the exact focused run. The Gitea deploy workflow may run Docker, registry, Coolify, migration, and web/API health steps automatically; record only redacted evidence: deployment status, image target names, immutable tag IDs, web/API service and revision health, worker execute-health status, worker group/queue summary, API requestId or x-request-id correlation, and queue/job identifiers. Worker live-revision proof is checked automatically through Coolify execute returning worker health JSON with revision data; if execute is unavailable, empty, or non-JSON, the automated rollout fails closed and operators must fix Coolify execute access before accepting the worker deploy. Do not paste database URLs, Redis URLs, Supabase keys, mail provider credentials, stack traces with secrets, webhook tokens, or container environment values.
Rollback uses immutable image tags instead of mutable guesses. Select the previous known-good SHA-derived tag for the affected target, redeploy it through the approved manual path, and confirm web/API health with GET /healthz and GET /readyz plus the documented API health endpoint while correlating request IDs across logs. For worker incidents, inspect the redacted worker health output, queue names, failed-job counts, and job/request correlation IDs; pause or drain queues through the operational tooling before replaying jobs. If readiness fails, capture the non-secret dependency diagnostic code, target name, tag, and correlation ID, then escalate with secret values omitted.
Health Baseline Health Baseline Gate
S02 enforces CI-safe static contracts for the web and API liveness/readiness endpoints. The web app exposes GET /healthz through apps/web/app/routes/healthz.tsx as a lightweight liveness route with no database dependency, and GET /readyz through apps/web/app/routes/readyz.tsx as a database-backed readiness route. The API exposes unauthenticated GET /healthz and GET /readyz; successful responses include non-secret JSON with service: "api", revision, and requestId, and /readyz returns HTTP 503 with a redacted dependency diagnostic code when readiness fails.
Auto-Mode-safe verification commands:
| Command | Purpose |
|---|---|
pnpm exec vitest run apps/web/app/routes/healthz.test.ts apps/web/app/routes/readyz.test.ts |
Focused web route coverage for liveness/readiness response behavior. |
pnpm exec vitest run apps/api/src/app.test.ts apps/api/src/openapi.test.ts |
Focused API route and OpenAPI coverage for /healthz and /readyz. |
pnpm exec vitest run scripts/verify-health-baseline.test.mjs |
Static verifier fixture coverage, including missing web/API endpoint regressions and worker DB-readiness regressions. |
node scripts/verify-health-baseline.mjs |
Static repository baseline check with deterministic path-specific PASS, OBSERVE, and FAIL lines for web, API, and worker health contracts. |
The verifier output must no longer contain web/API observations saying /healthz or /readyz is not present, and worker DB readiness must be reported as an enforced pass/fail baseline rather than an observation. Missing web routes, missing API routes, web liveness database usage, missing web readiness database checks, missing API /readyz 503 dependency diagnostics, and missing worker DB readiness are all baseline failures.
Manual runtime inspection remains separate from Auto-Mode. A human may inspect a deployed web or API runtime with GET /healthz and GET /readyz and may compare the API x-request-id response header with the JSON requestId value. Readiness diagnostics are intentionally non-secret: do not record database URLs, Supabase keys, Redis URLs, tokens, passwords, stack traces, or other secret values in docs, logs, tickets, or verification summaries. Live Docker smokes, browser flows, Cypress, Playwright, and other E2E checks remain manual runtime verification and are not part of this Auto-Mode health baseline gate unless a human explicitly approves the exact focused run.
Worker Docker Target Verification and Inspection
The worker target is statically/package verified by these CI-safe commands:
| Command | Purpose |
|---|---|
pnpm --filter @toolctrl/worker build |
Confirms the worker package emits compiled runtime files for node dist/index.js and node dist/health.js. |
pnpm exec vitest run apps/worker/src/index.test.ts apps/worker/src/health.test.ts scripts/verify-docker-baseline.test.mjs scripts/verify-health-baseline.test.mjs |
Exercises worker runtime/package tests and static verifier coverage, including worker tsx leakage and DB-readiness regressions. |
node scripts/verify-docker-baseline.mjs |
Performs the static Dockerfile/package contract check and emits path-specific PASS/FAIL output for worker Docker target copy paths, start scripts, health scripts, and runtime tsx usage. |
node scripts/verify-health-baseline.mjs |
Enforces that worker health statically reports Redis parse state and DB readiness without requiring live DB access. |
Manual Docker smoke verification is intentionally separate from Auto-Mode. A human may run a focused build such as docker build --target worker -t toolctrl-worker:smoke ., then run the image with safe environment key names only, for example WORKER_ENABLED, WORKER_QUEUE_GROUP, WORKER_HEALTH_PORT, WORKER_ADMIN_PORT, DATABASE_URL, BULLMQ_REDIS_URL, fallback REDIS_URL, group-specific fail-loud keys, and mail provider keys supplied by the local shell or secret manager rather than written into docs. To inspect the worker healthcheck inside a container, run node apps/worker/dist/health.js; the JSON output is expected to report whether the worker is enabled/configured, whether Redis configuration parses, the DB readiness state, queue names, and redacted dependency codes, without echoing Redis URLs, passwords, database connection strings, stack traces, tokens, or other deployment secret values. Auto-Mode did not run live Docker build/run smoke checks, browser flows, Playwright, Cypress, or E2E commands for S04; the recorded proof is limited to static/package verification.
API Docker Target Verification and Inspection
The API target is statically/package verified by these task-scoped commands:
| Command | Purpose |
|---|---|
pnpm --filter @toolctrl/api build |
Confirms the API package can compile apps/api/src into apps/api/dist, including dist/server.js for the Docker runtime start path. |
pnpm exec vitest run scripts/verify-docker-baseline.test.mjs |
Exercises the Docker baseline verifier test coverage, including API-target PASS/FAIL cases for required copies and tsx runtime leakage. |
node scripts/verify-docker-baseline.mjs |
Performs the static Dockerfile/package contract check and emits path-specific PASS/FAIL output for the API target. |
Runtime inspection for a manually run API container should use GET /v1/health. A healthy response is expected to identify service: "api" and include a requestId; startup logs should include the structured API listening message with a redacted port field. Safe environment keys that may be supplied by deployment automation include PORT, NODE_ENV, database connection variables, Supabase keys, and JWT/password-hash configuration keys; documentation and logs must not include secret values.
Scope Notes
Docker build/run smoke verification belongs to focused manual target checks. A manual Docker smoke check may optionally build and run the focused API target with docker build --target api ..., start the container, and request GET /v1/health, or build the focused worker target with docker build --target worker ... and inspect node apps/worker/dist/health.js inside the container. Auto-Mode must not run live Docker smoke, Playwright, Cypress, browser, or E2E commands unless a human explicitly approves that exact run. The recorded Auto-Mode verification for S03 and S04 is static/package verification only; it does not claim a live Docker runtime proof.