# 11. 배포와 운영 ## 1. 구성 ``` deploy/ ├─ Dockerfile # 멀티 스테이지: bun install → web 빌드 → server 번들 → oven/bun:1-slim 실행 이미지 ├─ docker-compose.yml # app + caddy, 볼륨: app-data(SQLite), caddy-data(인증서) ├─ Caddyfile # HTTPS 자동, /ws·/api → app:3000, 나머지 정적 파일 └─ .env.example # 필요한 환경 변수 목록(값 없음) ``` ### 환경 변수 | 이름 | 설명 | |---|---| | `PUBLIC_ORIGIN` | `https://<도메인>` (Origin 검사, OAuth 리다이렉트, 공유 링크) | | `DISCORD_CLIENT_ID` / `DISCORD_CLIENT_SECRET` | 디스코드 앱 | | `SESSION_SECRET` | 짧은 수명 서명 쿠키(OAuth state)용 32바이트 | | `DB_PATH` | 기본 `/data/app.db` | | `SUPERADMIN_DISCORD_IDS` | 슈퍼어드민 디스코드 ID(쉼표 구분). 기본 `293719842274541579,1352267557213573160` | | `TRUST_PROXY` | `1`이면 Caddy가 붙인 X-Forwarded-For로 IP 판단 | | `PORT` | 기본 3000 | | `LOG_LEVEL` | info | ### Caddy - `encode zstd gzip` - `/assets/*`: `Cache-Control: public, max-age=31536000, immutable` (파일명에 해시) - `index.html`: `no-cache` (배포 즉시 반영) - `/sw.js`·`/manifest.webmanifest`·`/offline.html`: `no-cache`(서비스 워커 갱신). 워커는 페이지와 `/api`·`/ws`를 저장하지 않는다(07 문서 11절). - 보안 헤더: CSP(04 문서 7절), `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, HSTS. - WebSocket 프록시는 Caddy 기본 지원. 연결 유지 시간 제한 없음. ## 2. 배포 절차 1. `main`에 커밋·푸시(Gitea `tkrmagid/joke-app`). 2. 서버에서 `git pull && docker compose -f deploy/docker-compose.yml up -d --build`. 3. 옛 컨테이너가 SIGTERM을 받으면 `bye: restart` 전송 후 종료(`stop_grace_period: 15s`), 새 컨테이너가 방 복구 후 시작. 4. 확인: `/healthz`(DB 열림, 방 복구 완료) 200, 로그에 오류 없음, 테스트 방 하나 만들어 오목 한 수. 5. 문제가 있으면 이전 이미지 태그로 되돌림(`docker compose` 이미지에 커밋 해시 태그). ## 3. 개발·테스트 환경 - 로컬: `bun install && bun run dev` → 웹 Vite(5173) + 서버(3000), Vite가 `/api`·`/ws`를 서버로 프록시. - 디스코드 로그인 없이도 게스트로 모든 기능 테스트 가능(디스코드 값이 없으면 버튼 숨김). - 공용 테스트 서버(사용자가 직접 접속해 보는 곳)는 배포 위치가 정해지면 같은 compose로 띄운다. ## 4. 백업과 복구 - 매일 04:00 `VACUUM INTO '/data/backup/app-YYYYMMDD.db'`, 14개 보관. - 복구: 컨테이너 중지 → 백업 파일을 `app.db`로 복사 → 시작. 진행 중이던 방은 백업 시점 상태로 돌아간다. ## 5. 감시 - `/healthz` 1분마다 확인(외부 업타임 감시 또는 호스트 cron), 실패 3번이면 알림(디스코드 웹훅, 선택). - 지표 `/internal/metrics`는 Caddy에서 외부 차단. - 로그: Docker json-file, 크기 제한 50MB × 5. ## 6. 정해야 할 것 (사용자 결정 필요) | 항목 | 선택지 | 기본 제안 | |---|---|---| | 서버 위치 | 이 호스트 / .5 Docker 호스트 / 외부 VPS | **.5로 결정(2026-10-06)**, 7절 | | 도메인 | `game.tkrmagid.kr` (확정) | 모든 기능 완성 후 DNS 연결 | | 디스코드 앱 | 개발자 포털에서 생성 → client ID/secret, Redirect URI 등록 | **생성됨(2026-10-06)**, 값은 .5의 `deploy/.env` | ## 7. 실제 배포: .5 Docker 호스트 + NPMplus (2026-10-06) .5(192.168.10.5)는 80/443을 쓰지 않고, 집 공인 IP의 80/443은 192.168.10.3의 NPMplus가 받는다(다른 tkrmagid.kr 사이트와 같은 방식). 그래서 Caddy 없이 앱만 띄우고 HTTPS·보안 헤더는 NPMplus가 맡는다. ``` 인터넷 → game.tkrmagid.kr(211.213.11.141) → NPMplus 192.168.10.3:443 → 192.168.10.5:2970 → app:3000 ``` - 코드: `root@192.168.10.5:/root/other/joke-app` (Gitea `tkrmagid/joke-app` clone) - 설정: 같은 곳 `deploy/.env` (권한 600, `APP_PORT=2970`, `SESSION_SECRET`은 서버에서 생성) - 실행: `cd /root/other/joke-app/deploy && docker compose -f docker-compose.proxy.yml up -d --build` (프로젝트 이름 `joke-app`, 볼륨 `joke-app_app-data`) - NPMplus 프록시 호스트 `game.tkrmagid.kr` → `http://192.168.10.5:2970`, WebSocket 허용. Advanced 칸에는 `deploy/npmplus-advanced.conf`(CSP 등 Caddyfile과 같은 헤더)를 넣는다. - 인증서: DNS에 `game` 레코드(`CNAME tkrmagid.kr` 또는 `A 211.213.11.141`)가 생긴 뒤 NPMplus에서 Let's Encrypt 인증서를 받고 Force SSL·HSTS·HTTP/2를 켠다. - 업데이트: `git pull && docker compose -f docker-compose.proxy.yml up -d --build` - 확인: `curl http://192.168.10.5:2970/healthz`, `curl -I https://game.tkrmagid.kr/`