handDoc 졸업프로젝트의 실제 배포 환경 구성 과정을 정리해보겠다.
현재 나의 드림잡인 클라우드 엔지니어와 관련하여 배포를 많이 맡아보고 싶었고, 그 결과 AWS IaaS 환경에서 1번, NCP 쿠버네티스 환경에서 1번으로 총 두 번 서로 다른 방식으로 배포해보는 경험을 했다. FE도 배포를 맡아, Vercel로 배포했으며, 이후 여력이 되면 AWS S3 + CloudFront 기반으로 정적 호스팅도 구현해볼 계획이다.
서비스는 React(Vercel) + Spring Boot + FastAPI + Nginx + RDS(MySQL) + MongoDB Atlas로 구성되며, 모든 백엔드, AI 서버는 단일 EC2(t3.micro) 위에서 Docker 기반으로 운영한다.
AI까지 하려면 인스턴스를 2개 써야 하나 고민이었는데, 일단 1개로 사용해보기로 결정했고 결국 1대로 잘 돌아가서 좋은 결정이었지 싶다 ㅎㅎ
1. 배포 목표
- 프리티어 EC2(t3.micro) 1대로 전체 서비스 운영
- 모든 서비스 Docker 컨테이너화
- Spring Boot는 Blue-Green 무중단 배포
- FastAPI는 추론 서버라 단일 배포로 운영
- MongoDB는 EC2 컨테이너 대신 MongoDB Atlas 사용
- Nginx로 Reverse Proxy + HTTPS 적용
2. 시스템 아키텍처
핵심 구조는 다음과 같다:
사용자
↳ Vercel(React)
↳ https://handdoc.store/
↳ Nginx(EC2)
├ /api → Spring Boot(Blue/Green)
├ /fastapi → FastAPI(Sign/Whisper)
└ SSL/TLS termination
↳ RDS(MySQL)
↳ MongoDB Atlas
Nginx가 모든 트래픽을 엔트리 포인트로 받고 경로 기반으로 컨테이너에 트래픽을 분기한다.
3. 서비스 엔드포인트
Spring Boot (Swagger)
프론트 → 백엔드 요청 예시
POST https://handdoc.store/api/v1/diagnosis/start
POST https://handdoc.store/api/v1/diagnosis/{id}/sign
POST https://handdoc.store/api/v1/diagnosis/{id}/speech
PATCH https://handdoc.store/api/v1/diagnosis/{id}/end
GET https://handdoc.store/api/v1/diagnosis/{id}/summary
FastAPI (Sign-to-Text WebSocket)
wss://handdoc.store/fastapi/ws
테스트:
npm i -g wscat
wscat -c wss://handdoc.store/fastapi/ws


아래와 같은 과정으로 SSL 인증 적용 + 도메인 연결하여 주소가 변경되었다!
//REST API Base URL
http://43.200.110.36/fastapi
//WebSocket URL
ws://43.200.110.36/fastapi/ws
⇒ wss://handdoc.store/fastapi/ws
4. EC2 SSH 접근 및 폴더 구조
접속
ssh -i handdoc-key.pem ec2-user@43.200.110.36
디렉토리 구조
/home/ec2-user/apps/
└── .env
└── deploy.sh
└── docker-compose.spring-blue.yml
└── docker-compose.spring-green.yml
└── docker-compose.fastapi.yml
/etc/nginx/conf.d/
└── handdoc.conf
└── active-upstream.conf
5. Nginx 설정
Reverse Proxy 라우팅
/api/* → Spring Boot (8080)
/fastapi/* → FastAPI (8000)
>> Nginx가 SSL 종료와 라우팅을 모두 담당한다. 무중단 배포 시 active-upstream.conf가 변경되며 nginx reload 수행!
5-1. EC2 보안그룹 설정 (Inbound Rules)
단일 EC2 위에서 Nginx, Spring Boot, FastAPI를 모두 운영하기 때문에
아래 포트들을 인바운드로 열어주었다.

| 포트 | 용도 |
| 80 | HTTP (Nginx) |
| 443 | HTTPS (SSL 인증서) |
| 8080 | Spring Boot Blue/Green API |
| 8000 | FastAPI 추론 서버 |
| 22 | SSH (팀원 IP /32로 제한) |
실제 설정 화면
8080 / 8000은 개발 단계에서는 전체 허용이지만,
운영 환경에서는 Nginx Reverse Proxy 뒤에서만 접근 가능하도록 좁히는 방향이 이상적이다.
6. Docker 로그 확인
Spring Boot
docker logs handdoc-spring-blue
docker logs handdoc-spring-green
FastAPI
docker logs handdoc-fastapi

Nginx
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log
7. 실행 확인
현재 실행 중 컨테이너 확인
docker ps
정상이라면 다음 둘이 항상 떠 있어야 한다:
- handdoc-spring-blue 또는 handdoc-spring-green
- handdoc-fastapi

8. Secrets / GitHub Actions 설정
핵심 Repository Secrets:
| Key | Value |
| DOCKER_USERNAME | 보안상 생략 |
| DOCKER_PASSWORD | {Docker PAT} |
| EC2_SSH_HOST | 43.200.110.36 |
| EC2_SSH_USER | ec2-user |
| EC2_SSH_KEY | (handdoc-key.pem 내용) |
>> Spring은 GitHub Actions로 Blue-Green 배포 자동화되며, FastAPI는 단일 Docker 배포로 구성된다.
9. RDS 설정(MySQL)
- RDS: handdoc
- 인스턴스: t3.micro
- 초기 스토리지: 30GB
- DB 접근: EC2 보안그룹 연결
- 타입: MySQL / TCP
탄력적 IP: 43.200.110.36
10. 완성된 전체 인프라 구조
EC2(t3.micro)
├─ Nginx
├─ Spring Boot (Blue)
├─ Spring Boot (Green)
└─ FastAPI (Sign/Whisper)
외부:
├─ Vercel(React FE)
├─ RDS(MySQL)
└─ MongoDB Atlas
11. 트러블슈팅 — FastAPI 컨테이너 unhealthy 문제 해결
배포 과정에서 FastAPI 컨테이너가 계속 unhealthy 로 떨어지는 문제가 있었다... 처음엔 코드 문제인가? Nginx 문제인가? 싶었는데, 원인은 전혀 다른 곳에 있었다.
문제 상황
FastAPI 컨테이너가 올라오긴 하는데, 몇 초 지나면 상태가 healthy → unhealthy 로 바뀌었다... docker ps 명령어를 입력해보면 빨갛게 떠 있었다.
따라서 팀원과 함께 원인을 계속 분석했다...

근데 결국 onnx 경량화 없이 해결하는 방법을 찾아내어 스스로 해결했당 ㅎ (뿌듯)
내가 내린 결론은 다음과 같다.
- AI 모델 로딩이 생각보다 오래 걸림 (TensorFlow Lite + MediaPipe 초기화)
- Docker 기본 healthcheck timeout이 3초라서 모델 로딩 중에 /health 응답을 못 받음
- 그 결과, 헬스체크 실패가 누적되어 FailingStreak = 30 → unhealthy 판정
즉, FastAPI는 열심히 모델을 메모리에 올리고 있을 뿐인데 Docker 입장에서는 '이 컨테이너 대답 안 하네?' 하고 바로 잘못된 판정 내린 것이라고 볼 수 있다.
모델 로딩이 이렇게 오래 걸릴 줄은 솔직히 예상 못 했다…
즉, FastAPI 자체 문제가 아니라 건강 체크가 너무 빨라서 AI 서버가 부팅을 다 하기 전에 '죽은 컨테이너'로 오해한 것이다.
- 모델 로딩 시간: 7~15초
- Docker healthcheck timeout: 3초
- 그 사이에 /health 요청 → 응답 없음 → 실패 판단
그래서 매번 unhealthy가 뜬 거구..
해결 방법 — healthcheck 타이밍 넉넉하게 주기!
해결은 간단했다. docker-compose.fastapi.yml 안의 healthcheck 시간을 FastAPI 특성에 맞춰 조정했다.
적용한 설정
interval: 30s # 검사 간격 넓힘
timeout: 15s # 응답 대기시간 여유줌
start_period: 60s # 모델 로딩 시간 고려해 1분 대기
수정 후 결과
healthcheck가 FastAPI의 'AI 모델 로딩 타임'을 기다려주기 시작했고 컨테이너는 정상적으로 healthy 상태를 유지했다.
또한 실제 Docker 로그에서도 모델 로딩 직후 바로 OK로 바뀌는 걸 확인했다.
결국 문제는 컨테이너가 아니라, 'AI 모델 초기화는 무겁다'는 걸 Healthcheck가 모르는 게 문제였다.
AI가 들어간 컨테이너는 반드시 healthcheck 시간을 모델 로딩 시간 기준으로 맞춰야 한다는 걸 이번 기회에 배워가게 되었다.

마무리
이 배포 구조는 단일 EC2로 비용을 최소화하면서도,
- 무중단 배포
- AI 모델 추론
- WebSocket
- SSL
- 프록시 라우팅
- DB 분리
까지 모두 포함한 효율적인 환경이라고 볼 수 있겠다~~!
'프로젝트 > EWHA 캡스톤 졸업프로젝트' 카테고리의 다른 글
| [졸업프로젝트] handDoc 수어 인식 모델 구축 튜토리얼 : MediaPipe → BiLSTM 학습까지 전 과정 (0) | 2025.11.24 |
|---|---|
| [졸업프로젝트] handDoc BE ERD 및 API 설계 (0) | 2025.11.23 |
| [졸업프로젝트] handDoc AI 모델(수어인식/음성교정) 구축 과정 (1) | 2025.11.18 |
| [졸업프로젝트] handDoc_ 서비스 핵심 기능과 전체 아키텍처 설계 (0) | 2025.11.18 |
| [졸업프로젝트] 청각장애인 대상 진료 플랫폼, handDoc 기획 (1) | 2025.11.12 |