On-Do 팀은 기업의 사업장별 기후 물리 리스크를 예측하는 웹 서비스를 개발했다. Vue.js 프론트엔드, Spring Boot 백엔드, FastAPI AI 에이전트, 그리고 별도의 ModelOps 서버로 구성된 마이크로서비스 아키텍처를 채택했고, 이를 GCP(Google Cloud Platform)에 배포했다. 이 글에서는 왜 Kubernetes 대신 Docker + Nginx를 선택했는지, 그리고 Cloudflare, Nginx, Docker가 각각 어떤 역할을 하는지 실제 프로젝트 사례를 통해 설명하겠다.
[사용자]
↓
[Cloudflare CDN + DDoS Protection]
↓
[DNS: on-do.site → GCP VM IP]
↓
[GCP VM Instance]
├─ [Nginx Proxy Manager] (:80, :443)
│ ├─ on-do.site → polaris-frontend:80
│ ├─ api.on-do.site → polaris-backend-java:8080
│ └─ ai-agent-api.skax.co.kr → polaris-backend-fastapi:8000
│
├─ [Docker Container: polaris-frontend] (Vue.js)
├─ [Docker Container: polaris-backend-java] (Spring Boot)
├─ [Docker Container: polaris-backend-fastapi] (FastAPI AI Agent)
├─ [별도 서버: ModelOps Server] (FastAPI ML Model Serving)
└─ [PostgreSQL Database]
역할:
DNS 관리: on-do.site 도메인을 GCP 서버 IP로 연결
무료 SSL 인증서: Cloudflare Proxy를 통해 HTTPS 자동 적용 (프론트엔드)
CDN: 정적 파일을 전 세계 엣지 서버에 캐싱하여 응답 속도 향상
DDoS 방어: 악의적인 트래픽 차단
우리 프로젝트에서:
프론트엔드(on-do.site)는 Cloudflare Proxy 사용 → CDN + 자동 HTTPS
백엔드(api.on-do.site)는 DNS only 모드 → Nginx에서 직접 SSL 처리
왜 백엔드는 Proxy를 끄나? Cloudflare Proxy를 켜면 Cloudflare가 중간에서 모든 요청을 처리하는데, 백엔드 API는 Nginx Proxy Manager에서 Let's Encrypt 인증서를 발급받아 직접 SSL을 처리하기 때문에 충돌이 발생한다.
역할:
리버스 프록시: 도메인별로 요청을 적절한 Docker 컨테이너로 전달
SSL 인증서 관리: Let's Encrypt를 통해 무료 SSL 인증서 자동 발급 및 갱신
포트 매핑: 외부 80/443 포트를 내부 컨테이너의 다양한 포트로 연결
우리 프로젝트에서:
on-do.site:443
→ Nginx Proxy Manager
→ http://polaris-frontend:80
api.on-do.site:443
→ Nginx Proxy Manager
→ http://polaris-backend-java:8080
ai-agent-api.skax.co.kr:443
→ Nginx Proxy Manager
→ http://polaris-backend-fastapi:8000
설정 예시:
Domain: on-do.site
Scheme: http
Forward Hostname: polaris-frontend
Forward Port: 80
SSL Certificate: Let's Encrypt (자동 발급)
Force SSL: ON
왜 Nginx Proxy Manager를 쓰나?
GUI로 간편하게 설정 가능 (nginx.conf 직접 수정 불필요)
SSL 인증서 자동 갱신
여러 서비스를 하나의 서버에서 도메인별로 분리
역할:
각 서비스를 독립적인 컨테이너로 실행
환경 변수 주입으로 설정 관리
이미지 기반 배포로 재현성 보장
우리 프로젝트에서:
docker run -d \
--name polaris-frontend \
--network web \
-p 80:80 \
asia-northeast3-docker.pkg.dev/.../polaris-frontend:latest
docker run -d \
--name polaris-backend-java \
--network web \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e DB_HOST=10.117.192.3 \
-e DB_PORT=5432 \
-e JWT_SECRET=${{ secrets.JWT_SECRET }} \
-e FASTAPI_API_KEY=${{ secrets.FASTAPI_API_KEY }} \
asia-northeast3-docker.pkg.dev/.../polaris-backend-java:latest
docker run -d \
--name polaris-backend-fastapi \
--network web \
-p 8000:8000 \
-e MODELOPS_API_URL=https://modelops.skax.co.kr \
asia-northeast3-docker.pkg.dev/.../polaris-backend-fastapi:latest
Docker Network: 모든 컨테이너를 web 네트워크에 연결하여 컨테이너 이름으로 서로 통신:
docker network create web
이렇게 하면:
Spring Boot에서 http://polaris-backend-fastapi:8000로 AI Agent 호출
AI Agent에서 외부 ModelOps 서버로 ML 모델 추론 요청
[사용자]
→ "on-do.site에서 리스크 분석 요청"
[프론트엔드 Vue.js]
→ API 호출: POST https://api.on-do.site/api/analysis/start
[백엔드 Spring Boot]
→ 데이터베이스에 분석 작업 저장
→ AI Agent 호출: POST http://polaris-backend-fastapi:8000/predict
[AI Agent FastAPI]
→ ModelOps 서버 호출: POST https://modelops.skax.co.kr/inference
→ ML 모델로 리스크 예측
→ 결과를 Spring Boot로 반환
[백엔드 Spring Boot]
→ 결과를 데이터베이스에 저장
→ 프론트엔드로 응답
[프론트엔드]
→ 사용자에게 리스크 점수 시각화
내부 (Docker 네트워크): 컨테이너 이름으로 통신 (빠름, 보안)
polaris-backend-java → polaris-backend-fastapi
외부 (인터넷): 도메인으로 통신
polaris-backend-fastapi → modelops.skax.co.kr
자동 스케일링 (HPA: Horizontal Pod Autoscaler)
무중단 배포 (롤링 업데이트)
자가 치유 (Pod 장애 시 자동 재시작)
서비스 디스커버리, 로드 밸런싱
우리가 Kubernetes를 선택하지 않은 이유:
서비스 4개 (프론트엔드, 백엔드, AI Agent, ModelOps)
단일 VM 인스턴스에서 충분히 실행 가능
트래픽이 많지 않아 오토스케일링 불필요
Kubernetes는 다음을 모두 이해해야 함:
항목 Docker + Nginx Kubernetes (GKE)
컴퓨팅 e2-medium VM 1대
$25/월 마스터 노드 + 워커 노드 3대
$150~300/월
관리 비용 무료 (직접 관리) 클러스터 관리 비용 별도
네트워크 무료 (단일 VM) 로드 밸런서 비용 별도
우리의 선택: 3개월 프로젝트에 $300/월은 과하다!
우리의 CI/CD (Docker + Nginx):
on:
workflow_run:
workflows: ['CI - Build & Push']
types: [completed]
branches: [main]
jobs:
deploy:
runs-on: ubuntu-22.04
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: SSH로 서버 배포
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
# GCP Artifact Registry 인증
echo '${{ secrets.GCP_SA_KEY }}' | docker login -u _json_key --password-stdin https://asia-northeast3-docker.pkg.dev
# 최신 이미지 pull
docker pull asia-northeast3-docker.pkg.dev/.../polaris-backend-java:latest
# 기존 컨테이너 중지 및 삭제
docker stop polaris-backend-java || true
docker rm polaris-backend-java || true
# 새 컨테이너 실행
docker run -d \
--name polaris-backend-java \
--network web \
--restart unless-stopped \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e JWT_SECRET="${{ secrets.JWT_SECRET }}" \
-e DB_HOST="${{ secrets.DB_HOST }}" \
-e DB_PORT="${{ secrets.DB_PORT }}" \
-e DB_NAME="${{ secrets.DB_NAME }}" \
-e DB_USERNAME="${{ secrets.DB_USERNAME }}" \
-e DB_PASSWORD="${{ secrets.DB_PASSWORD }}" \
-e SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT=org.hibernate.dialect.PostgreSQLDialect \
-e MAIL_HOST="${{ secrets.MAIL_HOST }}" \
-e MAIL_PORT="${{ secrets.MAIL_PORT }}" \
-e MAIL_USERNAME="${{ secrets.MAIL_USERNAME }}" \
-e MAIL_PASSWORD="${{ secrets.MAIL_PASSWORD }}" \
-e FASTAPI_API_KEY="${{ secrets.FASTAPI_API_KEY }}" \
asia-northeast3-docker.pkg.dev/.../polaris-backend-java:latest
# Health check (60초 타임아웃)
echo "Health check 중..."
for i in {1..60}; do
if docker exec polaris-backend-java curl -f -s http://localhost:8080/actuator/health > /dev/null 2>&1; then
echo "✓ 배포 성공! (${i}초 경과)"
exit 0
fi
sleep 1
done
echo "✗ Health check 실패 (60초 타임아웃)"
docker logs polaris-backend-java --tail 50
exit 1
배포 시 주의사항:
환경 변수 누락 방지: GitHub Secrets에 모든 필수 환경 변수 등록
Health Check 필수: 컨테이너가 정상 시작되었는지 확인
기존 컨테이너 정리: docker stop && docker rm 후 새 컨테이너 실행
배포
docker run -d --name myapp myimage
로그 확인
docker logs myapp
재시작
docker restart myapp
명령어 몇 줄로 모든 게 해결됨. Kubernetes의 복잡한 YAML 파일 불필요.
단일 VM: e2-medium (2 vCPU, 4GB RAM) → $25/월
Kubernetes: GKE 클러스터 최소 구성 → $150/월 이상
3개월 프로젝트 기준:
Docker + Nginx: $75
Kubernetes: $450
$375 절약!
컨테이너 내부 접속
docker exec -it polaris-backend-java bash
실시간 로그
docker logs -f polaris-backend-java
리소스 사용량
docker stats
문제 발생 시 바로 원인 파악 가능.
동시 접속자 수십~수백 명 처리 가능
Spring Boot의 Tomcat: 기본 200개 쓰레드
AI Agent의 Uvicorn: 비동기 처리로 높은 처리량
트래픽 급증 시 수동으로 인스턴스를 추가해야 함.
Kubernetes는 자동
kubectl scale deployment myapp --replicas=10
우리는 수동
docker run -d --name myapp-2 myimage
docker run -d --name myapp-3 myimage
docker stop myapp # ← 이 순간 서비스 중단!
docker run -d --name myapp myimage
Kubernetes의 롤링 업데이트는 중단 없이 배포 가능. 해결 방법: Blue-Green 배포
Green (새 버전) 실행
docker run -d --name myapp-green myimage
Nginx 설정 변경: myapp → myapp-green
Blue (구 버전) 중지
docker stop myapp
컨테이너가 죽으면 수동으로 재시작해야 함.
docker run -d --restart unless-stopped myapp # ← 이것으로 부분 해결
Kubernetes는 자동으로 Pod를 재시작함.
증상:
Failed to load ApplicationContext
Caused by: Could not resolve placeholder 'spring.mail.host'
원인: MailConfig가 @Configuration으로 무조건 로드되는데, 메일 환경 변수가 없으면 실패. 해결:
@Configuration
@ConditionalOnProperty(name = "spring.mail.host") // ← 추가
public class MailConfig {
// ...
}
증상:
Could not resolve placeholder 'AWS_ACCESS_KEY'
원인: GCP로 전환했는데 AWS S3 설정이 남아있었음. 해결:
S3Config.java 삭제
rm src/main/java/com/skax/physicalrisk/config/S3Config.java
application.yml에서 AWS 설정 제거
증상:
Schema-validation: missing table [analysis_jobs]
원인: ddl-auto: validate로 설정되어 테이블이 없으면 실패. 해결:
application-prod.yml
jpa:
hibernate:
ddl-auto: update # validate → update 변경
증상:
curl: (6) Could not resolve host: polaris-backend-fastapi
원인: 컨테이너들이 다른 네트워크에 있었음. 해결:
공통 네트워크 생성
docker network create web
모든 컨테이너를 web 네트워크에 연결
docker run -d --network web --name polaris-backend-java ...
docker run -d --network web --name polaris-backend-fastapi ...
다음 상황이 오면 Kubernetes를 고려해야 함:
동시 접속자: 수백 명 → 수천~수만 명
→ 오토스케일링 필수
서비스 개수: 4개 → 10개 이상
→ 서비스 디스커버리, 로드 밸런싱 필요
SLA: 99% → 99.9% 이상
→ 멀티 리전 배포, 자동 장애 복구 필요
GCP + AWS + Azure 동시 사용
→ Kubernetes는 클라우드 벤더 중립적
항목 | Docker + Nginx | Kubernetes
학습 곡선 낮음 ⭐ 높음 ⭐⭐⭐⭐⭐
초기 비용 낮음 ($25/월) 높음 ($150/월~)
배포 복잡도 낮음 높음
확장성 제한적 무제한
무중단 배포 수동 (Blue-Green) 자동 (Rolling)
자가 치유 제한적 (restart 옵션) 자동 (Pod 재시작)
모니터링 수동 (docker stats) 자동 (Prometheus)
적합한 규모 소규모~중규모 중규모~대규모
우리의 선택: Docker + Nginx
✅ 3개월 프로젝트 기간
✅ 4개 서비스 (프론트엔드, 백엔드, AI Agent, ModelOps)
✅ 중소규모 트래픽 (동시 접속자 수십~수백 명)
✅ 제한된 예산 ($75 vs $450)
✅ 빠른 배포와 디버깅
"최신 기술 = 좋은 기술" 이 아니다. 프로젝트 규모와 요구사항에 맞는 기술을 선택하는 게 중요하다.
복잡한 인프라는 디버깅도 어렵고, 팀원 온보딩도 어렵다. Docker + Nginx는 누구나 이해할 수 있다.
스타트업이나 소규모 프로젝트에서는 비용이 중요하다. Kubernetes로 $375를 절약한 건 큰 성과다.
지금은 Docker + Nginx지만, 나중에 Kubernetes로 전환할 수 있도록 설계했다:
12 Factor App 원칙 준수
환경 변수로 설정 관리
컨테이너 기반 배포
상태를 저장하지 않는 Stateless 서비스
"기술은 목적이 아니라 수단이다. 과하지도 부족하지도 않은, 딱 맞는 기술을 선택하자."