0467

인프라

[Docker] Docker Compose란?

| 서론

안녕하세요, 팡일입니다.

지난 포스팅에서는 Docker의 기본 개념과 컨테이너를 실행하는 방법에 대해 살펴보았습니다. 단일 컨테이너를 실행하는 수준까지는 비교적 간단하지만, 실제 프로젝트에서는 이야기가 조금 달라집니다.

웹 서버, 애플리케이션 서버, 데이터베이스, 캐시 서버 등 여러 서비스가 함께 동작해야 하고, 이를 각각 따로 실행하고 관리하다 보면 점점 복잡해지게 됩니다.

이번 포스팅에서는 이러한 문제를 해결하기 위해 사용하는 Docker Compose와, 더 나아가 이미지 최적화를 위한 멀티 스테이지 빌드, 그리고 실무에서 유용하게 사용하는 Docker 활용 팁까지 함께 정리해보겠습니다.

| Docker Compose

: 실제 프로젝트에서는 웹 서버, 앱 서버, DB, 캐시 등 여러 서비스가 함께 동작하는데, 이때 각각을 docker run으로 하나씩 실행하고 관리하려고 하면 생각보다 빠르게 복잡도가 증가합니다.

앱 서버를 실행하고 → DB를 띄우고 → 캐시 서버를 띄우고 → 각각 포트 연결하고 → 순서 맞추고…

예를 들어, 위의 과정을 매번 반복해야 한다면 개발 생산성이 크게 떨어지게 됩니다.

그래서 사용하는 것이 Docker Compose이며, 여러 컨테이너를 하나의 YAML 파일로 정의하고, 한 번의 명령으로 전체를 관리할 수 있게 해주는 도구입니다.

즉, “여러 컨테이너를 하나의 프로젝트처럼 관리한다”고 이해하면 쉽습니다.

1) docker-compose.yml 작성하기

: 아래의 예시는 Node.js doq + PostgreSQL + Redis로 구성된 예제입니다. 실무에서도 흔하게 사용되는 구조라, 흐름을 이해해두면 바로 활용할 수 있습니다.

code
# docker-compose.yml (compose.yml도 가능)
services:
  # 웹 애플리케이션
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache
    volumes:
      - .:/app
      - /app/node_modules

  # PostgreSQL 데이터베이스
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - db-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  # Redis 캐시
  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  db-data:

기서 중요한 포인트는 각 서비스가 네트워크로 자동 연결된다는 점입니다. 그래서 db, cache라는 이름만으로도 서로 통신할 수 있습니다.

아래는 주요 옵션들에 대한 설명입니다.

  • build : Dockerfile이 있는 경로

  • image : Docker Hub의 공식 이미지를 사용할 때 지정

  • ports : 호스트:컨테이너 형식으로 매핑

  • environment : 환경 변수를 설정

  • depends_on : 서비스 간 의존성을 정의하며, db가 먼저 시작된 후 app이 시작된다.

  • volumes : 데이터를 영구적으로 보존하거나, 호스트와 파일을 공유한다.

특히 volumes“코드 수정 시 바로 반영” + “데이터 유지” 두 가지에서 매우 중요합니다.

2) 주요 명령어

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

# 로그 확인
docker compose logs -f

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

# 전체 서비스 중지 및 삭제
docker compose down

# 볼륨까지 함께 삭제
docker compose down -v
  • docker compose up -d 한 번이면 앱, DB, 캐시 서버가 한꺼번에 실행됩니다.

  • 이게 왜 중요하냐면 팀원이 프로젝트를 클론하고, 단 한 줄로 동일한 환경을 구성할 수 있기 때문입니다.

| 멀티 스테이지 빌드 (Docker 이미지 크기를 10배 줄이는 방법)

일반적으로 Docker 이미지를 빌드하면, 빌드 도구와 의존성까지 모두 포함되어 이미지 크기가 매우 커질 수 있습니다.

예를 들어 Node.js + node_modules + 빌드 파일까지 전부 포함되면서 1GB 이상의 크기가 될 수 있습니다. 이걸 그대로 배포하면, 배포 속도 느림, 서버 비용 증가, 보안 위험 증가 등이 발생할 수 있습니다.

그래서 사용하는 것이 멀티 스테이지 빌드입니다.

1) 멀티 스테이지 빌드란?

: 하나의 Dockerfile 안에서 여러 개의 FROM 문을 사용하여, 빌드 단계와 실행 단계를 분리하는 기법입니다.

  • 빌드 결과물(artifact)만 최종 이미지에 복사하므로, 불필요한 빌드와 소스 코드를 제거할 수 있습니다.

2) 예제 : React 앱 멀티 스테이지 빌드

code
# ---- Build Stage ----
# Node.js 기반 경량 이미지(alpine)를 사용해서 빌드 환경 구성
FROM node:20-alpine AS build

# 컨테이너 내부 작업 디렉토리를 /app으로 설정
WORKDIR /app

# package.json, package-lock.json만 먼저 복사 (캐시 최적화)
COPY package*.json ./

# 의존성 설치 (npm ci는 lock 파일 기준으로 빠르고 정확하게 설치)
RUN npm ci

# 나머지 소스 코드 전체 복사
COPY . .

# 프로젝트 빌드 실행 (React/Vite/Next 등에서 dist 폴더 생성)
RUN npm run build

# ---- Production Stage ----
# 실제 서비스용 경량 nginx 이미지 사용
FROM nginx:alpine

# build 단계에서 생성된 정적 파일(dist)을 nginx 기본 경로로 복사
COPY --from=build /app/dist /usr/share/nginx/html

# 컨테이너가 80번 포트를 사용한다고 명시
EXPOSE 80

# nginx를 포그라운드 모드로 실행 (컨테이너 종료 방지)
CMD ["nginx", "-g", "daemon off;"]
  • Build Stage에서는 Node.js 환경에서 앱을 빌드

  • Production Stage에서는 경량 Nginx 이미지에 빌드 결과물(dist/)만 복사

→ 이를 통해 결과적으로, 빌드에 사용된 Node.js, node_modules, 소스 코드는 최종 이미지에 포함되지 않습니다.

3) 이미지 크기 비교

code
$ docker images
REPOSITORY          TAG       SIZE
my-app-normal       latest    1.2GB    # 일반 빌드
my-app-optimized    latest    80MB     # 멀티 스테이지 빌드
  • 같은 애플리케이션인데 이미지 크기가 약 15배 차이가 납니다.

  • 이미지가 작을수록 배포 속도가 빨라지고, 보안 공격 표면도 줄어듭니다.

| Docker 실무 꿀팁

1) 미사용 리소스 정리

: Docker를 오래 사용하면 중지된 컨테이너, 미사용 이미지, 네트워크가 쌓여 디스크를 차지합니다.

code
# 사용하지 않는 리소스 한 번에 정리
docker system prune -a

# 디스크 사용량 확인
docker system df
  • docker system prune -a는 모든 미사용 이미지를 삭제하기에, 프로덕션 서버에서는 신중하게 사용하는 것이 좋습니다.

  • 특히 로컬 개발 환경에서는 디스크 용량 터지는 원인 1순위입니다.

2) 컨테이너 모니터링

: CPU, 메모리, 네트워크 I/O를 실시간적으로 확인할 수 있으며, 성능 이슈가 발생했을 시 가장 먼저 확인하는 것을 추천합니다

code
# 컨테이너 리소스 실시간 모니터링
docker stats

3) 이미지 레이어 분석

: 어떤 레이어가 이미지 크기를 키우는지 한눈에 파악할 수 있어, 이미지 최적화에 유용합니다.

code
# 이미지 히스토리 확인 (각 레이어 크기)
docker history my-app

4) .dockerignore 활용

: .gitignore처럼, 빌드 컨텍스트에서 제외할 파일을 지정하며, 불필요한 파일이 포함되면 빌드 속도가 느려지고 이미지 크기가 커지게 됩니다.

CSS
node_modules
.git
.env
*.md
dist
build
.DS_Store
*.log

5) 볼륨으로 데이터 영구 보존

: 컨테이너를 삭제하면 내부 데이터도 함께 사라지는 특징이 있다. 따라서, DB처럼 데이터를 유지해야 하는 서비스에는 반드시 볼륨(Volume)을 사용하는 것이 좋습니다.

code
# 볼륨 생성
docker volume create my-data

# 볼륨 마운트하여 컨테이너 실행
docker run -d -v my-data:/var/lib/postgresql/data postgres:16-alpine

# 볼륨 목록 확인
docker volume ls
  • 볼륨은 컨테이너가 삭제되어도 데이터가 유지되며, 여러 컨테이너에서 공유할 수도 있습니다.

| 결론

이번 포스팅에서는 여러 컨테이너를 효율적으로 관리하기 위한 Docker Compose와, 이미지 최적화를 위한 멀티 스테이지 빌드, 그리고 실무에서 바로 활용할 수 있는 다양한 Docker 활용 팁까지 살펴보았습니다.

단순히 컨테이너를 실행하는 것을 넘어서, 아래의 세 가지가 Docker를 실무에서 제대로 사용하는 핵심이라고 볼 수 있습니다.

  • 여러 서비스를 하나의 프로젝트처럼 관리하고

  • 이미지를 가볍게 유지하며

  • 개발 환경과 운영 환경을 일관되게 가져가는 것

특히 Docker Compose는 팀 협업에서 큰 강점을 가지는데, 누구나 동일한 환경을 빠르게 구성할 수 있다는 점에서 개발 효율을 크게 높여줍니다. 또한 멀티 스테이지 빌드를 통해 이미지 크기를 줄이는 것은, 배포 속도와 안정성, 그리고 보안 측면에서도 매우 중요한 요소입니다.

처음에는 설정 파일이나 개념이 다소 많아 보일 수 있지만, 직접 한 번 구성해보고 실행해보면 흐름이 빠르게 익숙해질 것입니다.