CloudNative
애플리케이션별 Docker Multi-stage Build
Node.js, Spring Boot, Go, Python, React 애플리케이션의 Multi-stage Image Build 방법
1 ) 애플리케이션과 Multi-stage Build
애플리케이션의 언어와 Framework가 달라도 Multi-stage Build의 기본 흐름은 같다.
-
Builder Stage에서 의존성을 설치한다.
-
Source Code를 Build하거나 실행에 필요한 환경을 준비한다.
-
Runtime Stage를 별도의 Base Image로 시작한다.
-
Builder Stage에서 만든 산출물만 Runtime Stage로 복사한다.
-
최종 Image에서 Application을 실행한다.
언어별 차이는 최종 산출물과 실행 환경에 있다.
| 애플리케이션 | Builder Stage의 작업 | Runtime Stage로 전달하는 대상 | Runtime |
|---|---|---|---|
| Node.js | Package 설치 | Source Code와 설치된 Package | Node.js |
| Spring Boot | Gradle Build | JAR | JRE |
| Go | Source Code Compile | 실행 Binary | Alpine Linux |
| Python | 가상 환경에 Package 설치 | Python 가상 환경 | Python |
| React | 정적 File Build | dist Directory |
Nginx |
Spring Boot, Go, React는 Build 산출물이 명확하므로 Builder의 Build 도구를 최종 Image에서 제외하기 쉽다. Node.js와 Python은 Source Code를 Runtime에서 실행하므로 의존성 설치 결과를 별도 Stage로 전달한다.
2 ) Node.js Multi-stage Build
Node.js 예제는 Builder Stage에서 Package를 설치하고 Runtime Stage에서는 더 작은 Alpine 기반 Node.js Image로 Application을 실행한다.
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app ./
EXPOSE 3000
USER node
CMD ["node", "app.js"]
Build 과정은 다음과 같다.
-
node:20-slim에서package.json을 먼저 복사하고 Package를 설치한다. -
Source Code를
/app에 복사한다. -
node:20-alpine으로 Runtime Stage를 시작한다. -
Builder의
/app을 Runtime Stage로 복사한다. -
node사용자로app.js를 실행한다.
docker build -t nodeapp:2.0 .
이 예제는 Builder의 Application Directory 전체를 복사하므로 Source Code와 node_modules가 모두 Runtime Stage로 전달된다. 두 Stage를 분리하는 흐름과 더 작은 Runtime Base Image를 사용하는 방법을 보여주는 예제이다.
3 ) Spring Boot Gradle Multi-stage Build
Spring Boot는 JDK와 Gradle이 필요한 Build 환경과 JAR 실행에 필요한 JRE 환경을 분리한다.
프로젝트 준비
git clone https://github.com/itggangpae/spring_docker.git
cd spring_docker
Dockerfile
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY gradlew ./
COPY gradle ./gradle
RUN chmod +x gradlew
COPY build.gradle ./
COPY settings.gradle ./
RUN ./gradlew dependencies --no-daemon
COPY src ./src
RUN ./gradlew clean build -x test --no-daemon
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
이전 실습에서는 Host에서 Gradle Build를 수행하고 생성된 JAR를 Docker Image에 복사했다. Multi-stage Build에서는 다음 과정이 하나의 Dockerfile 안에서 이루어진다.
-
JDK Image에서 Gradle Wrapper로 JAR를 Build한다.
-
새로운 JRE Image를 Runtime Stage로 사용한다.
-
Builder에서 생성한 JAR만 Runtime Stage로 복사한다.
-
최종 Image에는 Gradle, Source Code와 JDK가 남지 않는다.
docker build -t spring-docker:2.0 .
build.gradle과 settings.gradle을 Source Code보다 먼저 복사하여 의존성 Layer를 분리했다. 의존성 설정이 바뀌지 않았다면 Source Code 변경 시 기존 의존성 Cache를 활용할 수 있다.
4 ) Go Multi-stage Build
Go는 Builder Stage에서 Source Code를 실행 Binary로 Compile하고 최종 Image에는 Binary만 복사한다.
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root
COPY --from=builder /app/main ./main
EXPOSE 8000
CMD ["./main"]
각 Stage의 역할은 다음과 같다.
-
Builder Stage는 Go Compiler와 Module 의존성을 사용한다.
-
CGO_ENABLED=0으로 C Library에 대한 동적 의존성을 사용하지 않는 Binary를 Build한다. -
Runtime Stage는
alpine:latest를 사용하고 HTTPS 인증에 필요한 CA 인증서를 설치한다. -
최종 Image에는 Go Compiler와 Source Code 대신
mainBinary만 포함한다.
docker build -t goapp:2.0 .
5 ) Python Multi-stage Build
Python은 Compile된 단일 실행 File보다 Source Code와 Package가 설치된 환경이 필요하다. Builder Stage에서 가상 환경을 만들고 Runtime Stage로 해당 가상 환경을 복사한다.
FROM python:3.12-slim AS builder
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
COPY . .
RUN adduser --disabled-password --gecos "" appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
주요 설정은 다음과 같다.
| 설정 | 역할 |
|---|---|
PYTHONDONTWRITEBYTECODE=1 |
.pyc File 생성 방지 |
PYTHONUNBUFFERED=1 |
표준 출력과 오류의 Buffering 비활성화 |
/opt/venv |
Builder와 Runtime에서 공통으로 사용하는 가상 환경 경로 |
appuser |
Application을 실행할 일반 사용자 |
docker build -t fastapiapp:2.0 .
강의에서는 처음에 일반 python Image를 사용한 뒤 python:3.12-slim으로 변경했다. Python은 Compiler를 완전히 제거하는 방식보다 작은 Base Image를 선택하고 가상 환경만 복사하는 방식으로 최종 Image를 줄인다.
6 ) React 정적 File Multi-stage Build
React Source Code는 Browser가 직접 실행할 수 있는 정적 HTML, JavaScript와 CSS File로 Build한다. Docker 환경에서는 Nginx나 Apache로 정적 File을 제공할 수 있고, Cloud 환경에서는 Web Endpoint를 제공하는 Object Storage에 배포할 수도 있다.
프로젝트 생성과 확인
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev -- --host 0.0.0.0
curl http://localhost:5173
Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:stable-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Builder Stage에서는 Vite가 React Source Code를 /app/dist의 정적 File로 변환한다. Runtime Stage에는 Node.js, npm, Source Code를 복사하지 않고 dist만 Nginx의 기본 Document Root에 복사한다.
.dockerignore
node_modules/
dist/
.git/
.DS_Store
Image Build와 Container 실행
docker build -t my-react-app .
docker container run \
--name my-react-app \
-d \
-p 8000:80 \
my-react-app
curl http://localhost:8000
7 ) 언어별 차이 비교
반복된 실습의 핵심은 Dockerfile의 형태가 아니라 Builder에서 Runtime으로 무엇을 전달하는지에 있다.
| 구분 | Node.js | Spring Boot | Go | Python | React |
|---|---|---|---|---|---|
| 실행 형태 | Source Code | JAR | Native Binary | Source Code | 정적 File |
| Build 도구 | npm | Gradle·JDK | Go Compiler | pip·가상 환경 | npm·Vite |
| Runtime | Node.js | JRE | Alpine | Python | Nginx |
| 전달 대상 | Application Directory | app.jar |
main |
/opt/venv |
/app/dist |
| 주요 절감 대상 | Builder Base Image | JDK·Gradle·Source | Compiler·Source | Build Cache | Node.js·Source |
Multi-stage Build가 모든 언어에서 동일한 크기 절감 효과를 만드는 것은 아니다. Compile 결과만 전달할 수 있는 Spring Boot, Go, React에서는 Build 환경과 Runtime 환경의 구분이 명확하다. Node.js와 Python은 Runtime에도 언어 실행 환경과 Package가 필요하므로 필요한 의존성을 선별하고 작은 Base Image를 사용하는 것이 중요하다.
8 ) 종합 실습
강의의 종합 실습은 Database와 Web Application을 Container로 구성하는 과정이다.
-
MySQL, MariaDB, PostgreSQL, MongoDB, Redis 중 하나를 선택한다.
-
외부에서 접속할 수 있도록 Database Container를 생성한다.
-
프로그래밍 언어로 Database에 접속하는 Web Application을 작성한다.
-
Table에 데이터 한 건을 삽입하고 전체 데이터를 조회하는 기능을 구현한다.
-
Web Application을 Multi-stage Build로 Docker Image로 만든다.
-
Image로 Container를 실행하고 외부에서 접속되는지 확인한다.
Kafka를 이용한 CQRS 구현은 별도의 Kafka·CQRS 실습 범위에 해당하므로 이 문서에서는 다루지 않는다.
최종 정리
Multi-stage Build는 Build 환경과 Runtime 환경을 서로 다른 Stage로 분리한다.
COPY --from으로 Builder에서 생성한 실행 산출물이나 의존성 환경만 Runtime Stage에 전달한다.Spring Boot는 JAR, Go는 실행 Binary, React는 정적 File을 최종 Image에 복사한다.
Node.js와 Python은 Runtime에도 언어 실행 환경과 Package가 필요하다.
의존성 File을 Source Code보다 먼저 복사하면 Build Cache를 효율적으로 사용할 수 있다.
최종 Stage에서 작은 Base Image와 일반 사용자를 사용하면 Image 크기와 실행 권한을 함께 관리할 수 있다.
댓글