본문 바로가기
IT/Cloud

[Cloud] Docker Compose 실전 가이드: 로컬 개발 환경 구축 및 관리

by 수누다 2026. 5. 19.

"팀원 컴퓨터에서는 되는데 제 컴퓨터에서는 왜 안 되죠?"

인프라 일을 13년 하면서 개발팀에서 가장 많이 들은 말 중 하나입니다. 그리고 솔직히 말씀드리면, 저도 초창기엔 이 문제로 꽤 많이 고생했거든요. 환경변수 하나 차이로 밤새 디버깅하고, OS 버전 달라서 라이브러리 충돌 나고... 생각만 해도 아찔하네요.

그래서 오늘은 Docker Compose를 활용해서 로컬 개발 환경을 제대로 구축하는 방법을 공유하려고 합니다. "내 컴퓨터에서는 되는데"라는 말을 팀에서 영원히 추방할 수 있는 방법이에요. 실제로 제가 홈랩이랑 팀 프로젝트에서 직접 써보면서 정리한 내용이라 꽤 실용적일 거라 생각합니다.

▲ Docker Compose로 구성한 로컬 개발 환경의 전체 구조 — 웹 서버, 앱 서버, 데이터베이스가 하나의 네트워크로 묶이는 모습

Docker Compose가 뭔지 먼저 짚고 넘어가요

Docker 자체는 이미 알고 계신 분들이 많을 텐데, Docker Compose(도커 컴포즈)는 조금 다른 개념이에요. 쉽게 말해서, 여러 개의 컨테이너(Container)를 하나의 파일로 정의하고 한 번에 관리하는 도구거든요.

예를 들어 웹 애플리케이션 하나를 띄우려면 보통 이런 것들이 필요하잖아요:

  • 프론트엔드 서버 (React, Vue 등)
  • 백엔드 API 서버 (Node.js, FastAPI 등)
  • 데이터베이스 (PostgreSQL, MySQL 등)
  • 캐시 서버 (Redis 등)

이걸 Docker 명령어로 하나하나 실행하면... 솔직히 너무 번거롭더라고요. 컨테이너마다 네트워크 연결하고, 볼륨 설정하고, 환경변수 넣고... 저도 처음엔 그렇게 했었는데 한 달도 안 돼서 포기했어요 ㅎㅎ.

그런데 Docker Compose를 쓰면 docker-compose.yml이라는 파일 하나에 이 모든 걸 정의하고, 명령어 한 줄로 전체를 올리고 내릴 수 있어요. 개발 생산성 측면에서 정말 게임 체인저였습니다.

Docker Compose vs. 일반 Docker 명령어 비교

항목 Docker 단독 사용 Docker Compose 사용
서비스 시작 컨테이너마다 개별 명령어 실행 docker compose up 하나로 끝
네트워크 설정 수동으로 네트워크 생성 및 연결 자동으로 네트워크 생성 및 연결
환경 관리 각 명령어에 -e 옵션 반복 입력 yml 파일에 한 번에 정의
팀 공유 실행 명령어 문서화 필요 yml 파일을 Git에 올리면 끝
재현성 낮음 (사람마다 다르게 실행 가능) 높음 (동일한 환경 보장)

시작 전 준비사항

본격적으로 들어가기 전에 환경 세팅부터 확인해 봅시다. 요즘은 Docker Desktop을 설치하면 Docker Compose도 함께 딸려오거든요. 예전엔 따로 설치해야 했는데, 이 부분은 많이 편해졌어요.

  1. Docker Desktop 공식 사이트에서 OS에 맞는 버전 다운로드 및 설치
  2. 설치 완료 후 터미널에서 버전 확인
# Docker 버전 확인
docker --version

# Docker Compose 버전 확인 (v2 기준)
docker compose version

💡 : 예전에는 docker-compose(하이픈 포함)로 실행했는데, 요즘 Compose V2부터는 docker compose(스페이스)로 바뀌었어요. 둘 다 동작하긴 하는데, 새 프로젝트라면 V2 방식으로 쓰는 게 좋습니다.

실전: docker-compose.yml 파일 작성하기

자, 이제 진짜 시작입니다. 실제 웹 애플리케이션 개발 환경을 예시로 만들어볼게요. Node.js 백엔드 + PostgreSQL 데이터베이스 + Redis 캐시 조합으로 가겠습니다. 제가 홈랩에서 사이드 프로젝트 할 때 자주 쓰는 스택이거든요.

프로젝트 디렉토리 구조

my-project/
├── docker-compose.yml       # 핵심 설정 파일
├── docker-compose.override.yml  # 로컬 개발용 오버라이드
├── .env                     # 환경변수 파일
├── backend/
│   ├── Dockerfile
│   └── src/
└── frontend/
    ├── Dockerfile
    └── src/

기본 docker-compose.yml 작성

version: '3.8'

services:
  # 백엔드 API 서버
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: myapp-backend
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://myuser:mypassword@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    volumes:
      - ./backend:/app          # 소스 코드 마운트 (핫 리로드 가능)
      - /app/node_modules       # node_modules는 컨테이너 것 사용
    depends_on:
      db:
        condition: service_healthy  # DB 헬스체크 통과 후 시작
      cache:
        condition: service_started
    networks:
      - app-network
    restart: unless-stopped

  # PostgreSQL 데이터베이스
  db:
    image: postgres:15-alpine
    container_name: myapp-db
    environment:
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mypassword
      - POSTGRES_DB=mydb
    volumes:
      - postgres-data:/var/lib/postgresql/data  # 데이터 영속성 보장
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql  # 초기화 스크립트
    ports:
      - "5432:5432"   # 로컬에서 DB 클라이언트로 직접 접속 가능
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  # Redis 캐시 서버
  cache:
    image: redis:7-alpine
    container_name: myapp-redis
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - app-network

# 볼륨(Volume) 정의: 컨테이너가 삭제돼도 데이터 유지
volumes:
  postgres-data:
  redis-data:

# 네트워크 정의: 서비스 간 내부 통신
networks:
  app-network:
    driver: bridge

여기서 중요한 포인트! depends_on을 쓸 때 단순히 서비스 이름만 쓰면 컨테이너가 시작된 것만 확인하고 실제로 준비가 됐는지는 모릅니다. condition: service_healthyhealthcheck를 함께 써야 PostgreSQL이 실제로 쿼리를 받을 준비가 됐을 때 백엔드가 시작돼요. 이거 몰라서 처음에 백엔드가 DB 연결 못 한다고 에러 뜨는 삽질을 꽤 했었더라고요 ㅎㅎ.

환경변수 파일(.env) 관리

민감한 정보는 절대 docker-compose.yml에 직접 넣으면 안 됩니다. .env 파일을 따로 만들고 Git에는 올리지 마세요.

# .env 파일
POSTGRES_USER=myuser
POSTGRES_PASSWORD=super_secret_password
POSTGRES_DB=mydb
NODE_ENV=development
APP_PORT=3000
# docker-compose.yml에서 .env 변수 참조
services:
  db:
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
# .gitignore에 반드시 추가
.env
.env.local
.env.production

▲ docker-compose.yml로 정의된 서비스들이 내부 네트워크(app-network)로 연결되고, 필요한 포트만 호스트에 노출되는 구조

개발 환경 전용 오버라이드 파일

이건 좀 꿀팁인데요. docker-compose.override.yml을 만들면 docker compose up할 때 자동으로 합쳐져서 적용됩니다. 로컬 개발에서만 필요한 설정을 분리할 수 있어서 정말 편하더라고요.

# docker-compose.override.yml (로컬 개발 전용)
version: '3.8'

services:
  backend:
    # 개발 중엔 빌드 없이 소스 코드 직접 마운트
    command: npm run dev   # nodemon 등 핫 리로드 명령어
    environment:
      - DEBUG=true
      - LOG_LEVEL=debug

  # 개발 환경에서만 DB 관리 UI 추가
  adminer:
    image: adminer
    container_name: myapp-adminer
    ports:
      - "8080:8080"
    networks:
      - app-network

자주 쓰는 Docker Compose 명령어 모음

이제 실제로 실행해 봅시다. 제가 매일 쓰는 명령어들 위주로 정리했어요.

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

# 빌드 포함해서 시작 (코드 변경 후)
docker compose up -d --build

# 특정 서비스만 시작
docker compose up -d backend

# 전체 서비스 중지
docker compose down

# 중지 + 볼륨까지 삭제 (DB 초기화할 때)
docker compose down -v

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

# 특정 서비스 로그 보기 (실시간)
docker compose logs -f backend

# 실행 중인 컨테이너에 접속
docker compose exec backend sh

# 데이터베이스 컨테이너에서 psql 실행
docker compose exec db psql -U myuser -d mydb

# 서비스 재시작
docker compose restart backend

⚠️ 실제로 겪은 트러블슈팅 사례들

이론은 깔끔한데, 실제로 쓰다 보면 별별 문제가 다 생기더라고요. 제가 겪었던 것들 공유합니다.

문제 1: 포트가 이미 사용 중이라는 에러

# 에러 메시지
Error response from daemon: Ports are not available: 
exposing port TCP 0.0.0.0:5432 -> 0.0.0.0:0: listen tcp 0.0.0.0:5432: 
bind: address already in use

로컬에 PostgreSQL이 이미 설치돼서 실행 중인 경우 자주 발생해요. 해결법은 두 가지예요:

# 방법 1: 로컬 PostgreSQL 서비스 중지
sudo systemctl stop postgresql   # Linux
brew services stop postgresql    # macOS

# 방법 2: docker-compose.yml에서 포트 변경
ports:
  - "5433:5432"   # 호스트 포트를 5433으로 변경

문제 2: 볼륨 마운트 후 node_modules 사라짐

이거 처음 겪으면 진짜 당황스러워요. 소스 코드 볼륨 마운트하면 로컬의 node_modules(없는 경우)가 컨테이너 안의 것을 덮어씌워버리는 현상이거든요.

# 해결법: node_modules를 별도 볼륨으로 분리
services:
  backend:
    volumes:
      - ./backend:/app
      - /app/node_modules   # 이 한 줄이 핵심!
                            # 익명 볼륨으로 컨테이너 내부 것을 보호

문제 3: 컨테이너 간 통신이 안 될 때

백엔드에서 DB 접속할 때 localhost로 접속하려다가 실패하는 경우가 많아요. 컨테이너 안에서 localhost는 그 컨테이너 자신을 가리키거든요.

# ❌ 잘못된 방법 (백엔드 컨테이너 안에서)
DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/mydb

# ✅ 올바른 방법 (서비스 이름을 호스트로 사용)
DATABASE_URL=postgresql://myuser:mypassword@db:5432/mydb
# 'db'는 docker-compose.yml에서 정의한 서비스 이름

같은 네트워크에 묶인 컨테이너끼리는 서비스 이름이 곧 호스트명이 됩니다. Docker의 내장 DNS가 자동으로 처리해줘요. 이거 알고 나면 진짜 편해집니다.

문제 4: M1/M2 Mac에서 이미지 아키텍처 오류

# 에러 메시지
WARNING: The requested image's platform (linux/amd64) does not match 
the detected host platform (linux/arm64/v8)

# 해결법: platform 명시
services:
  db:
    image: postgres:15-alpine
    platform: linux/amd64   # 또는 linux/arm64/v8

요즘 Apple Silicon Mac 쓰시는 분들 많은데, arm64를 지원하는 이미지를 쓰거나 platform을 명시해주면 해결돼요. 대부분의 공식 이미지들은 멀티 아키텍처를 지원하니까 크게 걱정 안 하셔도 됩니다.

✅ 결과 확인: 제대로 떴는지 검증하기

드디어 됐다! 이제 제대로 동작하는지 확인해봅시다.

# 전체 서비스 상태 확인
docker compose ps

# 예상 출력
NAME              IMAGE                COMMAND                  SERVICE    CREATED        STATUS                    PORTS
myapp-backend     myapp-backend        "docker-entrypoint.s…"   backend    2 minutes ago  Up 2 minutes              0.0.0.0:3000->3000/tcp
myapp-db          postgres:15-alpine   "docker-entrypoint.s…"   db         2 minutes ago  Up 2 minutes (healthy)    0.0.0.0:5432->5432/tcp
myapp-redis       redis:7-alpine       "docker-entrypoint.s…"   cache      2 minutes ago  Up 2 minutes              0.0.0.0:6379->6379/tcp

STATUS 컬럼에서 Up이면 실행 중, (healthy)면 헬스체크까지 통과한 것입니다. 🎉

# 네트워크 확인
docker network ls
docker network inspect myproject_app-network

# 컨테이너 간 통신 테스트
docker compose exec backend ping db
docker compose exec backend ping cache

# 백엔드 API 응답 확인
curl http://localhost:3000/health

▲ docker compose ps 명령어로 확인한 서비스 상태 — 모든 서비스가 Up 상태이고 DB는 healthy 헬스체크까지 통과한 모습

🎉 팀 협업에서 빛나는 Docker Compose의 진짜 가치

제가 Docker Compose를 팀 프로젝트에 도입하고 나서 가장 크게 달라진 점은 온보딩 시간이었어요. 예전엔 새 팀원이 오면 환경 세팅하는 데 하루 이상 걸리는 경우도 있었거든요. 근데 이제는 이렇게 끝납니다:

# 새 팀원 온보딩 전체 과정
git clone https://github.com/myteam/myproject.git
cd myproject
cp .env.example .env   # 환경변수 파일 복사 후 값 채우기
docker compose up -d   # 끝!

이 세 줄이면 끝이에요. 진짜로요. OS가 다르든, 로컬에 뭐가 설치돼 있든 상관없이 동일한 환경이 뜹니다. 개발 생산성 측면에서 이게 얼마나 큰 차이인지는 직접 경험해보시면 바로 느끼실 거예요.

Docker Compose 활용의 주요 이점

  • 환경 일관성: "내 컴퓨터에서는 되는데" 문제 완전 해결
  • 빠른 온보딩: 새 팀원도 몇 분 안에 개발 시작 가능
  • 격리된 환경: 프로젝트별로 독립된 환경, 충돌 없음
  • 버전 관리: 인프라 설정도 코드로 관리 (Infrastructure as Code)
  • 프로덕션 근접: 로컬과 운영 환경의 차이 최소화

▲ Docker Compose 도입 전후 로컬 개발 환경 비교 — 온보딩 시간, 환경 일관성, 협업 효율성 측면에서의 개선 효과

자주 묻는 질문 (FAQ)

Q. Docker Desktop 없이 Docker Compose만 쓸 수 있나요?

네, Linux 환경에서는 Docker Engine을 설치하고 Compose 플러그인을 별도로 설치하면 돼요. macOS나 Windows라면 Docker Desktop이 가장 간편한 방법이더라고요.

Q. docker-compose.yml 파일을 Git에 올려도 되나요?

네, 올려야 합니다! 이게 바로 팀 환경 공유의 핵심이거든요. 단, .env 파일은 절대 올리면 안 됩니다. .env.example 파일을 만들어서 어떤 변수가 필요한지만 공유하세요.

Q. 프로덕션 배포에도 Docker Compose를 쓰나요?

소규모 서비스라면 쓸 수 있어요. 하지만 스케일링이나 고가용성이 필요하다면 Kubernetes(쿠버네티스) 같은 오케스트레이션 도구를 고려해야 합니다. 이 부분은 다음 글에서 다룰 예정이에요.

Q. 컨테이너를 내렸다 올리면 데이터가 사라지나요?

docker compose down만 하면 볼륨은 유지돼요. 데이터까지 지우려면 docker compose down -v를 써야 하니까 주의하세요!

마무리: 이제 로컬 개발 환경 걱정은 끝

오늘 다룬 내용을 정리해볼게요:

  1. Docker Compose의 기본 개념과 일반 Docker 대비 장점
  2. 실전 docker-compose.yml 작성법 (서비스, 볼륨, 네트워크)
  3. 환경변수 분리와 오버라이드 파일 활용
  4. 자주 쓰는 명령어와 실제 트러블슈팅 사례

처음 Docker Compose를 접하면 yml 파일 문법이 좀 낯설게 느껴질 수 있어요. 저도 들여쓰기 하나 틀려서 에러 뜨는 걸 수십 번은 겪었으니까요. 근데 한 번 손에 익으면 이것 없이 개발하는 게 상상이 안 될 정도로 편해집니다.

다음 글에서는 Docker Compose로 구성한 환경을 기반으로 CI/CD 파이프라인과 연동하는 방법을 다뤄볼 예정이에요. 로컬에서 테스트한 환경을 그대로 자동화 배포에 활용하는 내용인데, 꽤 실용적인 내용이 될 것 같습니다.

궁금한 점이나 삽질 경험 있으시면 댓글로 편하게 남겨주세요. 저도 비슷한 거 겪어봤을 확률이 높거든요 😄