Docker Multi-Stage Build 활용 가이드
Docker Multi-Stage Build 활용 가이드
들어가며
Docker를 사용하다 보면 이미지 크기가 너무 커서 배포와 관리에 어려움을 겪는 경우가 많습니다. 특히 빌드 도구와 종속성이 많은 애플리케이션은 최종 이미지에 불필요한 파일들이 포함되어 이미지 크기가 기하급수적으로 커지는 문제가 발생합니다. Docker 17.05 버전부터 도입된 Multi-Stage Build는 이러한 문제를 효과적으로 해결할 수 있는 방법입니다.
이 글에서는 Docker Multi-Stage Build 기법을 사용하여 이미지 크기를 획기적으로 줄이고 빌드 프로세스를 최적화하는 방법을 다루겠습니다.
Multi-Stage Build란?
Multi-Stage Build는 하나의 Dockerfile 내에서 여러 FROM 명령을 사용하여 여러 단계로 빌드 프로세스를 나누는 기법입니다. 각 단계는 이전 단계의 결과물을 사용할 수 있으며, 최종 이미지에는 필요한 파일만 포함시킬 수 있습니다.
전통적인 방식의 문제점
기존에는 두 가지 접근 방식이 일반적이었습니다:
-
단일 Dockerfile에서 모든 작업 수행: 빌드에 필요한 모든 도구와 런타임 환경을 포함하여 이미지 크기가 커지는 문제 발생
dockerfile1FROM golang:1.18 2WORKDIR /app 3COPY . . 4RUN go build -o myapp 5EXPOSE 8080 6CMD ["./myapp"]
-
빌더 패턴 사용: 별도의 빌더 이미지와 실행 이미지를 사용하는 방식으로, 스크립트가 복잡해지고 관리가 어려움
bash1# 빌드 스크립트 2docker build -t myapp-builder -f Dockerfile.build . 3docker run --name builder myapp-builder 4docker cp builder:/app/myapp ./myapp 5docker build -t myapp -f Dockerfile.run .
Multi-Stage Build는 이러한 문제점을 해결하면서도 간결한 Dockerfile을 유지할 수 있게 해줍니다.
Multi-Stage Build 기본 구조
Multi-Stage Build의 기본 구조는 다음과 같습니다:
1# 첫 번째 단계: 빌드 환경
2FROM golang:1.18 AS builder
3WORKDIR /app
4COPY . .
5RUN go build -o myapp
6
7# 두 번째 단계: 실행 환경
8FROM alpine:3.16
9WORKDIR /app
10COPY /app/myapp .
11EXPOSE 8080
12CMD ["./myapp"]
이 Dockerfile에서:
- 첫 번째 단계에서는
golang:1.18
이미지를 사용하여 애플리케이션을 빌드합니다. - 두 번째 단계에서는 매우 가벼운
alpine:3.16
이미지를 기반으로 새로운 이미지를 생성합니다. COPY --from=builder
명령을 사용하여 첫 번째 단계(builder)에서 빌드한 실행 파일만 새 이미지로 복사합니다.
결과적으로 최종 이미지는 빌드 도구나 소스 코드를 포함하지 않고 실행 파일만 포함하므로 크기가 크게 줄어듭니다.
다양한 언어별 Multi-Stage Build 예제
Node.js 애플리케이션
1# 빌드 단계
2FROM node:16 AS builder
3WORKDIR /app
4COPY package*.json ./
5RUN npm install
6COPY . .
7RUN npm run build
8
9# 실행 단계
10FROM node:16-alpine
11WORKDIR /app
12COPY /app/dist ./dist
13COPY /app/node_modules ./node_modules
14COPY /app/package.json .
15EXPOSE 3000
16CMD ["npm", "start"]
Java (Spring Boot) 애플리케이션
1# 빌드 단계
2FROM maven:3.8-openjdk-17 AS builder
3WORKDIR /app
4COPY pom.xml .
5COPY src ./src
6RUN mvn package -DskipTests
7
8# 실행 단계
9FROM openjdk:17-jdk-slim
10WORKDIR /app
11COPY /app/target/*.jar app.jar
12EXPOSE 8080
13ENTRYPOINT ["java", "-jar", "app.jar"]
Python 애플리케이션
1# 빌드 단계
2FROM python:3.10 AS builder
3WORKDIR /app
4COPY requirements.txt .
5RUN pip install --user -r requirements.txt
6COPY . .
7
8# 실행 단계
9FROM python:3.10-slim
10WORKDIR /app
11COPY /root/.local /root/.local
12COPY /app .
13ENV PATH=/root/.local/bin:$PATH
14EXPOSE 5000
15CMD ["python", "app.py"]
C++ 애플리케이션
1# 빌드 단계
2FROM gcc:11 AS builder
3WORKDIR /app
4COPY . .
5RUN g++ -o myapp main.cpp -static
6
7# 실행 단계
8FROM scratch
9WORKDIR /app
10COPY /app/myapp .
11EXPOSE 8080
12CMD ["./myapp"]
scratch
는 완전히 비어 있는 이미지로, C++ 애플리케이션의 경우 정적으로 링크된 실행 파일만 포함하면 되기 때문에 이미지 크기를 최소화할 수 있습니다.
고급 Multi-Stage Build 기법
1. 조건부 빌드 단계 선택
특정 빌드 단계만 실행하도록 지정할 수 있습니다:
1# 빌드 단계만 실행
2docker build --target builder -t myapp-builder .
3
4# 전체 빌드 실행
5docker build -t myapp .
2. 여러 개의 FROM 문 사용
하나의 Dockerfile에서 여러 독립적인 이미지를 빌드할 수 있습니다:
1# API 서버 빌드
2FROM golang:1.18 AS api-builder
3WORKDIR /app/api
4COPY api/ .
5RUN go build -o server
6
7# 웹 UI 빌드
8FROM node:16 AS ui-builder
9WORKDIR /app/ui
10COPY ui/ .
11RUN npm install && npm run build
12
13# API 서버 이미지
14FROM alpine:3.16 AS api
15WORKDIR /app
16COPY /app/api/server .
17EXPOSE 8080
18CMD ["./server"]
19
20# 웹 UI 이미지
21FROM nginx:alpine AS ui
22COPY /app/ui/dist /usr/share/nginx/html
23EXPOSE 80
24CMD ["nginx", "-g", "daemon off;"]
이 Dockerfile에서 두 개의 독립적인 이미지를 빌드할 수 있습니다:
1docker build --target api -t myapp-api .
2docker build --target ui -t myapp-ui .
3. 빌드 캐시 최적화
종속성 파일을 먼저 복사하고 설치한 후 소스 코드를 복사하면 코드가 변경되어도 종속성 레이어는 캐시를 활용할 수 있습니다:
1FROM node:16 AS builder
2WORKDIR /app
3
4# 종속성 파일만 먼저 복사하고 설치
5COPY package*.json ./
6RUN npm install
7
8# 소스 코드 복사 및 빌드
9COPY . .
10RUN npm run build
11
12FROM node:16-alpine
13WORKDIR /app
14COPY /app/dist ./dist
15# ...
실제 사례 분석: 이미지 크기 비교
아래는 Node.js 애플리케이션을 빌드할 때 Multi-Stage Build를 사용한 경우와 사용하지 않은 경우의 이미지 크기 비교입니다:
빌드 방식 | 이미지 크기 | 빌드 시간 |
---|---|---|
단일 단계 | ~1.2GB | 45초 |
Multi-Stage | ~150MB | 55초 |
Multi-Stage Build를 사용하면 이미지 크기를 약 87% 줄일 수 있었습니다. 빌드 시간은 약간 증가했지만, 이미지 크기 감소로 인한 배포 시간 단축과 리소스 사용량 감소가 그 비용을 상쇄합니다.
주의사항 및 팁
주의사항
-
보안 인증 정보 처리: 빌드 단계에서 사용한 보안 인증 정보가 최종 이미지에 포함되지 않도록 주의해야 합니다.
dockerfile1# 잘못된 예 2FROM node:16 AS builder 3COPY .npmrc . # 인증 정보 포함 4RUN npm install 5 6# 올바른 예 7FROM node:16 AS builder 8ARG NPM_TOKEN 9RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc 10RUN npm install 11RUN rm -f .npmrc
-
최소 권한 원칙: 최종 이미지에서는 애플리케이션을 루트가 아닌 사용자로 실행하는 것이 좋습니다.
dockerfile1FROM node:16-alpine 2RUN addgroup -g 1001 -S appuser && adduser -u 1001 -S appuser -G appuser 3USER appuser 4COPY /app/dist ./dist
유용한 팁
-
빌드 인자 활용: 빌드 환경에 따라 다른 설정을 적용할 수 있습니다.
dockerfile1ARG ENV=production 2RUN if [ "$ENV" = "production" ]; then npm run build:prod; else npm run build:dev; fi
-
.dockerignore
파일 사용: 불필요한 파일이 Docker 컨텍스트에 포함되지 않도록 합니다.# .dockerignore node_modules npm-debug.log .git .gitignore
-
레이어 수 최소화: 관련 명령을 하나의 RUN 문으로 결합하여 레이어 수를 줄입니다.
dockerfile1# 여러 레이어 2RUN apt-get update 3RUN apt-get install -y curl 4RUN apt-get clean 5 6# 단일 레이어 7RUN apt-get update && \ 8 apt-get install -y curl && \ 9 apt-get clean
CI/CD 파이프라인 통합
Multi-Stage Build를 CI/CD 파이프라인에 통합하는 방법을 간단히 살펴보겠습니다.
GitHub Actions 예제
1name: Docker Build and Push
2
3on:
4 push:
5 branches: [ main ]
6
7jobs:
8 build:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v3
12
13 - name: Set up Docker Buildx
14 uses: docker/setup-buildx-action@v2
15
16 - name: Login to Docker Hub
17 uses: docker/login-action@v2
18 with:
19 username: ${{ secrets.DOCKERHUB_USERNAME }}
20 password: ${{ secrets.DOCKERHUB_TOKEN }}
21
22 - name: Build and push
23 uses: docker/build-push-action@v3
24 with:
25 context: .
26 push: true
27 tags: username/myapp:latest
28 cache-from: type=registry,ref=username/myapp:buildcache
29 cache-to: type=registry,ref=username/myapp:buildcache,mode=max
GitLab CI/CD 예제
1stages:
2 - build
3 - deploy
4
5variables:
6 DOCKER_HOST: tcp://docker:2375/
7 DOCKER_DRIVER: overlay2
8
9build:
10 stage: build
11 image: docker:20.10
12 services:
13 - docker:dind
14 script:
15 - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
16 - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG .
17 - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
결론
Docker Multi-Stage Build는 이미지 크기를 줄이고 빌드 프로세스를 최적화하는 강력한 기법입니다. 특히 마이크로서비스 아키텍처에서 여러 컨테이너를 배포하는 경우, 이미지 크기 최적화는 리소스 사용량과 배포 시간에 큰 영향을 미칩니다.
이 글에서 다룬 기법들을 적용하면 다음과 같은 이점을 얻을 수 있습니다:
- 이미지 크기 감소: 실행에 필요한 파일만 포함하여 이미지 크기를 최소화
- 보안 강화: 빌드 도구와 소스 코드가 최종 이미지에 포함되지 않아 공격 표면 감소
- 빌드 프로세스 간소화: 단일 Dockerfile로 복잡한 빌드 과정 관리
- 일관된 빌드 환경: 개발, 테스트, 프로덕션 환경 간의 일관성 유지
Docker를 사용하여 애플리케이션을 배포하는 개발자라면 Multi-Stage Build를 도입하여 컨테이너 이미지를 최적화하는 것을 적극 권장합니다.