14 KiB
서버 이관 런북 (초안) — 자택 개인 서버 → 학교 정보처 호스팅
초안 — 연구팀·정보처 협의 후 확정. 이 문서는 개발(트웬티온스) 몫인 기술 이관 절차 초안이다. 도메인·서버 사양·비용·보안 정책의 최종 결정 주체는 연구팀·한신대 정보처이며, 아래 값과 절차는 협의 결과에 맞춰 갱신한다. 스펙 근거:
hanshin-meeting-actions-2026-07-13.mdP3.
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에 주입(협의 필요).
- (a) 서버에 공인 IP가 있으면 Caddy 자동 TLS(Let's Encrypt) 사용 —
- 아웃바운드 네트워크 허용:
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)로 호출하는 방식이라 학교 서버에는 옮길 수 없다(개인 계정 인증에 묶임).
학교 서버에서는 반드시 다음으로 전환한다.
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로 예산 표시 가능. - 개인
claudeCLI와 상주풀은 불필요해지지만apps/api/engine_gateway/자체는 Anthropic API 모델 탐색·실행을 위해 계속 필요하다. 학교 서버 또는 별도 엔진 호스트에 게이트웨이를 실행하고 API 키는 그 프로세스에만 주입한다.
3. 이관 단계
3.1 코드 배치 + 환경파일 작성
git clone <저장소> vignette && cd vignette/infra
cp .env.example .env
# .env 편집 — 아래 §5 체크리스트의 "설정만 바꾸면 됨" 값을 학교 값으로 교체
배포 전 프리플라이트(읽기 전용 검증):
# 저장소 루트에서
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(자택)에서 덤프를 뜬다. 접속 방식에 맞춰 하나 선택.
# (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:<PW>@<OLD_HOST>:5432/vignette" \
--file=vignette-$(date +%F).dump
학교 서버에서 복원. 권장: 스키마 init 스크립트가 만든 빈 DB가 아니라, 완전히 빈 DB에 풀 덤프를 복원한다(스키마·데이터·RLS 정책을 덤프가 통째로 재현하도록).
# 새 호스트: 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 소스팩은 코드 기준으로 재동기화한다.
# 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)에 있다.
# 자택: 볼륨 내용 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 기동
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. 이관 후 검증 체크리스트
# 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·SSOT 대시보드에 반영한다.