본문 바로가기

개발 기록/미국 증시 분석 자동화 시스템 구축기

Docker Compose 4개 컨테이너 운영 구성 — Nginx 리버스 프록시, 민감 정보 볼륨 분리, 운영 이슈 [구축기 7편]

이전 글: 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개월 운영하면서 실제 수치로 확인한 것들, 잘 된 것과 실패한 것,
그리고 앞으로 고쳐야 할 기술 부채를 정리한다.