8d00d71072
카카오톡처럼 PC에 OS 토스트가 뜨도록 Web Notifications(Level1) +
Web Push/서비스워커(Level2)를 기존 인앱·SSE 알림 위에 얹는다.
중복 방지: 페이지가 열려 있으면 SSE 로컬 알림(포커스면 인앱만),
닫혀 있으면 서비스워커 푸시가 담당한다.
- 백엔드: push_subscriptions 엔티티 + WebPushService(VAPID 설정·
type→{title,body,url} 포매터·만료 구독 정리), notify()가 SSE와
함께 푸시 발송, push/subscribe·unsubscribe 엔드포인트, 마이그레이션,
web-push 의존성
- 프론트: public/sw.js(push/클릭), usePush 컴포저블(권한·구독, 결과
안내), notification.store 로컬 토스트, AppShell 'PC 알림 켜기' 버튼
- env/compose: VAPID 키 주입(공개키→프론트, 비밀키→백엔드)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
202 lines
6.5 KiB
YAML
202 lines
6.5 KiB
YAML
services:
|
|
# ----------------------------------------
|
|
# 1. Frontend (Nginx 정적 호스팅)
|
|
# ----------------------------------------
|
|
frontend:
|
|
container_name: ${PROJECT_NAME}-frontend
|
|
build:
|
|
context: ./${FRONTEND_DIR}
|
|
dockerfile: Dockerfile
|
|
args:
|
|
- BUILD_MODE=${BUILD_MODE}
|
|
- VITE_API_BASE_URL=${BACKEND_API_URL}
|
|
- VITE_AI_TIMEOUT_MS=${AI_TIMEOUT_MS}
|
|
# 브라우저가 접속할 LiveKit WebSocket URL
|
|
- VITE_LIVEKIT_URL=${LIVEKIT_WS_URL}
|
|
# 웹 푸시(VAPID) 공개키 — 브라우저 푸시 구독에 사용
|
|
- VITE_VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY}
|
|
expose:
|
|
- "80"
|
|
environment:
|
|
- NODE_ENV=${APP_ENV}
|
|
restart: unless-stopped
|
|
networks:
|
|
- app-network
|
|
- proxy
|
|
depends_on:
|
|
- backend
|
|
healthcheck:
|
|
# localhost 는 일부 이미지에서 IPv6(::1) 로 먼저 풀려 IPv4 전용 바인딩에 연결 실패 → 127.0.0.1 고정
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:80/ || exit 1"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '0.5'
|
|
memory: 512m
|
|
|
|
# ----------------------------------------
|
|
# 2. Backend (HTTP API & WebSocket 통합)
|
|
# ----------------------------------------
|
|
backend:
|
|
container_name: ${PROJECT_NAME}-backend
|
|
build:
|
|
context: ./${BACKEND_DIR}
|
|
dockerfile: Dockerfile
|
|
expose:
|
|
- "${BACKEND_PORT}"
|
|
- "${WEBSOCKET_PORT}"
|
|
env_file:
|
|
- ./${BACKEND_DIR}/.env.${APP_ENV}
|
|
environment:
|
|
- NODE_ENV=${APP_ENV}
|
|
- PORT=${BACKEND_PORT}
|
|
- WEBSOCKET_PORT=${WEBSOCKET_PORT}
|
|
- CORS_ORIGIN=${CORS_ORIGIN}
|
|
- DB_HOST=db
|
|
- DB_PORT=5432
|
|
- DB_USER=${DB_USER}
|
|
- DB_PASSWORD=${DB_PASSWORD}
|
|
- DB_NAME=${DB_NAME}
|
|
- REDIS_HOST=redis
|
|
- REDIS_PORT=6379
|
|
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
|
# AI 요청 타임아웃(ms) — 프론트(VITE_AI_TIMEOUT_MS)와 동일 값을 루트 .env 에서 공급
|
|
- AI_TIMEOUT_MS=${AI_TIMEOUT_MS}
|
|
# 화상회의(LiveKit) — 토큰 발급에 키/시크릿, 서버 API 호출에 내부 URL 사용
|
|
- LIVEKIT_API_KEY=${LIVEKIT_API_KEY}
|
|
- LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}
|
|
- LIVEKIT_URL=${LIVEKIT_URL}
|
|
# 드라이브(NCP Object Storage) — 비시크릿만 루트에서 주입(ACCESS/SECRET 키는 backend env_file)
|
|
- OBJECT_STORAGE_ENDPOINT=${OBJECT_STORAGE_ENDPOINT}
|
|
- OBJECT_STORAGE_REGION=${OBJECT_STORAGE_REGION}
|
|
- OBJECT_STORAGE_BUCKET=${OBJECT_STORAGE_BUCKET}
|
|
# 웹 푸시(VAPID) — 비밀 아닌 값만 루트에서 주입(PRIVATE_KEY 는 backend env_file)
|
|
- VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY}
|
|
- VAPID_SUBJECT=${VAPID_SUBJECT}
|
|
restart: unless-stopped
|
|
networks:
|
|
- app-network
|
|
- proxy
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
# API 는 전역 프리픽스 /api 하위에 마운트됨 — 루트(/)는 404 라 /api/health 를 프로브.
|
|
# localhost→IPv6(::1) 회피를 위해 127.0.0.1 고정.
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:${BACKEND_PORT}/api/health || exit 1"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 30s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '1.0'
|
|
memory: 1g
|
|
|
|
# ----------------------------------------
|
|
# 3. Database (PostgreSQL) — 내부 네트워크 전용
|
|
# ----------------------------------------
|
|
db:
|
|
container_name: ${PROJECT_NAME}-db
|
|
image: ${DB_IMAGE}
|
|
expose:
|
|
- "5432"
|
|
environment:
|
|
POSTGRES_USER: ${DB_USER}
|
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
POSTGRES_DB: ${DB_NAME}
|
|
volumes:
|
|
- ${DB_DATA_DIR}:/var/lib/postgresql/data
|
|
restart: always
|
|
networks:
|
|
- app-network
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '0.5'
|
|
memory: 512m
|
|
labels:
|
|
- "com.centurylinklabs.watchtower.enable=true"
|
|
|
|
# ----------------------------------------
|
|
# 4. Redis — 내부 네트워크 전용
|
|
# ----------------------------------------
|
|
redis:
|
|
container_name: ${PROJECT_NAME}-redis
|
|
image: ${REDIS_IMAGE}
|
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
|
expose:
|
|
- "6379"
|
|
volumes:
|
|
- ${REDIS_DATA_DIR}:/data
|
|
restart: always
|
|
networks:
|
|
- app-network
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "redis-cli -a ${REDIS_PASSWORD} ping | grep PONG"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 5s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '0.25'
|
|
memory: 256m
|
|
labels:
|
|
- "com.centurylinklabs.watchtower.enable=true"
|
|
|
|
# ----------------------------------------
|
|
# 5. LiveKit (화상회의 SFU) — 셀프호스팅 (운영 기본 서비스, 소스 직접 빌드)
|
|
# SFU 코어(pkg/sfu) 커스터마이즈를 위해 공식 이미지 대신 livekit-server/ 소스를
|
|
# 직접 빌드한다(git subtree, v1.8.4 기준). 소스·수정은 이 레포에서 관리한다.
|
|
# 운영(base)에서는 db·redis 처럼 항상 함께 기동된다.
|
|
# 개발(dev)에서는 docker-compose.dev.yml 이 selfhost-livekit 프로파일로 가두어
|
|
# 생성되지 않는다(개발은 LiveKit Cloud 사용).
|
|
# 배포·클러스터링 가이드: livekit/DEPLOY.md
|
|
# ----------------------------------------
|
|
livekit:
|
|
build:
|
|
context: ./livekit-server
|
|
dockerfile: Dockerfile
|
|
container_name: ${PROJECT_NAME}-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
|
|
proxy:
|
|
external: true |