개요
도커를 사용하여 애플리케이션을 컨테이너화할 때 이미지의 크기(Size)는 중요한 요소이다.
불필요한 파일이나 종속성이 포함된 거대한 이미지는 배포 속도를 저하시키게 된다.
이런 문제를 해결하기 위해 도커는 멀티스테이지 빌드 라는 강력한 기능을 제공한다.
이번 포스팅에서는 도커의 멀티스테이지 빌드에 대해 기록해보려고 한다.
멀티스테이지 빌드란?
멀티스테이지 빌드는 하나의 Dockerfile
에서 여러 개의 빌드 단계를 정의하여 최종 이미지를 생성하는 방법 이다.
각 단계는 이전 단계에서 생성된 아티팩트를 활용할 수 있으며, 최종 이미지에는 필요한 부분만 포함된다.
이를 통해 이미지 크기를 크게 줄이고, 보안도 향상시킬 수 있다.
동작 과정을 간단하게 살펴보자.
Multi Stage는 여러 개의 Base Image를 사용하여 docker build
를 수행한다.
Base Image는 docker build
에 사용하는 기본 이미지이다. Dockerfile
에서 FROM
옵션에 설정된 이미지를 뜻한다.
FROM
키워드를 기준으로 작업 공간이 분리되는데, 분리된 작업 공간을 stage
라고 칭한다.
Multi Stage는 stage가 2개 이상일 때 해당된다
위 예제를 docker build
로 실행하면 docker build 로그에서 stage가 출력된다.
stage가 2개 이므로 2개의 작업 영역이 분리되는 셈이다.
stage가 여러개라고 해서 도커이미지가 여러개 생성되는 거는 아니다.
가장 마지막에 실행된 stage
작업이 도커 이미지로 생성된다
아래 예제는 우분투 정보를 출력하는 Dockerfile
이다.
우분투18, 우분투16 버전 정보를 출력하지만, 가장 마지막에 실행된 우분투16 버전 출력이 도커이미지로 생성된다.
Multi Stage 활용사례
1. 도커 이미지 경량화
가장 많이 활용하는 사례는 도커 이미지의 사이즈를 줄이는 것이다. 모든 도커 이미지에 대한 사이즈를 줄일 수는 없고 빌드 단계가 있는 도커 이미지 등 단계가 분리되어 있는 Dockerfile만 적용 가능하다.
아래 예제는 Multi Stage를 사용했을 경우 도커 이미지 크기가 얼마나 줄었는지 보여주는 예시이다.
그렇다면 도커 이미지를 왜 줄여야 하는걸까?
그 이유는 바로 저장 장치(하드디스크 등) 용량을 줄이기 위해서이다
개발자가 아닌 서버 관리자 입장에서, 도커 이미지 크기를 줄이는 것은 매우 중요하다.
애플리케이션 버전 별로 도커 이미지를 관리해야 하는데, 도커 이미지의 용량이 500MB 이상이라면, 수 많은 도커 이미지들의 그 용량을 어떻게 감당 하겠는가?
2. 빌드 속도 향상
MultiStage를 사용하면 Dockerfile
작업을 병렬로 실행하여 빌드 속도를 향상 시킬 수 있다.
단, 병렬로 실행하고자 하는 작업에 연관관계가 없어야 한다!
멀티스테이지 빌드 핵심 구문
COPY --from
COPY --from
구문은 멀티스테이지 빌드에서 이전 단계의 이미지에서 파일이나 디렉토리를 복사할 때 사용된다.
간단한 예제를 통해 설명해보겠다.
예를 들어, 웹 애플리케이션을 빌드하는데 필요한 두 가지 단계가 있다고 가정해보자.
첫번째 단계에서는 소스 코드를 빌드하고,
두번째 단계에서는 정적 파일과 실행 가능한 애플리케이션을 생성하도록 코드를 작성해보자.
# 첫번째 단계: 빌드
FROM node:alpine AS builder
WORKDIR /app
COPY ./package*.json .
RUN npm install
COPY . .
RUN npm run build
# 두번째 단계: 애플리케이션 실행
FROM nginx:alpine
WORKDIR /usr/share/nginx/html
# 이전 단계의 빌더 이미지에서 생성된 파일들을 복사
COPY /app/build .
위의 코드에서 첫 번째 단계에서는 Node.js를 사용하여 소스 코드를 빌드한다.
이 단계에서 생성된 이미지는 builder
로 태그를 지정해주었다.
두 번째 단계에서는 Nginx를 사용하여 정적 파일을 제공하는데,
여기서 COPY --from=builder /app/build .
구문을 사용하여 이전 단계에서 생성된 builder 이미지에서 /app/build
디렉토리의 파일들을 현재 작업 디렉토리로 복사하였다.
실전 예제
Multi Stage를 적용하지 않으면 stage 한 개에 모든 작업 내용이 실행된다.
그러므로 최종 명령어를 실행하기 위한 불필요한 것들이 하나의 도커 이미지에 다 들어가게 되는 것이다.
이제, 위처럼 필요없는 파일들이 도커 이미지에 들어가 이미지 용량이 불필요하게 늘어나지 않도록,
도커 이미지를 경량화하는 예제들로 멀티스테이지 빌드를 연습해보자.
1. Go 애플리케이션 배포
시나리오
GO로 작성된 웹 서버를 도커로 배포하고자 한다.
빌드 단계에서는 Go 컴파일러가 필요하지만, 실행 단계에서는 빌드된 바이너리만 필요하다.
# 빌드 단계
FROM golang:1.18-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# 실행 단계
FROM alpine:latest
WORKDIR /app
COPY /app/myapp .
EXPOSE 8080
CMD ["./myapp"]
- 빌드 단계: Go 환경이 포함된 이미지로 애플리케이션을 빌드한다.
- 실행 단계: 경량화된 Alpine 이미지를 사용하여, 빌드된 바이너리만 복사한다.
2. React 애플리케이션 배포
시나리오
React로 만든 프론트엔드 애플리케이션을 Nginx를 통해 서비스하려고 한다.
빌드에는 node.js
가 필요하지만, 최종 이미지는 nginx
로 구성된다.
# 빌드 단계
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 실행 단계
FROM nginx:stable-alpine
COPY /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
- 빌드 단계: node.js 환경에서 애플리케이션을 빌드한다.
- 실행 단계: nginx 이미지를 사용하고, 빌드된 정적 파일을 nginx의 기본 경로에 복사한다
3. Java maven 프로젝트 배포
시나리오
Maven을 사용하는 Java 애플리케이션을 도커로 배포한다.
빌드 단계에서는 Maven이 필요하지만, 실행 단계에서는 빌드된 JAR 파일만 필요하다.
# 빌드 단계
FROM maven:3.8.5-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package
# 실행 단계
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY /app/target/myapp.jar .
EXPOSE 8080
CMD ["java", "-jar", "myapp.jar"]
- 빌드 단계: Maven을 사용하여 애플리케이션을 패키징한다.
- 실행단계: 빌드된 JAR 파일만 포함하여 이미지를 최소화한다.
4. Python 애플리케이션 배포
시나리오
파이썬으로 작성된 애플리케이션을 경량 이미지로 배포한다.
빌드 단계에서는 필요한 패키지를 설치하고, 실행 단계에서는 최소한의 환경만 사용한다.
# 빌드 단계
FROM python:3.10-slim AS builder
WORKDIR /app
USER root
COPY requirements.txt .
RUN pip install --user -r requirements.txt
COPY . .
# 실행 단계
FROM python:3.10-alpine
WORKDIR /app
COPY /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
EXPOSE 5000
CMD ["python", "app.py"]
- 빌드 단계: 필요한 패키지를 사용자(
root
) 디렉토리에 설치한다. - 실행 단계: 경량화된 alpine 이미지를 사용하고, 필요한 파일만 복사한다.