운영 설치

운영용 단일 서버 설치

영속 S3를 쓰는 서버의 ready 응답을 확인한다

무엇을 하게 되나

Linux 한 대에 게시 이미지를 digest로 고정해 설치하고 /readyz와 /docs 응답을 확인한다. 영속 AWS S3를 사용한다. 이 Compose 경로는 사용자 endpoint를 받지 않는다. 다른 S3 호환 endpoint가 필요하면 Kubernetes 설치를 따른다. 고가용성이 필요하면 Kubernetes를 선택한다.

준비물

항목요구
운영체제Linux. x86_64와 arm64 모두 된다
Docker Engine28 이상
Docker Compose2.24.6 이상
디스크Docker 데이터 루트가 XFS이고 prjquota로 mount돼 있다. preflight는 이것을 보지 않는다. 아니면 deploy에서 scheduler의 quota 검사가 실패해 설치가 멈춘다. ext4 서버는 XFS 디스크 준비를 먼저 끝낸다. Docker Desktop에서는 쓸 수 없다
시간NTP 동기화. 재부팅 뒤 Docker가 자동으로 시작한다
도구git, jq, curl, openssl, Bun 1.3.14, Docker Buildx 플러그인. Docker daemon에 접근할 수 있는 계정
object store영속 AWS S3 bucket. Compose test-ops는 AWS S3만 지원하며 사용자 endpoint를 받지 않는다. bucket 준비에서 보호 계약과 권한을 확인한다
외부ghcr.io와 Docker Hub에서 이미지를 받을 수 있다. 패키지는 로그인 없이 받는다. 세션은 api.anthropic.com과 github.com에 나간다

설치에는 저장소 checkout이 필요하다. compose 파일과 scripts/test-ops.sh를 checkout에서 읽기 때문이다. 이미지는 checkout에서 빌드하지 않는다.

단계

1. 버전과 소스 고정

현재 설정 문서·모델 연결·비밀키 계약을 지원하는 게시 태그를 변경 기록과 대조해 선택한다. 아래 두 placeholder를 패키지 계약에서 얻은 실제 태그와 같은 소스 commit으로 바꾼다. 이미지의 main 태그는 Git 태그가 아니므로 checkout 값과 같다고 가정하지 않는다.

TAG='<published-image-tag>'
SOURCE_REF='<matching-source-commit-or-release-tag>'
for app in control-host worker egress-proxy; do
  echo "$app $(docker buildx imagetools inspect "ghcr.io/jeongjaesoon/werft-$app:$TAG" --format '{{.Manifest.Digest}}')"
done

세 이미지 조회가 모두 성공하고 digest가 나온 뒤 진행한다.

git clone https://github.com/JeongJaeSoon/werft.git
cd werft
git checkout "$SOURCE_REF"
bun install --frozen-lockfile

checkout이 선택한 release의 commit이고 의존성 설치가 성공해야 한다. release 파일은 checkout 밖에 저장하여 깨끗한 checkout 조건을 유지한다.

2. 비밀과 설정 문서

비밀 값은 저장소 밖의 env 파일에만 둔다. 파일은 운영자만 읽을 수 있어야 한다. group이나 other에 권한이 있으면 스크립트가 거부한다.

sudo mkdir -p /etc/werft
sudo install -m 600 -o "$USER" infra/test-ops.env.example /etc/werft/test-ops.env
sudo install -d -m 700 -o "$USER" /var/lib/werft/test-ops

/etc/werft/test-ops.env를 편집기로 열어 다음을 채운다. sed -i처럼 임시 파일을 만드는 도구는 /etc/werft에서 Permission denied로 실패하니 제자리에 쓰는 편집기를 쓴다.

변수값
POSTGRES_PASSWORD무작위 값. 영문, 숫자, . _ ~ -만 쓴다
EGRESS_AUTHORIZER_TOKEN32자 이상 무작위 값
PLATFORM_SECRET_KEYopenssl rand -hex 32의 64자 hex. 잃으면 저장한 자격 증명을 읽지 못한다
BOOTSTRAP_TOKEN비워 둔다(주석 그대로). 첫 owner를 만드는 토큰은 API가 만들고, 4단계 deploy 뒤 scripts/test-ops.sh bootstrap-token이 한 번 보여 준다. 고정 값이 필요하면 주석을 풀고 32–256자 무작위 값을 넣는다. 그 값을 쓰는 설치에서는 위 명령이 토큰을 내지 않는다
CONSOLE_ORIGIN콘솔에서 GitHub App을 만들 때 필요한 브라우저의 콘솔 origin(scheme://host[:port]). 없으면 "GitHub에서 만들기" 버튼이 비활성화된다. Slack 계정 연결의 redirect와 Slack 이벤트 주소도 이 값에서 계산한다(없으면 Slack 연결이 꺼진다). 예시 env 파일에는 이 줄이 없으므로 직접 추가한다. SSH tunnel로 콘솔을 열면 http://127.0.0.1:<터널 포트>를 넣는다. 설치 뒤 추가·변경한 값은 start 대신 TEST_OPS_OBJECT_STORE=s3 scripts/test-ops.sh upgrade /var/lib/werft/test-ops/current.json으로 현재 release를 다시 적용해 반영한다

예시의 LocalStack 값을 운영 S3 값으로 바꾼다. 비밀을 명령줄이나 로그에 출력하지 않는다.

변수운영 설치 값
EXECUTION_INSTALLATION_ID다른 설치와 겹치지 않는 id. 배포 뒤 변경하지 않는다
PLATFORM_SETTINGS_DOCUMENT/etc/werft/settings.json
EXECUTION_WORKSPACE_QUOTAon
AWS_REGION, S3_BUCKET준비한 AWS bucket의 리전과 이름. 사용자 endpoint는 지원하지 않음
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYbucket 최소 권한의 API 자격 증명
EGRESS_CREDENTIAL_ALLOWLISTprovider·Git host와 <bucket>.s3.<region>.amazonaws.com:443. 점이 든 bucket 이름은 s3.<region>.amazonaws.com:443
EGRESS_CREDENTIAL_PRIVATE_ALLOWLIST빈 값. localstack:4566을 제거한다

모델 키는 설치 뒤 콘솔의 모델 연결에서 넣으므로 v0.1.2 태그는 ANTHROPIC_API_KEY를 비워 둔다. 단, v0.1.0 태그 checkout에서는 compose가 이 값을 필수로 요구하여 비어 있으면 preflight가 exit 1로 끝난다. 그 버전에서는 env 파일에 모델 키(또는 나중에 콘솔에서 바꿀 자리 값)를 넣는다. 비워도 되는 변경은 JeongJaeSoon/werft#1262에서 들어왔고, v0.1.2가 이를 담은 첫 게시 릴리스다. PLATFORM_SECRET_KEY를 잃으면 저장한 자격 증명을 읽지 못하므로 별도로 안전하게 백업한다.

설정 문서는 /etc/werft/settings.json에 둔다. 세션이 쓸 GitHub 저장소를 적는다. 비밀이 없다. 저장소 URL만 실제 값으로 바꾼다. 예시는 공개 저장소 기준이다. 저장소 항목에는 "auth": { "kind": "github_app" }가 있어야 한다. 없으면 가져올 때 그 저장소가 빠져 preflight가 설치를 막는다. 비공개 저장소의 인증은 API 서버 문서를 따른다.

migrate 컨테이너가 읽어야 하므로 파일은 0644로 둔다. 빈 파일을 먼저 만들고 편집기로 아래 내용을 채운다.

sudo install -m 644 -o "$USER" /dev/null /etc/werft/settings.json
{
  "version": 1,
  "connections": {},
  "repositories": {
    "sample-app": {
      "url": "https://github.com/<owner>/<repo>.git",
      "branch": "main",
      "auth": { "kind": "github_app" }
    }
  },
  "mcp_servers": {}
}

저장소 URL은 실제 사용할 공개 저장소로 바꾼다. 비공개 저장소의 GitHub App은 2단계 입력 목록의 CONSOLE_ORIGIN을 준비한 뒤 설치한 콘솔에서 구성한다. 파일은 비밀을 포함하지 않으며 migrate가 읽을 수 있어야 한다.

3. release 입력 생성

선택한 TAG를 유지한 셸에서 실행한다. DEPLOYMENT_DIR는 checkout 밖의 설치별 디렉터리다.

DEPLOYMENT_DIR=/var/lib/werft/test-ops
SETTINGS_DIGEST="$(bun run scripts/lib/test-ops.ts settings-digest /etc/werft/settings.json)"
digest() { docker buildx imagetools inspect "ghcr.io/jeongjaesoon/werft-$1:$TAG" --format '{{.Manifest.Digest}}'; }
jq -n \
  --arg commit "$(git rev-parse HEAD)" \
  --arg control "ghcr.io/jeongjaesoon/werft-control-host@$(digest control-host)" \
  --arg worker "ghcr.io/jeongjaesoon/werft-worker@$(digest worker)" \
  --arg egress "ghcr.io/jeongjaesoon/werft-egress-proxy@$(digest egress-proxy)" \
  --arg settings "$SETTINGS_DIGEST" \
  '{source_commit: $commit,
    images: {control_host: $control, worker: $worker, egress_proxy: $egress},
    settings_digest: $settings}' > "$DEPLOYMENT_DIR/release.json"

세 이미지 digest가 모두 비어 있지 않은지 확인한다.

4. S3를 명시해 검사·설치

TEST_OPS_OBJECT_STORE=s3 scripts/test-ops.sh preflight "$DEPLOYMENT_DIR/release.json"
TEST_OPS_OBJECT_STORE=s3 scripts/test-ops.sh deploy "$DEPLOYMENT_DIR/release.json"

preflight는 bucket 보호 설정과 scratch version의 쓰기·hold·읽기·hold 해제·삭제를 검사한다. quota는 deploy 때 scheduler가 검사한다. 둘 다 성공하고 서비스가 healthy가 되어야 한다. object store 방식은 첫 deploy 뒤 바꿀 수 없다.

env 파일에 BOOTSTRAP_TOKEN을 넣지 않았다면 deploy 뒤에 첫 owner 토큰을 받는다. 토큰은 컨테이너 로그가 아니라 API 컨테이너 안의 owner 전용 파일에 놓이고, 이 명령이 꺼내면서 지운다.

scripts/test-ops.sh bootstrap-token

출력된 값을 /bootstrap에 붙인다(첫 owner와 key). 같은 명령을 다시 실행하면 토큰 없이 종료 코드 1로 끝난다. 값을 놓쳤고 owner가 아직 없으면 scripts/test-ops.sh stop과 start로 API를 다시 띄워 새 토큰을 만든다(이전 토큰은 무효가 된다). 이미 BOOTSTRAP_TOKEN을 넣어 둔 설치는 바뀌는 것이 없다.

확인

scripts/test-ops.sh status
curl --max-time 10 -fsS -w '\n%{http_code}\n' http://127.0.0.1:3000/readyz
curl --max-time 10 -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/docs

ready 본문과 200, docs 200, 각 서비스 healthy를 확인한다. PostgreSQL은 loopback에만 있고 Docker TCP socket은 외부에서 닿지 않아야 한다. 운영 AWS S3 설치 전체와 데이터가 있는 복원은 이 튜토리얼에서 실행 확인하지 않았다. LocalStack의 빈 checkpoint 대조와 ready 200으로 이를 대신하지 않는다.

자주 막히는 곳

첫 deploy가 실패하면 시작된 컨테이너는 남아 있고 current.json이 없어 stop을 쓸 수 없다. 실패 로그에서 quota·네트워크·Secret 등 원인을 확인하고, 이번 설치의 project 이름을 확인한 뒤 다음처럼 내린다. -v는 붙이지 않는다.

PROJECT='<this-installation-project>'
printf '%s\n' '{"services":{"x":{"image":"alpine:3"}},"networks":{"default":{}}}' \
  | docker compose --project-name "$PROJECT" -f - down --remove-orphans

원인을 고친 뒤 같은 release 파일과 같은 환경으로 deploy를 다시 실행한다. 이미 성공한 설치를 다시 시작할 때는 start를 쓴다.

quota 오류는 XFS 준비, bucket 보호·권한 오류는 bucket 준비를 확인한다. env 권한 오류는 2단계의 파일 모드, settings digest 오류는 3단계의 digest·release 입력 생성을 확인한다. 첫 deploy가 실패해도 정상 배포의 상태 파일이 있다고 가정하지 않는다.

되돌리기

정상 설치는 중지·재시작으로 데이터를 보존한다. 실패한 첫 deploy는 원인과 이 project가 만든 리소스를 먼저 조사한다. reset이나 volume 삭제를 기본 정리로 사용하지 않는다. 필요한 데이터가 없음을 확인한 설치만 별도 제거 절차를 따른다.

다음에 읽을 곳

접속 경로 → 첫 owner와 key → 모델 연결. 운영 시작 전에 백업과 복원을 설정한다.

이전 주소 안내

이전 절에서 현재 문서 찾기

운영용 단일 서버 설치

XFS 디스크 준비

배포 패키지 계약

사용자 registry 이미지 빌드

첫 owner와 사용자 key 발급

모델 연결과 프로필 구성

Compose 중지·재시작·제거

사용자 접속 경로 구성

checkpoint bucket 준비

백업과 복원 훈련

이 페이지