feat(infra): 이식 가능한 Docker 배포 구성
- docker-compose.yml: web/api/db(pgvector)/rag/proxy 한 방 기동 - 엔진 어댑터 분리: ENGINE_URL/ENGINE_MODE로 로컬 claude -p ↔ API 전환(코드 무수정 이전) - Caddy: 자동 TLS + SSE flush/WSS 무버퍼(실시간 음성) - DEPLOYMENT.md: 이식/엔진분리/스케일업/외부노출 가이드 - 페이지 레이아웃 시안 6종(login/learner-home/session-review/professor/admin/settings)
This commit is contained in:
parent
709baadd01
commit
204726f707
11 changed files with 272 additions and 0 deletions
14
apps/api/Dockerfile
Normal file
14
apps/api/Dockerfile
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Vignette API (FastAPI) — 이식 가능한 슬림 이미지
|
||||||
|
FROM python:3.11-slim AS base
|
||||||
|
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 PIP_NO_CACHE_DIR=1
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 의존성 레이어 분리(캐시 효율)
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
# 엔진/DB/음성 엔드포인트는 전부 env 로 주입(이미지에 굽지 않음 = 이식성)
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
11
apps/api/Dockerfile.rag
Normal file
11
apps/api/Dockerfile.rag
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Vignette RAG 사이드카 — BGE-M3 임베딩 + BGE-reranker-v2-m3
|
||||||
|
# CPU로도 동작(20명 규모 충분), GPU 호스트면 nvidia runtime 으로 가속.
|
||||||
|
FROM python:3.11-slim
|
||||||
|
ENV PYTHONUNBUFFERED=1 HF_HOME=/models
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements-rag.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements-rag.txt
|
||||||
|
COPY rag/ ./rag/
|
||||||
|
EXPOSE 8080
|
||||||
|
# 모델은 첫 기동 시 /models 볼륨에 캐시 → 이식 시 볼륨만 옮기면 재다운로드 불필요
|
||||||
|
CMD ["uvicorn", "rag.server:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||||
5
apps/api/requirements-rag.txt
Normal file
5
apps/api/requirements-rag.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
FlagEmbedding
|
||||||
|
sentence-transformers
|
||||||
|
torch
|
||||||
6
apps/api/requirements.txt
Normal file
6
apps/api/requirements.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
asyncpg
|
||||||
|
pydantic
|
||||||
|
python-multipart
|
||||||
|
httpx
|
||||||
16
apps/web/Dockerfile
Normal file
16
apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Vignette Web (React 19 + Vite) — 멀티스테이지(빌드 → 정적 서빙)
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
# node 22 사용(23.x는 일부 빌드 segfault 이슈 회피)
|
||||||
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
|
RUN corepack enable && pnpm install --frozen-lockfile || npm install
|
||||||
|
COPY . .
|
||||||
|
ARG VITE_API_BASE=/api
|
||||||
|
ENV VITE_API_BASE=$VITE_API_BASE
|
||||||
|
RUN pnpm build || npm run build
|
||||||
|
|
||||||
|
# 정적 서빙(작고 이식성 높은 nginx)
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
5
apps/web/nginx.conf
Normal file
5
apps/web/nginx.conf
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
location / { try_files $uri /index.html; }
|
||||||
|
}
|
||||||
65
docs/DEPLOYMENT.md
Normal file
65
docs/DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Vignette 배포 가이드 — "아무 데나 설치 가능한" Docker 구성
|
||||||
|
|
||||||
|
목표: 어떤 호스트(네 PC / 학교 서버 / 클라우드 VM)든 `docker compose up -d` 한 방으로 뜨고, **옮길 때 환경변수 몇 줄만** 바꾼다.
|
||||||
|
|
||||||
|
## 1. 한 방 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd infra
|
||||||
|
cp .env.example .env # 값 채우기(아래 표)
|
||||||
|
docker compose up -d
|
||||||
|
# web/api/db/rag/proxy 컨테이너 기동. http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
전부 컨테이너: `web`(React/nginx) · `api`(FastAPI) · `db`(postgres16+pgvector) · `rag`(BGE-M3+reranker) · `proxy`(Caddy, TLS·SSE·WSS).
|
||||||
|
|
||||||
|
## 2. 핵심 — "엔진"만 컨테이너 밖 (이식성의 열쇠)
|
||||||
|
|
||||||
|
우리 엔진은 로컬 `claude -p`(네 PC의 claude CLI/OAuth에 묶임)라, 컨테이너에 가두면 못 옮긴다. 그래서 **엔진은 어댑터 뒤에 두고 `ENGINE_URL`/`ENGINE_MODE`로 가리킨다.** 옮길 때 이 두 줄만 바꾸면 됨 — 코드 무수정.
|
||||||
|
|
||||||
|
설정 경로 **3가지(사용자 선택)**:
|
||||||
|
1. **yml / .env** — `infra/.env`의 `ENGINE_URL`·`ENGINE_MODE` (기본·운영)
|
||||||
|
2. **docker compose** — `docker compose run -e ENGINE_MODE=messages_api ...` 즉석 오버라이드
|
||||||
|
3. **설정 페이지(관리자 UI)** — 런타임에 엔진/모델 전환 (DB에 저장, 재기동 불필요)
|
||||||
|
|
||||||
|
| 시나리오 | ENGINE_MODE | ENGINE_URL |
|
||||||
|
|---|---|---|
|
||||||
|
| **네 PC 운영** (요구사항: 로컬 claude -p) | `claude_p` | `http://host.docker.internal:9099` (호스트의 claude -p 게이트웨이) |
|
||||||
|
| **클라우드/타호스트 이전** | `messages_api` | (불필요, ANTHROPIC_API_KEY 사용) |
|
||||||
|
| **원격 엔진 PC** | `claude_p` | `http://<엔진PC-IP>:9099` |
|
||||||
|
|
||||||
|
> 로컬 claude -p 게이트웨이 = 호스트에서 도는 작은 프로세스(상주 멀티턴 풀, `claude -p --input-format stream-json`). 컨테이너 api 가 `host.docker.internal:9099`로 호출. 이 게이트웨이는 `apps/api/engine_gateway/`에 둔다(호스트 실행, 컨테이너 밖).
|
||||||
|
|
||||||
|
## 3. 환경변수 (.env)
|
||||||
|
|
||||||
|
| 변수 | 설명 |
|
||||||
|
|---|---|
|
||||||
|
| `POSTGRES_PASSWORD` | DB 비밀번호(필수) |
|
||||||
|
| `SESSION_SECRET` | 세션 서명키(필수, 랜덤) |
|
||||||
|
| `OPENAI_API_KEY` | 음성(STT/TTS) |
|
||||||
|
| `ANTHROPIC_API_KEY` | 엔진 폴백/클라우드 모드 |
|
||||||
|
| `ENGINE_MODE` / `ENGINE_URL` | 엔진 위치(위 표) |
|
||||||
|
| `SITE_ADDRESS` | 외부노출 도메인(예: `vignette.chanpaca.net`). 비우면 로컬 :80 |
|
||||||
|
| `ACME_EMAIL` | 외부노출 시 Let's Encrypt 자동 TLS용 |
|
||||||
|
| `HTTP_PORT`/`HTTPS_PORT` | 호스트 포트 매핑 |
|
||||||
|
|
||||||
|
## 4. 외부 공유 (chanpaca.net, 교수 테스트)
|
||||||
|
|
||||||
|
두 가지 — 호스트 상황에 맞게:
|
||||||
|
- **Caddy 자동 TLS**: 공인 IP·도메인 있으면 `SITE_ADDRESS=vignette.chanpaca.net` + `ACME_EMAIL` → Caddy가 인증서 자동 발급.
|
||||||
|
- **Cloudflare/Tailscale 터널**: 공인 IP 없을 때(네 PC). 단 **음성 SSE/WSS는 버퍼링 충돌**이 있어 Caddyfile에서 `flush_interval -1`로 흘려보냄(red team F-07 반영). 터널도 SSE 무버퍼 경로 분리 권장.
|
||||||
|
|
||||||
|
## 5. 스케일업 / 이전
|
||||||
|
|
||||||
|
- **수직**: compose 그대로, 호스트 사양만 키움.
|
||||||
|
- **수평**: `docker compose up --scale api=3` (api 무상태 설계 전제, 세션은 DB/Redis). proxy가 라운드로빈.
|
||||||
|
- **이전**: `pgdata`·`ragmodels` 볼륨만 새 호스트로 옮기고 `.env` 채워 `up`. (rag 모델 캐시 볼륨 덕에 재다운로드 0)
|
||||||
|
- **클라우드**: 동일 compose. 단 엔진을 `messages_api`로 바꾸거나 엔진 PC를 `ENGINE_URL`로 원격 지정.
|
||||||
|
|
||||||
|
## 6. 데이터 주권 / 보안
|
||||||
|
|
||||||
|
- DB는 NAS PostgreSQL(국내). 미성년 사례데이터는 repo·이미지에 절대 미포함(`.gitignore`).
|
||||||
|
- 외부 LLM 전송 전 PII 마스킹 미들웨어 통과(red team F-03).
|
||||||
|
- proxy 인증 게이트 뒤에서만 SSE/WSS 노출(무인증 IDOR 차단, F-20).
|
||||||
|
|
||||||
|
> 현 단계: 구성 골격(compose·Dockerfile·Caddy) 완비. 앱 코드(apps/web, apps/api) 채워지면 그대로 빌드된다.
|
||||||
15
infra/.env.example
Normal file
15
infra/.env.example
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Vignette infra env (예시). cp .env.example .env 후 값 채우기
|
||||||
|
POSTGRES_USER=vignette
|
||||||
|
POSTGRES_PASSWORD=change-me
|
||||||
|
POSTGRES_DB=vignette
|
||||||
|
SESSION_SECRET=change-me-random
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
# 엔진(이식성): 네 PC=claude_p, 클라우드=messages_api
|
||||||
|
ENGINE_MODE=claude_p
|
||||||
|
ENGINE_URL=http://host.docker.internal:9099
|
||||||
|
# 외부노출(비우면 로컬 :80)
|
||||||
|
SITE_ADDRESS=:80
|
||||||
|
ACME_EMAIL=
|
||||||
|
HTTP_PORT=8080
|
||||||
|
HTTPS_PORT=8443
|
||||||
29
infra/Caddyfile
Normal file
29
infra/Caddyfile
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Vignette 리버스프록시
|
||||||
|
# 로컬: http://localhost:8080
|
||||||
|
# 외부: 도메인 한 줄 바꾸면 Caddy가 Let's Encrypt 자동 TLS
|
||||||
|
{
|
||||||
|
# 외부노출 시 이메일만 채우면 자동 HTTPS. 로컬은 무시됨.
|
||||||
|
email {$ACME_EMAIL:admin@example.com}
|
||||||
|
}
|
||||||
|
|
||||||
|
{$SITE_ADDRESS::80} {
|
||||||
|
# 음성 WSS — 버퍼링 끄고 그대로 흘려보냄(상담 실시간성)
|
||||||
|
@ws path /api/voice/ws*
|
||||||
|
reverse_proxy @ws api:8000
|
||||||
|
|
||||||
|
# SSE(실시간 자막/피드백 스트림) — flush_interval -1 로 즉시 전달(버퍼링 충돌 방지)
|
||||||
|
@sse path /api/*/stream*
|
||||||
|
reverse_proxy @sse api:8000 {
|
||||||
|
flush_interval -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 일반 API
|
||||||
|
handle_path /api/* {
|
||||||
|
reverse_proxy api:8000
|
||||||
|
}
|
||||||
|
|
||||||
|
# 프론트(SPA)
|
||||||
|
handle {
|
||||||
|
reverse_proxy web:80
|
||||||
|
}
|
||||||
|
}
|
||||||
6
infra/db/init/01_extensions.sql
Normal file
6
infra/db/init/01_extensions.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Vignette DB 초기화: pgvector 확장 + 스키마(app/audit/kb)
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS app;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS audit;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS kb;
|
||||||
|
-- 상세 DDL은 docs/MEMORY_KNOWLEDGE_PERSONA_DESIGN.md / MASTERPLAN.md 참조
|
||||||
100
infra/docker-compose.yml
Normal file
100
infra/docker-compose.yml
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Vignette — 이식 가능한 단일 호스트 배포 구성
|
||||||
|
# 어디서든: cp .env.example .env && (값 채우고) && docker compose up -d
|
||||||
|
#
|
||||||
|
# 설계 원칙:
|
||||||
|
# - web/api/db/voice/rag 는 전부 컨테이너 → 호스트 무관하게 한 방에 뜬다.
|
||||||
|
# - "엔진"(로컬 claude -p)만 컨테이너 밖. api 는 ENGINE_URL 로 가리킨다.
|
||||||
|
# → 옮길 때 ENGINE_URL 한 줄만 바꾸면 됨(로컬 게이트웨이 ↔ API 폴백).
|
||||||
|
# - 시크릿/엔드포인트는 전부 .env. 이미지에 굽지 않는다(이식성).
|
||||||
|
|
||||||
|
name: vignette
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: pgvector/pgvector:pg16
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-vignette}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-vignette}
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
- ./db/init:/docker-entrypoint-initdb.d:ro # 스키마/확장 초기화 SQL
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-vignette}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks: [vignette]
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: ../apps/api
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db: { condition: service_healthy }
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-vignette}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-vignette}
|
||||||
|
# === 엔진(이식성 핵심) ===
|
||||||
|
# 로컬 운영: host 의 claude -p 게이트웨이를 가리킴(아래 extra_hosts 사용)
|
||||||
|
# 클라우드 이전: 이 값을 Messages API 어댑터 URL 로만 바꾸면 됨.
|
||||||
|
ENGINE_URL: ${ENGINE_URL:-http://host.docker.internal:9099}
|
||||||
|
ENGINE_MODE: ${ENGINE_MODE:-claude_p} # claude_p | messages_api
|
||||||
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} # 폴백/클라우드용
|
||||||
|
OPENAI_API_KEY: ${OPENAI_API_KEY:?set in .env} # 음성
|
||||||
|
RAG_EMBED_URL: http://rag:8080
|
||||||
|
SESSION_SECRET: ${SESSION_SECRET:?set in .env}
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway" # 컨테이너→호스트 엔진 게이트웨이 접근(Linux 포함)
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request,sys; urllib.request.urlopen('http://localhost:8000/health'); "]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks: [vignette]
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ../apps/web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
VITE_API_BASE: ${PUBLIC_API_BASE:-/api}
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on: [api]
|
||||||
|
networks: [vignette]
|
||||||
|
|
||||||
|
# RAG 사이드카: BGE-M3 임베딩 + BGE-reranker-v2-m3 (CPU 가능, GPU 있으면 가속)
|
||||||
|
rag:
|
||||||
|
build:
|
||||||
|
context: ../apps/api
|
||||||
|
dockerfile: Dockerfile.rag
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
EMBED_MODEL: BAAI/bge-m3
|
||||||
|
RERANK_MODEL: BAAI/bge-reranker-v2-m3
|
||||||
|
volumes:
|
||||||
|
- ragmodels:/models # 모델 캐시(재다운로드 방지 = 이식 시 볼륨만 옮기면 빠름)
|
||||||
|
networks: [vignette]
|
||||||
|
|
||||||
|
# 리버스프록시 + TLS. 외부노출(chanpaca.net)도 여기서. 끄려면 profiles 로 제외.
|
||||||
|
proxy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on: [web, api]
|
||||||
|
ports:
|
||||||
|
- "${HTTP_PORT:-8080}:80"
|
||||||
|
- "${HTTPS_PORT:-8443}:443"
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddydata:/data
|
||||||
|
networks: [vignette]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
ragmodels:
|
||||||
|
caddydata:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
vignette:
|
||||||
|
driver: bridge
|
||||||
Loading…
Add table
Add a link
Reference in a new issue