|
| 1 | +# N100 Prod Migration (staging → prod 단일) |
| 2 | + |
| 3 | +Week 3 부터 N100 에서는 **prod 만** 운영한다. dev push 는 GHCR 이미지 빌드까지만, N100 SSH 는 main push 일 때만. 이 문서는 호스트(나)가 N100 콘솔에서 한 번 하는 절차를 정리한다. |
| 4 | + |
| 5 | +기준 시점: PR #36 이후. v0.2.0 태그가 main 에 찍힌 상태. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 0. 사전 준비 |
| 10 | + |
| 11 | +- [ ] DuckDNS: `murun.duckdns.org` 등록 + 현재 N100 공인 IP 매핑 (확인: `dig +short murun.duckdns.org`) |
| 12 | +- [ ] 공유기: 외부 80/443 → N100 내부 IP 80/443 포트포워딩 |
| 13 | +- [ ] Google Cloud Console: 기존 `Murun Staging` OAuth client 이름을 `Murun Prod` 로 바꾸고 (보안키 그대로 재사용), Authorized redirect URI 에 아래 추가: |
| 14 | + - `https://murun.duckdns.org/api/auth/callback/google` |
| 15 | +- [ ] GitHub repo Secrets 등록 상태 확인 (`gh secret list` on N100 또는 로컬): |
| 16 | + - `N100_HOST` = 공인 IP 또는 `murun.duckdns.org` |
| 17 | + - `N100_USER` |
| 18 | + - `N100_SSH_KEY` |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## 1. 기존 staging 컨테이너 폐기 |
| 23 | + |
| 24 | +테스트 시점에 떴던 `murun-staging` 은 그냥 버린다. 데이터(테스트용 가입 1건 등)는 보존 안 함. |
| 25 | + |
| 26 | +```bash |
| 27 | +cd ~/murun-peterabcd/deploy |
| 28 | + |
| 29 | +# 1) staging 컨테이너 down + 볼륨 제거 |
| 30 | +docker compose -p murun-staging --env-file .env.staging down -v |
| 31 | + |
| 32 | +# 2) 혹시 남은 dangling volume 정리 |
| 33 | +docker volume ls | grep murun-staging # 비어있는지 확인 |
| 34 | +docker volume prune -f # (선택) 다른 dangling volume 정리 |
| 35 | + |
| 36 | +# 3) staging 용 env 파일 제거 (host-only, gitignore 됨) |
| 37 | +rm -f .env.staging |
| 38 | +``` |
| 39 | + |
| 40 | +확인: |
| 41 | + |
| 42 | +```bash |
| 43 | +docker compose ls # murun-staging 없어야 함 |
| 44 | +docker volume ls | grep murun # 비어있어야 함 |
| 45 | +``` |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +## 2. main 브랜치로 repo 동기화 |
| 50 | + |
| 51 | +deploy workflow 는 N100 의 `~/murun-peterabcd` 가 main branch 를 가리킨다는 가정. 첫 셋업 때 `dev` 였으면 main 으로 바꿔둔다. |
| 52 | + |
| 53 | +```bash |
| 54 | +cd ~/murun-peterabcd |
| 55 | +git fetch origin |
| 56 | +git checkout main |
| 57 | +git pull origin main |
| 58 | +``` |
| 59 | + |
| 60 | +이후 자동 배포가 SSH 로 들어와서 `git fetch + checkout -B main FETCH_HEAD` 로 갱신한다. |
| 61 | + |
| 62 | +--- |
| 63 | + |
| 64 | +## 3. `.env.prod` 작성 |
| 65 | + |
| 66 | +```bash |
| 67 | +cd ~/murun-peterabcd/deploy |
| 68 | +cp .env.example .env.prod |
| 69 | +``` |
| 70 | + |
| 71 | +`.env.prod` 안의 값: |
| 72 | + |
| 73 | +```env |
| 74 | +# --- compose / image --- |
| 75 | +IMAGE_TAG=prod # GH Actions main push 가 prod-<sha> 로 덮어씀 |
| 76 | +
|
| 77 | +# --- Caddy --- |
| 78 | +DOMAIN=murun.duckdns.org |
| 79 | +CADDY_HTTP_PORT=80 |
| 80 | +CADDY_HTTPS_PORT=443 |
| 81 | +
|
| 82 | +# --- App --- |
| 83 | +NODE_ENV=production |
| 84 | +DATABASE_URL=file:/app/data/murun.db |
| 85 | +NEXT_PUBLIC_APP_URL=https://murun.duckdns.org |
| 86 | +UPLOADS_DIR=/app/uploads |
| 87 | +
|
| 88 | +# --- Auth.js v5 --- |
| 89 | +AUTH_URL=https://murun.duckdns.org |
| 90 | +AUTH_SECRET=<openssl rand -base64 32 결과> |
| 91 | +AUTH_TRUST_HOST=true |
| 92 | +
|
| 93 | +# --- Google OAuth --- |
| 94 | +AUTH_GOOGLE_ID=<Murun Prod (=구 Murun Staging) client id> |
| 95 | +AUTH_GOOGLE_SECRET=<동일 secret> |
| 96 | +AUTH_GOOGLE_HD=snu.ac.kr |
| 97 | +``` |
| 98 | + |
| 99 | +AUTH_SECRET 생성: |
| 100 | + |
| 101 | +```bash |
| 102 | +openssl rand -base64 32 |
| 103 | +``` |
| 104 | + |
| 105 | +권한 확인 (다른 유저 못 읽게): |
| 106 | + |
| 107 | +```bash |
| 108 | +chmod 600 .env.prod |
| 109 | +``` |
| 110 | + |
| 111 | +--- |
| 112 | + |
| 113 | +## 4. 첫 prod 기동 (수동 한 번) |
| 114 | + |
| 115 | +GHCR 에서 `prod` 이미지를 받아 띄운다. main push 가 이미 한 번 이미지를 올려둔 상태 (v0.2.0 = `prod-579064b`). |
| 116 | + |
| 117 | +```bash |
| 118 | +cd ~/murun-peterabcd/deploy |
| 119 | + |
| 120 | +export MURUN_ENV_FILE=.env.prod |
| 121 | + |
| 122 | +# (선택) ghcr.io pull 인증. repo 가 public 이면 skip 가능. |
| 123 | +echo "<PAT_with_read:packages>" | docker login ghcr.io -u peterabcd --password-stdin |
| 124 | + |
| 125 | +docker compose -p murun-prod --env-file .env.prod pull |
| 126 | +docker compose -p murun-prod --env-file .env.prod up -d |
| 127 | +docker compose -p murun-prod --env-file .env.prod ps |
| 128 | +``` |
| 129 | + |
| 130 | +기대 상태: |
| 131 | + |
| 132 | +``` |
| 133 | +NAME IMAGE STATUS |
| 134 | +murun-prod-app-1 ghcr.io/.../murun-peterabcd:prod-... healthy |
| 135 | +murun-prod-caddy-1 caddy:2-alpine running |
| 136 | +``` |
| 137 | + |
| 138 | +로그: |
| 139 | + |
| 140 | +```bash |
| 141 | +docker compose -p murun-prod --env-file .env.prod logs -f app |
| 142 | +docker compose -p murun-prod --env-file .env.prod logs -f caddy # cert 발급 확인 |
| 143 | +``` |
| 144 | + |
| 145 | +Caddy 로그에서 "certificate obtained" / "served HTTPS for murun.duckdns.org" 확인. |
| 146 | + |
| 147 | +--- |
| 148 | + |
| 149 | +## 5. 첫 로그인 + ADMIN 승인 |
| 150 | + |
| 151 | +`https://murun.duckdns.org` 에 접속 → Google 로그인 (SNU 계정) → `/pending` 으로 이동. |
| 152 | + |
| 153 | +같은 N100 에서 본인 계정을 직접 admin 으로 승격: |
| 154 | + |
| 155 | +```bash |
| 156 | +cd ~/murun-peterabcd/deploy |
| 157 | +docker compose -p murun-prod --env-file .env.prod exec app node |
| 158 | +``` |
| 159 | + |
| 160 | +Node REPL: |
| 161 | + |
| 162 | +```js |
| 163 | +const { PrismaClient } = require("@prisma/client"); |
| 164 | +const db = new PrismaClient(); |
| 165 | + |
| 166 | +await db.user.updateMany({ |
| 167 | + where: { email: "본인@snu.ac.kr" }, |
| 168 | + data: { |
| 169 | + approved: true, |
| 170 | + role: "ADMIN", |
| 171 | + approvedAt: new Date(), |
| 172 | + }, |
| 173 | +}); |
| 174 | + |
| 175 | +await db.$disconnect(); |
| 176 | +.exit |
| 177 | +``` |
| 178 | + |
| 179 | +브라우저에서 새로고침 → `/admin/members` 접근 가능 여부 확인. |
| 180 | + |
| 181 | +--- |
| 182 | + |
| 183 | +## 6. 자동 배포 확인 |
| 184 | + |
| 185 | +다음 dev → main 머지 시 자동 진행: |
| 186 | + |
| 187 | +``` |
| 188 | +GitHub Actions |
| 189 | + ├─ Build & push image → ghcr.io ... :prod-<sha> |
| 190 | + ├─ Check deploy secrets → pass |
| 191 | + └─ Deploy to prod |
| 192 | + ├─ N100 SSH |
| 193 | + ├─ git checkout -B main FETCH_HEAD |
| 194 | + ├─ docker compose pull |
| 195 | + └─ docker compose up -d --remove-orphans |
| 196 | +``` |
| 197 | + |
| 198 | +수동 트리거가 필요할 때: |
| 199 | + |
| 200 | +```bash |
| 201 | +gh workflow run deploy.yml --ref main -f target=prod |
| 202 | +``` |
| 203 | + |
| 204 | +--- |
| 205 | + |
| 206 | +## 7. 트러블슈팅 컷 |
| 207 | + |
| 208 | +| 증상 | 원인/확인 | 조치 | |
| 209 | +|------|----------|------| |
| 210 | +| Caddy `tls obtain failed` | 80/443 외부 접근 불가 | 공유기 포트포워딩 / 방화벽 (`ufw status`) 재확인 | |
| 211 | +| `couldn't find env file: ./.env.prod` | `.env.prod` 누락 | 위 §3 다시 | |
| 212 | +| Google `redirect_uri_mismatch` | OAuth client 에 prod URI 미등록 | Authorized redirect URI 에 추가 | |
| 213 | +| `Invalid host header` | `AUTH_URL` mismatch | `AUTH_URL=https://murun.duckdns.org` 확인 | |
| 214 | +| 로그인 후 `/pending` 영원 | DB user `approved=false` | 위 §5 admin 승인 | |
| 215 | +| `prisma migrate deploy` 실패 | volume 권한 / 잘못된 DATABASE_URL | `docker volume inspect murun-prod_data` → `/app/data` 마운트 경로 확인 | |
| 216 | + |
| 217 | +--- |
| 218 | + |
| 219 | +## 8. 백업 (간단판) |
| 220 | + |
| 221 | +prod 데이터 = SQLite 파일 + uploads 디렉터리. 둘 다 host volume. |
| 222 | + |
| 223 | +```bash |
| 224 | +sudo tar czf ~/murun-backup-$(date +%Y%m%d).tar.gz \ |
| 225 | + /var/lib/docker/volumes/murun-prod_data/_data \ |
| 226 | + /var/lib/docker/volumes/murun-prod_uploads/_data |
| 227 | +``` |
| 228 | + |
| 229 | +cron 등록은 추후 (`scripts/backup.sh`). |
| 230 | + |
| 231 | +--- |
| 232 | + |
| 233 | +## 9. 롤백 |
| 234 | + |
| 235 | +```bash |
| 236 | +cd ~/murun-peterabcd/deploy |
| 237 | + |
| 238 | +# 직전 태그 확인 |
| 239 | +docker images ghcr.io/boostcampwm-snu-2026-1/murun-peterabcd --format '{{.Tag}}\t{{.CreatedAt}}' |
| 240 | + |
| 241 | +# 이전 prod-<sha> 로 되돌리기 |
| 242 | +IMAGE_TAG=prod-<previous-sha> MURUN_ENV_FILE=.env.prod \ |
| 243 | + docker compose -p murun-prod --env-file .env.prod up -d |
| 244 | +``` |
| 245 | + |
| 246 | +Schema 변경이 있던 release 면 migration rollback 까지 별도 작업 필요. v0.2.0 까지는 추가 migration 없음. |
0 commit comments