Docker Compose — 여러 컨테이너를 하나의 명령으로 관리하기

최병현·2026년 6월 15일

spring boot

목록 보기
26/34

1. 개념 소개

Spring Boot 애플리케이션을 Docker로 띄우다 보면 곧 한계를 느낀다. 애플리케이션만 컨테이너로 띄워서는 동작하지 않는다. MySQL, Redis 같은 의존 서비스들도 컨테이너로 함께 띄워야 한다.

이걸 매번 docker run 명령을 여러 번 따로 입력해서 관리하면 포트, 네트워크, 환경변수, 실행 순서까지 신경 써야 할 게 너무 많아진다.

Docker Compose는 여러 개의 컨테이너를 하나의 YAML 파일로 정의하고, 하나의 명령으로 함께 실행/종료/관리할 수 있게 해주는 도구다.

docker-compose.yml 파일 하나에 "어떤 컨테이너들이 필요하고, 서로 어떻게 연결되는지"를 정의하면, docker compose up 한 줄로 전체 구성이 한 번에 실행된다.


2. 왜 필요한가

Docker Compose 없이 Spring Boot + MySQL + Redis를 띄운다면 이렇게 해야 한다.

# 네트워크 생성
docker network create my-app-network

# MySQL 컨테이너 실행
docker run -d --name mysql-db \
  --network my-app-network \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=mydb \
  -v mysql-data:/var/lib/mysql \
  -p 3306:3306 \
  mysql:8.0

# Redis 컨테이너 실행
docker run -d --name redis-cache \
  --network my-app-network \
  -p 6379:6379 \
  redis:7

# 애플리케이션 컨테이너 실행 (DB, Redis보다 늦게 실행해야 함)
docker run -d --name my-app \
  --network my-app-network \
  -e SPRING_DATASOURCE_URL=jdbc:mysql://mysql-db:3306/mydb \
  -e SPRING_REDIS_HOST=redis-cache \
  -p 8080:8080 \
  my-app:1.0

명령어가 길고, 순서도 신경 써야 하고, 종료할 때도 컨테이너마다 따로 stop/rm을 해야 한다. Docker Compose는 이 전체 구성을 파일로 선언해두고 명령 한 번으로 처리한다.

실무에서 Docker Compose가 쓰이는 대표적인 상황이다.

  • 로컬 개발 환경 구성 — 애플리케이션 + DB + Redis + 메시지 큐를 한 번에 띄우기
  • 통합 테스트 환경 — CI에서 테스트용 DB를 임시로 띄우고 테스트 후 정리
  • 소규모 서비스 배포 — 단일 서버에서 여러 서비스를 함께 운영
  • 팀 온보딩 — 새 팀원이 docker compose up 한 줄로 전체 개발 환경 구축

3. 전체 동작 흐름

docker-compose.yml을 작성하고 docker compose up을 실행했을 때 내부에서 어떤 일이 벌어지는지 보자.

[docker-compose.yml 작성]
    services:
      app:    (Spring Boot)
      db:     (MySQL)
      redis:  (Redis)

    ↓

[docker compose up -d 실행]
    |
    | ① 전용 네트워크 생성
    |   - 기본적으로 프로젝트 이름 기반의 네트워크 자동 생성
    |   - 모든 서비스가 이 네트워크에 연결됨
    ↓
    | ② depends_on 순서에 따라 컨테이너 시작
    |   - db, redis 먼저 시작
    |   - app은 db, redis가 시작된 후 시작
    ↓
    | ③ 각 서비스마다 컨테이너 생성 및 실행
    |   - db 컨테이너: mysql 이미지 기반, 볼륨 마운트
    |   - redis 컨테이너: redis 이미지 기반
    |   - app 컨테이너: Dockerfile로 빌드 또는 지정된 이미지 사용
    ↓
[모든 컨테이너 실행 중]
    |
    | 컨테이너 간 통신: 서비스 이름을 호스트명으로 사용
    |   app → db:3306
    |   app → redis:6379
    ↓
    | 호스트 ↔ 컨테이너: ports 설정에 따라 포트 매핑
    |   localhost:8080 → app 컨테이너의 8080
    ↓

[docker compose down 실행]
    |
    | 모든 컨테이너 중지 및 삭제
    | 네트워크 삭제
    | (볼륨은 기본적으로 유지됨 — -v 옵션 필요)

핵심은 모든 서비스가 같은 네트워크에 자동으로 연결되고, 서비스 이름으로 서로를 찾을 수 있다는 것이다. 별도로 네트워크를 만들거나 IP를 관리할 필요가 없다.


4. 핵심 구성 요소

4-1. services

실행할 컨테이너들을 정의하는 부분이다. 각 서비스는 하나의 컨테이너에 대응한다. 서비스 이름(app, db 등)은 컨테이너 간 통신 시 호스트명으로 사용된다.

4-2. build vs image

  • build — 로컬의 Dockerfile을 사용해서 이미지를 빌드. 우리 애플리케이션처럼 직접 만든 코드
  • image — 이미 만들어진 이미지를 가져와서 사용. MySQL, Redis처럼 공식 이미지를 그대로 쓰는 경우

4-3. ports

호스트와 컨테이너 간의 포트 매핑이다. "호스트포트:컨테이너포트" 형식으로 작성한다. 컨테이너 간 통신에는 필요 없고, 호스트(개발자 PC)에서 접근할 때만 필요하다.

4-4. environment

컨테이너에 전달할 환경변수다. Spring Boot의 application.yml에서 ${}로 참조하는 값들을 여기서 주입한다.

4-5. depends_on

서비스 간 시작 순서를 정의한다. app이 db에 의존한다면 db가 먼저 시작된다.

단, depends_on은 "컨테이너가 시작됨"을 보장할 뿐, "서비스가 준비됨"을 보장하지 않는다. MySQL 컨테이너가 시작되었어도 MySQL이 연결을 받을 준비가 되기까지는 시간이 걸릴 수 있다. condition: service_healthy와 헬스체크를 함께 써야 진짜 준비 상태를 기다릴 수 있다.

4-6. volumes

컨테이너의 데이터를 영구 보관하기 위한 저장 공간이다. DB 데이터처럼 컨테이너가 재시작되어도 유지되어야 하는 데이터에 사용한다.

4-7. networks

서비스 간 통신을 위한 네트워크다. 별도로 정의하지 않아도 Compose가 기본 네트워크를 자동으로 생성해서 같은 docker-compose.yml 안의 모든 서비스를 연결한다.


5. Spring Boot에서 어떻게 연결되는가

Spring Boot 애플리케이션의 application.yml은 환경변수를 참조하도록 작성하고, Docker Compose가 그 환경변수 값을 주입하는 구조다.

# application.yml (Spring Boot 프로젝트 내부)
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
  data:
    redis:
      host: ${REDIS_HOST}
      port: ${REDIS_PORT}
# docker-compose.yml
services:
  app:
    build: .
    environment:
      DB_HOST: db        # 서비스 이름 = 호스트명
      DB_PORT: 3306
      DB_NAME: mydb
      DB_USERNAME: root
      DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      REDIS_HOST: redis
      REDIS_PORT: 6379

DB_HOST: db처럼 서비스 이름을 그대로 호스트명으로 사용한다는 점이 핵심이다. 로컬 개발에서 localhost:3306으로 접속하던 걸, 컨테이너 환경에서는 db:3306으로 바꿔주는 역할을 환경변수가 담당한다.


6. 간단한 예제 코드

6-1. Spring Boot + MySQL + Redis 전체 구성

# docker-compose.yml
version: "3.8"

services:
  app:
    build: .
    container_name: my-app
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: docker
      DB_HOST: db
      DB_PORT: 3306
      DB_NAME: mydb
      DB_USERNAME: root
      DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      REDIS_HOST: redis
      REDIS_PORT: 6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: mysql:8.0
    container_name: mysql-db
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: mydb
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 5s
      retries: 10

  redis:
    image: redis:7
    container_name: redis-cache
    ports:
      - "6379:6379"

volumes:
  mysql-data:
# .env 파일 (docker-compose.yml과 같은 위치)
MYSQL_ROOT_PASSWORD=secret1234

6-2. 실행 및 관리 명령어

# 전체 서비스 빌드 및 실행 (백그라운드)
docker compose up -d --build

# 실행 중인 서비스 상태 확인
docker compose ps

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

# 전체 서비스 중지 (컨테이너, 네트워크 삭제, 볼륨은 유지)
docker compose down

# 볼륨까지 함께 삭제 (DB 데이터도 삭제됨 — 주의!)
docker compose down -v

# 특정 서비스만 재시작
docker compose restart app

6-3. 환경별 설정 분리 — override 파일

로컬 개발과 운영 환경의 설정이 다를 때, 기본 파일 + 환경별 override 파일로 분리하는 방법이다.

# docker-compose.yml (공통 설정)
services:
  app:
    build: .
    environment:
      DB_HOST: db
# docker-compose.override.yml (로컬 개발 — 자동으로 병합됨)
services:
  app:
    ports:
      - "8080:8080"
    volumes:
      - ./logs:/app/logs  # 로그를 호스트에서 바로 확인
# docker-compose.prod.yml (운영 환경)
services:
  app:
    restart: always
    environment:
      SPRING_PROFILES_ACTIVE: prod
# 운영 환경 실행 시 명시적으로 파일 지정
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

6-4. 헬스체크로 의존성 준비 상태 확인

# db 서비스에 헬스체크 추가
db:
  image: mysql:8.0
  healthcheck:
    test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
    interval: 5s
    timeout: 5s
    retries: 10

# app 서비스가 db의 헬스체크 통과를 기다림
app:
  depends_on:
    db:
      condition: service_healthy  # 단순 시작이 아니라 "정상 동작"까지 대기

7. 자주 헷갈리는 부분

depends_on은 "시작 순서"만 보장한다

depends_on: db라고 쓰면 db 컨테이너가 시작된 후에 app 컨테이너가 시작된다는 것만 보장한다. MySQL 프로세스가 떠 있어도 초기화 스크립트 실행 중이라 아직 연결을 받을 준비가 안 됐을 수 있다. 이때 Spring Boot가 먼저 연결을 시도하면 연결 실패로 시작이 안 될 수 있다. healthcheck + condition: service_healthy로 진짜 준비 상태를 확인해야 한다.

컨테이너 간 통신은 서비스 이름, 호스트 접근은 localhost

  • 컨테이너 ↔ 컨테이너 — 서비스 이름 사용 (예: app에서 db:3306)
  • 호스트(개발자 PC) ↔ 컨테이너 — localhost:매핑된포트 사용 (예: localhost:3306)

이 둘을 혼동하면 "로컬에서는 DB Tool로 접속되는데 앱 컨테이너에서는 연결이 안 된다" 같은 문제가 생긴다.

volumes는 down 해도 남는다

docker compose down은 컨테이너와 네트워크만 삭제한다. 볼륨에 저장된 DB 데이터는 삭제되지 않고 유지된다. 다시 up하면 이전 데이터가 그대로 있는 DB가 뜬다. 완전히 초기화하려면 docker compose down -v를 써야 한다.

build 캐시 때문에 코드 변경이 반영 안 될 때

docker compose up은 이미지가 이미 있으면 재빌드하지 않는다. 코드를 수정했는데 컨테이너에 반영이 안 된다면 --build 옵션을 추가해서 강제로 다시 빌드해야 한다.

docker compose up -d --build

8. 실무에서 중요한 포인트

.env 파일로 민감 정보 분리

비밀번호, API 키 같은 값을 docker-compose.yml에 직접 쓰면 Git에 그대로 노출된다. .env 파일에 값을 정의하고 .gitignore에 추가하면 docker-compose.yml은 Git에 올라가도 안전하다.

# .env (Git에 커밋하지 않음)
MYSQL_ROOT_PASSWORD=secret1234
JWT_SECRET=my-secret-key

# docker-compose.yml에서 참조
environment:
  MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
  JWT_SECRET: ${JWT_SECRET}
# .gitignore
.env

로컬 개발과 운영 환경의 Compose 파일 분리

로컬 개발에서는 코드 변경 시 즉시 반영되도록 볼륨 마운트, 디버그 포트 노출 등을 추가하고, 운영에서는 restart: always, 리소스 제한 같은 설정이 필요하다. 하나의 파일로 모든 환경을 처리하려고 하면 설정이 꼬인다. 기본 파일 + 환경별 override 파일 조합을 권장한다.

Docker Compose는 운영 오케스트레이션 도구가 아니다

Docker Compose는 단일 서버에서 여러 컨테이너를 관리하기 위한 도구다. 여러 서버에 걸친 배포, 자동 스케일링, 무중단 배포, 장애 복구 같은 기능은 Kubernetes 같은 오케스트레이션 도구의 영역이다. 소규모 서비스나 로컬/스테이징 환경에는 적합하지만, 대규모 운영 환경에서는 Compose만으로는 한계가 있다는 점을 알아두자.

CI에서 통합 테스트용으로 활용

CI 파이프라인에서 테스트 실행 전 docker compose up -d db redis로 테스트에 필요한 의존 서비스만 띄우고, 테스트가 끝나면 docker compose down -v로 정리하는 패턴이 자주 쓰인다. 매번 깨끗한 상태의 DB에서 테스트를 실행할 수 있다.


9. 정리

  • Docker Compose는 여러 컨테이너의 구성을 YAML 파일로 정의하고 한 번에 관리할 수 있게 해주는 도구다
  • 모든 서비스는 자동으로 같은 네트워크에 연결되고, 서비스 이름이 컨테이너 간 통신의 호스트명이 된다
  • build는 직접 만든 이미지, image는 기존 이미지를 사용할 때 쓴다
  • depends_on은 시작 순서만 보장한다. 진짜 준비 상태를 기다리려면 healthcheck를 함께 써야 한다
  • docker compose down은 컨테이너와 네트워크만 삭제하고, 볼륨은 -v 옵션 없이는 유지된다
  • 민감 정보는 .env 파일로 분리하고 .gitignore에 추가한다
  • Docker Compose는 단일 서버용 도구이며, 대규모 운영에는 Kubernetes 같은 오케스트레이션 도구가 필요하다

10. 느낀 점

Docker만 배웠을 때는 컨테이너 하나 띄우는 게 전부였는데, 실제 서비스는 앱 혼자 동작하는 게 아니라 DB, 캐시랑 항상 같이 움직인다는 걸 Docker Compose를 쓰면서 체감했다.

특히 depends_on이 "시작 순서"만 보장한다는 걸 모르고 앱이 DB 연결 실패로 죽는 걸 보고 당황했던 기억이 있다. healthcheck를 추가하고 나서야 안정적으로 떴다. 이런 디테일은 직접 docker compose up을 돌려보고 로그를 확인해봐야 알게 되는 것 같다. Compose 파일 하나로 전체 개발 환경이 재현된다는 건 정말 편리한데, 그 편리함 뒤에 있는 동작 원리를 알아야 문제가 생겼을 때 헤매지 않는다는 걸 느꼈다.

profile
Develop

0개의 댓글