Search

Multi-stage builds

Explanation

오래된 빌드 방식의 경우엔, 모든 빌드 지침들(instructions)은 순차적이며 하나의 빌드 컨테이너 안에서 실행됨(의존성 설치, 코드 컴파일, 앱 패키징). 이러한 레이어들은 최종 이미지에서 끝남. 이러한 방식은 작동하긴 하지만 불필요한 무게와 보안 문제를 가지면서 사이즈를 불필요하게 키우게됨. 이때 Multi-stage 빌드 방식이 등장함.
Multi-stage 빌드는 Dockerfile 내에서 명확한 목적을 가진 각각의 다수의 스테이지를 도입함. 여러 개의 다른 환경들 내 빌드의 다른 부분들을 동시에 실행하는 능력을 생각하면 됨. 최종 런타임 환경으로부터 빌드 환경을 분리함으로써, 우리는 상당한 양의 이미지 사이즈와 공격 표면을 줄일 수 있게 됨. 이는 거대한 빌드 의존성을 가진 앱에게 특히나 유용함.
Multi-stage 빌드는 모든 타입의 앱들에게 유용함
인터프리터 언어(자바스크립트, 루비, 파이썬)의 경우, 하나의 스테이지에서 우리의 코드를 줄이고 빌드할 수 있으며 production 준비 상태의 파일을 더 작은 런타임 이미지에 복사할 수 있음. 이는 개발을 위해 우리들의 이미지를 최적화시킴
컴파일 언어(C, Go, Rust)의 경우, 하나의 스테이지에서 컴파일 가능하게 빌드해주며 컴파일된 바이너리를 최종 런타임 이미지에 복사할 수 있음. 전체 컴파일러를 최종 이미지에 번들링할 필요 없음
여기 pseudo-code를 이용해 multi-stage 빌드 구조의 간단한 예시가 있음. FROM 지시문과 새로운 AS <stage-name>을 주의깊게 볼 것. 추가적으로, COPY 지시문이 이전 스테이지( —from)을 복사한다는 것을 확인할 것
# Stage 1: Build Environment FROM builder-image AS build-stage # Install build tools (e.g., Maven, Gradle) # Copy source code # Build commands (e.g., compile, package) # Stage 2: Runtime environment FROM runtime-image AS final-stage # Copy application artifacts from the build stage (e.g., JAR file) COPY --from=build-stage /path/in/build/stage /path/to/place/in/final/stage # Define runtime configuration (e.g., CMD, ENTRYPOINT)
Docker
복사
이 Dockerfile은 두 단계를 사용함 :
build stage
앱을 컴파일하기 위해 필요한 빌드 툴들을 포함하는 base image를 사용함. 빌드 툴들을 설치하고, 소스 코드를 복사하고, 빌드 명령어를 실행하는 등의 명령어들을 포함함
final stage
앱을 실행하는 데에 적합한 더 작은 base 이미지를 사용함. 빌드 스테이지로부터 컴파일된 것들(artifacts : JAR file)을 복사함. 최종적으로, 앱을 시작하기 위해 런타임 구성을 정의(CMD, ENTRYPOINT를 사용해서)

Try it out

이번에는 샘플 자바 애플리케이션을 위한 간소하고 효율적인 도커 이미지들을 생성하기 위한 multi-stage 빌드의 강력함을 알아볼 것임. 예시로 Maven으로 빌드된 “Hello World” 스프링부트 기반 애플리케이션을 사용할 예정
1.
Docker Desktop을 설치
2.
pre-initialized project를 열어 ZIP file을 생성함 :
다음과 같은 화면이 보일 것
Generate를 선택하고 zip file을 다운받음.
3.
프로젝트 디렉토리로 이동. zip file을 풀고 나면, 다음과 같은 폴더 구조를 확인 가능 :
/src/main/java : 소스코드를 포함
/src/test/java : 테스트 코드를 포함
pom.xml : Project Object Model
Maven 프로젝트 설정의 핵심
커스텀된 프로젝트를 빌드하는 데 필요한 정보의 대부분을 포함하는 하나의 구성 파일
4.
“Hello World!”를 디스플레이하는 RESTful 웹 서비스를 생성
a.
src/main/java/com/example/spring_boot_docker/ 폴더 아래, SpringBootDockerApplication.java 파일을 다음 내용으로 수정 :
package com.example.spring_boot_docker; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @SpringBootApplication public class SpringBootDockerApplication { @RequestMapping("/") public String home() { return "Hello World"; } public static void main(String[] args) { SpringApplication.run(SpringBootDockerApplication.class, args); } }
Java
복사
SpringbootDockerApplication.java 파일은 com.example.spring_boot_docker 패키지를 선언하고 필요한 스프링 프레임워크들을 import해옴으로써 시작함. 이 자바 파일은 유저가 홈페이지를 방문하면 “Hello World!”를 응답하는 간단한 스프링 부트 웹앱을 생성함

Create the Dockerfile

이제 프로젝트가 있으므로, Dockerfile을 생성할 준비가 됨
1.
Dockerfile을 다른 폴더들 & 파일들을 포함하는 동일 폴더(ex. src, pom.xml 등등) 내에 생성
2.
Dockerfile 내에서, 다음 라인을 추가함으로써 base image를 정의 :
FROM eclipse-temurin:21.0.2_13-jdk-jammy
Docker
복사
3.
이제, WORKDIR 지침을 사용함으로써 워킹 디렉터리를 정의. 이는 컨테이너 이미지 내에 파일들이 복사되고 이후의 명령어들이 실행되는 곳을 명시하는 것임
WORKDIR /app
Docker
복사
4.
Maven wrapper script와 pom.xml 파일을 도커 컨테이너 내의 현재의 워킹 디렉터리 /app에 복사
COPY .mvn/ .mvn COPY mvnw pom.xml ./
Docker
복사
캐싱 및 더 빠른 빌드를 위해 실행하기 전에 미리 따로 해당 파일들을 복사
3,4번 내용을 추가하고 나서의 내용
5.
컨테이너 내에서 명령어를 실행. 이는 Maven wrapper(./mvnm)를 최종 JAR 파일을 빌드하지 않고(더 빠른 빌드에 유용) 프로젝트에 의존성을 설치하는 ./mvnw dependency:go-offline 명령어를 실행함.
RUN ./mvnw dependency:go-offline
Docker
복사
6.
컨테이너 내 /app 폴더에 host 머신에 있는 프로젝트로부터 src 폴더를 복사
COPY src ./src
Docker
복사
7.
컨테이너가 실행될 때 실행되는 기본 명령어를 설정. 이 명령어는 컨테이너가 Maven wrapper(./mvnw)를 스프링부트 애플리케이션을 빌드하고 실행하는 spring-boot:run 목표와 함께 실행하도록 지시함
CMD ["./mvnw", "spring-boot:run"]
Docker
복사
최종 Dockerfile 내용!

Build the container image

1.
도커 이미지를 빌드하기 위해 다음 명령어를 실행 :
docker build -t spring-helloworld .
Docker
복사
2.
docker image 명령어를 통해 이미지의 크기를 확인 :
docker images
Docker
복사
539MB를 차지. JDK, Maven toolchain과 같은 것들을 모두 포함하기 때문. 프로덕션 환경에서는 최종 이미지에 담을 필요가 없음

Run the Spring Boot application

1.
이미지를 빌드했으니, 이제 컨테이너를 실행 :
docker run -p 8080:8080 spring-helloworld
Docker
복사
터미널에 다음과 같은 모습이 보임 :
2.
http://localhost:8080 또는 아래 curl 명령어를 이용해, 웹 브라우저에서 “Hello World” 페이지에 접근 :
curl localhost:8080 Hello World
Shell
복사

Use multi-stage builds

1.
다음 Dockerfile을 고려하기 :
FROM eclipse-temurin:21.0.2_13-jdk-jammy AS builder WORKDIR /opt/app COPY .mvn/ .mvn COPY mvnw pom.xml ./ RUN ./mvnw dependency:go-offline COPY ./src ./src RUN ./mvnw clean install FROM eclipse-temurin:21.0.2_13-jre-jammy AS final WORKDIR /opt/app EXPOSE 8080 COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar ENTRYPOINT ["java", "-jar", "/opt/app/*.jar"]
Docker
복사
해당 도커 파일은 두 스테이지로 분리
first stage
JDK(Java Development Kit)를 제공하면서 이전 도커파일과 동일한 내용으로 유지. 빌더의 이름을 제공받음
second stage(final)
더 슬림한, JRE(Java Runtime Environment)를 포함하는 eclipse-temurin:21.0.2_13-jre-jammy 이미지를 사용. 해당 이미지는 실행 중인 컴파일된 애플리케이션(JAR file)에 충분한 JRE를 제공
multi-stage 빌드를 사용하면, 도커는 컴파일,패키징, 단위 테스트, 앱 런타임을 위해 하나의 base image를 사용하고, 앱 런타임을 위해 하나의 분리된 이미지를 사용함. 결과적으로, 최종 이미지는 어떠한 개발 및 디버깅 도구를 포함하지 않기에 사이즈 측면에서 더 작음. 최종 런타임 환경으로부터 빌드 환경을 분리시킴으로써, 우리는 이미지 크기를 상당히 줄이고 최종 이미지의 보안을 높일 수 있음
2.
이미지를 재빌드하고 프로덕션 사용에 준비된 빌드를 실행 :
docker build -t spring-helloworld-builder .
Shell
복사
현재 디렉터리에 위치한 Dockerfile로부터 final stage를 이용해 spring-helloworld-builder 이름의 이미지를 빌드하는 명령어
3.
docker images 명령어를 통해 이미지 사이즈 확인 :
docker images
Bash
복사
final 이미지는 291MB를 차지함. multi-stage 빌드 이전에는 539MB였음
각 스테이지를 최적화하고 필요한 것만 포함함으로써, 동일한 기능을 여전히 수행하면서 전체적으로 이미지 사이즈를 상당히 줄임. 성능도 높이지만 도커 이미지들을 가볍고 보안성도 더 높고 관리하기 쉬워지기도 함