Docker (12) - 멀티 스테이지 빌드와 Dockerfile 작성 원칙
앞선 글에서는 Dockerfile의 주요 명령어와 이미지 빌드 방법을 정리했다. 이번 글에서는 빌드 환경과 실행 환경을 분리하는 멀티 스테이지 빌드(Multi-stage build) 를 살펴본다.
멀티 스테이지 빌드를 사용하면 컴파일러, 패키지 관리자, 개발용 의존성처럼 빌드할 때만 필요한 도구를 최종 이미지에서 제외할 수 있다. 이번 글에서는 Java와 Gradle을 활용한 다단계 빌드 예제를 중심으로 Dockerfile 작성 원칙과 이미지 최적화 방법을 정리한다.
1. 멀티 스테이지 빌드란
멀티 스테이지 빌드는 하나의 Dockerfile 안에서 빌드 단계를 여러 개로 나누는 방식이다. 각 단계는 FROM 명령어로 시작하며, 필요한 경우 AS를 사용해 단계에 이름을 붙일 수 있다.
1
2
3
4
5
6
7
8
FROM 빌드에 필요한 이미지 AS builder
# 소스 코드 컴파일 및 패키지 설치
FROM 실행에 필요한 이미지
# 앞선 단계의 결과물만 복사
COPY --from=builder /app/build /app/build
각 단계는 독립된 이미지 환경처럼 실행되지만, 뒤의 단계에서 앞선 단계의 파일이나 디렉터리를 선택적으로 복사할 수 있다. 최종적으로 만들어지는 이미지는 마지막 FROM 단계의 내용과 그 단계에 복사한 산출물만 포함한다.
따라서 빌드 단계에는 JDK, 컴파일러, 개발용 라이브러리를 사용하고 실행 단계에는 JRE나 런타임 이미지처럼 더 작은 이미지를 사용할 수 있다.
| 구분 | 빌드 단계 | 실행 단계 |
|---|---|---|
| 목적 | 소스 코드 컴파일, 의존성 설치 | 애플리케이션 실행 |
| 포함 내용 | 컴파일러, 빌드 도구, 개발 의존성 | 실행 파일, 런타임, 필요한 라이브러리 |
| 최종 이미지 포함 여부 | 필요한 결과물만 선택적으로 포함 | 최종 이미지의 기준이 됨 |
2. Java와 Gradle 다단계 빌드
Java 애플리케이션은 JDK가 있어야 컴파일할 수 있지만, 빌드가 끝난 뒤 실행할 때는 JRE만 있어도 충분한 경우가 많다. 따라서 빌드 단계에서는 Java 17 JDK를 사용하고, 실행 단계에서는 Java 17 JRE를 사용한다.
Dockerfile은 다음과 같이 작성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM eclipse-temurin:17-jdk AS build
WORKDIR /app
COPY gradlew settings.gradle build.gradle ./
COPY gradle ./gradle
RUN ./gradlew dependencies --no-daemon || true
COPY src ./src
RUN ./gradlew bootJar --no-daemon
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
build 단계에서는 Java 17 JDK와 Gradle Wrapper를 사용해 Spring Boot 실행용 JAR 파일을 생성한다. dependencies 작업은 의존성 캐시를 준비하기 위한 단계이며, 환경에 따라 실패하더라도 다음 빌드를 진행하도록 || true를 붙였다. 이후 bootJar 작업으로 실행 가능한 JAR 파일을 만든다.
실행 단계에서는 /app/build/libs/ 아래에 생성된 JAR 파일만 app.jar라는 이름으로 복사하고, Java 17 JRE 환경에서 실행한다.
이전에는 외부 환경에서 애플리케이션을 빌드한 뒤 그 결과물을 이미지에 복사하는 방식이었다면, 멀티 스테이지 빌드에서는 Dockerfile 내부에서 bootJar 실행까지 수행한다. 최종 이미지에는 JDK나 Gradle 소스가 들어가지 않기 때문에 이미지 크기와 실행 환경의 불필요한 구성 요소를 줄일 수 있다.
3. 이미지 크기를 줄이는 Dockerfile 작성 원칙
이미지가 크면 빌드와 배포에 더 많은 시간과 저장 공간이 필요하다. 최종 이미지에 실제 실행에 필요하지 않은 파일이 들어가지 않도록 다음 원칙을 적용할 수 있다.
경량 베이스 이미지 선택
가능하다면 alpine이나 slim 같은 경량 이미지를 사용한다. 다만 이미지가 작다는 이유만으로 항상 적합한 것은 아니다. 애플리케이션이 필요로 하는 시스템 라이브러리와 호환성까지 확인한 뒤 선택해야 한다.
빌드 도구와 캐시 제거
멀티 스테이지 빌드로 컴파일러와 개발 도구를 실행 단계에서 제외한다. 패키지 설치 과정에서 생성된 캐시나 임시 파일도 최종 이미지에 남지 않도록 --no-cache 옵션이나 정리 명령을 활용한다.
Dockerfile 명령 순서 조정
Docker는 변경되지 않은 레이어를 빌드 캐시에서 재사용한다. 따라서 자주 바뀌는 소스 코드 복사는 뒤쪽에 두고, 변경이 드문 의존성 설치는 앞쪽에 배치하는 편이 효율적이다.
1
2
3
4
5
6
7
8
9
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
RUN chmod +x gradlew
RUN ./gradlew dependencies --no-daemon
# 소스 코드는 자주 바뀌므로 뒤에서 복사
COPY src src
소스 코드가 바뀌어도 Gradle 설정 파일이 바뀌지 않았다면 의존성 설치 레이어를 재사용할 수 있다.
.dockerignore 작성
이미지를 빌드할 때 지정한 디렉터리의 파일과 하위 디렉터리는 Docker context에 포함된다. Git 디렉터리, 로그, 로컬 의존성처럼 빌드에 필요하지 않은 항목은 .dockerignore로 제외한다.
1
2
3
4
5
.git
.gradle
build
*.log
.env
특히 .env나 개인 설정 파일처럼 비밀 정보가 들어갈 수 있는 파일은 이미지 빌드 컨텍스트에 들어가지 않도록 주의해야 한다.
4. 보안과 운영을 고려한 작성 원칙
root 사용자 사용 줄이기
컨테이너의 기본 사용자는 root다. 애플리케이션이 root 권한 없이 실행될 수 있다면 USER 명령으로 별도의 일반 사용자 계정을 지정하는 것이 안전하다.
명확한 이미지 태그 사용
latest 태그는 실제 이미지 내용이 언제 바뀌었는지 알기 어렵고, 동일한 태그가 다른 이미지로 교체될 수 있다. 배포 환경에서는 v1.0.1처럼 버전을 명확히 표시하는 태그를 사용하는 편이 좋다.
빌드 단계에 비밀 정보 전달하지 않기
비밀번호나 개인키 같은 민감한 값을 Dockerfile의 ARG, ENV, COPY로 직접 포함하면 이미지 레이어나 빌드 기록에 남을 수 있다. 인증 정보가 필요한 경우에는 Docker의 비밀 정보 전달 기능이나 배포 환경의 시크릿 관리 기능을 사용하는 것이 적절하다.
하나의 컨테이너에는 하나의 애플리케이션
하나의 컨테이너에 여러 애플리케이션을 넣으면 애플리케이션 간 결합도가 높아지고 확장과 장애 격리가 어려워진다. 가능한 경우 애플리케이션을 분리하고, 컨테이너 단위로 독립적인 배포와 확장이 가능하도록 구성한다.
5. Docker context와 개발 디렉터리
Docker 이미지를 빌드할 때 마지막에 지정하는 경로가 Docker context가 된다.
1
docker build -t springapp:1.0 .
여기서 .은 현재 디렉터리와 하위 파일 전체를 빌드 과정에서 참조할 수 있다는 뜻이다. 따라서 IaC 방식으로 개발할 때는 Dockerfile과 애플리케이션에 필요한 파일만 별도 디렉터리에 모아 사용하는 것이 좋다.
불필요하게 상위 디렉터리를 context로 지정하면 전송되는 파일이 많아지고, 실수로 민감한 파일이 빌드에 포함될 위험도 커진다. .dockerignore를 사용하더라도 애초에 적절한 범위의 디렉터리를 context로 지정하는 습관이 중요하다.
6. 서버리스 환경과 컨테이너
Dockerfile로 미리 실행 환경을 정의해두면 서버를 직접 배포하고 확장하는 작업을 줄일 수 있다. 클라우드의 컨테이너 기반 서버리스 환경에서는 이미지와 실행 설정을 제공하면 인프라의 프로비저닝, 확장, 고가용성 유지와 같은 작업을 플랫폼이 처리한다.
다만 서버리스 환경이 모든 운영 작업을 없애는 것은 아니다. 이미지 크기, 시작 시간, 포트 설정, 상태 저장 방식, 환경 변수와 비밀 정보 관리 등 애플리케이션에 필요한 실행 조건은 여전히 개발자가 명확히 정의해야 한다.
7. 정리
멀티 스테이지 빌드는 하나의 Dockerfile에서 빌드 환경과 실행 환경을 분리하는 방법이다. Java 애플리케이션은 빌드 단계에서 JDK와 Gradle로 JAR 파일을 생성하고, 실행 단계에는 JRE와 JAR 파일만 남기는 구조로 만들 수 있다.
좋은 Dockerfile은 단순히 명령어를 나열하는 데서 끝나지 않는다. 경량 베이스 이미지 선택, 빌드 캐시 활용, .dockerignore 작성, 일반 사용자 실행, 명확한 태그 사용까지 함께 고려해야 작고 안전하며 재현 가능한 이미지를 만들 수 있다.
특히 Docker context의 범위를 적절하게 정하고, 빌드 도구와 비밀 정보가 최종 이미지에 남지 않도록 관리하는 것이 중요하다. 이런 원칙을 적용하면 배포 속도와 이미지 관리 효율을 함께 개선할 수 있다.