들어가며
"제 로컬에서는 잘 되는데요." 개발자라면 한 번은 들어봤거나 직접 했을 말입니다. PostgreSQL 버전이 다르고, Redis가 설치되어 있지 않고, Kafka 설정이 미묘하게 다릅니다. 새로운 팀원이 합류하면 개발 환경 구축에만 하루 이상을 소비합니다. 이 모든 문제의 해답은 Docker Compose입니다.
docker compose up 한 줄이면 Spring Boot 앱이 필요로 하는 모든 인프라가 동일한 환경으로 구동됩니다. 이 글에서는 PostgreSQL, Redis, Kafka, Prometheus, Grafana까지 포함한 풀스택 개발 환경을 docker-compose.yml 하나로 구성하는 방법과, Spring Boot 3.1+의 Docker Compose 자동 인식 기능, 그리고 실전에서 자주 겪는 트러블슈팅까지 다루겠습니다.
1. 프로젝트 구조
my-project/
├── docker/
│ ├── docker-compose.yml
│ ├── docker-compose.dev.yml # 개발용 오버라이드
│ ├── docker-compose.test.yml # 테스트용 오버라이드
│ ├── .env # 환경변수
│ ├── prometheus/
│ │ └── prometheus.yml
│ ├── grafana/
│ │ └── datasources.yml
│ └── kafka/
│ └── init-topics.sh
├── src/
│ └── main/
│ └── resources/
│ └── application.yml
└── build.gradle
2. 핵심 docker-compose.yml
version: '3.8'
services:
# === 데이터베이스 ===
postgres:
image: postgres:16-alpine
container_name: my-postgres
environment:
POSTGRES_DB: ${DB_NAME:-myapp}
POSTGRES_USER: ${DB_USER:-myuser}
POSTGRES_PASSWORD: ${DB_PASSWORD:-mypassword}
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
ports:
- "${DB_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d # 초기화 SQL
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myuser} -d ${DB_NAME:-myapp}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- backend
# === 캐시 ===
redis:
image: redis:7-alpine
container_name: my-redis
command: redis-server --requirepass ${REDIS_PASSWORD:-redispass} --maxmemory 256mb --maxmemory-policy allkeys-lru
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redispass}", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- backend
# === 메시지 브로커 ===
zookeeper:
image: confluentinc/cp-zookeeper:7.6.0
container_name: my-zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
networks:
- backend
kafka:
image: confluentinc/cp-kafka:7.6.0
container_name: my-kafka
depends_on:
zookeeper:
condition: service_started
ports:
- "${KAFKA_PORT:-9092}:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:${KAFKA_PORT:-9092}
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
healthcheck:
test: ["CMD", "kafka-broker-api-versions", "--bootstrap-server", "localhost:29092"]
interval: 10s
timeout: 10s
retries: 5
networks:
- backend
# Kafka UI (개발 편의)
kafka-ui:
image: provectuslabs/kafka-ui:latest
container_name: my-kafka-ui
depends_on:
kafka:
condition: service_healthy
ports:
- "8089:8080"
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092
networks:
- backend
# === 모니터링 ===
prometheus:
image: prom/prometheus:v2.51.0
container_name: my-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=7d'
networks:
- backend
grafana:
image: grafana/grafana:10.4.0
container_name: my-grafana
depends_on:
- prometheus
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml
networks:
- backend
volumes:
postgres_data:
redis_data:
prometheus_data:
grafana_data:
networks:
backend:
driver: bridge
3. 환경변수 관리 (.env)
# .env 파일 - docker-compose.yml에서 ${변수명:-기본값}으로 참조
# Database
DB_NAME=myapp
DB_USER=myuser
DB_PASSWORD=mypassword
DB_PORT=5432
# Redis
REDIS_PASSWORD=redispass
REDIS_PORT=6379
# Kafka
KAFKA_PORT=9092
# App
SPRING_PROFILES_ACTIVE=dev
주의: .env 파일은 .gitignore에 추가하고, .env.example을 커밋하세요.
# .gitignore
.env
# .env.example (커밋 대상)
DB_NAME=myapp
DB_USER=myuser
DB_PASSWORD=changeme
REDIS_PASSWORD=changeme
4. Prometheus 설정
prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
scrape_interval: 5s
static_configs:
- targets: ['host.docker.internal:8080'] # 호스트의 Spring Boot
labels:
application: 'my-app'
- job_name: 'postgres'
static_configs:
- targets: ['postgres-exporter:9187']
- job_name: 'redis'
static_configs:
- targets: ['redis-exporter:9121']
grafana/datasources.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
5. healthcheck와 depends_on
서비스 간 의존 관계가 있을 때, healthcheck + depends_on condition을 사용하면 올바른 순서로 기동됩니다.
# depends_on의 condition 옵션
services:
app:
build: .
depends_on:
postgres:
condition: service_healthy # healthcheck 통과 후 시작
redis:
condition: service_healthy
kafka:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-myapp}
SPRING_DATASOURCE_USERNAME: ${DB_USER:-myuser}
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD:-mypassword}
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
SPRING_DATA_REDIS_PASSWORD: ${REDIS_PASSWORD:-redispass}
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:29092
| condition | 동작 | 사용 시점 |
|---|---|---|
| service_started | 컨테이너 시작되면 즉시 | 의존성 약한 서비스 |
| service_healthy | healthcheck 통과 후 | DB, 캐시 등 준비 필요한 서비스 |
| service_completed_successfully | 종료 코드 0으로 완료 후 | 초기화 스크립트 등 |
6. 프로필별 Compose 파일
환경별로 설정을 분리하면 유지보수가 쉬워집니다.
docker-compose.dev.yml (개발용 오버라이드)
version: '3.8'
services:
postgres:
ports:
- "5432:5432" # 외부 접속 허용 (DBeaver 등)
redis:
ports:
- "6379:6379"
kafka-ui:
profiles:
- dev # dev 프로필에서만 활성화
# pgAdmin (개발 편의)
pgadmin:
image: dpage/pgadmin4:latest
container_name: my-pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: admin@local.com
PGADMIN_DEFAULT_PASSWORD: admin
ports:
- "5050:80"
networks:
- backend
profiles:
- dev
docker-compose.test.yml (테스트용)
version: '3.8'
services:
postgres:
environment:
POSTGRES_DB: myapp_test
ports:
- "5433:5432" # 테스트용 포트 분리
tmpfs:
- /var/lib/postgresql/data # 메모리에서 실행 (빠름)
redis:
ports:
- "6380:6379" # 테스트용 포트 분리
tmpfs:
- /data
실행 명령어
# 개발 환경 (기본 + dev 오버라이드)
docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile dev up -d
# 테스트 환경
docker compose -f docker-compose.yml -f docker-compose.test.yml up -d
# 축약을 위한 Makefile
# Makefile
dev-up:
docker compose -f docker/docker-compose.yml -f docker/docker-compose.dev.yml --profile dev up -d
dev-down:
docker compose -f docker/docker-compose.yml -f docker/docker-compose.dev.yml --profile dev down
test-up:
docker compose -f docker/docker-compose.yml -f docker/docker-compose.test.yml up -d
logs:
docker compose -f docker/docker-compose.yml logs -f
clean:
docker compose -f docker/docker-compose.yml down -v --remove-orphans
7. Spring Boot 3.1+ Docker Compose 자동 인식
Spring Boot 3.1부터 spring-boot-docker-compose 모듈이 추가되어, 앱 시작 시 자동으로 Docker Compose를 실행하고 연결 정보를 주입합니다.
의존성 추가
dependencies {
// Spring Boot 3.1+
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
}
application.yml
spring:
docker:
compose:
enabled: true
file: docker/docker-compose.yml
lifecycle-management: start-and-stop # 앱 시작/종료 시 컨테이너도 관리
skip:
in-tests: false # 테스트에서도 사용할지 여부
# 아래 설정은 자동으로 감지되어 주입됨!
# spring.datasource.url
# spring.datasource.username
# spring.datasource.password
# spring.data.redis.host
# spring.data.redis.port
# spring.kafka.bootstrap-servers
동작 원리: Spring Boot가 docker-compose.yml의 서비스 이미지를 분석하여 자동으로 연결 정보를 구성합니다.
| Docker 이미지 | 자동 설정 항목 |
|---|---|
| postgres / mysql | spring.datasource.* |
| redis | spring.data.redis.* |
| confluentinc/cp-kafka | spring.kafka.* |
| mongo | spring.data.mongodb.* |
| elasticsearch | spring.elasticsearch.* |
| rabbitmq | spring.rabbitmq.* |
8. Spring Boot application.yml 연동
# Docker Compose 자동 인식을 쓰지 않는 경우의 수동 설정
spring:
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:myapp}
username: ${DB_USER:myuser}
password: ${DB_PASSWORD:mypassword}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 3000
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:redispass}
lettuce:
pool:
max-active: 16
max-idle: 8
kafka:
bootstrap-servers: ${KAFKA_HOST:localhost}:${KAFKA_PORT:9092}
consumer:
group-id: my-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# Actuator (Prometheus 연동)
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
metrics:
tags:
application: my-app
9. 볼륨 관리 전략
# Named Volume - Docker가 관리, 데이터 영속성 보장
volumes:
postgres_data: # docker volume ls로 확인 가능
redis_data:
# Bind Mount - 호스트 디렉토리를 직접 마운트
services:
postgres:
volumes:
# Named Volume: 데이터 영속화
- postgres_data:/var/lib/postgresql/data
# Bind Mount: 초기화 스크립트 (읽기 전용)
- ./init-scripts:/docker-entrypoint-initdb.d:ro
# tmpfs: 메모리에 마운트 (테스트용, 빠르지만 휘발성)
services:
postgres-test:
tmpfs:
- /var/lib/postgresql/data
# 볼륨 관리 명령어
# docker volume ls # 볼륨 목록
# docker volume inspect postgres_data # 볼륨 상세
# docker volume rm postgres_data # 볼륨 삭제
# docker compose down -v # 컨테이너 + 볼륨 모두 삭제
10. 네트워크 분리
services:
# 프론트엔드는 frontend 네트워크만
nginx:
networks:
- frontend
# 앱은 양쪽 네트워크에 연결
app:
networks:
- frontend
- backend
# DB는 backend 네트워크만 (외부 접근 차단)
postgres:
networks:
- backend
redis:
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 외부 인터넷 접근 차단
11. 실전 트러블슈팅
문제 1: 포트 충돌
# 에러: Bind for 0.0.0.0:5432 failed: port is already allocated
# 해결 1: 사용 중인 프로세스 확인
lsof -i :5432
# 해결 2: .env에서 포트 변경
DB_PORT=5433
# 해결 3: 로컬 PostgreSQL 중지
brew services stop postgresql@16
문제 2: 컨테이너 간 통신 실패
# 앱에서 DB 연결 실패 시
# 원인: localhost 대신 서비스명을 사용해야 함
# 잘못됨: jdbc:postgresql://localhost:5432/myapp
# 올바름: jdbc:postgresql://postgres:5432/myapp
# 디버깅: 컨테이너 내부에서 DNS 확인
docker compose exec app ping postgres
docker compose exec app nslookup postgres
문제 3: Kafka 연결 문제
# 흔한 실수: ADVERTISED_LISTENERS 설정 오류
# 컨테이너 내부 통신용: kafka:29092 (PLAINTEXT)
# 호스트에서 접근용: localhost:9092 (PLAINTEXT_HOST)
# Spring Boot 앱이 Docker 내부에서 실행될 때:
# spring.kafka.bootstrap-servers=kafka:29092
# Spring Boot 앱이 호스트에서 실행될 때:
# spring.kafka.bootstrap-servers=localhost:9092
문제 4: 볼륨 권한 문제
# PostgreSQL 데이터 디렉토리 권한 오류
# Error: data directory has wrong ownership
# 해결: Named Volume 사용 (Docker가 권한 관리)
volumes:
postgres_data: # Bind Mount 대신 Named Volume
# 또는 사용자 지정
services:
postgres:
user: "${UID:-1000}:${GID:-1000}"
문제 5: 메모리 부족
# Docker Desktop 메모리 제한 확인 (기본 2GB)
# Settings > Resources > Memory: 최소 4GB 권장
# 서비스별 메모리 제한 설정
services:
postgres:
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
kafka:
deploy:
resources:
limits:
memory: 1G
environment:
KAFKA_HEAP_OPTS: "-Xmx512m -Xms512m"
유용한 디버깅 명령어
# 컨테이너 상태 확인
docker compose ps
# 특정 서비스 로그 (실시간)
docker compose logs -f postgres
# 컨테이너 내부 접속
docker compose exec postgres bash
docker compose exec redis redis-cli -a redispass
# 리소스 사용량 모니터링
docker stats
# 전체 초기화 (데이터 포함)
docker compose down -v --remove-orphans
docker system prune -f
마치며
Docker Compose는 개발 환경의 "Infrastructure as Code"입니다. 한 번 잘 구성해두면 팀 전체가 동일한 환경에서 개발할 수 있고, 신규 입사자의 온보딩 시간이 획기적으로 줄어듭니다.
- docker-compose.yml에 모든 인프라를 정의하고, healthcheck + depends_on으로 안전한 기동 순서를 보장하세요.
- .env 파일로 환경변수를 관리하되, 민감 정보는 절대 커밋하지 마세요.
- 프로필별 오버라이드 파일(dev, test)로 환경을 분리하세요.
- Spring Boot 3.1+의 Docker Compose 자동 인식 기능을 활용하면 연결 설정을 수동으로 관리할 필요가 없습니다.
- Named Volume으로 데이터를 영속화하고, 테스트 환경에서는 tmpfs로 속도를 높이세요.
- 네트워크 분리로 불필요한 외부 접근을 차단하세요.
이 글의 docker-compose.yml을 여러분의 프로젝트에 맞게 수정하여 사용하시면 됩니다. docker compose up -d 한 줄로 완벽한 개발 환경을 시작하세요.
'DevOps' 카테고리의 다른 글
| Nginx 완벽 가이드 - 리버스 프록시부터 로드 밸런싱까지 (1) | 2026.04.15 |
|---|---|
| Git 고급 전략 - 브랜치 전략부터 위기 탈출까지 (1) | 2026.04.15 |
| Prometheus + Grafana 실전 구축 - Spring Boot 모니터링 완벽 가이드 (2) | 2026.04.09 |
| Terraform으로 인프라 코드화 - AWS 실전 예제로 배우는 IaC (0) | 2026.04.07 |
| Kubernetes 실전 운영 - Helm, Ingress, HPA 오토스케일링 (0) | 2026.04.07 |