<!-- canonical: https://docs.axelabs.ai/partner/deploy -->
<!-- source: content/partner/deploy.mdx -->

---
title: 배포 협의
description: macmini 배포 일정 · 사전 요구사항 · D-day 작업.
---

# 배포 협의

귀사 macmini 에 AXE Labs 스택을 배포하는 일정과 사전 요구사항.

## 사전 요구사항 (D-7)

| # | 항목 | 책임 |
|---|---|---|
| 1 | macmini 1대 (M2/M3, 16GB+ RAM, 512GB+ SSD) | 귀사 (구매) |
| 2 | 항상 켜진 상태 + 인터넷 안정 (유선 권장) | 귀사 |
| 3 | Tailscale 가입 + macmini 가 tailnet 에 합류 | 귀사 IT |
| 4 | 운영자의 Tailscale SSH key 허용 | 귀사 IT |
| 5 | Microsoft 365 tenant + 3개 app 등록 | 귀사 IT |
| 6 | `{customer}.axelabs.ai` 도메인 검증 | 귀사 IT + 액스코퍼레이션 주식회사 (DNS) |
| 7 | client_secret VALUE 안전 채널 전달 | 귀사 IT |

위 7 개 완료 = D-day 작업 진행 가능.

## D-day 작업 (운영자 측, 4시간)

운영자가 `axe onboard {customer} --apply` 실행. 18-step 자동화:

| Step | 작업 | 다운타임 |
|---|---|---|
| 1 | Tailscale status check | 0s |
| 2 | customer registry 확인 | 0s |
| 3 | SSH probe (macmini reachable?) | 0s |
| 4 | Cloudflare tunnel credential push | 0s |
| 5-9 | cloudflared launchd 설정 | 0s |
| 10 | frame docker preflight + git pull | ~30s |
| 11-16 | frame docker-compose bootstrap | ~5min (이미지 빌드) |
| 17 | health check | 0s |
| 18 | 운영자 알림 | — |

총 ~4시간 (이미지 빌드 + 첫 데이터 sync 포함).

## D+1 (검증)

- 운영자가 직접 첫 로그인 검증
- 귀사 admin 사용자 (`ADMIN_EMAIL`) 에게 권한 부여
- 검증 메일 발송

## D+2 ~ (운영)

- 귀사 직원 onboarding 시작
- 각자 [신규 직원 온보딩](/onboard) 따라 setup
- 1차 helpdesk = 귀사 IT (URL/secret 확인 등), 2차 = 운영자 (서버 측)

## 도입 후 귀사 macmini 의 실 컨테이너 구성

`axe onboard {customer} --apply` + `axe deploy blueprint/hive {customer}` 가 끝난 뒤 귀사 macmini 에 떠 있는 컨테이너 목록 (`docker ps` 결과 — `realchoice` 예시):

```
realchoice-macmini  (M2+, 16GB+, Tailscale 메시, 절전 OFF)
├ axelabs-realchoice-tunnel       (cloudflared, customer 별 독립 tunnel — origin 한 줄만)
├ frame-postgres :3700
├ frame-mcp-blue :3710 / frame-mcp-green :3711   (blue/green, 무중단 swap)
├ axe-frame-proxy :3712            (Caddy blue/green selector)         ⚠️ axe- prefix
├ frame-worker                      (cron + match_pending_sweep)
├ hive-postgres :3800
├ hive-mcp-blue :3810 / hive-mcp-green :3811     (blue/green)
├ axe-hive-proxy :3812             (Caddy blue/green selector)         ⚠️ axe- prefix
├ blueprint-postgres
├ blueprint-app :3100 + WebSocket :3101          (blue, dev/build 시 수십초 다운)
├ blueprint-app-green                              (passive, B-bp-bluegreen 후 활성)
├ blueprint-mcp-blue :3152 / blueprint-mcp-green :3153  (read-only MCP, blue/green)
├ stream-mcp :8780                                (귀사 기존 자산, manifest 정렬)
├ magnet-mcp :8770                                (귀사 기존 자산, manifest 정렬)
├ axe-vaultwarden                  (Vaultwarden core)                  ⚠️ axe- prefix
└ axe-vault-caddy :8222            (Vaultwarden front + OIDC)          ⚠️ axe- prefix
```

### 컨테이너 이름 컨벤션 — 정확히 무엇이 customer-aware 인가

| 패턴 | 예시 | customer 식별 |
|---|---|---|
| **customer-prefix 적용 (`axelabs-{customer}-...`)** | `axelabs-realchoice-tunnel` | ✅ — `axe onboard` (`_render_cloudflared_config`, `axe` CLI line 2829) 가 customer ID 박음. axe customer 는 `axelabs-axe-tunnel` 이 아니라 `axelabs-tunnel` (historical) |
| **customer-agnostic (`{service}-...`)** | `frame-postgres`, `hive-mcp-blue`, `blueprint-app`, `blueprint-mcp-green` | ✅ — customer-per-macmini 격리로 같은 host 에 다른 customer 가 없어 충돌 위험 0. macmini hostname (`realchoice-macmini`) 으로 식별 |
| **⚠️ historical `axe-` prefix 잔존** | `axe-frame-proxy`, `axe-hive-proxy`, `axe-vaultwarden`, `axe-vault-caddy` | ❌ — `frame/docker-compose.yml:197`, `hive/docker-compose.yml:124`, `vault/docker-compose.yml:29/66` 의 `container_name` 이 `axe-` 박혀 있음. **realchoice macmini 에서도 컨테이너 이름은 `axe-...`**. customer-per-macmini 격리상 충돌은 없지만 misleading. backlog 등재: [B-container-name-customer-prefix](/ops/backlog) |

⚠️ **즉 받은 도식의 `axe-frame-proxy` / `axe-vault-caddy` 가 "왜 axe-?"** 의문은 정확함 — 실제로 그 이름 그대로 떠 있는 게 맞음 (historical artifact). `axelabs-tunnel` 만 customer-prefix 누락 (이건 axe customer 전용 이름이라 realchoice 측은 `axelabs-realchoice-tunnel`).

향후 일반화는 [B-container-name-customer-prefix](/ops/backlog) — compose 의 `container_name` 을 `${CUSTOMER_PREFIX}-` 변수화. 실 운영 영향 없어 우선순위 中.

## 함정

### 사전 요구사항 단계 (D-7 ~ D-1)

| 함정 | 결과 | 회피 |
|---|---|---|
| Tailscale 미설치 | 운영자 push deploy 불가 | D-7 까지 설치 + key 등록 |
| macmini sleep 모드 | 새벽 backup 실패 | "절전 안 함" 설정 |
| 도메인 검증 누락 | Application ID URI 등록 거부 | 검증 후 app 등록 |
| client_secret 분실 | 재발급 + 운영자 측 swap | 회신 즉시 안전 보관 |

### D-day 18-step 자동화 단계 (2026-05-25 트루비아 측 발견 — B-vault-d-day-traps-2026-05-25 + B-onboard-d-day-traps-2026-05-25)

| 함정 | step | 증상 | 회피 |
|---|---|---|---|
| Private GitHub repo clone 인증 prompt | clone (step 10/11) | `axelabs-ai/frame` · `axelabs-ai/hive` clone 시 PAT prompt → SSH 자동 deploy hang | 운영자 임시 PAT 등록 후 customer 측 team add (D-dev-platform-2) |
| docker network `artemis_default` 사전 부재 | network create (pre step 11) | `external: true` declared network not found → compose up fail | `docker network create artemis_default` (`axe deploy` 의 `_svc_step_network` 빌트인 ✅, 2026-05-25 이후 자동) |
| Blueprint MCP app (4번째 Entra app) 등록 누락 | bootstrap.sh 또는 registry | App #4 미등록 → MCP OAuth 시 redirect 실패 | `bootstrap.sh` App #4 추가 (B-axelabs-bootstrap-blueprint-mcp-app 후속) |
| external volume 6개 (blueprint 측) 사전 부재 | docker compose up | `external: true` declared volume not found | compose 의 `external` 제거 또는 pre-step `docker volume create` (R6 후속) |
| `~/.axe/customers.yaml` 이 directory 로 잘못 사전 생성 | onboard step | file write fail | onboard step 의 `mkdir parent only` 정정 |
| ADMIN_TOKEN argon2 hash `$` interpolation | vault env_file (step 11) | compose v2 yaml expansion 으로 `$` segment 가 빈 값 — vault admin lockout | `$` → `$$` escape (`axe secret pull` 의 `escape_dollar` flag 빌트인 ✅) |
| Timshel vault image `wget` 없음 | compose healthcheck | `wget: command not found` → unhealthy 무한 루프 | healthcheck → `curl` 변경 |
| sso_nonce column 이름 (`code_verifier` → `verifier`) | vault recovery SQL | column not found | runbook 정정 ✅ (PostgreSQL/SQLite backend 분기) |
| bw CLI 2026.4+ Timshel SSO 호환성 | partner setup (bw login) | "Master password unlock data was not found" | bw 2025.7.0 pin: `npm install -g @bitwarden/cli@2025.7.0` (또는 axe.2 patch 적용 vault image) |
| bw data dir keychain restore | bw login | `rm -rf "~/.config/Bitwarden CLI/"` 후에도 옛 session 잔존 (macOS Keychain) | `BITWARDENCLI_APPDATA_DIR=$HOME/.bw-axe-customer` 로 fresh isolated dir |
| Caddyfile `/vault` no trailing slash | vault access (step 17 health check) | SPA HTML relative path 가 root 옆 resolve → asset 503 | `/vault` → `/vault/` permanent redirect (Caddy path-only destination trailing slash strip 함정) |

## Vault OIDC SSO 통합 — 기존 Vaultwarden 운영 중인 customer 용

귀사가 이미 [`soohunkang/vault`](https://github.com/soohunkang/vault) repo (운영자 측 vault platform) 또는 자체 fork 의 Vaultwarden 인스턴스를 운영 중이고 그것을 그대로 유지하기로 결정한 경우 (= RE^2 의 Q3 (a) 채택), `axec stack 의 별도 axe-vaultwarden 신설을 skip` 하고 기존 vault 에 OIDC SSO 만 추가합니다. 본 절은 그 통합 절차.

### 책임 분담

| 작업 | 누가 |
|---|---|
| `~/vault` repo / compose.yaml / env_file 의 OIDC env 추가 + force-recreate | **귀사 IT** (vault 운영 책임) |
| client_secret view-once URL 발사 | **운영자** (`axe secret send`) |
| cloudflared ingress 의 `/vault` path 를 503 placeholder → vault-caddy 호스트 포트로 변경 | **운영자** (axelabs.ai zone tunnel 운영 책임) |
| Vault Organization 생성 + 첫 admin invite | **귀사 admin** (web UI) |

### 절차 (양측 협업, ~30 분)

**1. 운영자 — client_secret view-once URL 발사**

```bash
axe secret send AZURE_{CUSTOMER_UP}_VAULTWARDEN_CLIENT_SECRET --service vault --customer {customer} --days 1 --access 1
```

귀사가 URL 받으면 1 회만 열어서 secret 값 캡처 후 즉시 안전 보관.

**2. 귀사 IT — `~/vault/` 의 env_file (또는 compose.yaml `environment:` 영역) 에 다음 12 key 추가/변경**

먼저 현 vault 의 secrets 보관 위치 확인:

```bash
cd ~/vault
grep -E 'env_file|environment:' compose.yaml | head -5
```

`env_file: ./vault.env` (또는 비슷한 경로) 가 보이면 그 파일 편집. compose.yaml `environment:` 블록 inline 도 가능.

추가/변경할 12 key (기존 SIGNUPS_ALLOWED 가 false 면 true 로 변경):

```bash
# === DOMAIN — axec stack 가 사용할 외부 URL (cloudflared ingress) ===
DOMAIN: "https://{customer}.axelabs.ai/vault"   # 기존 Tailscale FQDN 이면 변경

# === OIDC RP — Microsoft Entra ID ===
SSO_ENABLED: "true"
SSO_ONLY: "false"
SSO_AUTHORITY: "https://login.microsoftonline.com/{TENANT_ID}/v2.0"
SSO_CLIENT_ID: "{vaultwarden_client_id}"
SSO_CLIENT_SECRET: "{받은 secret 값}"
SSO_SCOPES: "openid profile email offline_access"
SSO_DEBUG_TOKENS: "false"

# === 4-block: 첫 employee 가입 차단 회피 (D-ops-24/26/27) ===
SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION: "true"
SSO_SIGNUPS_MATCH_EMAIL: "true"
SIGNUPS_ALLOWED: "true"                       # 기존 false 면 변경 필수
ORGANIZATION_INVITE_AUTO_ACCEPT: "true"
```

placeholder 4:
- `{TENANT_ID}` = `axelabs-bootstrap-{customer}.json` 의 `tenant_id`
- `{vaultwarden_client_id}` = 동일 JSON 의 `apps.vaultwarden.client_id`
- `{받은 secret 값}` = step 1 view-once URL
- `{customer}` / `{CUSTOMER_UP}` = customer id / 대문자

**3. 귀사 IT — force-recreate**

```bash
cd ~/vault
docker compose up -d --force-recreate vault-app   # 또는 vaultwarden / 본인 컨테이너명
```

**4. 운영자 — cloudflared ingress 의 `/vault` path 를 vault-caddy 로 라우팅**

운영자가 customer macmini 의 `~/.cloudflared/config.yml` 의 `/vault` block 을 503 placeholder → vault-caddy 의 host port (트루비아 vault-caddy 는 보통 `:8222`, HTTPS 는 `:8443`) 로 변경 후 cloudflared 재시작. 본 step 은 customer 측 추가 작업 없음.

**5. 검증 — SSO 직링크**

```
https://{customer}.axelabs.ai/vault/#/sso
```

웹 UI 에 SSO Identifier 입력 화면이 나오면 OK. Identifier = 운영자 측이 customer 별 명명 (예: `REALCHOICE` 대문자 권장 — Bitwarden Send 본문에 명시).

### 첫 employee 시점 — Vault Organization + invite (D-ops-32)

귀사 admin 이 SSO 로 첫 로그인 후 web vault 의 Admin Console:

1. **Organization 생성** — 이름 = `{CUSTOMER_UP}` 대문자 (예: `REALCHOICE`)
2. **Collection 4 개 신설** (D-ops-32 v1):
   - `Platform — Service Secrets` (operator only)
   - `Platform — Infrastructure` (operator only)
   - `{Customer Entity}` (전직원 RW)
   - `Shared Tools` (전직원 RW)
3. **직원 invite** — 기존 운영자 측 `invite-members.sh` 패턴 또는 web UI 직접
4. **dual-identity 권장** (D-ops-29) — admin 직원 별도 `ai@{customer-domain}` (automation) + 실제 사람 계정 2 identity 둘 다 Owner

### 함정

| 함정 | 결과 | 회피 |
|---|---|---|
| `SSO_ONLY: "true"` 설정 | Entra 장애 시 운영자 lockout (master password 진입 불가) | `false` 유지 (D-ops-27) |
| `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION: "false"` (Timshel default) | 첫 employee SSO "Your provider does not send email verification status" 차단 | `true` 명시 (D-ops-24) |
| `SIGNUPS_ALLOWED: "false"` | 첫 employee JIT signup "Failed to retrieve the invitation" 차단 | `true` (D-ops-26) |
| Vault UI 이메일-first 화면이 SSO Identifier 가림 | 직원이 가입 절차 못 찾음 | `/vault/#/sso` 직링크 안내 |
| client_secret 평문 메일 전송 | 영구 보관 노출 | view-once URL (axe secret send) |
| Vaultwarden Timshel 1.34.1-6 + bw CLI 2026.x | `TypeError: toWrappedAccountCryptographicState` crash | bw CLI 2025.7.0 pin (`npm install -g @bitwarden/cli@2025.7.0`) |

상세 결정 근거: [auth.mdx](/architecture/auth) · [D-ops-24](/ops/decisions#d-ops-24) · [D-ops-26](/ops/decisions#d-ops-26) · [D-ops-27](/ops/decisions#d-ops-27) · [D-ops-29](/ops/decisions#d-ops-29) · [D-ops-32](/ops/decisions#d-ops-32).

## 정기 점검

| 주기 | 작업 |
|---|---|
| 매일 | 자동 backup + integrity check (운영자 측) |
| 매주 | 운영자 → 귀사 IT health report |
| 매월 | secret 만료 30일 전 알림 (운영자 → 귀사) |
| 분기 | Restore drill (운영자 측 자동) |
| 연 1회 | 보안 감사 협의 |

## 종료/이전 시

귀사가 AXE Labs 서비스 종료 결정 시:

1. 데이터 export (frame DB dump + Vaultwarden export + Blueprint Workspaces)
2. cold storage SSD 1개 사본 귀사에 인도
3. macmini 의 AXE Labs 컨테이너 제거
4. Microsoft Entra ID 의 3개 app 비활성화 (귀사 측)
5. axelabs.ai zone 의 `{customer}` record 제거 (운영자 측)
6. customers.yaml 의 entry 제거 (운영자 측)

데이터 보관 정책: export 후 자체 보관. AXE Labs 측 백업은 종료 +90일 후 폐기.
