테마 전환

Docker Compose 멀티 서비스 오케스트레이션: 로컬 개발 환경을 한 번에 시작하기

Easton editorial illustration: route-map drafting table

입사 첫날 오후, 화면에 뜬 다섯 번째 오류 창을 바라보고 있었습니다. MySQL 포트는 이미 사용 중이었고, Redis 버전은 호환되지 않았으며, RabbitMQ는 아무리 해도 연결되지 않았습니다. 옆자리 선배가 화면을 보더니 한숨을 쉬며 물었습니다. “이걸 얼마나 설치한 거예요?”

“오전 9시부터 지금까지요.” 작은 목소리로 답했습니다.

4시간. 데이터베이스 3개를 설치하는 데 4시간이 걸렸습니다. 그런데 이것은 시작일 뿐이었습니다. ElasticSearch와 MongoDB도 설치해야 했습니다.

그때 처음 깨달았습니다. 로컬 개발 환경은 정말 큰 함정이며, 끝이 보이지 않을 정도로 깊다는 사실을 말입니다.

이후 우리 팀은 Docker Compose로 전환했습니다. 새 팀원이 저장소를 clone하고 docker-compose up -d를 실행하면 5분 뒤 모든 서비스가 시작됐습니다. MySQL, Redis, RabbitMQ, API, Web을 명령어 한 줄로 깔끔하게 올렸습니다. 프로젝트를 전환할 때는 다른 디렉터리로 이동해 그곳의 compose 파일을 시작하면 됐습니다. 정리할 때는 docker-compose down -v로 데이터 볼륨까지 흔적 없이 삭제했습니다.

이 글에서는 바로 이 변화를 다룹니다. Docker Compose로 여러 서비스를 구성해 로컬 개발 환경을 ‘악몽’에서 ‘명령어 한 줄이면 되는 일’로 바꾸는 방법을 살펴보겠습니다.

멀티 서비스 오케스트레이션이 필요한 이유

솔직히 10년 전 모놀리식 애플리케이션을 개발할 때만 해도 환경 구성은 아주 단순했습니다. JDK를 설치하고 데이터베이스 연결 문자열을 설정하면 프로젝트가 실행됐습니다. 하지만 지금은 다릅니다.

대부분의 프로젝트는 마이크로서비스 아키텍처로 분리됐거나 최소한 프런트엔드와 백엔드가 나뉘어 있습니다. 일반적인 로컬 개발 환경에는 적어도 프런트엔드 Web 서비스, 백엔드 API 서비스, 비즈니스 데이터를 저장하는 MySQL, 캐시와 Session을 처리하는 Redis, 비동기 메시지를 처리하는 RabbitMQ가 필요합니다. 검색을 위한 ElasticSearch나 로그 저장용 MongoDB를 추가하는 프로젝트도 있습니다.

여기서 문제가 시작됩니다.

수동 설치의 고통

모든 컴퓨터에 각각 설치해야 합니다. MySQL은 5.7과 8.0 중 어떤 버전을 쓸지 정해야 합니다. 잘못된 버전을 설치하면 SQL 문법이 호환되지 않습니다. Redis는 포트를 설정해야 합니다. 기본값은 6379지만 다른 서비스가 이미 사용하고 있을 수도 있습니다. RabbitMQ를 쓰려면 Erlang 실행 환경까지 설치해야 합니다. 저는 RabbitMQ 설치 안내만 30분 동안 읽은 적도 있습니다.

설치를 마친 뒤에는 버전 충돌도 생깁니다. 로컬에서 다른 프로젝트를 실행한 적이 있다면 MySQL에 이전 버전의 데이터가 남아 있을 수 있습니다. 포트가 충돌해 서비스가 시작되지 않기도 합니다. 한참 원인을 찾고 나서야 종료되지 않은 좀비 프로세스 때문이라는 사실을 발견하기도 합니다.

가장 힘든 것은 프로젝트 전환입니다. 프로젝트 A 작업을 마치고 프로젝트 B로 바꾸려는데 프로젝트 A의 MySQL이 3306을 사용 중이고 프로젝트 B도 3306을 원합니다. 구성 파일을 수정하거나 프로젝트 A의 서비스를 중지해야 합니다. 구성을 바꾸고 이틀 뒤 프로젝트 A로 돌아가면 다시 원래대로 되돌려야 합니다.

이런 작업이 계속 반복됩니다.

팀 협업의 악몽

“제 컴퓨터에서는 되는데요.”

모든 팀이 한 번쯤 들어 본 말일 겁니다. 새 팀원이 입사해 코드를 clone하고 환경을 설치했지만 실행되지 않습니다. 왜 그럴까요? 기존 프로젝트는 MySQL 5.7을 쓰는데 새 팀원은 8.0을 설치했을 수 있습니다. 기존 프로젝트의 Redis에는 비밀번호가 없지만 새 팀원의 로컬 Redis에는 비밀번호가 설정돼 있을 수도 있습니다. 구성 파일을 여러 곳 수정해도 여전히 실행되지 않습니다.

결국 선배가 와서 도와주고 두 사람이 반나절 넘게 매달린 뒤에야 환경이 정상적으로 동작합니다.

그렇게 하루가 사라집니다.

Compose의 해결 방식

Docker Compose의 접근 방식은 단순합니다. 모든 서비스를 컨테이너로 패키징하고 하나의 구성 파일로 통합 관리합니다.

각 데이터베이스를 어떻게 설치하고, 어떤 버전을 설정하며, 포트를 어떻게 할당할지 일일이 신경 쓸 필요가 없습니다. Compose 파일에 명확히 작성한 뒤 한 번에 시작하면 모든 서비스가 구성대로 실행됩니다. 프로젝트를 전환할 때는 디렉터리를 바꾸고 다른 Compose 파일을 시작합니다. 정리할 때는 명령어 한 줄로 모든 컨테이너와 볼륨을 삭제합니다.

이는 ‘컴퓨터 부품을 직접 조립하는 방식’에서 ‘완제품을 사는 방식’으로 바뀌는 것과 비슷합니다. 메모리를 어떻게 꽂고 그래픽 카드 전원 케이블을 어떻게 연결할지 신경 쓰지 않아도 됩니다. 제조사가 이미 조립했으므로 전원을 켜면 바로 사용할 수 있습니다.

docker-compose.yml 핵심 구성

먼저 전체 구성 파일을 살펴보겠습니다. 프로젝트에 Web 프런트엔드, API 백엔드, MySQL 데이터베이스, Redis 캐시까지 네 서비스가 있다고 가정합니다.

# docker-compose.yml
version: "3.8"  # Compose 파일 버전. 3.8은 대부분의 구성 옵션을 지원합니다.

services:
  # 프런트엔드 Web 서비스
  web:
    build: ./frontend  # 로컬 frontend 디렉터리에서 이미지 빌드
    ports:
      - "3000:3000"  # 호스트 3000 -> 컨테이너 3000
    depends_on:
      - api  # api 서비스에 의존하므로 api가 먼저 시작됩니다.
    environment:
      - API_URL=http://api:8080  # 프런트엔드에서 백엔드에 접근할 주소

  # 백엔드 API 서비스
  api:
    build: ./backend  # 로컬 backend 디렉터리에서 이미지 빌드
    ports:
      - "8080:8080"
    depends_on:
      - mysql
      - redis  # 데이터베이스와 캐시에 의존
    environment:
      - DB_HOST=mysql  # 데이터베이스 주소(컨테이너 이름)
      - DB_PORT=3306
      - DB_USER=root
      - DB_PASSWORD=dev123  # 개발 환경 비밀번호. 운영 환경에서는 .env 파일 사용
      - REDIS_HOST=redis
      - REDIS_PORT=6379

  # MySQL 데이터베이스
  mysql:
    image: mysql:8.0  # 빌드하지 않고 공식 이미지를 직접 사용
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=dev123
      - MYSQL_DATABASE=myapp  # 데이터베이스 자동 생성
    volumes:
      - mysql_data:/var/lib/mysql  # 볼륨에 데이터를 영구 저장

  # Redis 캐시
  redis:
    image: redis:7-alpine  # alpine 버전은 크기가 더 작습니다.
    ports:
      - "6379:6379"

volumes:
  mysql_data:  # MySQL 데이터를 영구 저장할 데이터 볼륨 정의

핵심 필드 설명

services 아래에 모든 서비스를 정의합니다. 각 서비스는 세 가지 방식으로 가져올 수 있습니다. build로 로컬 코드에서 빌드하거나, image로 공식 이미지를 직접 내려받거나, 두 방식을 결합해 특정 이미지를 기반으로 로컬에서 빌드할 수 있습니다.

ports는 포트를 매핑합니다. 형식은 "호스트 포트:컨테이너 포트"입니다. 위 구성에서는 Web을 3000, API를 8080, MySQL을 3306, Redis를 6379에 매핑했습니다. 로컬 포트가 이미 사용 중이라면 호스트 포트를 "13006:3306"처럼 바꿀 수 있습니다. 그러면 localhost:13006으로 MySQL에 연결합니다.

depends_on은 시작 순서를 제어합니다. MySQL과 Redis가 먼저 시작되고 그 서비스에 의존하는 API가 다음으로, API에 의존하는 Web이 마지막으로 시작됩니다. 다만 여기에는 주의할 점이 있으며 뒤에서 설명하겠습니다.

environment는 환경 변수를 설정합니다. 데이터베이스 비밀번호, 연결 주소, 포트 번호를 모두 이곳에서 구성할 수 있습니다. 운영 환경에서는 비밀번호를 여기에 직접 쓰지 말고 .env 파일이나 환경 변수 주입을 사용해야 합니다.

volumes는 데이터를 영구 저장합니다. MySQL 데이터는 mysql_data 볼륨에 저장되므로 컨테이너를 삭제해도 사라지지 않습니다. 다음에 시작할 때도 데이터가 그대로 남아 있습니다.

흔히 빠지는 함정

컨테이너 이름은 곧 서비스 이름입니다. 위 구성에서 API 서비스가 데이터베이스에 연결할 때 DB_HOST=mysql을 사용하며 localhost를 사용하지 않습니다. 왜 그럴까요?

각 컨테이너는 독립된 네트워크 환경이기 때문입니다. API 컨테이너 안에서 localhost는 API 컨테이너 자신을 가리키며 호스트 머신이나 MySQL 컨테이너를 가리키지 않습니다. Compose는 내부 네트워크를 자동으로 만들고 서비스끼리 서비스 이름을 사용해 서로 접근하게 합니다. mysql이라는 이름이 네트워크에서 MySQL 컨테이너의 주소가 됩니다.

저도 처음 Compose 파일을 작성할 때 이 함정에 빠졌습니다. localhost:3306을 사용했더니 아무리 해도 데이터베이스에 연결되지 않았습니다. 나중에야 컨테이너 이름을 사용해야 한다는 사실을 알았습니다.

서비스 의존성과 시작 순서

depends_on은 단순해 보입니다. MySQL이 먼저 시작되고 API가 그다음 시작됩니다. 하지만 실제로는 미묘한 문제가 있습니다.

Compose의 depends_on은 컨테이너의 시작 순서만 보장하며 서비스가 준비되는 순서까지 보장하지 않습니다. 다시 말해 MySQL 컨테이너는 시작됐지만 MySQL 서비스는 아직 연결을 받을 준비가 안 됐을 수 있습니다. 데이터베이스를 초기화하고 구성을 불러오며 수신 포트를 시작하는 중일 수 있습니다. 이때 API 서비스가 연결을 시도하면 실패할 가능성이 큽니다.

저도 이런 상황을 겪었습니다. docker-compose up을 실행하자마자 API에서 데이터베이스 연결 실패 오류가 났습니다. 10초 뒤 다시 시도하니 성공했습니다. MySQL 컨테이너는 시작됐지만 MySQL 서비스가 아직 준비되지 않았기 때문입니다.

해결 방법 1: 상태 확인

Compose 구성에 상태 확인을 추가할 수 있습니다. 상태 확인을 통과한 뒤에야 해당 서비스에 의존하는 서비스가 시작됩니다.

services:
  mysql:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s  # 5초마다 한 번 확인
      timeout: 3s   # 제한 시간
      retries: 10   # 10번 실패해야 unhealthy로 판정
    # ... 기타 구성

  api:
    depends_on:
      mysql:
        condition: service_healthy  # MySQL 상태 확인을 통과할 때까지 대기

MySQL은 mysqladmin ping 명령을 실행해 연결을 받을 수 있는지 확인합니다. 5초마다 한 번씩 최대 10번, 즉 50초 동안 기다립니다. 상태 확인을 통과한 뒤에야 API가 시작됩니다.

이 방법은 효과적이지만 단점도 있습니다. 서비스마다 상태 확인 구성을 작성해야 합니다. Redis 같은 일부 공식 이미지는 편리한 상태 확인 명령을 제공하지 않으므로 직접 방법을 마련해야 합니다.

해결 방법 2: 애플리케이션 계층 재시도

더 간단한 방법은 애플리케이션 코드에 재시도 로직을 추가하는 것입니다. 연결에 실패하면 몇 초 기다렸다가 다시 시도합니다. MySQL 시작이 느리다면 준비될 때까지 기다리면 됩니다.

지수 백오프는 자주 사용하는 전략입니다. 첫 번째에는 1초, 두 번째에는 2초, 세 번째에는 4초처럼 대기 시간을 점차 늘립니다. 대부분의 데이터베이스는 30초 안에 준비됩니다.

Node.js에서는 mysql2의 연결 풀 구성을 사용할 수 있습니다.

const pool = mysql.createPool({
  host: 'mysql',
  port: 3306,
  user: 'root',
  password: 'dev123',
  database: 'myapp',
  waitForConnections: true,  // 연결이 가능해질 때까지 대기
  connectionLimit: 10,
  queueLimit: 0,
});

Python에서는 tenacity 라이브러리로 재시도할 수 있습니다.

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=2, max=10))
def connect_db():
    return mysql.connector.connect(host='mysql', ...)

두 방법 중 선택하기

상태 확인은 더 정확합니다. 데이터베이스가 실제로 준비된 뒤에야 API가 시작됩니다. 하지만 구성은 조금 번거롭고 데이터베이스마다 해당하는 확인 명령을 작성해야 합니다.

애플리케이션 계층 재시도는 더 간단합니다. 코드에 몇 줄만 추가하면 되고 Compose 구성을 수정하지 않아도 됩니다. 단점은 API가 시작된 뒤 일정 시간 동안 계속 재시도하므로 로그에 연결 실패 오류가 남는다는 점입니다. 최종 결과에는 영향을 주지 않습니다.

개인적으로는 더 간편하고 대부분의 상황에서 잘 동작하는 애플리케이션 계층 재시도를 선호합니다. 상태 확인은 시작 시간이 특히 긴 서비스에 보조 수단으로 사용합니다.

다중 환경 구성 전략

로컬 개발, 테스트, 운영 환경은 보통 구성이 서로 다릅니다. 예를 들어 개발 환경에서는 로컬 디버깅을 위해 데이터베이스 포트를 외부에 노출해야 하지만 운영 환경에서는 그럴 필요가 없습니다. 데이터베이스는 컨테이너 내부 네트워크에서만 접근할 수 있으면 됩니다.

이 모든 구성을 한 파일에 작성하면 환경을 바꿀 때마다 파일을 수정하고 나중에 다시 되돌려야 합니다. 번거롭고 실수하기도 쉽습니다.

Compose는 기본 파일과 오버라이드 파일이라는 방식으로 이 문제를 해결합니다.

기본 파일: 공통 구성

docker-compose.yml에는 서비스 정의, 이미지 버전, 내부 네트워크, 데이터 볼륨처럼 모든 환경에서 공유하는 구성을 작성합니다.

# docker-compose.yml(기본 구성)
version: "3.8"

services:
  web:
    build: ./frontend
    # ports는 작성하지 않고 오버라이드 파일에서 보충합니다.

  api:
    build: ./backend
    environment:
      - DB_HOST=mysql
      - REDIS_HOST=redis
    # ports는 작성하지 않습니다.

  mysql:
    image: mysql:8.0
    volumes:
      - mysql_data:/var/lib/mysql
    # ports는 작성하지 않습니다. 운영 환경에서는 노출할 필요가 없습니다.

  redis:
    image: redis:7-alpine

volumes:
  mysql_data:

포트 매핑을 작성하지 않은 이유는 환경마다 포트 구성이 다르기 때문입니다.

개발 환경 오버라이드 파일

docker-compose.override.yml에는 포트 매핑, 개발용 비밀번호, 디버깅용 환경 변수처럼 개발 환경에만 필요한 구성을 추가합니다.

# docker-compose.override.yml(개발 환경)
version: "3.8"

services:
  web:
    ports:
      - "3000:3000"  # 로컬 접근을 위해 개발 환경에서 포트 노출

  api:
    ports:
      - "8080:8080"
    environment:
      - DEBUG=true  # 개발 환경에서 디버그 모드 활성화

  mysql:
    ports:
      - "3306:3306"  # 연결과 디버깅을 위해 개발 환경에서 DB 포트 노출
    environment:
      - MYSQL_ROOT_PASSWORD=dev123  # 개발 환경용 간단한 비밀번호

  redis:
    ports:
      - "6379:6379"

Compose에는 기본 동작이 하나 있습니다. docker-compose up을 실행하면 docker-compose.ymldocker-compose.override.yml을 자동으로 병합합니다. 두 파일의 구성이 합쳐지고 override의 구성이 기본 구성을 덮어씁니다.

따라서 로컬 개발에서는 docker-compose up을 바로 실행해 개발 환경의 전체 구성을 얻을 수 있습니다.

운영 환경 오버라이드 파일

docker-compose.prod.yml에는 포트를 외부에 노출하지 않고 운영 비밀번호를 사용하며 외부 서비스에 연결하는 등 운영 환경 구성을 추가합니다.

# docker-compose.prod.yml(운영 환경)
version: "3.8"

services:
  web:
    # 포트를 노출하지 않고 리버스 프록시(nginx)에서 접근합니다.

  api:
    environment:
      - DB_HOST={{DB_HOST}}  # 파일에 비밀번호를 쓰지 않고 환경 변수에서 읽기
      - DB_PASSWORD={{DB_PASSWORD}}

  mysql:
    # 포트를 노출하지 않아 외부에서 직접 연결할 수 없습니다.
    environment:
      - MYSQL_ROOT_PASSWORD={{MYSQL_ROOT_PASSWORD}}

운영 환경을 시작할 때는 -f 옵션으로 구성 파일을 지정합니다.

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

두 파일이 합쳐지고 prod 구성이 기본 구성을 덮어씁니다. 데이터베이스 포트는 노출되지 않고 비밀번호는 환경 변수에서 읽습니다.

환경 변수 주입

운영 환경 비밀번호를 파일에 작성해서는 안 됩니다. Compose는 .env 파일이나 시스템 환경 변수에서 값을 읽을 수 있습니다.

# .env 파일(git에 커밋하지 마세요)
DB_HOST=prod-mysql.internal
DB_PASSWORD=super_secret_password_123
MYSQL_ROOT_PASSWORD=another_secret

구성 파일에서는 {{VAR:-default}} 문법으로 값을 참조합니다.

environment:
  - DB_HOST={{DB_HOST:-localhost}}  # DB_HOST가 없으면 localhost 사용
  - DB_PASSWORD={{DB_PASSWORD:-dev123}}

.env 파일은 버전 관리에 커밋하지 마세요. .env.example에 예시 값을 작성하고 팀원이 복사한 뒤 자신의 실제 구성을 입력하게 합니다.

원클릭 명령 실전

구성을 작성했으니 이제 시작, 중지, 디버깅을 해보겠습니다. 다음 명령만 익혀도 일상 작업은 대부분 처리할 수 있습니다.

모든 서비스 시작하기

docker-compose up -d

up은 모든 서비스를 시작합니다. -d는 백그라운드 실행(detached mode)을 의미하므로 터미널을 점유하지 않습니다. -d를 생략하면 모든 서비스 로그가 터미널에 바로 출력되며 Ctrl+C로 중지할 수 있습니다.

서비스를 시작하면 Compose가 image를 사용하는 경우 이미지를 내려받고, build를 사용하는 경우 이미지를 빌드한 뒤 컨테이너를 만들고 서비스를 시작합니다. 처음에는 이미지를 내려받아야 하므로 비교적 오래 걸립니다. 이후에는 이미지가 이미 있으므로 더 빠르게 시작됩니다.

서비스 상태 확인하기

docker-compose ps

모든 컨테이너의 실행 상태를 나열합니다. 출력은 다음과 비슷합니다.

NAME                COMMAND             SERVICE   STATUS    PORTS
myapp-web-1         "npm start"         web       running   0.0.0.0:3000->3000/tcp
myapp-api-1         "node index.js"     api       running   0.0.0.0:8080->8080/tcp
myapp-mysql-1       "mysqld"            mysql     running   0.0.0.0:3306->3306/tcp
myapp-redis-1       "redis-server"      redis     running   0.0.0.0:6379->6379/tcp

STATUS가 running이면 정상적으로 실행 중입니다. exited 또는 error가 표시되면 서비스 시작에 실패한 것입니다.

서비스 로그 확인하기

docker-compose logs -f api

API 서비스의 로그를 확인합니다. -f는 지속적으로 추적(follow)한다는 뜻이며 새 로그를 실시간으로 표시합니다. -f를 생략하면 기존 로그만 표시합니다.

서비스 이름을 생략하고 docker-compose logs -f를 실행하면 모든 서비스의 로그가 표시되지만 로그가 많을 때는 매우 복잡해집니다.

중지하고 정리하기

docker-compose down

모든 컨테이너를 중지하고 컨테이너와 네트워크를 삭제합니다. 데이터 볼륨은 삭제하지 않으므로 MySQL 데이터는 그대로 남습니다.

데이터 볼륨까지 완전히 정리하려면 다음 명령을 사용합니다.

docker-compose down -v

-v는 데이터 볼륨을 삭제합니다. 다음에 시작하면 MySQL이 다시 초기화되고 데이터가 모두 지워집니다. 디버깅할 때 자주 쓰는 명령입니다. 데이터가 꼬였다면 지우고 다시 시작할 수 있습니다.

이미지 다시 빌드하기

코드를 수정해 이미지를 다시 빌드해야 한다면 다음 명령을 사용합니다.

docker-compose build api

API 서비스의 이미지만 빌드합니다. 빌드를 마치면 컨테이너를 다시 시작해야 합니다.

docker-compose up -d api

빌드와 재시작을 한 번에 처리할 수도 있습니다.

docker-compose up -d --build api

--build는 이미지가 이미 있어도 강제로 다시 빌드합니다.

자주 쓰는 명령 요약표

명령기능
docker-compose up -d모든 서비스를 백그라운드에서 시작
docker-compose ps실행 상태 확인
docker-compose logs -f apiAPI 로그 확인
docker-compose down컨테이너 중지 및 삭제
docker-compose down -v컨테이너와 데이터 볼륨 중지 및 삭제
docker-compose restart apiAPI 서비스 다시 시작
docker-compose build apiAPI 이미지 다시 빌드

이 명령들로 일상 작업의 90%를 처리할 수 있습니다. 나머지 명령(exec, cp, top)은 필요할 때 문서를 찾아보면 됩니다.

마무리

기존 방식과 Compose 방식의 효율을 비교해 보겠습니다.

작업기존 방식Compose 방식
새 팀원의 환경 설치4~8시간5분(clone + up)
프로젝트 전환구성 수정, 서비스 중지, 재시작디렉터리 이동 후 다른 compose 시작
환경 정리수동 제거 및 남은 프로세스 확인명령어 한 줄로 컨테이너와 볼륨 삭제
팀 환경 일관성컴퓨터마다 다를 수 있음구성 파일 통일로 완전히 동일한 환경 유지

차이는 분명합니다.

아직도 데이터베이스를 수동으로 설치하고, 구성 파일을 수정하고, 포트 충돌을 추적하고 있다면 Docker Compose를 사용해 보세요. API 하나와 MySQL 하나가 있는 간단한 프로젝트부터 시작하면 됩니다. docker-compose.yml을 작성해 실행해 보세요. 익숙해지면 Redis와 RabbitMQ를 추가하고 다중 환경 구성도 적용할 수 있습니다.

팀에서 도입하려면 docker-compose.ymldocker-compose.override.yml을 저장소에 커밋하고 README에 시작 절차를 적어 두세요. 새 팀원은 코드를 clone한 뒤 명령어 한 줄로 개발 환경을 준비할 수 있습니다.

이 방식이 ‘환경 구성 문서’를 작성하는 것보다 훨씬 안정적입니다. 문서는 오래되지만 구성 파일은 그렇지 않습니다.

Docker Compose 멀티 서비스 오케스트레이션 실전

Docker Compose로 Web, API, MySQL, Redis 네 서비스를 구성해 로컬 개발 환경을 한 번에 시작합니다.

⏱️ Estimated time: 15 min

  1. 1

    Step 1: docker-compose.yml 파일 만들기

    프로젝트 루트 디렉터리에 구성 파일을 만듭니다.

    ```yaml
    version: "3.8"
    services:
    web:
    build: ./frontend
    ports: ["3000:3000"]
    depends_on: [api]
    api:
    build: ./backend
    ports: ["8080:8080"]
    depends_on: [mysql, redis]
    mysql:
    image: mysql:8.0
    environment:
    MYSQL_ROOT_PASSWORD: dev123
    MYSQL_DATABASE: myapp
    redis:
    image: redis:7-alpine
    ```

    주의: 컨테이너 간 통신에는 localhost가 아니라 서비스 이름(예: DB_HOST=mysql)을 사용합니다.
  2. 2

    Step 2: 모든 서비스 시작하기

    docker-compose.yml이 있는 디렉터리에서 다음 명령을 실행합니다.

    ```bash
    docker-compose up -d
    ```

    • 처음 시작할 때는 이미지를 내려받으므로 시간이 더 걸립니다.
    • 이후에는 기존 이미지를 사용하므로 빠르게 시작됩니다.
    • -d를 생략하면 터미널을 점유한 채 로그가 표시됩니다.
  3. 3

    Step 3: 서비스 실행 상태 확인하기

    모든 컨테이너가 정상적으로 시작됐는지 확인합니다.

    ```bash
    docker-compose ps
    ```

    • STATUS가 running이면 정상입니다.
    • exited 또는 error가 있으면 logs로 원인을 확인합니다.
    ```bash
    docker-compose logs api
    ```
  4. 4

    Step 4: 다중 환경 구성하기(선택 사항)

    개발 환경용 docker-compose.override.yml을 만듭니다.

    ```yaml
    version: "3.8"
    services:
    mysql:
    ports: ["3306:3306"]
    ```

    • Compose가 override 파일을 자동으로 병합합니다.
    • 운영 환경에서는 -f로 파일을 지정합니다.
    ```bash
    docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
    ```
  5. 5

    Step 5: 환경 정리하기

    모든 컨테이너를 중지하고 삭제합니다.

    ```bash
    docker-compose down # 데이터 볼륨 유지
    docker-compose down -v # 데이터 볼륨 삭제(데이터 초기화)
    ```

    • 프로젝트를 전환할 때는 down으로 정리합니다.
    • 데이터를 초기화하고 다시 시작하려면 down -v를 사용합니다.

FAQ

docker-compose.yml과 Dockerfile의 차이는 무엇인가요?
Dockerfile은 단일 이미지의 빌드 단계를 정의하고, docker-compose.yml은 여러 컨테이너가 어떻게 함께 동작할지 정의합니다. 간단히 말해 Dockerfile은 이미지를 만들고 Compose는 컨테이너를 관리합니다. 일반적으로 프로젝트에는 Dockerfile 하나 또는 서비스별 Dockerfile 여러 개와 docker-compose.yml 하나가 있습니다.
depends_on은 서비스가 준비될 때까지 보장하나요?
아닙니다. depends_on은 컨테이너 시작 순서만 보장하며 서비스가 준비된 상태까지 보장하지 않습니다. MySQL 컨테이너가 시작돼도 서비스는 아직 초기화 중일 수 있습니다. 해결 방법은 두 가지입니다. docker-compose.yml에 healthcheck를 추가하거나 애플리케이션 코드에 재시도 로직을 넣을 수 있습니다. 더 간단하고 안정적인 애플리케이션 계층 재시도를 권장합니다.
컨테이너 안에서 호스트 머신의 서비스에 어떻게 접근하나요?
Docker Desktop에서는 host.docker.internal을 사용하고 Linux에서는 호스트 머신의 IP를 사용합니다. 예를 들어 호스트 머신에서 Redis를 실행 중이라면 컨테이너에서 host.docker.internal:6379로 연결합니다. Linux에서는 host.docker.internal에 추가 구성이 필요합니다. 더 간단한 방법으로 --network host 모드를 사용할 수 있지만 네트워크 격리가 사라집니다.
데이터는 어디에 저장되며 컨테이너를 삭제하면 사라지나요?
volumes로 구성한 데이터는 Docker가 관리하는 데이터 볼륨에 저장되므로 컨테이너를 삭제해도 사라지지 않습니다. docker-compose down은 컨테이너와 네트워크만 삭제하고 볼륨은 삭제하지 않습니다. 데이터를 초기화하려면 docker-compose down -v를 사용합니다. 개발 환경에서 데이터가 꼬였을 때 자주 사용하는 명령으로, 데이터를 지우고 처음부터 다시 시작할 수 있습니다.
다중 환경 구성(dev/test/prod)은 어떻게 관리하나요?
기본 파일과 오버라이드 파일을 함께 사용하는 전략을 적용합니다. docker-compose.yml에는 공통 구성을, docker-compose.override.yml에는 개발 환경 구성을 작성합니다. Compose는 override 파일을 기본으로 자동 로드합니다. docker-compose.prod.yml에는 운영 환경 구성을 작성합니다. 시작할 때 -f 옵션으로 docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d처럼 지정합니다.
포트가 이미 사용 중이면 어떻게 하나요?
호스트 포트 매핑을 변경합니다. 예를 들어 MySQL의 기본 매핑이 3306:3306인데 로컬 3306 포트가 사용 중이라면 13006:3306으로 바꾸고 localhost:13006으로 연결합니다. 또는 ports 구성을 제거해 포트를 아예 매핑하지 않고 컨테이너 내부 네트워크에서만 접근하면 더 안전합니다.

3분 읽기 · 게시일: 2026년 4월 9일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog