15 KiB
배포 가이드 (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 생성:
# 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 생성:
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:
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 생성:
DB_USERNAME=sr_admin
DB_PASSWORD=<복잡한_비밀번호>
JWT_SECRET=<openssl rand -base64 64>
4.3 Custom App 등록
TrueNAS GUI → Apps → Launch Docker Image 대신 Custom App 사용:
-
App Name:
sr-system -
Image: (docker-compose 방식 사용)
-
Add Container → docker-compose:
TrueNAS Apps → Discover Apps → Custom App 선택 후 아래 내용 입력:
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에 복사 후 빌드:
# 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)
# 로컬 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:
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 라우팅 설정
cloudflared tunnel route dns sr-system-tunnel api.sr.example.com
5.4 TrueNAS에 cloudflared 컨테이너 배포
방법 A: Docker Compose에 포함
docker/infra/docker-compose.yml에 cloudflared 추가:
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에서 개별 배포
- TrueNAS → Apps → Discover Apps → Custom App
- App Name:
sr-tunnel - Image:
cloudflare/cloudflared:latest - Entrypoint:
tunnel --no-autoupdate run - Environment Variables:
TUNNEL_TOKEN:<Tunnel Token>
5.5 Tunnel Token 발급
cloudflared tunnel token sr-system-tunnel > tunnel-token.txt
# 출력된 token을 CLOUDFLARE_TUNNEL_TOKEN 환경변수로 등록
5.6 동작 확인
# 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 설정
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:
NEXT_PUBLIC_API_URL=https://api.sr.example.com/api/v1
6.3 Cloudflare Pages 연결
- Cloudflare Dashboard → Workers & Pages → Pages
- Connect to Git → GitHub 저장소 선택
- Build settings:
- Framework preset: Next.js
- Build command:
npm run build && npm run export - Build output:
out
- Environment Variables (Advanced):
NEXT_PUBLIC_API_URL:https://api.sr.example.com/api/v1
- Deploy
6.4 Custom Domain 설정
Deploy 완료 후:
- Pages 프로젝트 → Custom domains → Set up custom domain
sr.example.com입력- Cloudflare가 자동으로 DNS 레코드 생성 (Proxy ON = 주황 구름)
7. 5단계: CI/CD
7.1 Backend (GitHub Actions)
.github/workflows/deploy-backend.yml:
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:
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 헬스체크
# 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 로그 확인
# 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 (수동):
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 추가:
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 연결 안 됨
# cloudflared 로그 확인
docker logs sr-tunnel
# DNS 확인
cloudflared tunnel info sr-system-tunnel
# 재시작
docker restart sr-tunnel
Spring Boot 컨테이너 Crash
# 로그 확인
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.comDNS가 Tunnel UUID를 가리키는지 확인- Spring Boot가 정상 기동했는지 확인 (
http://localhost:8080/actuator/health) - Cloudflare Dashboard → SSL/TLS → Full (Strict) 확인