Merge feature/video-meeting-phase6: 화상회의 단계 6(셀프호스팅 배포 구성)
This commit is contained in:
@@ -52,3 +52,16 @@ CORS_ORIGIN=http://localhost:5173
|
||||
# AI_TIMEOUT_MS: AI 요청 타임아웃(ms). 백엔드 Claude 호출 abort + 프론트 axios 에 동일 적용.
|
||||
# 프론트에는 build arg VITE_AI_TIMEOUT_MS 로 주입된다.
|
||||
AI_TIMEOUT_MS=60000
|
||||
|
||||
# ==========================================
|
||||
# 화상회의 (LiveKit) — 셀프호스팅 또는 Cloud
|
||||
# ==========================================
|
||||
# [셀프호스팅] livekit/DEPLOY.md 참고. selfhost-livekit 프로파일로 컨테이너 기동.
|
||||
# LIVEKIT_WS_URL : 브라우저 접속(리버스 프록시 TLS) → wss://livekit.<운영도메인>
|
||||
# LIVEKIT_URL : 백엔드 → LiveKit 내부 API(도커 네트워크) → http://livekit:7880
|
||||
# [Cloud] LIVEKIT_WS_URL/URL 을 wss://<project>.livekit.cloud 로, 키를 Cloud 값으로.
|
||||
# LIVEKIT_API_SECRET 은 충분히 강력하게(최소 32자).
|
||||
LIVEKIT_API_KEY=change-me-livekit-key
|
||||
LIVEKIT_API_SECRET=change-me-livekit-secret-min-32-characters
|
||||
LIVEKIT_WS_URL=wss://livekit.example.com
|
||||
LIVEKIT_URL=http://livekit:7880
|
||||
|
||||
@@ -54,3 +54,6 @@ Thumbs.db
|
||||
# 디자인 시안 단독 HTML(참고용, 대용량 번들) — 커밋 제외
|
||||
frontend/*단독*.html
|
||||
frontend/*단일 파일*.html
|
||||
|
||||
# LiveKit 클러스터 설정 사본(redis 비밀번호 포함) — 커밋 제외
|
||||
livekit/*.local.yaml
|
||||
|
||||
@@ -150,6 +150,39 @@ services:
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
|
||||
# ----------------------------------------
|
||||
# 5. LiveKit (화상회의 SFU) — 셀프호스팅 (opt-in 프로파일)
|
||||
# 실행: docker compose --profile selfhost-livekit --env-file .env.production up -d --build
|
||||
# 미사용시(예: LiveKit Cloud) 프로파일을 켜지 않으면 생성되지 않는다.
|
||||
# 배포·클러스터링 가이드: livekit/DEPLOY.md
|
||||
# ----------------------------------------
|
||||
livekit:
|
||||
image: livekit/livekit-server:v1.8.4
|
||||
container_name: ${PROJECT_NAME}-livekit
|
||||
profiles:
|
||||
- selfhost-livekit
|
||||
command: --config /etc/livekit/livekit.yaml
|
||||
environment:
|
||||
# 키/시크릿은 "key: secret" 형식으로 주입(콜론+공백 때문에 전체를 따옴표로 감쌈)
|
||||
- "LIVEKIT_KEYS=${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
|
||||
expose:
|
||||
- "7880" # 시그널링/HTTP API — 리버스 프록시(TLS)에서 wss 로 라우팅
|
||||
ports:
|
||||
- "7881:7881" # ICE/TCP 폴백
|
||||
- "7882:7882/udp" # ICE/UDP (mux)
|
||||
volumes:
|
||||
# 클러스터링 시에는 redis 비밀번호가 담긴 사본을 마운트(DEPLOY.md 참고)
|
||||
- ./livekit/livekit.prod.yaml:/etc/livekit/livekit.yaml:ro
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- app-network
|
||||
- proxy
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2.0'
|
||||
memory: 2g
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# LiveKit 셀프호스팅 배포 가이드 (화상회의 단계 6)
|
||||
|
||||
운영에서 LiveKit(SFU)을 직접 호스팅하는 방법. **단일 노드 → 멀티 노드 클러스터** 순으로 설명한다.
|
||||
(간단히 가려면 운영도 LiveKit Cloud 를 그대로 쓸 수 있다 — 그 경우 `.env.production` 의 `LIVEKIT_*` 만 Cloud 값으로 두고 아래 셀프호스팅 절차는 생략.)
|
||||
|
||||
> 로컬 dev 에서 셀프호스팅이 막혔던 건 WSL2/Docker Desktop 네트워킹 제약 때문이며, **공인 IP 가 있는 실서버에서는 발생하지 않는다.**
|
||||
|
||||
---
|
||||
|
||||
## 1. 사전 준비
|
||||
|
||||
- 공인 IP 를 가진 리눅스 서버(도커 설치)
|
||||
- 도메인 + DNS: `livekit.<도메인>` A 레코드 → 서버 공인 IP
|
||||
- **리버스 프록시(TLS 종단)**: 기존 `proxy` 외부 네트워크의 프록시(Traefik/nginx 등)
|
||||
- 방화벽 개방:
|
||||
- `443/tcp` — 시그널링(wss, 프록시 경유)
|
||||
- `7881/tcp` — ICE/TCP 폴백
|
||||
- `7882/udp` — ICE/UDP(미디어)
|
||||
|
||||
---
|
||||
|
||||
## 2. 키 생성
|
||||
|
||||
API Key 는 임의 식별자, Secret 은 32자 이상 강력한 난수.
|
||||
|
||||
```bash
|
||||
echo "LIVEKIT_API_KEY=$(openssl rand -hex 8)"
|
||||
echo "LIVEKIT_API_SECRET=$(openssl rand -hex 32)"
|
||||
```
|
||||
|
||||
`.env.production` 에 채운다:
|
||||
|
||||
```dotenv
|
||||
LIVEKIT_API_KEY=<생성한 key>
|
||||
LIVEKIT_API_SECRET=<생성한 secret(32자+)>
|
||||
LIVEKIT_WS_URL=wss://livekit.<도메인> # 브라우저 접속(프론트 빌드에 주입)
|
||||
LIVEKIT_URL=http://livekit:7880 # 백엔드 → LiveKit 내부 API
|
||||
```
|
||||
|
||||
> `LIVEKIT_WS_URL` 은 프론트 빌드 시 `VITE_LIVEKIT_URL` 로 **굽혀진다**. 값 변경 시 프론트 재빌드 필요.
|
||||
|
||||
---
|
||||
|
||||
## 3. 기동 (단일 노드)
|
||||
|
||||
```bash
|
||||
docker compose --profile selfhost-livekit --env-file .env.production up -d --build
|
||||
```
|
||||
|
||||
- `selfhost-livekit` 프로파일을 켜야 LiveKit 컨테이너가 생성된다(미지정 시 미생성).
|
||||
- 설정: `livekit/livekit.prod.yaml`(키는 `LIVEKIT_KEYS` env 로 주입), `use_external_ip: true` 로 공인 IP 자동 광고.
|
||||
|
||||
---
|
||||
|
||||
## 4. 리버스 프록시 라우팅 (시그널링 wss)
|
||||
|
||||
`wss://livekit.<도메인>` → 컨테이너 `livekit:7880` 으로 라우팅(WebSocket 업그레이드 + TLS 종단).
|
||||
미디어(7881/7882)는 프록시를 거치지 않는다 — 직접 공인 IP 로 통신.
|
||||
|
||||
**Traefik(라벨) 예:**
|
||||
|
||||
```yaml
|
||||
# docker-compose 의 livekit 서비스에 labels 추가(운영 프록시 구성에 맞게):
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.livekit.rule=Host(`livekit.<도메인>`)"
|
||||
- "traefik.http.routers.livekit.entrypoints=websecure"
|
||||
- "traefik.http.routers.livekit.tls.certresolver=<resolver>"
|
||||
- "traefik.http.services.livekit.loadbalancer.server.port=7880"
|
||||
```
|
||||
|
||||
**nginx 예:**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name livekit.<도메인>;
|
||||
# ssl_certificate ... ;
|
||||
location / {
|
||||
proxy_pass http://livekit:7880;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 검증
|
||||
|
||||
1. 브라우저에서 회의 입장(두 기기/네트워크에서 영상 확인)
|
||||
2. 부하 테스트:
|
||||
```bash
|
||||
LIVEKIT_URL=wss://livekit.<도메인> \
|
||||
LIVEKIT_API_KEY=<key> LIVEKIT_API_SECRET=<secret> \
|
||||
./livekit/load-test.sh full
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 멀티 노드 클러스터링 (수평 확장)
|
||||
|
||||
대규모(예: 160명 다수 방)에서 노드 간 트래픽을 분산하려면 Redis 로 LiveKit 노드들을 묶는다.
|
||||
|
||||
1. **설정에 redis 블록 추가** — 비밀번호가 들어가므로 커밋 파일이 아닌 사본에 둔다.
|
||||
```bash
|
||||
cp livekit/livekit.prod.yaml livekit/livekit.prod.local.yaml # gitignore 대상
|
||||
```
|
||||
`livekit.prod.local.yaml` 에 추가:
|
||||
```yaml
|
||||
redis:
|
||||
address: redis:6379
|
||||
db: 1 # 앱 캐시(db 0)와 분리
|
||||
password: <REDIS_PASSWORD> # 루트 .env 의 REDIS_PASSWORD
|
||||
```
|
||||
compose 의 livekit 볼륨을 이 사본으로 교체(override 파일 권장).
|
||||
|
||||
2. **노드 확장**
|
||||
- 단일 호스트에서 replica 만 늘리는 것은 **진짜 분산이 아니다**(같은 머신·NIC).
|
||||
- 실제 분산 = **여러 호스트** 각각 LiveKit 실행 + **공유 Redis** + 시그널링은 프록시가 여러 노드로 LB. RTC 는 각 노드의 공인 IP 로 직접.
|
||||
- LiveKit 이 방을 노드에 분배(room→node)하고, 한 방이 여러 노드에 걸치면 **캐스케이딩**으로 노드 간 미디어를 릴레이한다.
|
||||
- Simulcast/SVC 로 수신측 상황에 맞는 화질 레이어만 전달(기본 활성).
|
||||
|
||||
3. 부하 분산 검증: `./livekit/load-test.sh full` 로 다수 참여자를 투입하고, 노드별 참여자/대역폭 분포를 모니터링.
|
||||
|
||||
---
|
||||
|
||||
## 7. 운영 팁
|
||||
|
||||
- **UDP 수신 버퍼 상향**(권장): `sysctl -w net.core.rmem_max=16777216 net.core.wmem_max=16777216` (영구화는 `/etc/sysctl.conf`). 미설정 시 LiveKit 이 경고 로그를 남긴다.
|
||||
- **모니터링**: LiveKit Prometheus 메트릭(`/metrics`) + Grafana — 노드별 참여자/트랙/CPU/대역폭.
|
||||
- **리소스**: compose 의 `deploy.resources.limits` 를 트래픽에 맞게 조정(기본 2 CPU / 2GB).
|
||||
- `.gitignore` 에 `livekit/*.local.yaml` 추가(비밀번호 포함 사본 커밋 방지).
|
||||
@@ -0,0 +1,34 @@
|
||||
# LiveKit 셀프호스팅 운영 설정 (단계 6)
|
||||
# - 단일 노드 기준. 멀티 노드 클러스터링은 아래 redis 블록을 활성화한다(livekit/DEPLOY.md 참고).
|
||||
# - API 키/시크릿은 환경변수 LIVEKIT_KEYS 로 주입한다(이 파일에 비밀값을 두지 않는다).
|
||||
# - 시그널링(7880)은 리버스 프록시에서 TLS 종단 후 라우팅한다(wss://livekit.<도메인>).
|
||||
# - 미디어(RTC)는 프록시를 거치지 않고 서버 공인 IP 로 직접 통신한다.
|
||||
|
||||
# 시그널링 / HTTP API (리버스 프록시 뒤)
|
||||
port: 7880
|
||||
|
||||
rtc:
|
||||
# ICE/TCP 폴백 포트 (UDP 차단 환경 대비)
|
||||
tcp_port: 7881
|
||||
# ICE/UDP 단일 mux 포트 (방화벽 단순화 — 이 포트만 열면 됨)
|
||||
udp_port: 7882
|
||||
# 운영: 서버의 공인 IP 를 ICE 후보로 자동 광고
|
||||
use_external_ip: true
|
||||
|
||||
logging:
|
||||
level: info
|
||||
|
||||
# 내장 TURN 은 기본 비활성. 엄격한 방화벽 뒤 클라이언트를 지원하려면
|
||||
# DEPLOY.md 의 TURN/TLS 설정을 참고해 활성화한다.
|
||||
turn:
|
||||
enabled: false
|
||||
|
||||
# ── 멀티 노드 클러스터링(수평 확장) ──────────────────────────────
|
||||
# 2대 이상의 LiveKit 노드를 Redis 로 묶어 트래픽을 분산한다.
|
||||
# Redis 비밀번호가 들어가므로(비밀값) 이 블록은 커밋 파일에 두지 않고,
|
||||
# 배포 시 이 파일을 복사한 사본(gitignore)에 채워 마운트한다. (livekit/DEPLOY.md 참고)
|
||||
#
|
||||
# redis:
|
||||
# address: redis:6379
|
||||
# db: 1 # 앱 캐시(db 0)와 분리
|
||||
# password: <REDIS_PASSWORD>
|
||||
Reference in New Issue
Block a user