Reference and explanationReference

Deployment package contract

Understand image, chart, and manifest fields

Contract

Pin deployment inputs by digest rather than moving tags. Published images are indexes containing linux/amd64 and linux/arm64. Use tags to locate commits, then deploy one matching source commit and all three image digests as a set.

PackageLocationmain mergev* tag
Imagesghcr.io/jeongjaesoon/werft-control-host, werft-worker, werft-worker-codex, werft-egress-proxyTag main-<12>Tag v<version>
Helm chartoci://ghcr.io/jeongjaesoon/werftVersion <Chart.yaml version>-main-<12>Version <version> (without v)
SDKnpm @werft-ai/sdkPublished separately from main images. Check the published version in SDK installationCheck the SDK release version separately

Images are indexes containing linux/amd64 and linux/arm64; pin installations by digest. Charts include the three image digests and worker runtime. Packages are downloadable without signing in. <12> is the first 12 characters of the commit SHA. Main versions have no meaningful ordering: select the commit of a successful Images workflow run and specify it exactly.

ImageContentsExecution
werft-control-hostOne apps/control-host executable serves api, scheduler, and reconciler roles. --filter installs only that app's dependency closure, excluding Agent SDK/Claude Code executable (the build checks that node_modules/@anthropic-ai is absent). Default uid 1000; only the scheduler role uses Compose user: "0:0" for root Docker socket accessbun run apps/control-host/src/main.ts <role> (default CMD: api)
werft-worker--filter ./apps/worker installs only the worker and its workspace dependency closure (no pg, drizzle, or web console packages). SDK 0.3.270, bundled Claude Code 2.1.270, git, non-root uid 1000, and precreated /workspace owned by 1000 (LocalDockerBackend volume contract). Build resolves the executable via resolvePinnedClaudeExecutable(), links it at /usr/local/bin/claude, and runs claude --versionbun run apps/worker/src/main.ts — claims one session using the scheduler-provided bootstrap identity in env, then runs the WorkerHost loop
(scheduler role)The only service mounting the Docker socket. Runs as root: socket ownership is equivalent to daemon-host root, and socket gids vary across daemons, making fixed uids less portableIn Compose, the supervisor (main.ts scheduler) requests a pass from a child process (--serve) every SCHEDULER_INTERVAL_SEC (default 5 seconds). Failed/killed passes end with their child; the next pass uses a new child (execution). After SCHEDULER_MAX_CONSECUTIVE_FAILURES (3) consecutive failures (excluding degraded exit 76), the loop exits 1 and restart: unless-stopped restarts it (visible in compose ps). Without a completed successful/degraded pass for SCHEDULER_HEALTH_STALE_SEC (200 seconds), main.ts scheduler --health becomes unhealthy. Database stalls make the pass exit 1 itself: scheduler/reconciler use API-equivalent pool timeouts (connect 5 seconds, statement 10 seconds, read 20 seconds). After connection loss, store calls fail immediately. Database wait is bounded at roughly 45 seconds: failed statement (5+20 seconds) plus pass lock release (20 seconds). SCHEDULER_PASS_TIMEOUT_SEC (180 seconds) is an outer watchdog larger than that 45-second bound and one worker stop (stop timeout 120 seconds + request timeout 30 seconds). It kills other hangs and counts them as failures (Docker does not restart on unhealthy alone). If pool timeouts increase, raise this above connect + 2 × read too
werft-egress-proxyOnly apps/egress-proxy source (no dependencies, one stage). Code remains root-owned while execution uses uid 1000, preventing the proxy from changing next-start codebun run src/main.ts — Compose egress-proxy, the only external path attached to every worker network

Read a release manifest

Each v* tag produces a GitHub Release with release-manifest.json attached. Operators need not search runs or artifacts. No checkout is required; gh and jq suffice. Set TAG below to the Release tag to inspect.

TAG=v0.1.2   # the release tag to read
gh release download "$TAG" -R JeongJaeSoon/werft -p release-manifest.json -D "$TAG"   # one directory per tag
jq -r '.images[] | "\(.image)@\(.digest)"' "$TAG/release-manifest.json"   # image digests (deployment uses control-host, worker and egress-proxy)
jq -c .workerRuntime "$TAG/release-manifest.json"                          # worker runtime
jq -r '"\(.chart.ref) \(.chart.version)"' "$TAG/release-manifest.json"     # chart location and version
helm show chart "$(jq -r .chart.ref "$TAG/release-manifest.json")" --version "$(jq -r .chart.version "$TAG/release-manifest.json")"

Because the repository is public, you can also download without gh: mkdir -p "$TAG" && curl -fsSL -o "$TAG/release-manifest.json" "https://github.com/JeongJaeSoon/werft/releases/download/$TAG/release-manifest.json". To compare with the registry, pass a digest to docker buildx imagetools inspect <image>@<digest>.

FieldValue
schemaManifest format version, currently 1
tag, commitRelease tag and its commit (40-character hex)
images.<app>.imageghcr.io/<owner>/werft-<app>; <app> is control-host, worker, worker-codex, or egress-proxy; worker-codex is the Codex-engine worker image and is not part of the chart's deployment set
images.<app>.digestDigest of the index containing both architectures. Pin deployment to it
images.<app>.platforms.amd64, .arm64Manifest digest for that architecture
workerRuntimeClaude runtime in the worker image (engine, sdkVersion, cliVersion, profileDigestScheme)
workerCodexRuntimeCodex runtime in the worker-codex image (same fields as workerRuntime, engine is codex)
chart.name, chart.version, chart.appVersion, chart.refChart published by the same run; chart.ref is its oci:// location

Values come from digests reread and verified from the registry by promote, and charts read without signing in by chart-publish. Manifest generation also verifies the chart package's Chart.yaml/values.yaml contain matching digests and versions. An attached manifest is immutable. Rerunning the same tag with different content fails the job.

Installers obtain image lines for single-server installation from this manifest.

Main packages and releases

main-<12> is not a release. Confirm its Images run published all three images and the chart, then select the exact version. SHA strings are not chronological versions; do not use Helm --devel to choose the latest. An empty unverifiedSources array in a main publication confirms all source archives were checked. An absent field means no verification record exists. Official releases require all source archives to be verified before publication.

Published charts include all three image digests and worker runtime. Repeating these in installation values or using --reuse-values overrides the next chart's new values. Worker runtime consists of engine/SDK/CLI/profile digest scheme; it is distinct from the image digest.

Nonempty unverifiedSources can mean the corresponding Debian sources were not actually archived. Do not treat this as the source-archive guarantee of an official release. Old main-<12> images from before multi-architecture publication may contain only amd64; check index platforms on ARM servers too. Do not combine three digests into a deployment set if publication is incomplete or a lookup failed.

Install without a checkout: release bundles

GitHub Releases for v* include werft-test-ops-<tag>.tar.gz, containing Compose files, scripts/test-ops.sh, and example settings. Images are downloaded separately, pinned to the release's release-manifest.json digests. Bundles remove the checkout preparation and host Bun requirements from single-server installation. Commands from download through deploy are in repository docs/operator/test-ops.md (the release-bundle installation section).

Checkout installationBundle installation
Requirementsgit, jq, curl, bun 1.3.14, checkout of deployment commitdocker, jq, curl
preflight deploy start stop status key reset manifestSupportedSupported
upgrade backup reseed restore-drillSupportedRejected; use a checkout

Runtime of custom-built images

API and slack roles read current/previous worker runtime combinations from WORKER_IMAGE_RUNTIME and WORKER_IMAGE_PREVIOUS_RUNTIME. Do not type these manually: copy the runtime field from the worker digest JSON produced by images.yml ({"engine":…,"sdkVersion":…,"cliVersion":…,"profileDigestScheme":…}). The only current engine is claude; other names fail chart rendering with images.worker.runtime.engine must be one of: claude. Custom-built images lack that JSON. Extract it with the same command as images.yml:

docker run --rm -i --network none --entrypoint bun <worker-image> \
  run apps/worker/src/image-runtime.ts <<<'{}' | jq -c 'del(.profiles)'

Custom registry builds retain your image digests throughout. For publication and promotion, follow the contributor documentation.

On this page