- 홈 탭: 장르 가로 스크롤 카드 (Tabler Icons 픽토그램) - 홈 탭: 가격/지역/내위치 필터 2줄 배치 - 리스트 탭: 기존 바텀시트 필터 UI 유지 - cuisine-icons: Tabler 아이콘 매핑 추가 (getTablerCuisineIcon) - 드래그 스크롤 장르 카드에 적용 - 배포 가이드 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8.1 KiB
8.1 KiB
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)
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
#!/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
코드 수정 후 반영 방법
# 프론트엔드: 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) 전용
- standalone 모드(
- 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 |
| frontend | icn.ocir.io/idyhsdamac8c/tasteby/frontend:TAG |
3001 | 200m |
| redis | docker.io/library/redis:7-alpine |
6379 | 100m |
배포 명령어 (deploy.sh)
# 전체 배포 (백엔드 + 프론트엔드)
./deploy.sh "배포 메시지"
# 백엔드만 배포
./deploy.sh --backend-only "백엔드 수정 사항"
# 프론트엔드만 배포
./deploy.sh --frontend-only "프론트 수정 사항"
# 드라이런 (실제 배포 없이 확인)
./deploy.sh --dry-run "테스트"
deploy.sh 동작 순서
- 버전 계산: 최신 git tag에서 patch +1 (v0.1.9 → v0.1.10)
- Docker 빌드: Colima로
linux/arm64이미지 빌드 (로컬 Mac에서)- 백엔드:
backend-java/Dockerfile→ multi-stage (JDK build → JRE runtime) - 프론트:
frontend/Dockerfile→ multi-stage (node build → standalone runtime)
- 백엔드:
- OCIR Push:
icn.ocir.io/idyhsdamac8c/tasteby/{backend,frontend}:TAG+:latest - K8s 배포:
kubectl set image→kubectl rollout status(롤링 업데이트) - Git tag:
vX.Y.Z태그 생성 후 origin push
Docker 빌드 상세
백엔드 Dockerfile (multi-stage):
# 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):
# Build: node:22-alpine에서 npm ci + npm run build
# Runtime: node:22-alpine에서 standalone 출력물 복사 + node server.js
# ⚠️ standalone 모드는 Docker(prod) 전용. .next/static과 public을 직접 복사해야 함
Ingress 설정
# 주요 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
운영 확인 명령어
# 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 (컨테이너 레지스트리) 인증
# 로그인
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 인바운드 추가 |