- 홈 탭: 장르 가로 스크롤 카드 (Tabler Icons 픽토그램) - 홈 탭: 가격/지역/내위치 필터 2줄 배치 - 리스트 탭: 기존 바텀시트 필터 UI 유지 - cuisine-icons: Tabler 아이콘 매핑 추가 (getTablerCuisineIcon) - 드래그 스크롤 장르 카드에 적용 - 배포 가이드 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
263 lines
8.1 KiB
Markdown
263 lines
8.1 KiB
Markdown
# 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 인바운드 추가 |
|