홈 탭 장르 카드 UI + Tabler Icons 적용 + 지역 필터 추가
- 홈 탭: 장르 가로 스크롤 카드 (Tabler Icons 픽토그램) - 홈 탭: 가격/지역/내위치 필터 2줄 배치 - 리스트 탭: 기존 바텀시트 필터 UI 유지 - cuisine-icons: Tabler 아이콘 매핑 추가 (getTablerCuisineIcon) - 드래그 스크롤 장르 카드에 적용 - 배포 가이드 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
262
docs/deployment-guide.md
Normal file
262
docs/deployment-guide.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# Tasteby 배포 가이드
|
||||
|
||||
## 환경 요약
|
||||
|
||||
| 항목 | Dev (개발) | Prod (운영) |
|
||||
|------|-----------|-------------|
|
||||
| URL | dev.tasteby.net | www.tasteby.net |
|
||||
| 호스트 | 로컬 Mac mini | OKE (Oracle Kubernetes Engine) |
|
||||
| 프로세스 관리 | PM2 | Kubernetes Deployment |
|
||||
| 프론트엔드 실행 | `npm run dev` (Next.js dev server) | `node server.js` (standalone 빌드) |
|
||||
| 백엔드 실행 | `./gradlew bootRun` | `java -jar app.jar` (bootJar 빌드) |
|
||||
| Redis | 로컬 Redis 서버 | K8s Pod (redis:7-alpine) |
|
||||
| TLS | Nginx(192.168.0.147) + Certbot | cert-manager + Let's Encrypt |
|
||||
| 리버스 프록시 | Nginx (192.168.0.147 → 192.168.0.208) | Nginx Ingress Controller (K8s) |
|
||||
| 도메인 DNS | dev.tasteby.net → Mac mini IP | www.tasteby.net → OCI NLB 217.142.131.194 |
|
||||
|
||||
---
|
||||
|
||||
## 1. Dev 환경 (dev.tasteby.net)
|
||||
|
||||
### 구조
|
||||
|
||||
```
|
||||
브라우저 → dev.tasteby.net (HTTPS)
|
||||
↓
|
||||
Nginx (192.168.0.147) — Certbot Let's Encrypt TLS
|
||||
├── /api/* → proxy_pass http://192.168.0.208:8000 (tasteby-api)
|
||||
└── /* → proxy_pass http://192.168.0.208:3001 (tasteby-web)
|
||||
↓
|
||||
Mac mini (192.168.0.208) — PM2 프로세스 매니저
|
||||
├── tasteby-api → ./gradlew bootRun (:8000)
|
||||
└── tasteby-web → npm run dev (:3001)
|
||||
```
|
||||
|
||||
- **192.168.0.147**: Nginx 리버스 프록시 서버 (TLS 종료, Certbot 자동 갱신)
|
||||
- **192.168.0.208**: Mac mini (실제 앱 서버, PM2 관리)
|
||||
|
||||
### PM2 프로세스 구성 (ecosystem.config.js)
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "tasteby-api",
|
||||
cwd: "/Users/joungmin/workspaces/tasteby/backend-java",
|
||||
script: "./start.sh", // gradlew bootRun 실행
|
||||
interpreter: "/bin/bash",
|
||||
},
|
||||
{
|
||||
name: "tasteby-web",
|
||||
cwd: "/Users/joungmin/workspaces/tasteby/frontend",
|
||||
script: "npm",
|
||||
args: "run dev", // ⚠️ 절대 standalone으로 바꾸지 말 것!
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### 백엔드 start.sh
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
export JAVA_HOME="/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home"
|
||||
export PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH"
|
||||
set -a
|
||||
source /Users/joungmin/workspaces/tasteby/backend/.env # 환경변수 로드
|
||||
set +a
|
||||
exec ./gradlew bootRun
|
||||
```
|
||||
|
||||
### 코드 수정 후 반영 방법
|
||||
|
||||
```bash
|
||||
# 프론트엔드: npm run dev라서 코드 수정 시 자동 Hot Reload (재시작 불필요)
|
||||
|
||||
# 백엔드: 코드 수정 후 재시작 필요
|
||||
pm2 restart tasteby-api
|
||||
|
||||
# 전체 재시작
|
||||
pm2 restart tasteby-api tasteby-web
|
||||
|
||||
# PM2 상태 확인
|
||||
pm2 status
|
||||
|
||||
# 로그 확인
|
||||
pm2 logs tasteby-api --lines 50
|
||||
pm2 logs tasteby-web --lines 50
|
||||
```
|
||||
|
||||
### 주의사항
|
||||
|
||||
- `tasteby-web`은 반드시 `npm run dev`로 실행 (dev server)
|
||||
- standalone 모드(`node .next/standalone/server.js`)로 바꾸면 static/public 파일을 못 찾아서 404 발생
|
||||
- standalone은 prod(Docker/K8s) 전용
|
||||
- dev 포트: 프론트 3001, 백엔드 8000 (3000은 Gitea가 사용 중)
|
||||
- 환경변수는 `backend/.env`에서 로드
|
||||
|
||||
---
|
||||
|
||||
## 2. Prod 환경 (www.tasteby.net)
|
||||
|
||||
### 구조
|
||||
|
||||
```
|
||||
브라우저 → www.tasteby.net (HTTPS)
|
||||
↓
|
||||
OCI Network Load Balancer (217.142.131.194)
|
||||
↓ 80→NodePort:32530, 443→NodePort:31437
|
||||
Nginx Ingress Controller (K8s)
|
||||
├── /api/* → backend Service (:8000)
|
||||
└── /* → frontend Service (:3001)
|
||||
```
|
||||
|
||||
### 클러스터 정보
|
||||
|
||||
- **OKE 클러스터**: tasteby-cluster-prod
|
||||
- **노드**: ARM64 × 2 (2 CPU / 8GB)
|
||||
- **네임스페이스**: tasteby
|
||||
- **K8s context**: `context-c6ap7ecrdeq`
|
||||
|
||||
### Pod 구성
|
||||
|
||||
| Pod | Image | Port | 리소스 |
|
||||
|-----|-------|------|--------|
|
||||
| backend | `icn.ocir.io/idyhsdamac8c/tasteby/backend:TAG` | 8000 | 500m~1 CPU, 768Mi~1536Mi |
|
||||
| frontend | `icn.ocir.io/idyhsdamac8c/tasteby/frontend:TAG` | 3001 | 200m~500m CPU, 256Mi~512Mi |
|
||||
| redis | `docker.io/library/redis:7-alpine` | 6379 | 100m~200m CPU, 128Mi~256Mi |
|
||||
|
||||
### 배포 명령어 (deploy.sh)
|
||||
|
||||
```bash
|
||||
# 전체 배포 (백엔드 + 프론트엔드)
|
||||
./deploy.sh "배포 메시지"
|
||||
|
||||
# 백엔드만 배포
|
||||
./deploy.sh --backend-only "백엔드 수정 사항"
|
||||
|
||||
# 프론트엔드만 배포
|
||||
./deploy.sh --frontend-only "프론트 수정 사항"
|
||||
|
||||
# 드라이런 (실제 배포 없이 확인)
|
||||
./deploy.sh --dry-run "테스트"
|
||||
```
|
||||
|
||||
### deploy.sh 동작 순서
|
||||
|
||||
1. **버전 계산**: 최신 git tag에서 patch +1 (v0.1.9 → v0.1.10)
|
||||
2. **Docker 빌드**: Colima로 `linux/arm64` 이미지 빌드 (로컬 Mac에서)
|
||||
- 백엔드: `backend-java/Dockerfile` → multi-stage (JDK build → JRE runtime)
|
||||
- 프론트: `frontend/Dockerfile` → multi-stage (node build → standalone runtime)
|
||||
3. **OCIR Push**: `icn.ocir.io/idyhsdamac8c/tasteby/{backend,frontend}:TAG` + `:latest`
|
||||
4. **K8s 배포**: `kubectl set image` → `kubectl rollout status` (롤링 업데이트)
|
||||
5. **Git tag**: `vX.Y.Z` 태그 생성 후 origin push
|
||||
|
||||
### Docker 빌드 상세
|
||||
|
||||
**백엔드 Dockerfile** (multi-stage):
|
||||
```dockerfile
|
||||
# Build: eclipse-temurin:21-jdk에서 gradlew bootJar
|
||||
# Runtime: eclipse-temurin:21-jre에서 java -jar app.jar
|
||||
# JVM 옵션: -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC
|
||||
```
|
||||
|
||||
**프론트엔드 Dockerfile** (multi-stage):
|
||||
```dockerfile
|
||||
# Build: node:22-alpine에서 npm ci + npm run build
|
||||
# Runtime: node:22-alpine에서 standalone 출력물 복사 + node server.js
|
||||
# ⚠️ standalone 모드는 Docker(prod) 전용. .next/static과 public을 직접 복사해야 함
|
||||
```
|
||||
|
||||
### Ingress 설정
|
||||
|
||||
```yaml
|
||||
# 주요 annotation
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod # 자동 TLS 인증서
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true" # HTTP → HTTPS 리다이렉트
|
||||
nginx.ingress.kubernetes.io/from-to-www-redirect: "true" # tasteby.net → www 리다이렉트
|
||||
|
||||
# 라우팅
|
||||
www.tasteby.net/api/* → backend:8000
|
||||
www.tasteby.net/* → frontend:3001
|
||||
```
|
||||
|
||||
### TLS 인증서 (cert-manager)
|
||||
|
||||
- ClusterIssuer: `letsencrypt-prod`
|
||||
- HTTP-01 challenge 방식 (포트 80 필수)
|
||||
- Secret: `tasteby-tls`
|
||||
- 인증서 상태 확인: `kubectl get certificate -n tasteby`
|
||||
|
||||
### 운영 확인 명령어
|
||||
|
||||
```bash
|
||||
# Pod 상태
|
||||
kubectl get pods -n tasteby
|
||||
|
||||
# 로그 확인
|
||||
kubectl logs -f deployment/backend -n tasteby
|
||||
kubectl logs -f deployment/frontend -n tasteby
|
||||
|
||||
# 인증서 상태
|
||||
kubectl get certificate -n tasteby
|
||||
|
||||
# Ingress 상태
|
||||
kubectl get ingress -n tasteby
|
||||
|
||||
# 롤백 (이전 이미지로)
|
||||
kubectl rollout undo deployment/backend -n tasteby
|
||||
kubectl rollout undo deployment/frontend -n tasteby
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. OCI 네트워크 구성
|
||||
|
||||
### VCN 서브넷
|
||||
|
||||
| 서브넷 | CIDR | 용도 |
|
||||
|--------|------|------|
|
||||
| oke-k8sApiEndpoint-subnet | 10.0.0.0/28 | K8s API 서버 |
|
||||
| oke-nodesubnet | 10.0.10.0/24 | 워커 노드 |
|
||||
| oke-svclbsubnet | 10.0.20.0/24 | NLB (로드밸런서) |
|
||||
|
||||
### 보안 리스트 (Security List)
|
||||
|
||||
**LB 서브넷** (oke-svclbsubnet):
|
||||
- Ingress: `0.0.0.0/0` → TCP 80, 443
|
||||
- Egress: `10.0.10.0/24` → all (노드 서브넷 전체 허용)
|
||||
|
||||
**노드 서브넷** (oke-nodesubnet):
|
||||
- Ingress: `10.0.10.0/24` → all (노드 간 통신)
|
||||
- Ingress: `10.0.0.0/28` → TCP all (API 서버)
|
||||
- Ingress: `0.0.0.0/0` → TCP 22 (SSH)
|
||||
- Ingress: `10.0.20.0/24` → TCP 30000-32767 (LB → NodePort)
|
||||
- Ingress: `0.0.0.0/0` → TCP 30000-32767 (NLB preserve-source 대응)
|
||||
|
||||
> ⚠️ NLB `is-preserve-source: true` 설정으로 클라이언트 원본 IP가 보존됨.
|
||||
> 따라서 노드 서브넷에 `0.0.0.0/0` → NodePort 인바운드가 반드시 필요.
|
||||
|
||||
---
|
||||
|
||||
## 4. OCIR (컨테이너 레지스트리) 인증
|
||||
|
||||
```bash
|
||||
# 로그인
|
||||
docker login icn.ocir.io -u idyhsdamac8c/oracleidentitycloudservice/<email> -p <auth-token>
|
||||
```
|
||||
|
||||
- Registry: `icn.ocir.io/idyhsdamac8c/tasteby/`
|
||||
- K8s imagePullSecret: `ocir-secret` (namespace: tasteby)
|
||||
|
||||
---
|
||||
|
||||
## 5. 자주 하는 실수 / 주의사항
|
||||
|
||||
| 실수 | 원인 | 해결 |
|
||||
|------|------|------|
|
||||
| dev에서 static 404 | PM2를 standalone 모드로 바꿈 | `npm run dev`로 원복 |
|
||||
| prod HTTPS 타임아웃 | NLB 보안 리스트 NodePort 불일치 | egress를 노드 서브넷 all 허용 |
|
||||
| 인증서 발급 실패 | 포트 80 방화벽 차단 | LB 서브넷 ingress 80 + 노드 서브넷 NodePort 허용 |
|
||||
| OKE에서 이미지 pull 실패 | CRI-O short name 불가 | `docker.io/library/` 풀네임 사용 |
|
||||
| NLB 헬스체크 실패 | preserve-source + 노드 보안 리스트 | 0.0.0.0/0 → NodePort 인바운드 추가 |
|
||||
Reference in New Issue
Block a user