이전 글: 6편 — Claude로 블로그 초안 자동 생성 + Notion 동기화
다음 글: 8편 — 3개월 운영 후기 — 잘 된 것, 실패한 것, 다음 목표
왜 Docker Compose를 선택했나
개인 프로젝트라도 운영 환경에서는 로컬 개발 환경과 완전히 분리되어야 한다.
Docker Compose를 선택한 이유는 세 가지다.
- 환경 일관성: 로컬 맥에서 개발하고 리눅스 서버에서 운영해도 동일하게 동작한다
- 서비스 격리: 컨테이너별로 독립 실행되어 한 서비스 재시작이 다른 서비스에 영향을 주지 않는다
- 민감 정보 분리: API 키와 DB 비밀번호를 코드 밖으로 빼는 구조를 강제하게 된다
Kubernetes는 단일 호스트 운영에는 과하다고 생각하였으며,
Docker Compose로 충분히 관리 가능하다는 결론을 내렸다.
4개 컨테이너 역할 분담
컨테이너는 총 4개다.
각각 하나의 역할만 담당한다.
| 컨테이너 | 역할 | 포트 |
|---|---|---|
| stock-nginx | 리버스 프록시 + 이미지 서빙 | :80 |
| stock-app | Spring Boot REST API | :8080 (내부) |
| stock-front | Vue.js 정적 파일 서빙 | (내부) |
| mysql-server | 데이터베이스 | :3306 (내부) |
외부에서 직접 접근할 수 있는 포트는 :80 하나만 열어둠으로서,
나머지는 모두 stock-network 브리지 내부에서만 연결되게 된다.
Spring Boot가 jdbc:mysql://mysql:3306/stockdb로 접속하는 것도 컨테이너 이름이 DNS로 해석되기 때문이다.

민감 정보 분리 전략
API 키와 DB 비밀번호를 소스코드에 포함하지 않는 것이 이 구성에서 가장 신경 쓴 부분이다.
방법은 볼륨 마운트로 관리하는 것이다.
application-prod.properties 파일을 Git 외부 경로에 보관하고, 컨테이너 실행 시 주입한다.
# docker-compose.yml
services:
spring-boot:
volumes:
- /Volumes/ServerData/config/application-prod.properties:/app/config/
- /Volumes/ServerData/docker/db-data:/var/lib/mysql
- /Volumes/ServerData/images:/app/images
environment:
- SPRING_PROFILES_ACTIVE=prod
# application-prod.properties (Git 미포함)
spring.datasource.url=jdbc:mysql://mysql:3306/stockdb
spring.datasource.password=YOUR_DB_PASSWORD
openai.api.key=YOUR_OPENAI_KEY
anthropic.api.key=YOUR_ANTHROPIC_KEY
gemini.api.key=YOUR_GEMINI_KEY
Git에는 application.properties(기본값)와 application-local.properties(로컬 개발용)만 포함되며application-prod.properties는 .gitignore에 명시하고, 서버에만 직접 관리한다.

Nginx 설정에서 중요한 두 가지
첫째, 타임아웃을 300초로 늘렸다.
기본값 60초로는 Claude API 응답을 기다리는 도중 Nginx가 연결을 끊어버린다.
처음에 이 설정 없이 배포했다가 블로그 생성 엔드포인트가 계속 502로 끊겼다.
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
AI API 호출이 포함된 엔드포인트는 응답이 느릴 수 있다.
60초 기본값으로 운영하면 긴 응답을 기다리는 모든 요청이 502가 된다.
둘째, 이미지는 Nginx가 직접 서빙한다.
Spring Boot와 Nginx가 같은 볼륨을 마운트하고 있다.
Spring Boot가 /app/images/에 이미지를 저장하면 Nginx가 /images/ URL로 즉시 서빙한다.
Spring Boot를 경유하지 않으므로 불필요한 I/O가 없다.
location /images/ {
alias /app/images/;
expires 30d;
add_header Cache-Control "public, immutable";
}
운영 중 발견한 문제들
3개월 운영하면서 코드 레벨이 아닌 인프라 레벨에서 발견한 문제를 정리했다.
모두 "당장 동작은 하지만 운영 서버라면 고쳐야 하는 것들"이다.

이 네 가지 중 MySQL 포트 노출은 우선적으로 고칠 예정이며,
나머지 세 개는 개인 운영 서버라는 점에서 감수하고 있지만,
실제 서비스 수준으로 올리려면 모두 처리할 계획이다.
정리
개인 프로젝트지만 운영 환경에서 배운 것들을 정리하면 다음과 같다.
| 항목 | 선택 | 이유 |
|---|---|---|
| 오케스트레이션 | Docker Compose | 단일 호스트에 Kubernetes는 과함 |
| 외부 노출 포트 | :80 하나만 | Nginx가 모든 트래픽 진입점 |
| 민감 정보 | 볼륨 마운트 주입 | Git 저장소에 절대 포함하지 않음 |
| Nginx 타임아웃 | 300초 | AI API 응답 대기 고려 |
| 이미지 서빙 | Nginx 직접 | Spring Boot 경유 불필요 I/O 제거 |
다음 편은 이 시리즈의 마지막이다.
3개월 운영하면서 실제 수치로 확인한 것들, 잘 된 것과 실패한 것,
그리고 앞으로 고쳐야 할 기술 부채를 정리한다.