# 서버 이관 런북 (초안) — 자택 개인 서버 → 학교 정보처 호스팅 > **초안 — 연구팀·정보처 협의 후 확정.** 이 문서는 개발(트웬티온스) 몫인 *기술 이관 절차* 초안이다. > 도메인·서버 사양·비용·보안 정책의 최종 결정 주체는 연구팀·한신대 정보처이며, 아래 값과 절차는 협의 결과에 맞춰 갱신한다. > 스펙 근거: [`hanshin-meeting-actions-2026-07-13.md`](./hanshin-meeting-actions-2026-07-13.md) P3. ## 0. 요약 (한 장) - **현재 운영 형태**: 윤찬 자택 Windows PC에서 `scripts/start-public-runtime.ps1`로 **베어 프로세스**(uvicorn + `vite preview` + 로컬 `claude -p` 게이트웨이)를 띄우고, **Cloudflare 터널**로 공개하며, **Windows 작업 스케줄러 워치독**이 살아있게 유지한다. → 이 경로 전체가 Windows 종속이라 리눅스로 그대로 못 옮긴다. - **이관 목표 형태**: 학교 리눅스 서버에서 **`infra/docker-compose.yml` 스택**(web·api·db·proxy)을 그대로 기동. Docker 스택은 이식성을 전제로 설계돼 있어 Windows 런타임 스크립트 계층을 대부분 대체한다. - **가장 큰 이관 블로커 2개**: ① 엔진 모드 `claude_cli`(개인 구독 CLI 인증) → `claude_api`(`ANTHROPIC_API_KEY`) 전환 필수. ② 하드코딩된 도메인/리다이렉트 URI/허용 호스트를 학교 도메인으로 교체(일부는 **빌드 시점**에 굽혀서 재빌드 필요). --- ## 1. 사전 요구사항 (이관 시작 전 확보) ### 1.1 정보처가 준비할 것 - **리눅스 호스트**: Docker Engine + Docker Compose v2 설치 가능한 서버(Ubuntu 22.04 LTS 등). 20명 파일럿 기준 최소 4 vCPU / 8GB RAM / 40GB SSD 권장(부하 검증 결과에 맞춰 조정). - **공개 도메인 2개**: 웹용 1개 + API용 1개(예: `vignette.hs.ac.kr`, `api-vignette.hs.ac.kr`). **웹과 API는 별도 호스트네임**이어야 한다(OAuth 콜백·CORS 설계 전제). - **DNS A/AAAA 레코드**: 위 두 도메인이 서버 공인 IP로 향하도록 설정. - **TLS**: 다음 중 하나. - (a) 서버에 공인 IP가 있으면 **Caddy 자동 TLS**(Let's Encrypt) 사용 — `.env`의 `SITE_ADDRESS`, `ACME_EMAIL`만 채우면 됨. - (b) 정보처가 기관 인증서를 강제하면 Caddy 앞단에 리버스 프록시를 두거나 인증서를 Caddy에 주입(협의 필요). - **아웃바운드 네트워크 허용**: `api.openai.com`, `api.anthropic.com`(엔진 claude_api 모드), Google OAuth 엔드포인트로의 HTTPS egress. ### 1.2 연구팀·개발이 준비할 것 - **Google OAuth 클라이언트**: Google Cloud Console에서 **Authorized redirect URI에 `https://api-<학교도메인>/auth/callback` 추가**(기존 chanpaca/18ka URI는 유지 또는 정리). Client ID/Secret 확보. - **API 키**: `OPENAI_API_KEY`(음성 STT/TTS), `ANTHROPIC_API_KEY`(엔진 claude_api 모드 — 아래 §2 참조). - **관리자·교수자 이메일 목록**: `AUTH_SUPER_ADMIN_EMAILS`, `AUTH_ADMIN_EMAILS`, `AUTH_TEACHER_EMAILS`, 허용 도메인 `AUTH_ALLOWED_EMAIL_DOMAINS`(파일럿 참가자 도메인 포함). - **현재 운영 데이터 스냅샷**: DB 덤프 + uploads 디렉터리(아래 §4). --- ## 2. 엔진 모드 주의 — claude_cli → claude_api (필수) 현재 운영은 `ENGINE_MODE=claude_cli`다. 이는 **호스트에 상주하는 개인 `claude` CLI(개인 구독 OAuth 인증)** 를 게이트웨이(`http://host.docker.internal:9099`)로 호출하는 방식이라 **학교 서버에는 옮길 수 없다**(개인 계정 인증에 묶임). 학교 서버에서는 반드시 다음으로 전환한다. ```dotenv ENGINE_MODE=claude_api ENGINE_URL=http://host.docker.internal:9099 # ANTHROPIC_API_KEY는 엔진 게이트웨이 호스트에 연구팀/기관 발급 키로 주입 ``` - 코드는 `claude_api`를 게이트웨이 provider로 지원한다. 관리자는 `/admin/ai` 또는 `/settings`에서 게이트웨이가 Anthropic API로 조회한 모델과 지원 추론 강도만 선택할 수 있다. 키가 없거나 조회가 실패하면 해당 provider는 선택 불가 상태가 되고 잘못된 설정은 저장되지 않는다. - 비용: 회의 기록상 실사용 지난달 ~$10, 20명 액티브 시 최대 $100 이내 추정. 과금은 `ANTHROPIC_API_KEY` 계정으로 발생 → 연구팀/기관 계정 사용 권장. 관리자 UI에서 `ADMIN_USAGE_BUDGET_USD`로 예산 표시 가능. - 개인 `claude` CLI와 상주풀은 불필요해지지만 `apps/api/engine_gateway/` 자체는 Anthropic API 모델 탐색·실행을 위해 계속 필요하다. 학교 서버 또는 별도 엔진 호스트에 게이트웨이를 실행하고 API 키는 그 프로세스에만 주입한다. --- ## 3. 이관 단계 ### 3.1 코드 배치 + 환경파일 작성 ```bash git clone <저장소> vignette && cd vignette/infra cp .env.example .env # .env 편집 — 아래 §5 체크리스트의 "설정만 바꾸면 됨" 값을 학교 값으로 교체 ``` 배포 전 프리플라이트(읽기 전용 검증): ```bash # 저장소 루트에서 python scripts/check-deploy-preflight.py --env-file infra/.env # DB 앱롤까지 검증하려면 app-role DATABASE_URL 을 넘기고 --require-app-role ``` 이 스크립트는 필수 env 누락, `change-me` 류 플레이스홀더, prod에서 켜지면 안 되는 플래그(`AUTH_DEV_LOGIN_ENABLED` 등), 잘못된 `ENGINE_MODE`를 잡아준다. ### 3.2 DB dump / restore 현재 DB(자택)에서 덤프를 뜬다. 접속 방식에 맞춰 하나 선택. ```bash # (A) DB가 docker compose db 컨테이너인 경우 — 자택 서버에서 docker compose exec -T db pg_dump -U vignette_owner -Fc vignette > vignette-$(date +%F).dump # (B) DB가 외부/NAS PostgreSQL인 경우 — 접속 문자열로 직접 pg_dump --format=custom --no-owner --no-privileges \ --dbname="postgresql://vignette_owner:@:5432/vignette" \ --file=vignette-$(date +%F).dump ``` 학교 서버에서 복원. **권장: 스키마 init 스크립트가 만든 빈 DB가 아니라, 완전히 빈 DB에 풀 덤프를 복원**한다(스키마·데이터·RLS 정책을 덤프가 통째로 재현하도록). ```bash # 새 호스트: db 컨테이너만 먼저 띄우되 init 스크립트와 충돌을 피하려면 # - 신규 빈 볼륨 + init 스크립트로 스키마 생성 후 --data-only 복원, 또는 # - init 없이 빈 DB 생성 후 풀 덤프 복원(권장, 아래) docker compose up -d db docker compose exec -T db psql -U vignette_owner -c "CREATE DATABASE vignette_new;" cat vignette-YYYY-MM-DD.dump | docker compose exec -T db \ pg_restore --no-owner --no-privileges --clean --if-exists \ -U vignette_owner -d vignette_new # 검증 후 vignette_new 를 정식 DB로 승격(또는 처음부터 vignette 로 복원) ``` > ⚠️ **RLS/앱롤 주의**: 스키마는 `infra/db/init/`의 `01_extensions.sql … 06_session_evaluation.sql`이 **빈 볼륨 최초 기동 시 1회만** 실행한다. 여기엔 RLS 정책(`04_audit_eval_rls.sql`)과 앱 전용 롤 생성(`99_app_role.sh`)이 포함된다. 풀 덤프 복원 시 롤/권한이 덤프에 없으면(=`--no-owner --no-privileges` 사용) **복원 후 `99_app_role.sh` 상당의 앱롤 GRANT를 다시 적용**해야 API app-role(`vignette_app`)이 붙는다. 프리플라이트 `--require-app-role`로 확인. ### 3.3 persona / KB 데이터 이동 페르소나·평가·라이브코칭 KB는 **저장소가 원본을 소유**하고 스크립트로 DB에 동기화한다. DB 덤프에 이미 승인된 `persona_card` 행이 포함되지만, KB 소스팩은 코드 기준으로 재동기화한다. ```bash # dry-run(기본): repo 소스팩 해시 vs 활성 kb.document 비교 python scripts/sync-persona-sources.py # 실제 반영 python scripts/sync-persona-sources.py --apply ``` - 페르소나 시드 재생성이 필요하면 `python scripts/materialize-persona-seeds.py`. - 원칙: prod에서는 `AUTO_SEED_PERSONAS=false`, `ALLOW_SEED_PERSONA_FALLBACK=false` 유지(승인된 DB 행만 사용). 추고록 원본 등 학교 재산 데이터는 repo·이미지에 넣지 않는다. ### 3.4 uploads 디렉터리 이동 사용자 업로드(교수자 자료 등)는 `apiuploads` 도커 볼륨(`/app/uploads`, `USER_UPLOAD_DIR`)에 있다. ```bash # 자택: 볼륨 내용 tar 로 추출 docker run --rm -v vignette_apiuploads:/data -v "$PWD":/backup alpine \ tar czf /backup/uploads-$(date +%F).tgz -C /data . # 학교: 새 볼륨에 복원 docker run --rm -v vignette_apiuploads:/data -v "$PWD":/backup alpine \ tar xzf /backup/uploads-YYYY-MM-DD.tgz -C /data ``` ### 3.5 기동 ```bash cd infra docker compose build # 웹은 도메인이 빌드에 굽히므로 §5 빌드 인자 확인 후 빌드 docker compose up -d docker compose ps # web/api/db/proxy healthy 확인 ``` --- ## 4. 이식성 이슈 체크리스트 ### 4.1 설정만 바꾸면 되는 것 (env / DNS / 콘솔 — 재빌드·코드수정 불필요) | 항목 | 현재 하드코딩/기본값 | 이관 시 조치 | |---|---|---| | API 리다이렉트 URI | `OAUTH_REDIRECT_URI=https://api-vignette.chanpaca.net/auth/callback` | 학교 API 도메인으로 교체 + Google Console에 등록 | | 프론트 기준 URL | `FRONTEND_BASE_URL=https://vignette.chanpaca.net` | 학교 웹 도메인 | | 콜백→프론트 매핑 | `FRONTEND_ORIGIN_MAP={api-vignette.chanpaca.net:…, api-vnet.18ka.net:…}` | 학교 `api-호스트: https://웹-호스트` | | CORS 허용 | `CORS_ORIGINS=[chanpaca.net, 18ka.net, pages.dev, localhost…]` | 학교 웹 도메인만(로컬 항목 제거) | | 슈퍼관리자 | `AUTH_SUPER_ADMIN_EMAILS=[yunchan@twentyoz.kr, hoonjungkoo@hs.ac.kr]` | 운영 담당자 이메일 | | 허용 도메인 | `AUTH_ALLOWED_EMAIL_DOMAINS=[hs.ac.kr, twentyoz.kr]` | 파일럿 참가 도메인 | | 시크릿 | `POSTGRES_PASSWORD/APP_DB_PASSWORD/SESSION_SECRET`(=`change-me…`) | 랜덤 강값으로 교체 | | TLS/노출 | `SITE_ADDRESS=:80`, `ACME_EMAIL=`(빈값) | 학교 웹 도메인 + 관리자 메일 | | 포트 | `HTTP_PORT=8080`, `HTTPS_PORT=8443` | 정보처 방화벽 정책에 맞춰 | | 엔진 | `ENGINE_MODE=claude_cli` | `claude_api` + `ANTHROPIC_API_KEY`(§2) | ### 4.2 고쳐야(재빌드/치환해야) 넘어가는 것 | 항목 | 위치 | 왜 블로커인가 | 조치 | |---|---|---|---| | **웹 허용 호스트** | `apps/web/vite.config.ts` `defaultAllowedHosts`(chanpaca/18ka/ts.net 배열) | `vite preview`가 Host 헤더를 검증 → 학교 도메인 누락 시 전체 화면이 403 블랭크. **빌드 시점 값**이라 재빌드 필요 | 빌드 전 `VITE_ALLOWED_HOSTS=<학교 웹 도메인>` 환경변수로 주입(코드 수정 없이 가능) | | **웹 API 베이스** | compose `web` 빌드 인자 `VITE_API_BASE=${PUBLIC_API_BASE:-/api}` | 프론트가 붙는 API 경로가 **빌드에 굽힘** | Caddy 프록시로 `/api` 동일 오리진 유지 시 기본값 그대로 OK. 별도 API 도메인 직결이면 `PUBLIC_API_BASE` 지정 후 재빌드 | | **엔진 인증 방식** | `ENGINE_MODE=claude_cli` 런타임 경로 | 개인 구독 CLI 인증 → 학교서버 이식 불가 | §2대로 `claude_api` 전환(코드 지원됨, env 전환) | | **Windows 런타임 계층 전체** | `scripts/*.ps1`, `.vbs`, Windows 작업 스케줄러(`install-public-runtime-task.ps1`, `register-boot-task.ps1`), `Get-CimInstance`, winget 경로 | Windows 전용. `$Workspace="D:\workspace\vignette"`, `$Python=%LOCALAPPDATA%\…`, `$Cloudflared`, `$USERPROFILE\.cloudflared` 등 하드코딩 | 리눅스에서는 **docker compose 스택 + systemd/cron 헬스체크**로 대체(PowerShell 스크립트 미사용). Cloudflare 터널은 공인 IP+Caddy 자동TLS로 대체 권장 | | **config.py 하드코딩 기본값** | `apps/api/app/config.py`의 `oauth_redirect_uri/frontend_origin_map/cors_origins/auth_super_admin_emails` 기본값에 chanpaca·18ka·특정 이메일 | env로 override되지만, env 누락 시 자택 값으로 조용히 폴백될 위험 | env를 반드시 채우고(§4.1), 이관 확정 후 기본값도 중립화 검토(별도 PR) | > **비밀 주입 방식**: 현재는 `infra/.env` **평문 파일** 한 장(요구변수는 compose `${VAR:?}` 문법으로 미설정 시 기동 실패 → 누락 방어는 됨). 학교 서버에서는 `.env` 파일 권한 `chmod 600` + repo 미포함 확인, 가능하면 정보처 시크릿 관리 수단으로 승격 협의. 시크릿 매니저/볼트는 현재 미사용. --- ## 5. 이관 후 검증 체크리스트 ```bash # 1) 헬스 — environment=prod, db=true, engine=true, engine_mode=claude_api 확인 curl -s https://api-<학교도메인>/health | jq # 기대: {"status":"ok","environment":"prod","db":true,"engine":true,"engine_mode":"claude_api", ...} # 2) dev-login 꺼짐 확인 — prod에서 AUTH_DEV_LOGIN_ENABLED=false 여야 함(preflight도 검증) # /auth/dev-login 류가 비활성인지, 로그인은 Google OAuth로만 되는지 확인 # 3) OAuth 왕복 — Google 로그인 → 콜백 → pending 승인 대기 → 슈퍼관리자 승인 → 온보딩 진입 # 4) /turn 스모크 — 세션 1개 생성 후 발화 1턴 왕복(내담자 응답 + 라이브 코칭 카드 수신) # 5) 백엔드 pytest cd apps/api && python -m pytest -q # 6) (선택) 20 동시 세션 부하 — scripts/load-test-sessions.py 로 병목 재확인 ``` 추가 확인: - 음성 SSE/WSS 경로가 Caddy `flush_interval -1`로 버퍼링 없이 흐르는지(자막·실시간 피드백). - persona 목록이 승인 DB 행 기준으로 뜨는지(시드 폴백 아님). - uploads 업로드→변환→원본 파기 흐름(P4)이 실데이터로 동작하는지. --- ## 6. 롤백 / 주의 - 이관은 **자택 운영을 내리기 전에** 학교 서버에서 병렬로 세워 검증하고, 검증 통과 후 DNS를 전환한다(다운타임 최소화). - DB 덤프는 **이관 시점 스냅샷**이다. 병렬 운영 중 자택에서 발생한 신규 데이터는 최종 컷오버 시점에 재덤프·재복원(또는 컷오버 동안 자택 쓰기 중지). - OAuth redirect URI를 학교 도메인으로 바꾸는 순간 기존 도메인 로그인은 깨질 수 있으니, 컷오버 타이밍을 연구팀과 합의. - 확정 후 이 문서의 "초안" 표기를 제거하고 [`DEPLOYMENT.md`](../DEPLOYMENT.md)·SSOT 대시보드에 반영한다.