# 배포 가이드 (Deployment Guide) ## 1. 전체 아키텍처 ### 시스템 구성도 ``` [User Browser] │ ├── https://sr.example.com ──────────────────────┐ │ │ ▼ ▼ Cloudflare DNS ─── Cloudflare Pages ──── Cloudflare Tunnel (도메인 관리) (Next.js 정적 호스팅) (outbound 전용) │ │ http://localhost:8080 ▼ TrueNAS Scale ├── Docker: Spring Boot (:8080) ├── Docker: PostgreSQL (:5432) └── Dataset: 첨부파일 저장소 ``` ### 데이터 흐름 ``` 1. User → https://sr.example.com → Cloudflare Pages (Next.js SPA) 2. SPA → GET https://api.sr.example.com/v1/tickets │ 3. Cloudflare DNS → Cloudflare Tunnel (443) → cloudflared 컨테이너 4. cloudflared → http://spring-boot:8080/api/v1/tickets 5. Spring Boot → PostgreSQL (:5432) / Dataset (/data/attachments) ``` ### 포트 오픈 현황 | 방향 | 프로토콜 | 포트 | 설명 | |------|---------|------|------| | **아웃바운드** | TCP | 443 | cloudflared → Cloudflare Edge | | 인바운드 | **없음** | - | 포트포워딩 불필요 | TrueNAS에 **열린 인바운드 포트가 0개**. cloudflared가 아웃바운드 연결만 생성하여 Cloudflare와 통신. --- ## 2. 사전 준비 ### 2.1 Cloudflare - Cloudflare 계정 (https://dash.cloudflare.com) - 도메인 Cloudflare DNS 위임 완료 - Cloudflare Pages 가입 (무료 티어 가능) ### 2.2 TrueNAS Scale - TrueNAS Scale 24.04+ (Linux 기반) - Apps 기능 활성화: **Apps → Settings → Choose Pool** (앱 전용 Dataset 선택) - Docker 네트워크 확인: `docker network ls` ### 2.3 GitHub - Frontend Repository (`sr-frontend`) - Backend Repository (`sr-backend`) - GitHub Secrets 등록 준비 --- ## 3. 1단계: Backend Docker화 ### 3.1 Dockerfile Spring Boot 프로젝트 루트에 `Dockerfile` 생성: ```dockerfile # Build stage FROM gradle:8.7-jdk17 AS builder WORKDIR /app COPY build.gradle settings.gradle ./ COPY src ./src RUN gradle build -x test --no-daemon # Runtime stage FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=builder /app/build/libs/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] ``` ### 3.2 application-prod.yml `src/main/resources/application-prod.yml` 생성: ```yaml spring: datasource: url: jdbc:postgresql://postgres:5432/sr_system username: ${DB_USERNAME} password: ${DB_PASSWORD} jpa: hibernate: ddl-auto: validate show-sql: false servlet: multipart: max-file-size: 50MB max-request-size: 100MB app: file: upload-dir: /data/attachments jwt: secret: ${JWT_SECRET} server: port: 8080 logging: level: com.hq.support.portal: INFO ``` ### 3.3 docker-compose.yml 최상위 `docker/infra/docker-compose.yml`: ```yaml version: "3.8" services: postgres: image: postgres:16 container_name: sr-postgres restart: unless-stopped environment: POSTGRES_DB: sr_system POSTGRES_USER: ${DB_USERNAME} POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - /mnt/apps/sr-system/postgres:/var/lib/postgresql/data ports: - "127.0.0.1:5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d sr_system"] interval: 10s timeout: 5s retries: 5 spring-boot: build: context: . dockerfile: Dockerfile container_name: sr-backend restart: unless-stopped depends_on: postgres: condition: service_healthy environment: SPRING_PROFILES_ACTIVE: prod DB_USERNAME: ${DB_USERNAME} DB_PASSWORD: ${DB_PASSWORD} JWT_SECRET: ${JWT_SECRET} volumes: - /mnt/apps/sr-system/attachments:/data/attachments ports: - "127.0.0.1:8080:8080" ``` > **중요:** `ports`가 `127.0.0.1:8080:8080`으로 설정됨. 외부 직접 접근 불가, cloudflared를 통해서만 접근 가능. --- ## 4. 2단계: TrueNAS Scale 배포 ### 4.1 Dataset 생성 TrueNAS GUI → Storage → Create Dataset: | 항목 | 경로 | 용도 | |------|------|------| | App Config | `/mnt/apps/sr-system` | docker-compose, 환경변수 | | PostgreSQL Data | `/mnt/apps/sr-system/postgres` | DB 데이터 | | Attachments | `/mnt/apps/sr-system/attachments` | 첨부파일 저장소 | ### 4.2 환경변수 파일 `/mnt/apps/sr-system/.env` 생성: ```bash DB_USERNAME=sr_admin DB_PASSWORD=<복잡한_비밀번호> JWT_SECRET= ``` ### 4.3 Custom App 등록 TrueNAS GUI → Apps → Launch Docker Image 대신 **Custom App** 사용: 1. **App Name:** `sr-system` 2. **Image:** (docker-compose 방식 사용) 3. **Add Container → docker-compose:** TrueNAS Apps → Discover Apps → Custom App 선택 후 아래 내용 입력: ```yaml services: postgres: image: postgres:16 container_name: sr-postgres restart: unless-stopped env_file: /mnt/apps/sr-system/.env volumes: - /mnt/apps/sr-system/postgres:/var/lib/postgresql/data networks: - sr-network spring-boot: image: sr-backend:latest container_name: sr-backend restart: unless-stopped depends_on: postgres: condition: service_healthy env_file: /mnt/apps/sr-system/.env environment: SPRING_PROFILES_ACTIVE: prod volumes: - /mnt/apps/sr-system/attachments:/data/attachments networks: - sr-network networks: sr-network: driver: bridge ``` > **참고:** docker-compose 방식 대신 TrueNAS Apps GUI의 **Custom App → Container Images**에서 각 이미지를 개별 등록하고 `Network`를 동일한 bridge 네트워크로 연결해도 됨. ### 4.4 수동 빌드 (초기) 백엔드 코드를 TrueNAS에 복사 후 빌드: ```bash # TrueNAS SSH 접속 cd /mnt/apps/sr-system git clone https://github.com/your-org/sr-backend.git cd sr-backend docker build -t sr-backend:latest . docker compose up -d ``` --- ## 5. 3단계: Cloudflare Tunnel 설치 ### 5.1 Cloudflare Tunnel 생성 (로컬 CLI) ```bash # 로컬 PC에서 cloudflared 설치 brew install cloudflare/cloudflare/cloudflared # Cloudflare 로그인 cloudflared tunnel login # Tunnel 생성 cloudflared tunnel create sr-system-tunnel # 출력 예시: Tunnel ID: abc12345-xxxx-xxxx-xxxx-xxxxxxxxxxxx # 인증 파일: ~/.cloudflared/abc12345-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json ``` ### 5.2 Config 파일 생성 `~/.cloudflared/config.yml`: ```yaml tunnel: abc12345-xxxx-xxxx-xxxx-xxxxxxxxxxxx credentials-file: /home/.cloudflared/abc12345-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json ingress: - hostname: api.sr.example.com service: http://localhost:8080 - service: http_status:404 ``` ### 5.3 DNS 라우팅 설정 ```bash cloudflared tunnel route dns sr-system-tunnel api.sr.example.com ``` ### 5.4 TrueNAS에 cloudflared 컨테이너 배포 **방법 A: Docker Compose에 포함** `docker/infra/docker-compose.yml`에 cloudflared 추가: ```yaml services: # ... postgres, spring-boot ... cloudflared: image: cloudflare/cloudflared:latest container_name: sr-tunnel restart: unless-stopped command: tunnel --no-autoupdate run environment: - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN} networks: - sr-network ``` **방법 B: TrueNAS Apps에서 개별 배포** 1. TrueNAS → Apps → Discover Apps → Custom App 2. **App Name:** `sr-tunnel` 3. **Image:** `cloudflare/cloudflared:latest` 4. **Entrypoint:** `tunnel --no-autoupdate run` 5. **Environment Variables:** - `TUNNEL_TOKEN`: `` ### 5.5 Tunnel Token 발급 ```bash cloudflared tunnel token sr-system-tunnel > tunnel-token.txt # 출력된 token을 CLOUDFLARE_TUNNEL_TOKEN 환경변수로 등록 ``` ### 5.6 동작 확인 ```bash # Tunnel 로그 확인 docker logs sr-tunnel # 정상 출력 예시: # 2026/05/21 10:00:00 INF Connection abc12345 registered # 2026/05/21 10:00:00 INF Route created hostname=api.sr.example.com # 2026/05/21 10:00:00 INF tunnelId=abc12345 # Spring Boot 헬스체크 curl https://api.sr.example.com/api/v1/actuator/health # {"status":"UP"} ``` --- ## 6. 4단계: Frontend Cloudflare Pages 배포 ### 6.1 next.config.ts 설정 ```typescript import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "export", // 정적 사이트 내보내기 images: { unoptimized: true, // 정적 export 시 필수 }, trailingSlash: true, distDir: "out", }; export default nextConfig; ``` > **참고:** API Routes나 Server Components가 필요하면 `output: 'export'` 대신 `@cloudflare/next-on-pages` 사용. ### 6.2 환경변수 `.env.production`: ```bash NEXT_PUBLIC_API_URL=https://api.sr.example.com/api/v1 ``` ### 6.3 Cloudflare Pages 연결 1. Cloudflare Dashboard → **Workers & Pages → Pages** 2. **Connect to Git** → GitHub 저장소 선택 3. **Build settings:** - Framework preset: **Next.js** - Build command: `npm run build && npm run export` - Build output: `out` 4. **Environment Variables (Advanced):** - `NEXT_PUBLIC_API_URL`: `https://api.sr.example.com/api/v1` 5. **Deploy** ### 6.4 Custom Domain 설정 Deploy 완료 후: 1. Pages 프로젝트 → **Custom domains** → **Set up custom domain** 2. `sr.example.com` 입력 3. Cloudflare가 자동으로 DNS 레코드 생성 (Proxy ON = 주황 구름) --- ## 7. 5단계: CI/CD ### 7.1 Backend (GitHub Actions) `.github/workflows/deploy-backend.yml`: ```yaml name: Deploy Backend on: push: branches: [main] paths: - "backend/**" - "docker/**" jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: "17" distribution: "temurin" - name: Build with Gradle run: | cd backend ./gradlew build -x test - name: Build Docker Image run: | cd backend docker build -t ghcr.io/${{ github.repository }}/sr-backend:latest . docker tag ghcr.io/${{ github.repository }}/sr-backend:latest \ ghcr.io/${{ github.repository }}/sr-backend:${{ github.sha }} - name: Push to GitHub Container Registry run: | echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin docker push ghcr.io/${{ github.repository }}/sr-backend --all-tags - name: Deploy to TrueNAS uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.TRUENAS_HOST }} username: ${{ secrets.TRUENAS_SSH_USER }} key: ${{ secrets.TRUENAS_SSH_KEY }} script: | cd /mnt/apps/sr-system docker compose pull spring-boot docker compose up -d spring-boot docker image prune -f ``` ### 7.2 Frontend (GitHub Actions) `.github/workflows/deploy-frontend.yml`: ```yaml name: Deploy Frontend on: push: branches: [main] paths: - "frontend/**" jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "20" - name: Install & Build run: | cd frontend npm ci npm run build - name: Deploy to Cloudflare Pages uses: cloudflare/pages-action@v1 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} projectName: sr-frontend directory: frontend/out gitHubToken: ${{ secrets.GITHUB_TOKEN }} ``` ### 7.3 GitHub Secrets 등록 | Secret | 값 | |--------|-----| | `TRUENAS_HOST` | TrueNAS IP 주소 (내부망) | | `TRUENAS_SSH_USER` | SSH 사용자 | | `TRUENAS_SSH_KEY` | SSH Private Key | | `CLOUDFLARE_API_TOKEN` | Cloudflare API Token (Pages 권한) | | `CLOUDFLARE_ACCOUNT_ID` | Cloudflare Account ID | | `TUNNEL_TOKEN` | Cloudflare Tunnel Token | --- ## 8. 보안 설정 ### 8.1 Cloudflare WAF 규칙 Cloudflare Dashboard → Security → WAF: **규칙 1: API 경로만 허용** ``` Field: Hostname Operator: equals Value: api.sr.example.com Then: URI Path starts with /api/v1 OR /actuator/health → Allow Else → Block ``` **규칙 2: Rate Limiting** ``` When: requests match api.sr.example.com Rate: 1000 requests per 5 minutes Action: Block ``` ### 8.2 Cloudflare Access (선택 - VPN 대체) Zero Trust → Access → Applications: - **App Domain:** `api.sr.example.com` - **Policy:** 특정 이메일/Google/OAuth 로그인 필수 - **Session Duration:** 24h > Cloudflare Access를 활성화하면 API도 인증된 사용자만 접근 가능. 단, 프론트엔드 SPA에서 API 호출 시 Access Token 처리 로직이 추가로 필요. ### 8.3 SSL 설정 Cloudflare Dashboard → SSL/TLS: - **Full (Strict)** 선택 - TrueNAS 측에서는 별도 SSL 인증서 불필요 (cloudflared가 종단 암호화) ### 8.4 DB 접근 제한 PostgreSQL이 `127.0.0.1:5432`에서만 리스닝. 외부 접근 차단. Docker 네트워크 내에서만 `postgres:5432`로 접근 가능. --- ## 9. 운영 ### 9.1 헬스체크 ```bash # Spring Boot Actuator curl https://api.sr.example.com/api/v1/actuator/health # DB 연결 확인 curl https://api.sr.example.com/api/v1/actuator/health/db ``` ### 9.2 로그 확인 ```bash # TrueNAS SSH 접속 후 docker logs sr-backend # Spring Boot 로그 docker logs sr-postgres # PostgreSQL 로그 docker logs sr-tunnel # Tunnel 로그 docker compose logs -f # 실시간 로그 스트림 ``` ### 9.3 DB 백업 **ZFS Snapshot (자동):** TrueNAS GUI → Storage → `/mnt/apps/sr-system/postgres` → Snapshot: - Schedule: Daily at 03:00 - Retention: 30일 **pg_dump (수동):** ```bash docker exec sr-postgres pg_dump -U sr_admin sr_system > \ /mnt/apps/sr-system/backups/sr_system_$(date +%Y%m%d).sql ``` ### 9.4 Watchtower (자동 업데이트) docker-compose에 Watchtower 추가: ```yaml services: watchtower: image: containrrr/watchtower container_name: sr-watchtower restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock command: --interval 3600 --cleanup ``` 매시간 이미지 체크 → 새 이미지 있으면 자동 재배포. --- ## 10. 문제 해결 ### Tunnel 연결 안 됨 ```bash # cloudflared 로그 확인 docker logs sr-tunnel # DNS 확인 cloudflared tunnel info sr-system-tunnel # 재시작 docker restart sr-tunnel ``` ### Spring Boot 컨테이너 Crash ```bash # 로그 확인 docker logs sr-backend --tail 50 # DB 연결 테스트 docker exec sr-postgres pg_isready -U sr_admin -d sr_system # 환경변수 확인 docker exec sr-backend env | grep -E "DB_|JWT_|SPRING_" ``` ### Cloudflare Pages 502 Error - Cloudflare Tunnel이 실행 중인지 확인 - `api.sr.example.com` DNS가 Tunnel UUID를 가리키는지 확인 - Spring Boot가 정상 기동했는지 확인 (`http://localhost:8080/actuator/health`) - Cloudflare Dashboard → SSL/TLS → **Full (Strict)** 확인