Files
joke-app/docs/11-deployment-ops.md
EJClaw 6390e3e64c 게임 4개 원래 이름으로, .5 + NPMplus 배포 구성
- 보석 상인 → 스플렌더, 벽 쌓기 길찾기 → 쿼리도, 숫자 타일 → 루미큐브, 주사위 땅따먹기 → 부루마불
- deploy/docker-compose.proxy.yml: Caddy 없이 앱만(.5:2970), HTTPS·헤더는 NPMplus
- deploy/npmplus-advanced.conf: Caddyfile과 같은 보안 헤더
- 문서: 2026-10-06 결정, 배포 7절 (e2e 구십구 0 카드 방향 버튼 반영 포함)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:16:32 +09:00

76 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` |
| `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/`