Docker Compose로 로컬 개발 환경 구축하기: 누구나 5분 안에 실행되는 설정

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

개발팀에서 이 말이 나오기 시작하면 십중팔구 환경 문제다. Node 버전 다르고, MySQL 버전 다르고, 환경변수 세팅도 제각각. Docker Compose는 이 문제를 구조적으로 해결한다. 프로젝트 루트에 파일 하나 두면, 어떤 머신에서도 docker compose up한 줄로 동일한 환경이 뜬다.

이 글은 “Docker가 뭔지는 알겠는데 Compose는 어떻게 써야 하는지 모르겠다”는 단계에 있는 분들을 위한 실전 가이드다. 기본 세팅부터 팀 협업에서 실제로 도움이 되는 구성까지 다룬다.


시작 전 확인사항

Docker Desktop(Mac/Windows) 또는 Docker Engine + Docker Compose CLI(Linux)가 설치되어 있어야 한다.

bash

docker --version       # Docker version 24.x 이상 권장
docker compose version # Docker Compose v2.x 이상 권장 (v1의 docker-compose와 다름)

팁: Docker Compose V2부터는 docker-compose(하이픈)가 아니라 docker compose(공백)로 실행한다. 레거시 명령어와 혼용하면 옵션 동작이 미묘하게 달라지므로 통일하는 게 좋다.


기본 구조 먼저 잡기

전형적인 웹 서비스 기준으로 Node.js 앱 + PostgreSQL + Redis 조합을 예시로 쓴다. 프로젝트 구조는 이렇게 시작한다.

my-project/
├── docker-compose.yml
├── docker-compose.override.yml   # 로컬 전용 설정 (git 제외)
├── .env.example                  # 팀 공유용 환경변수 템플릿
├── .env                          # 실제 환경변수 (git 제외)
└── app/
    ├── Dockerfile
    └── src/

.env와 docker-compose.override.yml은 .gitignore에 추가한다. .env.example만 커밋해서 팀원이 참고할 수 있게 한다.


docker-compose.yml 작성

yaml

# docker-compose.yml
version: '3.9'
services:
  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    ports:
      - "${APP_PORT:-3000}:3000"
    volumes:
      - ./app:/usr/src/app          # 소스 코드 마운트 (핫리로드 가능)
      - /usr/src/app/node_modules   # node_modules는 컨테이너 것 사용
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy  # DB 헬스체크 통과 후 앱 시작
      cache:
        condition: service_started
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data   # 데이터 영속성
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # 초기 스키마 (선택)
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 5s
      timeout: 5s
      retries: 5
  cache:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
volumes:
  postgres_data:
  redis_data:

자주 놓치는 포인트: depends_on만으로는 부족하다

yaml

# ❌ 잘못된 예: DB가 프로세스는 떴지만 아직 연결 불가능한 상태일 수 있음
depends_on:
  - db
# ✅ 올바른 예: healthcheck 통과 후에 앱 컨테이너 시작
depends_on:
  db:
    condition: service_healthy

depends_on은 컨테이너가 “시작됐는지”만 확인한다. PostgreSQL은 프로세스가 올라와도 실제 연결을 받기까지 몇 초 걸린다. healthcheck + condition: service_healthy 조합이 맞다.


환경변수 관리

bash

# .env.example (팀 공유, git 커밋)
APP_PORT=3000
POSTGRES_USER=devuser
POSTGRES_PASSWORD=changeme
POSTGRES_DB=myapp_dev
# .env (실제 사용, git 제외)
APP_PORT=3000
POSTGRES_USER=devuser
POSTGRES_PASSWORD=my_actual_password
POSTGRES_DB=myapp_dev

Compose는 기본적으로 같은 디렉토리의 .env를 자동으로 읽는다. ${VAR:-default} 문법으로 기본값을 지정하면 .env가 없어도 동작한다.


개발 전용 오버라이드 설정

docker-compose.override.yml은 docker compose up을 실행할 때 자동으로 병합된다. 로컬에서만 필요한 설정을 여기에 넣으면 기본 파일을 건드리지 않아도 된다.

yaml

# docker-compose.override.yml (git 제외)
services:
  app:
    environment:
      - DEBUG=true
      - LOG_LEVEL=verbose
    # 개발 중 디버거 포트 열기
    ports:
      - "9229:9229"
  db:
    # 로컬에서 DB 클라이언트(TablePlus 등)로 직접 접속할 때
    ports:
      - "5432:5432"

팁: DB 포트를 기본 파일에 열어두면 팀원 간 포트 충돌이 생길 수 있다. 오버라이드에 넣어서 로컬에서만 선택적으로 여는 게 깔끔하다.


Dockerfile: 개발 환경에 맞게 최적화

dockerfile

# app/Dockerfile
FROM node:20-alpine
WORKDIR /usr/src/app
# 의존성 캐시 레이어 분리 (소스 변경 시 npm install 재실행 방지)
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
# nodemon으로 핫리로드 (개발용)
CMD ["npx", "nodemon", "src/index.js"]

레이어 캐싱이 핵심이다. package.json을 먼저 복사하고 npm ci를 실행하면, 소스 코드만 바꿨을 때는 패키지 설치 레이어를 재사용한다. 순서를 바꾸면 소스 한 줄 고칠 때마다 npm install이 다시 돌아간다.


자주 쓰는 명령어 정리

명령어설명
docker compose up -d백그라운드로 전체 서비스 실행
docker compose up app특정 서비스만 실행
docker compose logs -f app앱 로그 실시간 확인
docker compose exec app sh컨테이너 셸 접속
docker compose exec db psql -U devuser myapp_devDB 직접 접속
docker compose down컨테이너 중지 및 제거
docker compose down -v볼륨까지 삭제 (DB 초기화)
docker compose build --no-cache캐시 없이 이미지 재빌드
docker compose ps실행 중인 서비스 상태 확인

주의: docker compose down -v는 PostgreSQL 데이터를 포함한 볼륨을 전부 삭제한다. 로컬 데이터가 날아가도 되는 상황인지 확인하고 실행할 것.


팀 협업 시 실제로 도움 되는 설정들

신규 팀원 온보딩 스크립트

bash

#!/bin/bash
# setup.sh
cp .env.example .env
echo "⚙️  .env 파일을 프로젝트에 맞게 수정하세요."
echo ""
docker compose pull        # 이미지 미리 받기
docker compose build       # 앱 이미지 빌드
docker compose up -d       # 서비스 시작
# DB 마이그레이션 대기 후 실행
sleep 5
docker compose exec app npm run migrate
echo "✅ 환경 세팅 완료. http://localhost:3000 에서 확인하세요."

컨테이너 내부에서 다른 서비스에 접근하는 방법

js

// ❌ 잘못된 예: localhost는 컨테이너 자신을 가리킴
const db = new Pool({ host: 'localhost', port: 5432 });
// ✅ 올바른 예: docker-compose.yml의 서비스 이름이 호스트명
const db = new Pool({ host: 'db', port: 5432 });

Compose는 같은 네트워크 안의 서비스들을 서비스 이름으로 DNS 조회할 수 있게 해준다. dbcache 같은 서비스 이름을 그대로 호스트명으로 사용하면 된다.


FAQ

Q. 볼륨 마운트를 쓰면 node_modules 때문에 오류가 나는데, 왜 그런가요? 호스트 OS와 컨테이너의 아키텍처가 달라 node_modules 안의 네이티브 바이너리가 호환되지 않기 때문이다. 위 예시처럼 - /usr/src/app/node_modules를 별도 익명 볼륨으로 분리하면, 컨테이너 안에서 설치된 모듈이 호스트 디렉토리에 덮어씌워지지 않는다.

Q. Mac에서 볼륨 마운트 성능이 너무 느린데 해결 방법이 있나요? Docker Desktop 4.6 이상에서는 VirtioFS를 기본으로 사용해서 예전보다 많이 나아졌다. 추가로 Compose 파일에 volumes 옵션에 cached 또는 delegated를 붙이거나, 최근엔 docker compose + --file-sharing-path 옵션을 활용하는 방법도 있다. 그래도 느리다면 소스 디렉토리만 골라서 마운트하고 불필요한 디렉토리는 .dockerignore에 추가한다.

Q. 서비스가 여러 개인데 특정 서비스만 재시작하고 싶어요. docker compose restart app으로 특정 서비스만 재시작할 수 있다. 이미지를 다시 빌드해야 한다면 docker compose up -d --build app을 쓴다.

Q. docker-compose.yml을 여러 환경(개발/스테이징/프로덕션)에 맞게 관리하려면요? -f 플래그로 파일을 명시적으로 지정하거나 병합할 수 있다. 예를 들어 docker compose -f docker-compose.yml -f docker-compose.staging.yml up 식으로 스테이징 전용 오버라이드를 적용한다. 프로덕션 배포에는 Compose 대신 Kubernetes나 Docker Swarm을 고려하는 게 일반적이다.

Q. .env 파일 말고 환경변수를 주입하는 다른 방법이 있나요? docker compose up 실행 전에 셸에서 직접 환경변수를 설정하면 .env보다 우선순위가 높다. CI/CD 환경에서는 파이프라인의 secrets 기능을 활용하고, docker compose --env-file ./config/.env.ci up처럼 파일 경로를 명시할 수도 있다.

Q. 컨테이너가 계속 재시작되는데 원인을 어떻게 확인하나요? docker compose logs app 또는 docker compose logs --tail=50 app으로 로그를 확인한다. 컨테이너가 즉시 종료되는 경우는 docker compose ps에서 Exit 코드가 나오는데, 0이 아니면 애플리케이션 오류다. docker compose run --rm app sh로 셸을 열어서 직접 확인하는 것도 유용하다.


마무리 체크리스트

이 체크리스트를 한 번 통과하면 팀에서 쓸 수 있는 수준의 Compose 설정이 된다.

  •  .env와 docker-compose.override.yml이 .gitignore에 추가되어 있는가?
  •  .env.example이 최신 상태로 커밋되어 있는가?
  •  depends_on에 healthcheck 조건이 설정되어 있는가?
  •  node_modules(또는 유사한 의존성 디렉토리)가 별도 익명 볼륨으로 분리되어 있는가?
  •  DB 등 데이터 볼륨이 named volume으로 선언되어 있는가?
  •  앱 컨테이너 내부에서 서비스 이름(호스트명)으로 DB에 접속하고 있는가?
  •  setup.sh 같은 온보딩 스크립트가 있어 신규 팀원이 README 없이도 실행 가능한가?

여기까지 세팅했다면 기본기는 갖춰진 거다. 다음 단계로는 프로덕션용 멀티스테이지 Dockerfile 최적화, 또는 GitHub Actions에서 Compose를 이용한 통합 테스트 자동화를 다뤄볼 만하다.

댓글 남기기