Skip to content

Commit a9ed8b6

Browse files
committed
chore(deploy): N100 prod-only, dev push builds image only (#36)
1 parent 3e1a611 commit a9ed8b6

4 files changed

Lines changed: 139 additions & 91 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
name: Deploy
22

3+
# 정책 (Week 3~):
4+
# - N100 에는 prod 단일 환경만 운영.
5+
# - dev push → staging-* GHCR image build/push 까지만. "컨테이너 빌드가 깨지지 않는가" 안전망용.
6+
# N100 에는 절대 SSH 안 함.
7+
# - main push → prod-* GHCR image build/push + N100 SSH 배포.
8+
# - workflow_dispatch 로 임의 ref 에 대해 prod target 수동 배포 가능.
9+
#
10+
# Staging 컨테이너를 N100 에서 안 돌리기로 결정한 배경은 회고-Week2 + 회고-Week3 참조.
311
on:
412
push:
513
branches: [dev, main]
614
workflow_dispatch:
715
inputs:
816
target:
9-
description: "배포 대상 (staging | prod)"
17+
description: "수동 배포 대상 (prod 만 지원)"
1018
required: true
11-
default: "staging"
19+
default: "prod"
1220
type: choice
13-
options: [staging, prod]
21+
options: [prod]
1422

15-
# 동시 배포 금지. 같은 대상에 새 push 가 오면 in-progress 취소.
23+
# 동시 배포 금지. 같은 ref 에 새 push 가 오면 in-progress 취소.
1624
concurrency:
1725
group: deploy-${{ github.ref_name }}
1826
cancel-in-progress: false
@@ -27,7 +35,7 @@ env:
2735

2836
jobs:
2937
# ---------------------------------------------------------------------------
30-
# 1) build image → ghcr.io 푸시
38+
# 1) build image → ghcr.io 푸시 (dev=staging tag, main=prod tag, dispatch=prod tag)
3139
# ---------------------------------------------------------------------------
3240
build:
3341
name: Build & push image
@@ -36,6 +44,7 @@ jobs:
3644
outputs:
3745
image_tag: ${{ steps.meta.outputs.image_tag }}
3846
target: ${{ steps.meta.outputs.target }}
47+
deploy_to_n100: ${{ steps.meta.outputs.deploy_to_n100 }}
3948

4049
steps:
4150
- name: Checkout
@@ -47,15 +56,20 @@ jobs:
4756
# 우선순위: workflow_dispatch.inputs.target → branch
4857
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
4958
target="${{ inputs.target }}"
59+
deploy_to_n100="true"
5060
elif [ "${{ github.ref_name }}" = "main" ]; then
5161
target="prod"
62+
deploy_to_n100="true"
5263
else
64+
# dev push (또는 그 외 브랜치) — image 만 빌드하고 N100 SSH 는 안 함.
5365
target="staging"
66+
deploy_to_n100="false"
5467
fi
5568
sha_short="$(echo "${{ github.sha }}" | cut -c1-7)"
5669
image_tag="${target}-${sha_short}"
5770
echo "target=$target" >> "$GITHUB_OUTPUT"
5871
echo "image_tag=$image_tag" >> "$GITHUB_OUTPUT"
72+
echo "deploy_to_n100=$deploy_to_n100" >> "$GITHUB_OUTPUT"
5973
echo "image_ref=${REGISTRY}/${IMAGE}:${image_tag}" >> "$GITHUB_OUTPUT"
6074
echo "image_ref_target=${REGISTRY}/${IMAGE}:${target}" >> "$GITHUB_OUTPUT"
6175
@@ -82,12 +96,14 @@ jobs:
8296
cache-to: type=gha,mode=max
8397

8498
# ---------------------------------------------------------------------------
85-
# 2) N100 SSH secrets 준비 여부 확인. 미등록이면 이미지 build/push 까지만 하고 deploy 는 skip.
99+
# 2) N100 SSH secrets 준비 여부 확인. 미등록이면 build 까지만 하고 deploy 는 skip.
86100
# ---------------------------------------------------------------------------
87101
deploy-preflight:
88102
name: Check deploy secrets
89103
runs-on: ubuntu-latest
90104
timeout-minutes: 3
105+
needs: build
106+
if: needs.build.outputs.deploy_to_n100 == 'true'
91107
outputs:
92108
deploy_ready: ${{ steps.secrets.outputs.deploy_ready }}
93109
steps:
@@ -105,30 +121,28 @@ jobs:
105121
N100_USER: ${{ secrets.N100_USER }}
106122
N100_SSH_KEY: ${{ secrets.N100_SSH_KEY }}
107123

108-
109124
# ---------------------------------------------------------------------------
110-
# 3) SSH 로 N100 접속 → docker compose pull && up -d
125+
# 3) main / dispatch 한정. SSH 로 N100 접속 → docker compose pull && up -d
111126
# ---------------------------------------------------------------------------
112127
deploy:
113128
name: Deploy to ${{ needs.build.outputs.target }}
114129
needs: [build, deploy-preflight]
115-
if: needs.deploy-preflight.outputs.deploy_ready == 'true'
130+
if: needs.build.outputs.deploy_to_n100 == 'true' && needs.deploy-preflight.outputs.deploy_ready == 'true'
116131
runs-on: ubuntu-latest
117132
timeout-minutes: 10
118-
# prod 는 환경(environment) 보호로 수동 승인 가능
133+
# prod environment 에 Required reviewers 를 걸면 수동 승인 게이트가 된다.
119134
environment:
120135
name: ${{ needs.build.outputs.target }}
121136

122137
steps:
123-
124138
- name: Pull & restart on N100
125139
uses: appleboy/ssh-action@v1.0.3
126140
with:
127141
host: ${{ secrets.N100_HOST }}
128142
username: ${{ secrets.N100_USER }}
129143
key: ${{ secrets.N100_SSH_KEY }}
130-
# 호스트의 deploy/ 디렉터리에서 pull → up.
131-
# IMAGE_TAG selects the pushed GHCR image; MURUN_ENV_FILE selects .env.staging/.env.prod.
144+
# 호스트의 deploy/ 디렉터리에서 git sync → image pull → compose up.
145+
# IMAGE_TAG GHCR 이미지 태그, MURUN_ENV_FILE .env.prod 고정 (N100=prod only).
132146
script: |
133147
set -e
134148
cd ${MURUN_DEPLOY_DIR:-~/murun-peterabcd/deploy}

deploy/README.md

Lines changed: 101 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,144 +2,178 @@
22

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

5+
## 정책 요약 (Week 3 ~)
6+
7+
- **N100 = prod 단일.** staging 은 N100 에서 안 돌린다. dev 작업은 로컬에서 직접 `pnpm dev`.
8+
- **자동 배포는 `main` push 일 때만.** `dev` push 는 GHCR 에 `staging-<sha>` 이미지를 빌드/푸시까지만 한다 ("컨테이너 빌드가 깨지지 않는가" 안전망용). N100 에는 떨어지지 않는다.
9+
- **DuckDNS + Let's Encrypt HTTPS.** `DOMAIN=murun.duckdns.org` 로 Caddy 자동 발급.
10+
511
## 0. CI/CD (자동 배포) 사용 시 (권장)
612

7-
GitHub Actions 가 dev push → staging, main push → prod 로 자동 배포한다.
8-
첫 셋업 한 번만 호스트에서 manual init 후, 그 다음부턴 코드만 push 하면 됨.
13+
GitHub Actions 가 `main` push → prod 배포한다. 첫 셋업 한 번만 호스트에서 manual init 후, 그 다음부턴 main 에 PR 머지하면 자동 반영.
914

1015
### 호스트 1회 init
1116

1217
```bash
13-
# 1) repo clone (한 번만)
18+
# 1) repo clone (한 번만; deploy workflow 의 git sync 경로와 일치해야 함)
19+
cd ~
1420
git clone https://github.com/boostcampwm-snu-2026-1/murun-peterabcd.git
15-
cd murun-peterabcd/deploy
21+
cd murun-peterabcd
1622

17-
# 2) staging / prod 각각 .env 준비
18-
cp .env.example .env.staging
19-
cp .env.example .env.prod
20-
# (DOMAIN, AUTH_*, GOOGLE_*, DATABASE_URL, NEXT_PUBLIC_APP_URL 채우기)
23+
# main 을 default 로 둠 (workflow 가 main ref 기준으로 fetch/checkout 한다)
24+
git checkout main
2125

22-
# 3) ghcr.io pull 인증 (한 번만, repo 가 public 이면 skip 가능)
23-
echo "<PAT_with_read:packages>" | docker login ghcr.io -u <github_user> --password-stdin
26+
cd deploy
2427

25-
# 4) 첫 가동 (이미지 빌드는 GH Actions 가 대신, 여기선 pull 만)
26-
export MURUN_ENV_FILE=.env.staging
27-
export IMAGE_TAG=staging
28+
# 2) prod .env 준비
29+
cp .env.example .env.prod
30+
# 안에서 채울 키:
31+
# DOMAIN=murun.duckdns.org
32+
# NEXT_PUBLIC_APP_URL=https://murun.duckdns.org
33+
# AUTH_URL=https://murun.duckdns.org
34+
# AUTH_SECRET=$(openssl rand -base64 32)
35+
# AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET (Google Cloud Console)
36+
# DATABASE_URL=file:/app/data/murun.db
37+
# UPLOADS_DIR=/app/uploads
38+
# AUTH_TRUST_HOST=true
39+
# AUTH_GOOGLE_HD=snu.ac.kr
40+
# IMAGE_TAG=prod (GH Actions 가 main push 시 prod-<sha> 로 덮어씀)
41+
42+
# 3) (필요 시) ghcr.io pull 인증
43+
echo "<PAT_with_read:packages>" | docker login ghcr.io -u <github_user> --password-stdin
2844

29-
docker compose -p murun-staging --env-file .env.staging pull
30-
docker compose -p murun-staging --env-file .env.staging up -d
45+
# 4) 첫 가동
46+
export MURUN_ENV_FILE=.env.prod
47+
docker compose -p murun-prod --env-file .env.prod pull
48+
docker compose -p murun-prod --env-file .env.prod up -d
3149
```
3250

3351
### GitHub Actions secrets 등록 (Settings → Secrets and variables → Actions)
3452

3553
|||
3654
|----|------|
37-
| `N100_HOST` | N100 의 공인 IP 또는 DDNS 도메인 |
55+
| `N100_HOST` | N100 의 공인 IP 또는 DDNS 도메인 (`murun.duckdns.org`) |
3856
| `N100_USER` | SSH 로그인 유저 |
3957
| `N100_SSH_KEY` | private SSH key 내용 전체 (BEGIN/END 포함). 공개키는 N100 의 `~/.ssh/authorized_keys` 에 등록 |
4058

4159
SSH key 생성 (호스트에서):
60+
4261
```bash
4362
ssh-keygen -t ed25519 -C "gh-actions-murun-deploy" -f ~/.ssh/n100_gh_deploy
4463
cat ~/.ssh/n100_gh_deploy.pub >> ~/.ssh/authorized_keys
64+
chmod 700 ~/.ssh
65+
chmod 600 ~/.ssh/authorized_keys
4566
# private key 를 GitHub secret 에 등록:
4667
gh secret set N100_SSH_KEY < ~/.ssh/n100_gh_deploy
4768
gh secret set N100_HOST --body "<ip-or-domain>"
4869
gh secret set N100_USER --body "<user>"
4970
```
5071

51-
Secrets 가 아직 없으면 GitHub Actions 는 GHCR 이미지 build/push 까지만 성공 처리하고, SSH deploy job 은 skip 된다. N100 준비 후 위 3개 secret 을 넣으면 다음 dev push 부터 staging deploy 까지 진행된다.
72+
Secrets 가 등록되어 있지 않으면 main push 도 image build/push 까지만 성공 처리하고, SSH deploy job 은 skip 된다.
5273

5374
### (옵션) prod environment 보호
5475

55-
main 으로의 자동 배포 전에 수동 승인이 필요하면 Settings → Environments → "prod" 생성 후 "Required reviewers" 본인 추가.
76+
main → prod 자동 배포 전에 수동 승인이 필요하면 Settings → Environments → `prod` 생성 후 "Required reviewers" 본인 추가.
77+
78+
### 수동 트리거
5679

57-
## 1. 수동 배포 (CI/CD 우회) — 첫 배포 또는 디버깅
80+
특정 ref 를 prod 로 즉시 배포:
5881

5982
```bash
60-
# 호스트에 git clone
61-
git clone https://github.com/boostcampwm-snu-2026-1/murun-peterabcd.git
62-
cd murun-peterabcd/deploy
83+
gh workflow run deploy.yml --ref main -f target=prod
84+
```
6385

64-
# compose용 env
65-
cp .env.example .env
66-
# (도메인 붙기 전엔 DOMAIN=:80 그대로 두기)
86+
## 1. 수동 배포 (CI/CD 우회) — 디버깅 / 첫 배포
6787

68-
# compose 변수와 app 환경변수를 같은 파일에서 읽음
69-
# 기본 수동 실행은 .env, staging/prod 분리는 MURUN_ENV_FILE 로 지정
88+
```bash
89+
cd ~/murun-peterabcd/deploy
7090

91+
# 위의 .env.prod 가 이미 준비됐다고 가정
92+
export MURUN_ENV_FILE=.env.prod
7193

72-
# 빌드 + 기동
73-
docker compose up -d --build
94+
# 이미지가 없거나 강제로 rebuild 하고 싶을 때 (오래 걸림)
95+
docker compose -p murun-prod --env-file .env.prod up -d --build
7496

75-
# 헬스체크
76-
curl -fsS http://localhost/ && echo OK
97+
# 이미지를 GHCR 에서 pull 만 (보통은 이쪽)
98+
docker compose -p murun-prod --env-file .env.prod pull
99+
docker compose -p murun-prod --env-file .env.prod up -d
77100
```
78101

79-
## 2. 업데이트 배포
102+
헬스체크:
80103

81104
```bash
82-
cd murun-peterabcd
83-
git pull
84-
cd deploy
85-
MURUN_ENV_FILE=.env docker compose --env-file .env up -d --build
86-
docker image prune -f
105+
docker compose -p murun-prod --env-file .env.prod ps
106+
curl -fsS http://localhost/api/health && echo OK
87107
```
88108

89-
## 3. staging + prod 동시 운영
90-
91-
같은 디렉터리에 `.env.staging`, `.env.prod` 를 별도로 두고 project name 으로 분리.
109+
## 2. 업데이트 배포 (수동)
92110

93111
```bash
94-
# staging (포트 8080; .env.staging 안에 CADDY_HTTP_PORT=8080 권장)
95-
MURUN_ENV_FILE=.env.staging docker compose -p murun-staging --env-file .env.staging up -d --build
96-
97-
# prod (포트 80)
98-
MURUN_ENV_FILE=.env.prod docker compose -p murun-prod --env-file .env.prod up -d --build
112+
cd ~/murun-peterabcd
113+
git pull
114+
cd deploy
115+
MURUN_ENV_FILE=.env.prod docker compose -p murun-prod --env-file .env.prod pull
116+
MURUN_ENV_FILE=.env.prod docker compose -p murun-prod --env-file .env.prod up -d
117+
docker image prune -f
99118
```
100119

101-
볼륨은 project name prefix 가 자동으로 붙어 분리됨 (`murun-staging_data` vs `murun-prod_data`).
102-
103-
## 4. 데이터 위치 (백업 대상)
120+
## 3. 데이터 위치 (백업 대상)
104121

105122
| docker volume | 호스트 경로 (Linux 기본) | 용도 |
106123
|---------------|--------------------------|------|
107-
| `murun_data` | `/var/lib/docker/volumes/murun_data/_data` | SQLite 파일 |
108-
| `murun_uploads` | `/var/lib/docker/volumes/murun_uploads/_data` | 단체사진 원본 + next/image 캐시 |
109-
| `murun_caddy_data` | `/var/lib/docker/volumes/murun_caddy_data/_data` | Let's Encrypt 인증서 |
124+
| `murun-prod_data` | `/var/lib/docker/volumes/murun-prod_data/_data` | SQLite 파일 |
125+
| `murun-prod_uploads` | `/var/lib/docker/volumes/murun-prod_uploads/_data` | 단체사진 원본 + next/image 캐시 |
126+
| `murun-prod_caddy_data` | `/var/lib/docker/volumes/murun-prod_caddy_data/_data` | Let's Encrypt 인증서 |
110127

111-
백업 cron 은 후속 PR 에서 (`scripts/backup.sh`).
128+
백업 cron 은 후속 작업 (`scripts/backup.sh`).
112129

113-
## 5. 롤백
130+
## 4. 롤백
114131

115132
```bash
116133
# 직전 이미지 태그 확인
117134
docker images ghcr.io/boostcampwm-snu-2026-1/murun-peterabcd --format '{{.Tag}}\t{{.CreatedAt}}'
118135

119-
# 특정 태그로 되돌리기
120-
IMAGE_TAG=<tag> MURUN_ENV_FILE=.env.staging docker compose --env-file .env.staging up -d
136+
# 특정 태그로 되돌리기 (예: 직전 prod 빌드)
137+
IMAGE_TAG=prod-<sha> MURUN_ENV_FILE=.env.prod docker compose -p murun-prod --env-file .env.prod up -d
121138
```
122139

123-
## 6. 트러블슈팅
140+
## 5. 트러블슈팅
124141

125142
```bash
126143
# 컨테이너 상태
127-
docker compose ps
144+
docker compose -p murun-prod --env-file .env.prod ps
128145

129146
# app 로그
130-
docker compose logs -f app
147+
docker compose -p murun-prod --env-file .env.prod logs -f app
131148

132-
# caddy 로그
133-
docker compose logs -f caddy
149+
# caddy 로그 (인증서 발급 / TLS 문제)
150+
docker compose -p murun-prod --env-file .env.prod logs -f caddy
134151

135152
# 강제 재기동
136-
docker compose restart app
153+
docker compose -p murun-prod --env-file .env.prod restart app
137154
```
138155

139-
## 7. 도메인 붙인 후 (예: duckdns)
156+
## 6. DuckDNS HTTPS 활성 절차
157+
158+
1. DuckDNS 에서 `murun.duckdns.org` 발급 + N100 공인 IP 등록 (DDNS 클라이언트로 자동 갱신 권장)
159+
2. 공유기에서 외부 80/443 → N100 내부 IP 의 80/443 포트포워딩
160+
3. `deploy/.env.prod` 의 값 확인:
161+
```env
162+
DOMAIN=murun.duckdns.org
163+
NEXT_PUBLIC_APP_URL=https://murun.duckdns.org
164+
AUTH_URL=https://murun.duckdns.org
165+
```
166+
4. `docker compose -p murun-prod --env-file .env.prod up -d` (Caddy 재기동 → Let's Encrypt 자동 발급)
167+
5. `docker compose ... logs caddy` 에서 "certificate obtained for murun.duckdns.org" 확인
168+
169+
## 7. Staging 환경 (옵션, N100 외 어딘가에서 돌릴 때)
170+
171+
기본 정책상 N100 에서는 staging 을 돌리지 않는다. 필요하면 다른 호스트/포트로:
172+
173+
```bash
174+
MURUN_ENV_FILE=.env.staging \
175+
CADDY_HTTP_PORT=8080 CADDY_HTTPS_PORT=8443 \
176+
docker compose -p murun-staging --env-file .env.staging up -d --build
177+
```
140178

141-
1. duckdns 에서 `murun.duckdns.org` 발급 + 현재 공인 IP 등록
142-
2. 공유기 80/443 포트포워딩 → N100 내부 IP
143-
3. `deploy/.env``DOMAIN=murun.duckdns.org` 로 변경
144-
4. `docker compose up -d` (Caddy 재기동되며 Let's Encrypt 자동 발급)
145-
5. 첫 인증서 발급 후 `docker compose logs caddy` 에서 "certificate obtained" 확인
179+
이 경우에도 GH Actions 자동 배포 대상은 아님. 항상 수동.

deploy/docker-compose.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# 뮤런 — 자체 N100 호스트 배포 compose.
22
#
3-
# 사용 (호스트):
3+
# 정책: N100 = prod 단일. staging 은 N100 에서 안 돌린다 (deploy/README.md 참조).
4+
#
5+
# 사용 (호스트, 수동):
46
# cd deploy
5-
# cp .env.example .env # 같은 디렉터리에 prod env 보관 (gitignore됨)
6-
# docker compose up -d --build
7+
# cp .env.example .env.prod
8+
# MURUN_ENV_FILE=.env.prod docker compose -p murun-prod --env-file .env.prod up -d
79
#
8-
# staging 과 prod 동시 운영 시:
9-
# docker compose -p murun-staging --env-file .env.staging up -d --build
10-
# docker compose -p murun-prod --env-file .env.prod up -d --build
11-
# (volume 이름이 project name 앞에 붙어 자동 분리)
10+
# 자동 배포는 GitHub Actions `Deploy` workflow 가 처리한다 (`.github/workflows/deploy.yml`).
11+
# volume 이름은 project name 앞에 붙어 분리됨 (e.g. `murun-prod_data`).
1212

1313
name: murun
1414

0 commit comments

Comments
 (0)