[태그:] Docker

  • Docker Compose 멀티 컨테이너 환경, 명령어 한 줄로 DB부터 프론트까지 띄우는 법

    Docker Compose 멀티 컨테이너 환경, 명령어 한 줄로 DB부터 프론트까지 띄우는 법

    💡 Tip. 바쁜 현대인들을 위한 본문 요약

    • Docker Compose는 멀티 컨테이너 환경을 YAML 한 파일로 정의하고 docker compose up 한 줄로 실행하는 도구
    • compose.yml에 services, networks, volumes 3가지만 이해하면 대부분의 로컬 개발환경 구성 가능
    • Node.js + PostgreSQL + Redis 스택을 예제로, 볼륨 마운트와 핫 리로드까지 실전 세팅 정리
    • 포트 충돌, 네트워크 에러 등 흔한 트러블슈팅 5가지 해결법 포함
    • 프로덕션 전환 시 고려할 최적화 포인트와 Kubernetes 마이그레이션 기준까지 분석

    2024 Stack Overflow 개발자 설문에서 전문 개발자의 59%가 Docker를 사용한다고 응답했습니다.
    그런데 정작 Docker Compose 멀티 컨테이너 환경을 제대로 세팅해서 쓰는 비율은 체감상 절반도 안 됩니다.
    DB는 로컬에 직접 설치하고, Redis는 따로 띄우고, 백엔드와 프론트를 각각 다른 터미널에서 실행하는 팀을 여전히 많이 봅니다.

    저도 3년 전까지 그랬습니다.
    신규 팀원이 합류할 때마다 "PostgreSQL 14 설치하고, Redis 7 올리고, .env 파일 이렇게 세팅하고…" 같은 온보딩 문서를 매번 업데이트했습니다.
    그러다 compose.yml 하나로 통일한 뒤 온보딩 시간이 평균 4시간에서 15분으로 줄었습니다.

    이 글에서는 Docker Compose 멀티 컨테이너 환경을 처음부터 실전 수준까지 세팅하는 과정을 다룹니다.
    compose.yml 문법, 실제 스택 예제 3가지, 흔한 에러와 해결법까지 정리했습니다.

    🤔 Docker Compose 멀티 컨테이너, 왜 필요한가

    A of multiple colorful shipping containers stacked neatly...

    Docker 컨테이너 하나만 쓰는 건 쉽습니다.
    문제는 실제 서비스가 단일 컨테이너로 동작하는 경우가 거의 없다는 점입니다.

    일반적인 웹 애플리케이션 하나를 띄우려면 최소 3개 프로세스가 필요합니다.
    API 서버, 데이터베이스, 캐시 서버.
    여기에 프론트엔드 개발 서버, 메시지 큐, 모니터링 도구까지 붙으면 5–7개는 기본입니다.

    수동 관리의 비용

    docker run 명령어를 컨테이너마다 따로 실행하면 어떻게 될까요.

    • 컨테이너 5개에 각각 네트워크 연결, 볼륨 마운트, 환경 변수를 지정해야 합니다
    • 실행 순서가 꼬이면 DB가 안 뜬 상태에서 API 서버가 연결을 시도합니다
    • 팀원마다 실행 순서, 포트 번호, 볼륨 경로가 미묘하게 달라집니다

    📊 데이터: Docker 공식 문서에 따르면, Compose를 사용하면 멀티 페이지 온보딩 가이드를 단일 Compose 파일과 몇 개의 명령어로 대체할 수 있습니다. (Docker Docs)

    Docker Compose가 해결하는 것

    Docker Compose는 이 모든 설정을 compose.yml 한 파일에 선언합니다.
    docker compose up 한 줄이면 정의된 모든 서비스가 올바른 순서로, 같은 네트워크 안에서 시작됩니다.

    핵심은 재현 가능성입니다.
    누가 실행해도, 어떤 OS에서 실행해도 동일한 환경이 만들어집니다.
    "제 컴퓨터에서는 잘 되는데요"라는 말이 사라지는 겁니다.

    📝 Step 1: compose.yml 기본 구조 이해하기

    A of a YAML document with colorful code blocks floating a...

    Docker Compose 멀티 컨테이너 환경의 시작은 compose.yml 파일 작성입니다.
    이 파일 하나에 서비스, 네트워크, 볼륨 세 가지를 정의합니다.

    compose.yml의 3대 요소

    services:
      api:
        image: node:20-alpine
        ports:
          - "3000:3000"
        volumes:
          - ./src:/app/src
        depends_on:
          - db
          - cache
    
      db:
        image: postgres:16-alpine
        environment:
          POSTGRES_DB: myapp
          POSTGRES_USER: dev
          POSTGRES_PASSWORD: devpass
        volumes:
          - pgdata:/var/lib/postgresql/data
        ports:
          - "5432:5432"
    
      cache:
        image: redis:7-alpine
        ports:
          - "6379:6379"
    
    volumes:
      pgdata:
    

    📌 핵심: services는 실행할 컨테이너 목록, volumes는 데이터 영속성, networks는 생략 시 Compose가 자동 생성합니다. 이 3가지만 이해하면 80%는 끝입니다.

    주요 키워드 해설

    • image: 사용할 Docker 이미지. postgres:16-alpine처럼 태그까지 명시하는 것이 좋습니다
    • build: 이미지 대신 Dockerfile로 직접 빌드할 때 사용합니다
    • ports: "호스트:컨테이너" 형식으로 포트를 매핑합니다
    • volumes: 호스트 디렉토리나 네임드 볼륨을 컨테이너에 마운트합니다
    • depends_on: 서비스 시작 순서를 제어합니다
    • environment: 환경 변수를 직접 지정하거나 .env 파일에서 불러옵니다

    Compose v1 vs v2 차이

    2023년 7월 이후 Docker Desktop에서 Compose v1(docker-compose)은 공식 지원이 종료되었습니다.
    현재는 docker compose(하이픈 없음)가 표준입니다.
    파일명도 docker-compose.yml 대신 compose.yml공식 권장입니다.

    💡 팁: 기존 프로젝트에서 docker-compose.yml을 쓰고 있다면 파일명만 compose.yml로 바꿔도 됩니다. 문법은 동일합니다.

    🛠️ Step 2: 실전 예제 3가지 — 스택별 Docker Compose 멀티 컨테이너 세팅

    A of three colorful server rack towers side by side

    이론만으로는 감이 안 옵니다.
    실제 프로젝트에서 바로 복사해서 쓸 수 있는 3가지 스택을 정리했습니다.

    예제 1: Node.js + PostgreSQL + Redis

    가장 범용적인 웹 백엔드 스택입니다.
    제가 실무에서 가장 많이 쓰는 조합이기도 합니다.

    services:
      api:
        build:
          context: .
          dockerfile: Dockerfile
        ports:
          - "3000:3000"
        volumes:
          - ./src:/app/src
          - /app/node_modules
        environment:
          DATABASE_URL: postgres://dev:devpass@db:5432/myapp
          REDIS_URL: redis://cache:6379
        depends_on:
          db:
            condition: service_healthy
          cache:
            condition: service_started
        command: npm run dev
    
      db:
        image: postgres:16-alpine
        environment:
          POSTGRES_DB: myapp
          POSTGRES_USER: dev
          POSTGRES_PASSWORD: devpass
        volumes:
          - pgdata:/var/lib/postgresql/data
          - ./init.sql:/docker-entrypoint-initdb.d/init.sql
        ports:
          - "5432:5432"
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U dev -d myapp"]
          interval: 5s
          timeout: 5s
          retries: 5
    
      cache:
        image: redis:7-alpine
        ports:
          - "6379:6379"
    
    volumes:
      pgdata:
    

    ⚠️ 주의: depends_on만으로는 DB가 "쿼리를 받을 준비"가 됐는지 보장하지 않습니다. 반드시 healthcheckcondition: service_healthy를 함께 사용하세요.

    핵심 포인트가 몇 가지 있습니다.

    • DATABASE_URL에서 호스트명을 db로 지정합니다. Compose가 자동 생성하는 네트워크 안에서 서비스 이름이 DNS 호스트명이 됩니다
    • volumes/app/node_modules를 추가한 이유는 호스트의 node_modules와 컨테이너의 것이 충돌하는 걸 방지하기 위해서입니다
    • init.sqldocker-entrypoint-initdb.d에 마운트하면 최초 실행 시 자동으로 SQL이 실행됩니다

    예제 2: Python FastAPI + MongoDB

    데이터 스키마가 유동적인 프로젝트에서 자주 쓰는 조합입니다.

    services:
      api:
        build: .
        ports:
          - "8000:8000"
        volumes:
          - ./app:/code/app
        environment:
          MONGODB_URL: mongodb://root:rootpass@mongo:27017/mydb?authSource=admin
        depends_on:
          mongo:
            condition: service_healthy
        command: uvicorn app.main:app --host 0.0.0.0 --reload
    
      mongo:
        image: mongo:7
        environment:
          MONGO_INITDB_ROOT_USERNAME: root
          MONGO_INITDB_ROOT_PASSWORD: rootpass
        volumes:
          - mongodata:/data/db
        ports:
          - "27017:27017"
        healthcheck:
          test: echo 'db.runCommand("ping").ok' | mongosh --quiet
          interval: 10s
          timeout: 5s
          retries: 5
    
      mongo-express:
        image: mongo-express:latest
        ports:
          - "8081:8081"
        environment:
          ME_CONFIG_MONGODB_ADMINUSERNAME: root
          ME_CONFIG_MONGODB_ADMINPASSWORD: rootpass
          ME_CONFIG_MONGODB_URL: mongodb://root:rootpass@mongo:27017/
        depends_on:
          - mongo
    
    volumes:
      mongodata:
    

    💡 팁: mongo-express는 MongoDB의 웹 UI 관리 도구입니다. 개발 환경에서만 사용하고, 프로덕션에서는 반드시 제거하세요.

    예제 3: 풀스택 (Next.js + NestJS + PostgreSQL + Redis)

    프론트엔드까지 포함한 풀스택 구성입니다.

    services:
      frontend:
        build:
          context: ./frontend
          target: development
        ports:
          - "3000:3000"
        volumes:
          - ./frontend/src:/app/src
          - /app/node_modules
        environment:
          NEXT_PUBLIC_API_URL: http://localhost:4000
        command: npm run dev
    
      backend:
        build:
          context: ./backend
          target: development
        ports:
          - "4000:4000"
        volumes:
          - ./backend/src:/app/src
          - /app/node_modules
        environment:
          DATABASE_URL: postgres://dev:devpass@db:5432/myapp
          REDIS_URL: redis://cache:6379
        depends_on:
          db:
            condition: service_healthy
          cache:
            condition: service_started
        command: npm run start:dev
    
      db:
        image: postgres:16-alpine
        environment:
          POSTGRES_DB: myapp
          POSTGRES_USER: dev
          POSTGRES_PASSWORD: devpass
        volumes:
          - pgdata:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U dev -d myapp"]
          interval: 5s
          timeout: 5s
          retries: 5
    
      cache:
        image: redis:7-alpine
    
    volumes:
      pgdata:
    

    이 구성에서 프론트엔드의 NEXT_PUBLIC_API_URLlocalhost:4000인 이유에 주목하세요.
    프론트엔드는 브라우저에서 실행되기 때문에 컨테이너 내부 DNS(backend)가 아닌 호스트 포트를 사용해야 합니다.
    서버 사이드 렌더링 시에는 http://backend:4000을 별도 환경 변수로 분리하는 것이 정석입니다.

    ⚡ Step 3: 볼륨 마운트와 핫 리로드 — Docker Compose 멀티 컨테이너 개발 효율 높이기

    A of a circular arrow refresh icon above a laptop screen ...

    Docker 컨테이너 안에서 개발할 때 가장 불편한 점은 코드를 수정할 때마다 이미지를 다시 빌드해야 한다는 것입니다.
    볼륨 마운트와 핫 리로드를 조합하면 이 문제를 완전히 해결할 수 있습니다.

    바인드 마운트 vs 네임드 볼륨

    volumes:
      # 바인드 마운트: 호스트 디렉토리를 직접 연결
      - ./src:/app/src
    
      # 네임드 볼륨: Docker가 관리하는 영속 스토리지
      - pgdata:/var/lib/postgresql/data
    
      # 익명 볼륨: 컨테이너 내부 경로를 호스트에서 덮어쓰지 않도록 보호
      - /app/node_modules
    

    바인드 마운트는 소스 코드 동기화에 사용합니다.
    네임드 볼륨은 DB 데이터처럼 컨테이너를 삭제해도 유지해야 하는 데이터에 사용합니다.

    📌 핵심: node_modules에 익명 볼륨을 거는 이유는 호스트의 macOS/Windows용 네이티브 모듈이 Linux 컨테이너와 호환되지 않기 때문입니다. 이걸 빠뜨리면 sharp, bcrypt 같은 네이티브 바인딩 모듈에서 에러가 발생합니다.

    핫 리로드 세팅 포인트

    Node.js 기준으로 핫 리로드가 작동하려면 3가지 조건이 맞아야 합니다.

    1. 소스 디렉토리가 바인드 마운트되어 있어야 합니다
    2. 개발 서버가 파일 변경을 감지할 수 있어야 합니다 (nodemon, tsx –watch 등)
    3. 파일시스템 이벤트가 컨테이너에 전달되어야 합니다

    macOS와 Windows에서는 Docker Desktop이 파일시스템 이벤트를 가상화하기 때문에 폴링(polling) 방식을 사용해야 할 수 있습니다.
    nodemon의 경우 --legacy-watch 옵션, Webpack은 watchOptions.poll: 1000으로 설정합니다.

    ⚠️ 주의: macOS에서 바인드 마운트 성능이 느리다면 Docker Desktop 설정에서 VirtioFS(기본값)를 확인하세요. 이전 gRPC FUSE 대비 파일 I/O 속도가 최대 3배 개선됩니다.

    .env 파일로 환경 변수 분리

    하드코딩된 비밀번호를 compose.yml에 직접 넣는 것은 좋지 않습니다.
    .env 파일을 분리하세요.

    # .env
    POSTGRES_DB=myapp
    POSTGRES_USER=dev
    POSTGRES_PASSWORD=devpass
    REDIS_PORT=6379
    
    # compose.yml
    services:
      db:
        image: postgres:16-alpine
        environment:
          POSTGRES_DB: ${POSTGRES_DB}
          POSTGRES_USER: ${POSTGRES_USER}
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    

    .env 파일은 반드시 .gitignore에 추가하고, .env.example을 레포에 커밋해서 필요한 변수 목록만 공유합니다.

    ⚠️ 트러블슈팅 — Docker Compose 멀티 컨테이너에서 자주 겪는 에러 5가지

    A of a wrench and gear icon next to a warning triangle sign

    Docker Compose를 처음 도입할 때 거의 반드시 만나는 에러들입니다.
    처음에는 제 경우에도 포트 충돌과 네트워크 에러에서 30분씩 헤맸습니다.

    에러 1: 포트 충돌 (port is already allocated)

    Error response from daemon: driver failed programming external connectivity:
    Bind for 0.0.0.0:5432 failed: port is already allocated
    

    호스트에 이미 PostgreSQL이 설치되어 있으면 5432 포트가 충돌합니다.

    • 해결 1: 호스트의 PostgreSQL 서비스를 중지합니다
    • 해결 2: 포트를 변경합니다 ("5433:5432")
    • 해결 3: ports를 제거하고 Compose 내부 네트워크만 사용합니다 (다른 서비스에서 db:5432로 접근)

    💡 팁: lsof -i :5432로 해당 포트를 점유하고 있는 프로세스를 확인할 수 있습니다.

    에러 2: 서비스 간 연결 실패 (ECONNREFUSED)

    Error: connect ECONNREFUSED 127.0.0.1:5432
    

    API 서버에서 DB 연결 시 localhost127.0.0.1을 사용하면 발생합니다.
    Compose 네트워크 안에서는 서비스 이름을 호스트명으로 사용해야 합니다.

    ❌ DATABASE_URL=postgres://dev:devpass@localhost:5432/myapp
    ✅ DATABASE_URL=postgres://dev:devpass@db:5432/myapp
    

    에러 3: 볼륨 퍼미션 에러

    Linux에서 바인드 마운트한 디렉토리의 소유자가 컨테이너 내부 사용자와 다르면 권한 에러가 발생합니다.
    PostgreSQL의 경우 데이터 디렉토리 소유자가 postgres 사용자(UID 999)여야 합니다.

    # 네임드 볼륨을 사용하면 Docker가 퍼미션을 자동 관리합니다
    volumes:
      pgdata:
    

    바인드 마운트가 꼭 필요하다면 Dockerfile에서 USER를 호스트 UID와 맞추거나, chown을 entrypoint에서 실행하세요.

    에러 4: depends_on이 DB 준비를 보장하지 않음

    depends_on은 컨테이너 시작 순서만 보장합니다.
    PostgreSQL 컨테이너가 시작되었다고 해서 쿼리를 받을 준비가 된 것은 아닙니다.
    초기화 SQL 실행, WAL 복구 등이 끝나야 실제로 접속이 가능합니다.

    depends_on:
      db:
        condition: service_healthy
    

    healthcheckcondition: service_healthy를 반드시 조합하세요.

    에러 5: 이미지 캐시로 변경사항 미반영

    docker compose up --build
    

    Dockerfile을 수정했는데 변경사항이 반영되지 않을 때는 --build 플래그를 추가합니다.
    레이어 캐시를 완전히 무시하려면 docker compose build --no-cache를 실행하세요.

    📊 데이터: Docker는 레이어 캐싱을 통해 변경되지 않은 서비스의 컨테이너를 재사용합니다. Docker Compose 공식 문서에 따르면 이 기능 덕분에 환경 변경을 빠르게 반영할 수 있습니다.

    ✅ 마무리 — Docker Compose 멀티 컨테이너 세팅 체크리스트

    A of a checklist clipboard with colorful checkmarks next ...

    직접 여러 프로젝트에 적용해보면 Docker Compose 멀티 컨테이너 세팅은 한 번 익히면 모든 프로젝트에 복사해서 쓸 수 있는 자산이 됩니다.

    아래 체크리스트로 점검하세요.

    1. compose.yml 파일에 모든 서비스가 정의되어 있는가
    2. DB 서비스에 healthcheck가 설정되어 있는가
    3. 소스 코드 디렉토리가 바인드 마운트되어 있는가
    4. node_modules 등 네이티브 바인딩 경로에 익명 볼륨이 걸려 있는가
    5. 환경 변수가 .env 파일로 분리되어 있는가
    6. .env.gitignore에 포함되어 있는가
    7. 서비스 간 통신에 localhost 대신 서비스 이름을 사용하고 있는가

    📌 핵심: docker compose up -d로 백그라운드 실행, docker compose logs -f api로 특정 서비스 로그만 추적, docker compose down -v로 볼륨까지 완전 삭제. 이 3가지 명령어만 기억하면 일상 운용은 충분합니다.

    리눅스 서버 보안 설정을 마친 VPS에 Docker Compose를 올리면 로컬과 동일한 환경을 서버에서도 재현할 수 있습니다.
    반복적인 배포 파이프라인을 구축하고 싶다면 n8n 업무 자동화 가이드도 참고해 보세요.

    🔍 Root Cause (근본 원인 분석)

    "로컬에서는 되는데 다른 사람 컴퓨터에서는 안 된다"는 문제의 근본 원인은 암묵적 의존성입니다.

    운영체제 버전, 설치된 라이브러리 버전, 환경 변수, 포트 설정, 파일 경로 구조 등이 개발자마다 다릅니다.
    README에 "PostgreSQL 14 이상 설치 필요"라고 적어도 누군가는 16을 쓰고, 누군가는 13을 쓰고 있습니다.
    이 미묘한 차이가 "내 컴퓨터에서만 재현되는 버그"를 만듭니다.

    Docker Compose는 이 암묵적 의존성을 명시적 선언으로 바꿉니다.
    postgres:16-alpine이라고 적으면 모든 팀원이 정확히 같은 버전, 같은 OS 기반의 PostgreSQL을 사용합니다.
    이것이 "Infrastructure as Code"의 가장 기본적인 형태입니다.

    ⚙️ Engineering Rationale (공학적 근거)

    Docker Compose를 선택하는 공학적 이유는 복잡도 대비 효용에 있습니다.

    도구 학습 곡선 로컬 개발 프로덕션 적합 규모
    docker run 수동 실행 낮음 불편 부적합 컨테이너 1–2개
    Docker Compose 중간 최적 소규모 가능 컨테이너 2–15개
    Kubernetes 높음 과도함 최적 컨테이너 15개 이상
    Docker Swarm 중간 불편 중규모 컨테이너 5–30개

    로컬 개발과 소규모 서비스(컨테이너 15개 미만)에서 Docker Compose는 복잡도 대비 최선의 선택입니다.
    Kubernetes는 로컬 개발에는 과도한 오버헤드를 가져옵니다(minikube, kind 등 추가 도구 필요).

    ⚠️ 주의: 프로덕션에서 Docker Compose를 쓸 때는 restart: unless-stopped 정책, 로그 로테이션(logging.options), 리소스 제한(deploy.resources)을 반드시 설정하세요. 이 3가지가 빠지면 장애 시 자동 복구가 안 되거나 디스크가 가득 차는 상황이 발생합니다.

    🚀 Optimization Point (최적화 포인트)

    빌드 캐시 최적화

    Dockerfile에서 COPY package*.jsonRUN npm ciCOPY . . 순서로 작성하면 소스 코드만 변경됐을 때 의존성 설치 레이어를 캐시에서 재사용합니다.
    이것만으로 빌드 시간이 평균 60–70% 단축됩니다.

    COPY package.json package-lock.json ./
    RUN npm ci --production=false
    COPY . .
    

    프로파일(profiles)로 선택적 서비스 실행

    모니터링, 디버깅 도구 등 항상 필요하지 않은 서비스는 profiles로 분리합니다.

    services:
      adminer:
        image: adminer
        ports:
          - "8080:8080"
        profiles:
          - debug
    

    docker compose up으로는 실행되지 않고, docker compose --profile debug up으로만 실행됩니다.
    불필요한 리소스 소모를 줄이는 간단하면서도 효과적인 방법입니다.

    Kubernetes 마이그레이션 시점

    서비스가 15개를 넘어가거나, 오토스케일링이 필요하거나, 무중단 배포가 필수라면 Kubernetes 전환을 검토할 시점입니다.
    kompose convert 명령어로 compose.yml을 Kubernetes 매니페스트로 자동 변환할 수 있지만, 실무에서는 변환 결과를 그대로 쓰기보다 Helm 차트로 재구성하는 것을 권장합니다.

    📊 데이터: Compose Specification은 Compose v1(2.x/3.x)의 레거시 포맷을 통합한 최신 표준으로, Docker Compose CLI v1.27.0 이상(Compose v2)에서 구현됩니다. (Compose file reference)