본문으로 건너뛰기

Docker Compose로 Nginx + Tomcat 구성

Docker Compose는 여러 컨테이너로 구성된 애플리케이션을 단일 YAML 파일로 정의하고 관리하는 도구입니다. Nginx, Spring Boot(Tomcat), PostgreSQL을 각각 별도 컨테이너로 실행하면서 하나의 명령으로 전체 스택을 기동하고 중단할 수 있습니다. 이 챕터에서는 실제 운영 환경에 가까운 완전한 docker-compose.yml 예시와 함께 각 설정의 의미를 깊이 있게 살펴봅니다.


docker compose vs docker-compose 차이 (v1 vs v2)

Docker Compose는 두 가지 버전이 존재합니다.

구분docker-compose (v1)docker compose (v2)
설치별도 Python 패키지Docker Engine 내장 플러그인
명령어docker-compose updocker compose up
파일명docker-compose.ymldocker-compose.yml (동일)
지원 상태2023년 7월 EOL현재 공식 지원
성능상대적으로 느림더 빠른 병렬 처리

현재는 반드시 docker compose (공백, v2)를 사용하세요. Docker Desktop 및 최신 Docker Engine에는 v2가 기본 포함되어 있습니다.


프로젝트 디렉터리 구조

프로젝트 루트/
├── docker-compose.yml
├── docker-compose.override.yml # 개발 환경 자동 적용
├── docker-compose.prod.yml # 프로덕션 환경
├── .env # 환경 변수 파일 (git 제외)
├── .env.example # 환경 변수 예시 (git 포함)
├── Dockerfile # Spring Boot 애플리케이션 이미지
├── nginx/
│ ├── nginx.conf # Nginx 메인 설정
│ └── conf.d/
│ └── app.conf # 가상 호스트 / 프록시 설정
├── db/
│ └── init/
│ └── 01-schema.sql # DB 초기화 스크립트
└── src/ # Spring Boot 소스 코드

환경 변수 파일 (.env)

민감한 정보는 코드에 직접 포함하지 않고 .env 파일로 분리합니다. Docker Compose는 자동으로 프로젝트 루트의 .env 파일을 읽어 환경 변수를 주입합니다.

# .env (실제 값, git에서 제외 - .gitignore에 추가)
POSTGRES_DB=myapp_db
POSTGRES_USER=myapp_user
POSTGRES_PASSWORD=SuperSecurePassword123!
POSTGRES_HOST=db
POSTGRES_PORT=5432

APP_PORT=8080
SPRING_PROFILES_ACTIVE=prod

NGINX_HTTP_PORT=80
NGINX_HTTPS_PORT=443
# .env.example (템플릿, git에 포함)
POSTGRES_DB=myapp_db
POSTGRES_USER=myapp_user
POSTGRES_PASSWORD=change_me_in_production
POSTGRES_HOST=db
POSTGRES_PORT=5432

APP_PORT=8080
SPRING_PROFILES_ACTIVE=prod

NGINX_HTTP_PORT=80
NGINX_HTTPS_PORT=443

docker-compose.yml 전체 예시

아래는 Nginx + Spring Boot + PostgreSQL을 구성하는 완전한 docker-compose.yml입니다. 각 설정 항목에 주석을 달아 의미를 명확히 설명합니다.

version: '3.9'

services:

# ════════════════════════════════════════════════════
# 1. Nginx - 리버스 프록시 / 정적 파일 서빙
# ════════════════════════════════════════════════════
nginx:
image: nginx:1.25-alpine # 특정 버전 고정 (latest 사용 지양)
container_name: nginx-proxy
ports:
- "${NGINX_HTTP_PORT:-80}:80" # 호스트:컨테이너 포트 매핑
- "${NGINX_HTTPS_PORT:-443}:443"
volumes:
# 설정 파일: 읽기 전용(:ro)으로 마운트
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
# SSL 인증서: Let's Encrypt certbot 연동
- certbot-conf:/etc/letsencrypt:ro
- certbot-www:/var/www/certbot:ro
# 로그: named volume으로 영속 보관
- nginx-logs:/var/log/nginx
# 정적 파일: Spring Boot가 생성한 파일 공유
- static-files:/usr/share/nginx/html/static:ro
environment:
- TZ=Asia/Seoul
depends_on:
app:
condition: service_healthy # app 헬스체크 통과 후에 시작
networks:
- frontend
restart: unless-stopped
# 리소스 제한 (프로덕션 권장)
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M

# ════════════════════════════════════════════════════
# 2. Spring Boot - WAS (Web Application Server)
# ════════════════════════════════════════════════════
app:
build:
context: . # Dockerfile 위치
dockerfile: Dockerfile
args:
- BUILD_VERSION=1.0.0
image: myapp:latest # 빌드된 이미지에 태그 지정
container_name: spring-app
# ports 대신 expose: 컨테이너 네트워크 내부에만 노출
expose:
- "8080"
volumes:
- app-logs:/app/logs
- static-files:/app/static # Nginx와 정적 파일 공유
environment:
# Spring 설정
- SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-prod}
# DB 연결 (서비스 이름 'db'가 DNS로 자동 해석됨)
- SPRING_DATASOURCE_URL=jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
- SPRING_DATASOURCE_USERNAME=${POSTGRES_USER}
- SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD}
# JVM 튜닝
- JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseContainerSupport
- TZ=Asia/Seoul
depends_on:
db:
condition: service_healthy # DB 헬스체크 통과 후에 시작
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s # 30초마다 체크
timeout: 10s # 10초 내 응답 없으면 실패
retries: 3 # 3회 연속 실패 시 unhealthy
start_period: 90s # 시작 후 90초간은 실패해도 무시 (JVM 워밍업)
networks:
- frontend
- backend
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2.0'
memory: 1536M

# ════════════════════════════════════════════════════
# 3. PostgreSQL - 데이터베이스
# ════════════════════════════════════════════════════
db:
image: postgres:16-alpine
container_name: postgres-db
expose:
- "5432" # 외부 노출 없음, backend 네트워크만
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- PGDATA=/var/lib/postgresql/data/pgdata
- TZ=Asia/Seoul
volumes:
- postgres-data:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d:ro # 초기화 SQL 자동 실행
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- backend
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M

# ════════════════════════════════════════════════════
# 네트워크 정의
# ════════════════════════════════════════════════════
networks:
frontend:
driver: bridge
name: app-frontend
backend:
driver: bridge
name: app-backend
internal: true # 외부 인터넷 접근 완전 차단

# ════════════════════════════════════════════════════
# 볼륨 정의
# ════════════════════════════════════════════════════
volumes:
postgres-data:
name: myapp-postgres-data
nginx-logs:
name: myapp-nginx-logs
app-logs:
name: myapp-app-logs
static-files:
name: myapp-static-files
certbot-conf:
name: myapp-certbot-conf
certbot-www:
name: myapp-certbot-www

Nginx 설정 파일 (볼륨 주입 패턴)

컨테이너 이미지 재빌드 없이 Nginx 설정을 변경할 수 있도록 설정 파일을 bind mount로 주입합니다.

# nginx/conf.d/app.conf
upstream spring_app {
server app:8080; # Docker DNS: 서비스명 'app'이 자동 해석
keepalive 32;
}

server {
listen 80;
server_name example.com www.example.com;

# Let's Encrypt 인증서 발급 경로
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}

# HTTP → HTTPS 리다이렉트
location / {
return 301 https://$host$request_uri;
}
}

server {
listen 443 ssl http2;
server_name example.com www.example.com;

# SSL 인증서
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;

# 정적 파일 직접 서빙
location /static/ {
root /usr/share/nginx/html;
expires 30d;
add_header Cache-Control "public, immutable";
}

# API / 동적 요청 프록시
location / {
proxy_pass http://spring_app;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
}
}

주요 Docker Compose 명령어

서비스 시작 및 관리

# 모든 서비스 백그라운드 실행
docker compose up -d

# 특정 서비스만 실행
docker compose up -d nginx app

# 이미지 재빌드 후 실행 (코드 변경 시)
docker compose up -d --build

# 강제 재생성 (캐시 무시)
docker compose up -d --force-recreate

로그 확인

# 전체 서비스 실시간 로그
docker compose logs -f

# 특정 서비스 로그
docker compose logs -f app

# 최근 100줄만 출력
docker compose logs --tail=100 nginx

# 타임스탬프 포함
docker compose logs -f -t db

상태 확인

# 서비스 상태 및 포트 확인
docker compose ps

# 상세 정보 (헬스체크 상태 포함)
docker compose ps --format json | jq .

# 리소스 사용량 확인
docker stats $(docker compose ps -q)

서비스 중단 및 정리

# 서비스 중단 (컨테이너 제거, 볼륨 유지)
docker compose down

# 볼륨까지 모두 삭제 (데이터 초기화, 주의!)
docker compose down -v

# 이미지까지 모두 삭제
docker compose down --rmi all -v

컨테이너 내부 접근

# Nginx 컨테이너 셸 접속
docker compose exec nginx sh

# Spring Boot 컨테이너 셸 접속
docker compose exec app bash

# PostgreSQL 접속
docker compose exec db psql -U ${POSTGRES_USER} -d ${POSTGRES_DB}

서비스 스케일 업

# app 서비스를 3개 인스턴스로 확장
docker compose up -d --scale app=3

# 확인
docker compose ps

스케일 업 시 주의사항:

  • container_name이 고정되어 있으면 스케일 업 불가 → 제거해야 함
  • Nginx upstream 블록은 DNS round-robin 방식으로 자동 분산됨
  • ports를 직접 매핑하면 포트 충돌 발생 → expose만 사용
# 스케일 업을 위한 app 서비스 수정 (container_name, ports 제거)
app:
build: .
expose:
- "8080"
# container_name: spring-app ← 이 줄 제거

health check 설정 심화

헬스체크는 컨테이너가 단순히 실행 중인지가 아니라, 실제로 정상 동작하는지를 확인합니다. depends_oncondition: service_healthy와 함께 사용하면 의존 서비스가 완전히 준비된 후에만 다음 서비스가 시작됩니다.

# Spring Boot 헬스체크 (Spring Actuator 사용)
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 90s # 컨테이너 시작 후 이 시간 동안은 실패 무시

# PostgreSQL 헬스체크
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5

# Nginx 헬스체크
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 60s
timeout: 10s
retries: 3

헬스체크 상태는 docker compose ps에서 (healthy), (unhealthy), (starting) 로 확인할 수 있습니다.


고수 팁

1. 이미지 버전 고정

image: nginx:latest 대신 image: nginx:1.25-alpine처럼 구체적인 버전을 지정하면 예상치 못한 업데이트로 인한 장애를 방지할 수 있습니다.

2. .dockerignore로 빌드 컨텍스트 최소화

# .dockerignore
target/
*.log
.git/
.env
node_modules/

3. Nginx 설정 문법 검사

# 설정 파일 변경 후 문법 확인
docker compose exec nginx nginx -t

# 문법 이상 없으면 무중단 리로드
docker compose exec nginx nginx -s reload

4. 컨테이너 재시작 없이 환경 변수 변경

환경 변수를 변경하면 컨테이너 재생성이 필요합니다. docker compose up -d는 변경 사항을 자동으로 감지하여 해당 서비스만 재생성합니다.