From 2f728e7b13234dcda5d26947ccf34609c2e6eb88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=ED=98=84=EB=AF=BC?= Date: Sun, 7 Jun 2026 15:44:35 +0900 Subject: [PATCH] chore(deploy): N100 prod-only, dev push builds image only --- .github/workflows/deploy.yml | 40 ++++++--- deploy/README.md | 168 +++++++++++++++++++++-------------- deploy/docker-compose.yml | 14 +-- docs/wiki/02-Tech-Stack.md | 8 +- 4 files changed, 139 insertions(+), 91 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 344edc0..ea489a2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,18 +1,26 @@ name: Deploy +# 정책 (Week 3~): +# - N100 에는 prod 단일 환경만 운영. +# - dev push → staging-* GHCR image build/push 까지만. "컨테이너 빌드가 깨지지 않는가" 안전망용. +# N100 에는 절대 SSH 안 함. +# - main push → prod-* GHCR image build/push + N100 SSH 배포. +# - workflow_dispatch 로 임의 ref 에 대해 prod target 수동 배포 가능. +# +# Staging 컨테이너를 N100 에서 안 돌리기로 결정한 배경은 회고-Week2 + 회고-Week3 참조. on: push: branches: [dev, main] workflow_dispatch: inputs: target: - description: "배포 대상 (staging | prod)" + description: "수동 배포 대상 (prod 만 지원)" required: true - default: "staging" + default: "prod" type: choice - options: [staging, prod] + options: [prod] -# 동시 배포 금지. 같은 대상에 새 push 가 오면 in-progress 취소. +# 동시 배포 금지. 같은 ref 에 새 push 가 오면 in-progress 취소. concurrency: group: deploy-${{ github.ref_name }} cancel-in-progress: false @@ -27,7 +35,7 @@ env: jobs: # --------------------------------------------------------------------------- - # 1) build image → ghcr.io 푸시 + # 1) build image → ghcr.io 푸시 (dev=staging tag, main=prod tag, dispatch=prod tag) # --------------------------------------------------------------------------- build: name: Build & push image @@ -36,6 +44,7 @@ jobs: outputs: image_tag: ${{ steps.meta.outputs.image_tag }} target: ${{ steps.meta.outputs.target }} + deploy_to_n100: ${{ steps.meta.outputs.deploy_to_n100 }} steps: - name: Checkout @@ -47,15 +56,20 @@ jobs: # 우선순위: workflow_dispatch.inputs.target → branch if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then target="${{ inputs.target }}" + deploy_to_n100="true" elif [ "${{ github.ref_name }}" = "main" ]; then target="prod" + deploy_to_n100="true" else + # dev push (또는 그 외 브랜치) — image 만 빌드하고 N100 SSH 는 안 함. target="staging" + deploy_to_n100="false" fi sha_short="$(echo "${{ github.sha }}" | cut -c1-7)" image_tag="${target}-${sha_short}" echo "target=$target" >> "$GITHUB_OUTPUT" echo "image_tag=$image_tag" >> "$GITHUB_OUTPUT" + echo "deploy_to_n100=$deploy_to_n100" >> "$GITHUB_OUTPUT" echo "image_ref=${REGISTRY}/${IMAGE}:${image_tag}" >> "$GITHUB_OUTPUT" echo "image_ref_target=${REGISTRY}/${IMAGE}:${target}" >> "$GITHUB_OUTPUT" @@ -82,12 +96,14 @@ jobs: cache-to: type=gha,mode=max # --------------------------------------------------------------------------- - # 2) N100 SSH secrets 준비 여부 확인. 미등록이면 이미지 build/push 까지만 하고 deploy 는 skip. + # 2) N100 SSH secrets 준비 여부 확인. 미등록이면 build 까지만 하고 deploy 는 skip. # --------------------------------------------------------------------------- deploy-preflight: name: Check deploy secrets runs-on: ubuntu-latest timeout-minutes: 3 + needs: build + if: needs.build.outputs.deploy_to_n100 == 'true' outputs: deploy_ready: ${{ steps.secrets.outputs.deploy_ready }} steps: @@ -105,30 +121,28 @@ jobs: N100_USER: ${{ secrets.N100_USER }} N100_SSH_KEY: ${{ secrets.N100_SSH_KEY }} - # --------------------------------------------------------------------------- - # 3) SSH 로 N100 접속 → docker compose pull && up -d + # 3) main / dispatch 한정. SSH 로 N100 접속 → docker compose pull && up -d # --------------------------------------------------------------------------- deploy: name: Deploy to ${{ needs.build.outputs.target }} needs: [build, deploy-preflight] - if: needs.deploy-preflight.outputs.deploy_ready == 'true' + if: needs.build.outputs.deploy_to_n100 == 'true' && needs.deploy-preflight.outputs.deploy_ready == 'true' runs-on: ubuntu-latest timeout-minutes: 10 - # prod 는 환경(environment) 보호로 수동 승인 가능 + # prod environment 에 Required reviewers 를 걸면 수동 승인 게이트가 된다. environment: name: ${{ needs.build.outputs.target }} steps: - - name: Pull & restart on N100 uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.N100_HOST }} username: ${{ secrets.N100_USER }} key: ${{ secrets.N100_SSH_KEY }} - # 호스트의 deploy/ 디렉터리에서 pull → up. - # IMAGE_TAG selects the pushed GHCR image; MURUN_ENV_FILE selects .env.staging/.env.prod. + # 호스트의 deploy/ 디렉터리에서 git sync → image pull → compose up. + # IMAGE_TAG 는 GHCR 이미지 태그, MURUN_ENV_FILE 은 .env.prod 고정 (N100=prod only). script: | set -e cd ${MURUN_DEPLOY_DIR:-~/murun-peterabcd/deploy} diff --git a/deploy/README.md b/deploy/README.md index 40a415f..5a4ca8a 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -2,144 +2,178 @@ 자체 N100 호스트에서 Docker Compose 로 띄우는 절차. 사람(호스트)만 본다. +## 정책 요약 (Week 3 ~) + +- **N100 = prod 단일.** staging 은 N100 에서 안 돌린다. dev 작업은 로컬에서 직접 `pnpm dev`. +- **자동 배포는 `main` push 일 때만.** `dev` push 는 GHCR 에 `staging-` 이미지를 빌드/푸시까지만 한다 ("컨테이너 빌드가 깨지지 않는가" 안전망용). N100 에는 떨어지지 않는다. +- **DuckDNS + Let's Encrypt HTTPS.** `DOMAIN=murun.duckdns.org` 로 Caddy 자동 발급. + ## 0. CI/CD (자동 배포) 사용 시 (권장) -GitHub Actions 가 dev push → staging, main push → prod 로 자동 배포한다. -첫 셋업 한 번만 호스트에서 manual init 후, 그 다음부턴 코드만 push 하면 됨. +GitHub Actions 가 `main` push → prod 배포한다. 첫 셋업 한 번만 호스트에서 manual init 후, 그 다음부턴 main 에 PR 머지하면 자동 반영. ### 호스트 1회 init ```bash -# 1) repo clone (한 번만) +# 1) repo clone (한 번만; deploy workflow 의 git sync 경로와 일치해야 함) +cd ~ git clone https://github.com/boostcampwm-snu-2026-1/murun-peterabcd.git -cd murun-peterabcd/deploy +cd murun-peterabcd -# 2) staging / prod 각각 .env 준비 -cp .env.example .env.staging -cp .env.example .env.prod -# (DOMAIN, AUTH_*, GOOGLE_*, DATABASE_URL, NEXT_PUBLIC_APP_URL 채우기) +# main 을 default 로 둠 (workflow 가 main ref 기준으로 fetch/checkout 한다) +git checkout main -# 3) ghcr.io pull 인증 (한 번만, repo 가 public 이면 skip 가능) -echo "" | docker login ghcr.io -u --password-stdin +cd deploy -# 4) 첫 가동 (이미지 빌드는 GH Actions 가 대신, 여기선 pull 만) -export MURUN_ENV_FILE=.env.staging -export IMAGE_TAG=staging +# 2) prod .env 준비 +cp .env.example .env.prod +# 안에서 채울 키: +# DOMAIN=murun.duckdns.org +# NEXT_PUBLIC_APP_URL=https://murun.duckdns.org +# AUTH_URL=https://murun.duckdns.org +# AUTH_SECRET=$(openssl rand -base64 32) +# AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET (Google Cloud Console) +# DATABASE_URL=file:/app/data/murun.db +# UPLOADS_DIR=/app/uploads +# AUTH_TRUST_HOST=true +# AUTH_GOOGLE_HD=snu.ac.kr +# IMAGE_TAG=prod (GH Actions 가 main push 시 prod- 로 덮어씀) + +# 3) (필요 시) ghcr.io pull 인증 +echo "" | docker login ghcr.io -u --password-stdin -docker compose -p murun-staging --env-file .env.staging pull -docker compose -p murun-staging --env-file .env.staging up -d +# 4) 첫 가동 +export MURUN_ENV_FILE=.env.prod +docker compose -p murun-prod --env-file .env.prod pull +docker compose -p murun-prod --env-file .env.prod up -d ``` ### GitHub Actions secrets 등록 (Settings → Secrets and variables → Actions) | 키 | 값 | |----|------| -| `N100_HOST` | N100 의 공인 IP 또는 DDNS 도메인 | +| `N100_HOST` | N100 의 공인 IP 또는 DDNS 도메인 (`murun.duckdns.org`) | | `N100_USER` | SSH 로그인 유저 | | `N100_SSH_KEY` | private SSH key 내용 전체 (BEGIN/END 포함). 공개키는 N100 의 `~/.ssh/authorized_keys` 에 등록 | SSH key 생성 (호스트에서): + ```bash ssh-keygen -t ed25519 -C "gh-actions-murun-deploy" -f ~/.ssh/n100_gh_deploy cat ~/.ssh/n100_gh_deploy.pub >> ~/.ssh/authorized_keys +chmod 700 ~/.ssh +chmod 600 ~/.ssh/authorized_keys # private key 를 GitHub secret 에 등록: gh secret set N100_SSH_KEY < ~/.ssh/n100_gh_deploy gh secret set N100_HOST --body "" gh secret set N100_USER --body "" ``` -Secrets 가 아직 없으면 GitHub Actions 는 GHCR 이미지 build/push 까지만 성공 처리하고, SSH deploy job 은 skip 된다. N100 준비 후 위 3개 secret 을 넣으면 다음 dev push 부터 staging deploy 까지 진행된다. +Secrets 가 등록되어 있지 않으면 main push 도 image build/push 까지만 성공 처리하고, SSH deploy job 은 skip 된다. ### (옵션) prod environment 보호 -main 으로의 자동 배포 전에 수동 승인이 필요하면 Settings → Environments → "prod" 생성 후 "Required reviewers" 본인 추가. +main → prod 자동 배포 전에 수동 승인이 필요하면 Settings → Environments → `prod` 생성 후 "Required reviewers" 본인 추가. + +### 수동 트리거 -## 1. 수동 배포 (CI/CD 우회) — 첫 배포 또는 디버깅 +특정 ref 를 prod 로 즉시 배포: ```bash -# 호스트에 git clone -git clone https://github.com/boostcampwm-snu-2026-1/murun-peterabcd.git -cd murun-peterabcd/deploy +gh workflow run deploy.yml --ref main -f target=prod +``` -# compose용 env -cp .env.example .env -# (도메인 붙기 전엔 DOMAIN=:80 그대로 두기) +## 1. 수동 배포 (CI/CD 우회) — 디버깅 / 첫 배포 -# compose 변수와 app 환경변수를 같은 파일에서 읽음 -# 기본 수동 실행은 .env, staging/prod 분리는 MURUN_ENV_FILE 로 지정 +```bash +cd ~/murun-peterabcd/deploy +# 위의 .env.prod 가 이미 준비됐다고 가정 +export MURUN_ENV_FILE=.env.prod -# 빌드 + 기동 -docker compose up -d --build +# 이미지가 없거나 강제로 rebuild 하고 싶을 때 (오래 걸림) +docker compose -p murun-prod --env-file .env.prod up -d --build -# 헬스체크 -curl -fsS http://localhost/ && echo OK +# 이미지를 GHCR 에서 pull 만 (보통은 이쪽) +docker compose -p murun-prod --env-file .env.prod pull +docker compose -p murun-prod --env-file .env.prod up -d ``` -## 2. 업데이트 배포 +헬스체크: ```bash -cd murun-peterabcd -git pull -cd deploy -MURUN_ENV_FILE=.env docker compose --env-file .env up -d --build -docker image prune -f +docker compose -p murun-prod --env-file .env.prod ps +curl -fsS http://localhost/api/health && echo OK ``` -## 3. staging + prod 동시 운영 - -같은 디렉터리에 `.env.staging`, `.env.prod` 를 별도로 두고 project name 으로 분리. +## 2. 업데이트 배포 (수동) ```bash -# staging (포트 8080; .env.staging 안에 CADDY_HTTP_PORT=8080 권장) -MURUN_ENV_FILE=.env.staging docker compose -p murun-staging --env-file .env.staging up -d --build - -# prod (포트 80) -MURUN_ENV_FILE=.env.prod docker compose -p murun-prod --env-file .env.prod up -d --build +cd ~/murun-peterabcd +git pull +cd deploy +MURUN_ENV_FILE=.env.prod docker compose -p murun-prod --env-file .env.prod pull +MURUN_ENV_FILE=.env.prod docker compose -p murun-prod --env-file .env.prod up -d +docker image prune -f ``` -볼륨은 project name prefix 가 자동으로 붙어 분리됨 (`murun-staging_data` vs `murun-prod_data`). - -## 4. 데이터 위치 (백업 대상) +## 3. 데이터 위치 (백업 대상) | docker volume | 호스트 경로 (Linux 기본) | 용도 | |---------------|--------------------------|------| -| `murun_data` | `/var/lib/docker/volumes/murun_data/_data` | SQLite 파일 | -| `murun_uploads` | `/var/lib/docker/volumes/murun_uploads/_data` | 단체사진 원본 + next/image 캐시 | -| `murun_caddy_data` | `/var/lib/docker/volumes/murun_caddy_data/_data` | Let's Encrypt 인증서 | +| `murun-prod_data` | `/var/lib/docker/volumes/murun-prod_data/_data` | SQLite 파일 | +| `murun-prod_uploads` | `/var/lib/docker/volumes/murun-prod_uploads/_data` | 단체사진 원본 + next/image 캐시 | +| `murun-prod_caddy_data` | `/var/lib/docker/volumes/murun-prod_caddy_data/_data` | Let's Encrypt 인증서 | -백업 cron 은 후속 PR 에서 (`scripts/backup.sh`). +백업 cron 은 후속 작업 (`scripts/backup.sh`). -## 5. 롤백 +## 4. 롤백 ```bash # 직전 이미지 태그 확인 docker images ghcr.io/boostcampwm-snu-2026-1/murun-peterabcd --format '{{.Tag}}\t{{.CreatedAt}}' -# 특정 태그로 되돌리기 -IMAGE_TAG= MURUN_ENV_FILE=.env.staging docker compose --env-file .env.staging up -d +# 특정 태그로 되돌리기 (예: 직전 prod 빌드) +IMAGE_TAG=prod- MURUN_ENV_FILE=.env.prod docker compose -p murun-prod --env-file .env.prod up -d ``` -## 6. 트러블슈팅 +## 5. 트러블슈팅 ```bash # 컨테이너 상태 -docker compose ps +docker compose -p murun-prod --env-file .env.prod ps # app 로그 -docker compose logs -f app +docker compose -p murun-prod --env-file .env.prod logs -f app -# caddy 로그 -docker compose logs -f caddy +# caddy 로그 (인증서 발급 / TLS 문제) +docker compose -p murun-prod --env-file .env.prod logs -f caddy # 강제 재기동 -docker compose restart app +docker compose -p murun-prod --env-file .env.prod restart app ``` -## 7. 도메인 붙인 후 (예: duckdns) +## 6. DuckDNS HTTPS 활성 절차 + +1. DuckDNS 에서 `murun.duckdns.org` 발급 + N100 공인 IP 등록 (DDNS 클라이언트로 자동 갱신 권장) +2. 공유기에서 외부 80/443 → N100 내부 IP 의 80/443 포트포워딩 +3. `deploy/.env.prod` 의 값 확인: + ```env + DOMAIN=murun.duckdns.org + NEXT_PUBLIC_APP_URL=https://murun.duckdns.org + AUTH_URL=https://murun.duckdns.org + ``` +4. `docker compose -p murun-prod --env-file .env.prod up -d` (Caddy 재기동 → Let's Encrypt 자동 발급) +5. `docker compose ... logs caddy` 에서 "certificate obtained for murun.duckdns.org" 확인 + +## 7. Staging 환경 (옵션, N100 외 어딘가에서 돌릴 때) + +기본 정책상 N100 에서는 staging 을 돌리지 않는다. 필요하면 다른 호스트/포트로: + +```bash +MURUN_ENV_FILE=.env.staging \ + CADDY_HTTP_PORT=8080 CADDY_HTTPS_PORT=8443 \ + docker compose -p murun-staging --env-file .env.staging up -d --build +``` -1. duckdns 에서 `murun.duckdns.org` 발급 + 현재 공인 IP 등록 -2. 공유기 80/443 포트포워딩 → N100 내부 IP -3. `deploy/.env` 에 `DOMAIN=murun.duckdns.org` 로 변경 -4. `docker compose up -d` (Caddy 재기동되며 Let's Encrypt 자동 발급) -5. 첫 인증서 발급 후 `docker compose logs caddy` 에서 "certificate obtained" 확인 +이 경우에도 GH Actions 자동 배포 대상은 아님. 항상 수동. diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 5a8d9c9..c3a198a 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,14 +1,14 @@ # 뮤런 — 자체 N100 호스트 배포 compose. # -# 사용 (호스트): +# 정책: N100 = prod 단일. staging 은 N100 에서 안 돌린다 (deploy/README.md 참조). +# +# 사용 (호스트, 수동): # cd deploy -# cp .env.example .env # 같은 디렉터리에 prod env 보관 (gitignore됨) -# docker compose up -d --build +# cp .env.example .env.prod +# MURUN_ENV_FILE=.env.prod docker compose -p murun-prod --env-file .env.prod up -d # -# staging 과 prod 동시 운영 시: -# docker compose -p murun-staging --env-file .env.staging up -d --build -# docker compose -p murun-prod --env-file .env.prod up -d --build -# (volume 이름이 project name 앞에 붙어 자동 분리) +# 자동 배포는 GitHub Actions `Deploy` workflow 가 처리한다 (`.github/workflows/deploy.yml`). +# volume 이름은 project name 앞에 붙어 분리됨 (e.g. `murun-prod_data`). name: murun diff --git a/docs/wiki/02-Tech-Stack.md b/docs/wiki/02-Tech-Stack.md index c49b1da..9b2cf73 100644 --- a/docs/wiki/02-Tech-Stack.md +++ b/docs/wiki/02-Tech-Stack.md @@ -149,12 +149,12 @@ murun-peterabcd/ ▲ │ GitHub Actions가 SSH로: │ docker compose pull && up -d - │ staging: dev push → 자동 - │ prod: main push → 수동 승인 후 + │ main push → prod 자동 배포 + │ dev push → GHCR image 빌드/푸시만 (N100 미배포) ``` -- staging/prod 분리: 같은 이미지를 `-p murun-staging` / `-p murun-prod` 다른 project name으로 동시에. 볼륨·포트·서브도메인 전부 분리. -- 빌드는 GitHub Actions에서 → `ghcr.io/boostcampwm-snu-2026-1/murun-peterabcd:` 로 push. N100은 pull만. +- **N100 = prod 단일.** staging 컨테이너는 N100 에서 안 돌린다. dev 개발은 로컬 `pnpm dev` 로 충분하다 본다. +- 빌드는 GitHub Actions에서 → `ghcr.io/boostcampwm-snu-2026-1/murun-peterabcd:` 로 push. N100은 main push 일 때만 pull + restart. ## 6. 채택하지 않은 것