Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"

Expand All @@ -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:
Expand All @@ -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}
Expand Down
168 changes: 101 additions & 67 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,144 +2,178 @@

자체 N100 호스트에서 Docker Compose 로 띄우는 절차. 사람(호스트)만 본다.

## 정책 요약 (Week 3 ~)

- **N100 = prod 단일.** staging 은 N100 에서 안 돌린다. dev 작업은 로컬에서 직접 `pnpm dev`.
- **자동 배포는 `main` push 일 때만.** `dev` push 는 GHCR 에 `staging-<sha>` 이미지를 빌드/푸시까지만 한다 ("컨테이너 빌드가 깨지지 않는가" 안전망용). 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 "<PAT_with_read:packages>" | docker login ghcr.io -u <github_user> --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-<sha> 로 덮어씀)

# 3) (필요 시) ghcr.io pull 인증
echo "<PAT_with_read:packages>" | docker login ghcr.io -u <github_user> --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 "<ip-or-domain>"
gh secret set N100_USER --body "<user>"
```

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=<tag> MURUN_ENV_FILE=.env.staging docker compose --env-file .env.staging up -d
# 특정 태그로 되돌리기 (예: 직전 prod 빌드)
IMAGE_TAG=prod-<sha> 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 자동 배포 대상은 아님. 항상 수동.
14 changes: 7 additions & 7 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading
Loading