DevOps

Docker Compose 실전 - Spring Boot + DB + Redis + Kafka 개발 환경 구축

백엔드 개발자 김승원 2026. 4. 14. 09:51

들어가며

"제 로컬에서는 잘 되는데요." 개발자라면 한 번은 들어봤거나 직접 했을 말입니다. 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 한 줄로 완벽한 개발 환경을 시작하세요.