레퍼런스·설명레퍼런스

배포 패키지 계약

이미지와 chart와 manifest의 필드 의미를 확인한다

계약

배포 입력은 움직이는 태그 대신 digest로 고정한다. 게시 이미지는 linux/amd64와 linux/arm64를 담은 index다. 태그는 commit을 찾는 데 사용하고, 설치에서는 같은 소스 commit과 세 이미지 digest를 한 세트로 사용한다.

패키지주소main 머지v* 태그
이미지ghcr.io/jeongjaesoon/werft-control-host, werft-worker, werft-worker-codex, werft-egress-proxy태그 main-<12>태그 v<버전>
Helm chartoci://ghcr.io/jeongjaesoon/werft버전 <Chart.yaml version>-main-<12>버전 <버전>(v 없음)
SDKnpm @werft-ai/sdkmain 이미지 게시와 별도. SDK 설치에서 배포 버전 확인SDK 배포 버전을 별도로 확인

이미지는 linux/amd64와 linux/arm64를 담은 index이고, 설치에는 digest로 고정한다. chart는 세 이미지의 digest와 worker runtime을 담는다. 패키지는 로그인 없이 받는다. <12>는 commit SHA의 앞 12자이고, main 버전끼리는 순서에 뜻이 없으므로 Images workflow에서 초록색으로 끝난 run의 commit을 골라 정확히 적는다.

이미지내용실행 주체
werft-control-hostapps/control-host 실행물 하나로 api·scheduler·reconciler role을 모두 돌린다. --filter로 그 앱의 closure만 설치하며 Agent SDK·Claude Code executable을 담지 않는다(빌드가 node_modules/@anthropic-ai 부재를 확인). 기본 uid 1000, scheduler role만 compose user: "0:0"로 root가 되어 Docker socket을 쥔다bun run apps/control-host/src/main.ts <role> (CMD 기본은 api)
werft-worker--filter ./apps/worker로 worker와 그것이 의존하는 workspace의 closure만 설치한다(pg·drizzle·웹 콘솔 패키지는 없다). SDK 0.3.270과 번들 Claude Code 2.1.270, git, non-root(uid 1000), /workspace를 1000 소유로 미리 생성(LocalDockerBackend의 volume 계약). 빌드 시 resolvePinnedClaudeExecutable()로 executable 경로를 확정해 /usr/local/bin/claude로 걸고 claude --version을 실행한다bun run apps/worker/src/main.ts — scheduler가 env로 넘긴 bootstrap identity로 세션 하나를 claim하고 WorkerHost 루프를 돈다
(scheduler role)Docker socket을 mount하는 유일한 서비스이며 root로 실행한다(socket 소유자는 어차피 daemon host의 root와 같고, socket gid는 daemon마다 달라 고정 uid가 이식성을 깎기만 한다)compose에서는 감독 루프(main.ts scheduler)가 SCHEDULER_INTERVAL_SEC(기본 5초)마다 자식 프로세스(--serve)에 pass 하나를 요청한다. 실패하거나 kill된 pass는 자식과 함께 끝나고 다음 pass는 새 자식에서 돈다(실행). 실패 pass가 SCHEDULER_MAX_CONSECUTIVE_FAILURES(3)번 이어지면(degraded exit 76은 세지 않는다) 루프가 exit 1 해 restart: unless-stopped가 재시작하고(compose ps에 드러남), SCHEDULER_HEALTH_STALE_SEC(200초) 동안 완료 pass(성공 또는 degraded)가 없으면 healthcheck(main.ts scheduler --health)가 unhealthy가 된다. DB가 멈추면 pass가 스스로 exit 1로 끝난다: scheduler·reconciler pool은 API와 같은 timeout(connect 5초·statement 10초·read 20초)을 쓰고, 연결을 한 번 잃은 뒤의 store 호출은 기다리지 않고 바로 실패하므로 DB 대기는 실패한 statement(5+20초)와 pass lock 해제(20초)를 합친 약 45초가 상한이다. SCHEDULER_PASS_TIMEOUT_SEC(180초)는 이 45초와 worker 정지 한 번(stop timeout 120초 + request timeout 30초)보다 크게 두는 바깥 watchdog이며, 그 밖의 hang을 kill해 실패로 센다(unhealthy만으로는 Docker가 재시작하지 않는다). pool timeout을 늘리면 이 값도 connect + 2 × read보다 크게 올린다
werft-egress-proxyapps/egress-proxy의 소스만(의존성 없음, 한 stage). 코드는 root 소유로 두고 uid 1000으로 실행해 proxy가 다음 기동의 코드를 고치지 못한다bun run src/main.ts — compose egress-proxy, 모든 worker 네트워크에 붙는 유일한 바깥 경로

릴리스 manifest 읽기

v* 태그마다 GitHub Release가 하나 생기고, 파일 release-manifest.json이 붙는다. 운영자는 run이나 artifact를 찾지 않는다. 저장소 checkout도 필요 없고 gh와 jq면 된다. 아래 TAG에 읽으려는 Release의 태그를 넣는다.

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")"

저장소가 공개이므로 gh 없이 mkdir -p "$TAG" && curl -fsSL -o "$TAG/release-manifest.json" "https://github.com/JeongJaeSoon/werft/releases/download/$TAG/release-manifest.json"으로도 받는다. 레지스트리와 대조하려면 digest 하나를 docker buildx imagetools inspect <이미지>@<digest>에 넣는다.

필드값
schemamanifest 형식 버전. 지금은 1
tag, commit릴리스 태그와 그 commit(40자 hex)
images.<app>.imageghcr.io/<owner>/werft-<app>. <app>은 control-host, worker, worker-codex, egress-proxy. worker-codex는 Codex 엔진 worker 이미지이고 chart의 배포 세트에는 들어가지 않는다
images.<app>.digest두 아키텍처를 담는 index의 digest. 배포는 이 값으로 고정한다
images.<app>.platforms.amd64, .arm64그 아키텍처의 manifest digest
workerRuntimeworker 이미지가 담은 Claude 실행 환경(engine, sdkVersion, cliVersion, profileDigestScheme)
workerCodexRuntimeworker-codex 이미지가 담은 Codex 실행 환경(workerRuntime과 같은 필드, engine은 codex)
chart.name, chart.version, chart.appVersion, chart.ref같은 run이 게시한 chart. chart.ref는 oci:// 위치

값은 promote가 레지스트리에서 다시 읽어 확인한 digest와, chart-publish가 로그인 없이 읽어 확인한 chart에서 온다. manifest를 만들 때 chart 패키지의 Chart.yaml·values.yaml이 같은 digest와 버전을 담는지도 대조한다. 한번 붙은 manifest는 바뀌지 않는다. 같은 태그를 다시 돌려 다른 내용이 나오면 job이 실패한다.

설치하는 운영자는 이 manifest에서 단일 서버 설치에 넣을 이미지 줄을 얻는다.

main 패키지와 릴리스

main-<12>는 릴리스가 아니다. 해당 commit의 Images 실행에서 세 이미지·chart가 모두 게시됐는지 확인하고 정확한 버전을 선택한다. SHA 문자열은 시간순 버전이 아니므로 Helm의 --devel로 최신을 고르지 않는다. main 게시의 unverifiedSources가 빈 배열이면 모든 소스 보관을 확인한 것이고, 필드가 없으면 확인 기록이 없는 것이다. 공식 릴리스는 소스 보관을 모두 확인해야 게시된다.

게시 chart는 세 이미지 digest와 worker runtime을 담는다. 설치별 values에 이 값을 다시 적거나 --reuse-values를 쓰면 다음 chart의 새 값을 덮는다. worker runtime은 engine·SDK·CLI·profile digest 방식으로 구성되며 이미지 digest 자체와는 다르다.

unverifiedSources가 남아 있으면 해당 Debian 소스가 실제로 보관되지 않았을 가능성도 있다. 공식 릴리스 수준의 소스 보관 보증으로 간주하지 않는다. multi-arch 게시 도입 전의 오래된 main-<12>는 amd64만 담을 수 있으므로 ARM 서버에서는 index의 platform 목록도 확인한다. 게시가 일부만 끝났거나 조회가 실패한 세 digest를 한 배포 세트로 사용하지 않는다.

checkout 없이 설치하기: 릴리스 번들

v* 릴리스의 GitHub Release에는 compose 파일, scripts/test-ops.sh, 예시 설정을 담은 werft-test-ops-<tag>.tar.gz가 붙는다. 이미지는 담지 않고, 릴리스 release-manifest.json의 digest로 이미 게시된 이미지를 받는다. 번들을 쓰면 단일 서버 설치의 checkout 준비와 호스트 bun이 필요 없다. 다운로드부터 deploy까지의 명령은 저장소의 docs/operator/test-ops.md("릴리스 번들로 설치")에 있다.

checkout 설치번들 설치
필요한 것git, jq, curl, bun 1.3.14, 배포 commit의 checkoutdocker, jq, curl
preflight deploy start stop status key reset manifest지원지원
upgrade backup reseed restore-drill지원거부. checkout에서 한다

직접 빌드한 이미지의 runtime

API와 slack role은 WORKER_IMAGE_RUNTIME과 WORKER_IMAGE_PREVIOUS_RUNTIME으로 현재·직전 worker 이미지의 런타임 조합을 읽는다. 값은 손으로 적지 않고, images.yml이 남긴 worker digest JSON의 runtime 필드를 그대로 넣는다({"engine":…,"sdkVersion":…,"cliVersion":…,"profileDigestScheme":…}). engine은 지금 claude뿐이고, chart는 다른 이름을 렌더링 때 images.worker.runtime.engine must be one of: claude로 거절한다. 직접 빌드한 이미지에는 그 JSON이 없다. images.yml과 같은 명령으로 이미지에서 읽는다.

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

사용자 registry 빌드는 사용자 이미지의 digest를 끝까지 유지한다. 게시·승격 절차는 기여자 문서를 따른다.

이 페이지