왜 Docker 이미지를 최적화해야 하나?
Spring Boot 애플리케이션의 기본 Docker 이미지는 300~500MB에 달합니다. 이미지가 클수록 빌드·푸시·풀 시간이 길어지고, 디스크 사용량이 증가하며, K8s 환경에서 Pod 시작 시간에 직접 영향을 줍니다. Layered JAR, Jib, Cloud Native Buildpacks 세 가지 접근법으로 이미지 크기와 빌드 속도를 최적화할 수 있습니다.
3가지 접근법 비교
| 방식 | Dockerfile 필요 | 이미지 크기 | 캐시 효율 | 커스터마이징 |
|---|---|---|---|---|
| 기본 Dockerfile | O | 300~500MB | 낮음 | 자유 |
| Layered JAR | O | 200~350MB | 높음 | 중간 |
| Jib | X | 180~300MB | 높음 | 플러그인 설정 |
| Buildpacks | X | 250~400MB | 중간 | 제한적 |
1. Layered JAR: Dockerfile 최적화
Spring Boot 3.x는 JAR을 4개 레이어로 분리하여 Docker 캐시를 극대화합니다.
# 레이어 구조 확인
java -Djarmode=layertools -jar app.jar list
# 출력:
# dependencies ← 변경 빈도 낮음 (캐시 적중률 높음)
# spring-boot-loader
# snapshot-dependencies
# application ← 변경 빈도 높음 (매 배포마다 변경)
최적화된 Multi-stage Dockerfile
# ─── Stage 1: 빌드 ───
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /build
# Gradle 캐시 최적화: 의존성 먼저 다운로드
COPY build.gradle.kts settings.gradle.kts gradle.properties ./
COPY gradle ./gradle
RUN --mount=type=cache,target=/root/.gradle
./gradlew dependencies --no-daemon
# 소스 복사 후 빌드
COPY src ./src
RUN --mount=type=cache,target=/root/.gradle
./gradlew bootJar --no-daemon -x test
# 레이어 추출
RUN java -Djarmode=layertools -jar build/libs/*.jar extract
--destination extracted
# ─── Stage 2: 실행 ───
FROM eclipse-temurin:21-jre-alpine AS runtime
# 보안: 비루트 사용자
RUN addgroup -S app && adduser -S app -G app
USER app
WORKDIR /app
# 변경 빈도 낮은 순서대로 COPY (캐시 최적화)
COPY --from=builder /build/extracted/dependencies/ ./
COPY --from=builder /build/extracted/spring-boot-loader/ ./
COPY --from=builder /build/extracted/snapshot-dependencies/ ./
COPY --from=builder /build/extracted/application/ ./
# JVM 튜닝
ENV JAVA_OPTS="-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0
-Djava.security.egd=file:/dev/./urandom"
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["sh", "-c",
"java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]
2. Jib: Dockerfile 없는 빌드
Google의 Jib은 Docker 데몬 없이 최적화된 이미지를 빌드합니다.
// build.gradle.kts
plugins {
id("com.google.cloud.tools.jib") version "3.4.0"
}
jib {
from {
image = "eclipse-temurin:21-jre-alpine"
platforms {
platform {
architecture = "amd64"
os = "linux"
}
platform {
architecture = "arm64"
os = "linux"
}
}
}
to {
image = "registry.example.com/my-app"
tags = setOf("latest", project.version.toString())
auth {
username = System.getenv("REGISTRY_USER")
password = System.getenv("REGISTRY_PASS")
}
}
container {
jvmFlags = listOf(
"-XX:+UseContainerSupport",
"-XX:MaxRAMPercentage=75.0",
"-Djava.security.egd=file:/dev/./urandom"
)
ports = listOf("8080")
user = "1000:1000"
creationTime.set("USE_CURRENT_TIMESTAMP")
// 환경변수
environment = mapOf(
"SPRING_PROFILES_ACTIVE" to "production",
"TZ" to "Asia/Seoul"
)
// 레이블
labels = mapOf(
"maintainer" to "team@example.com",
"version" to project.version.toString()
)
}
// 레이어 커스터마이징
extraDirectories {
paths {
path {
setFrom("src/main/resources/static")
into = "/app/static"
}
}
}
}
# 빌드 명령어
# 레지스트리에 직접 푸시 (Docker 데몬 불필요)
./gradlew jib
# 로컬 Docker 데몬에 빌드
./gradlew jibDockerBuild
# tar 파일로 내보내기
./gradlew jibBuildTar
Jib 레이어 구조
| 레이어 | 내용 | 변경 빈도 |
|---|---|---|
| Base Image | JRE + OS | 월 1회 |
| Dependencies | 외부 라이브러리 | 주 1회 |
| Resources | static, templates | 격주 |
| Classes | 애플리케이션 코드 | 매 배포 |
3. Cloud Native Buildpacks
Spring Boot의 내장 Buildpacks 지원으로 Dockerfile 없이 이미지를 생성합니다.
# Gradle
./gradlew bootBuildImage --imageName=registry.example.com/my-app:latest
# Maven
./mvnw spring-boot:build-image
-Dspring-boot.build-image.imageName=registry.example.com/my-app:latest
// build.gradle.kts — Buildpacks 커스터마이징
tasks.named<BootBuildImage>("bootBuildImage") {
imageName.set("registry.example.com/my-app:${project.version}")
// 베이스 이미지 (Paketo Buildpack)
builder.set("paketobuildpacks/builder-jammy-tiny:latest")
// JVM 메모리 설정
environment.set(mapOf(
"BP_JVM_VERSION" to "21",
"BPE_JAVA_TOOL_OPTIONS" to
"-XX:MaxRAMPercentage=75.0",
"BPE_SPRING_PROFILES_ACTIVE" to "production"
))
// 빌드 캐시
buildCache {
volume {
name.set("buildpack-cache")
}
}
}
JVM 컨테이너 튜닝
컨테이너 환경에서 JVM 메모리를 올바르게 설정하는 것이 핵심입니다.
# 컨테이너 메모리 인식 (JDK 17+)
-XX:+UseContainerSupport # 컨테이너 메모리 제한 인식
-XX:MaxRAMPercentage=75.0 # 컨테이너 메모리의 75% 힙 할당
-XX:InitialRAMPercentage=50.0 # 시작 시 50% 힙 할당
# GC 설정 (컨테이너 최적)
-XX:+UseG1GC # G1 GC (기본)
-XX:MaxGCPauseMillis=200 # 최대 GC 일시정지 200ms
# 빠른 시작
-XX:TieredStopAtLevel=1 # C1 컴파일만 (시작 속도 우선)
-Djava.security.egd=file:/dev/./urandom # 난수 생성 속도 개선
# CDS (Class Data Sharing) — Spring Boot 3.3+
-XX:SharedArchiveFile=app-cds.jsa # 사전 로드된 클래스 캐시
멀티 아키텍처 빌드
ARM(Apple Silicon, Graviton)과 AMD64 모두 지원하는 이미지를 빌드합니다.
# Docker Buildx 멀티 플랫폼
docker buildx create --name multiplatform --use
docker buildx build
--platform linux/amd64,linux/arm64
--tag registry.example.com/my-app:latest
--push .
# Jib: 위 설정의 platforms 블록으로 자동 지원
CI/CD 파이프라인 예시
# .github/workflows/build.yml
name: Build & Push Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Setup Gradle Cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('**/*.gradle.kts') }}
- name: Build with Jib
run: ./gradlew jib
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASS: ${{ secrets.REGISTRY_PASS }}
이미지 크기 분석
# dive: 레이어별 크기 분석
dive registry.example.com/my-app:latest
# docker history
docker history my-app:latest --no-trunc
# 이미지 크기 비교 (실측 예시)
# 기본 Dockerfile + JDK: 487MB
# Layered JAR + JRE-alpine: 198MB (59% 감소)
# Jib + JRE-alpine: 185MB (62% 감소)
# Buildpacks (tiny): 267MB (45% 감소)
.dockerignore
.git
.gradle
build
!build/libs/*.jar
.idea
*.iml
src/test
docker-compose*.yml
README.md
보안 스캔
빌드된 이미지의 CVE 취약점을 CI에서 자동 스캔합니다.
# Trivy 취약점 스캔
trivy image --severity HIGH,CRITICAL
registry.example.com/my-app:latest
# Grype 스캔
grype registry.example.com/my-app:latest
# CI 통합: 취약점 발견 시 빌드 실패
trivy image --exit-code 1 --severity CRITICAL
registry.example.com/my-app:latest
운영 체크리스트
| 항목 | 확인 사항 |
|---|---|
| 베이스 이미지 | JRE-alpine 사용 (JDK 대비 60% 감소) |
| 레이어 순서 | 변경 빈도 낮은 것 → 높은 것 |
| 비루트 실행 | USER 1000:1000 또는 app 사용자 |
| JVM 메모리 | MaxRAMPercentage=75 (OOM 방지) |
| HEALTHCHECK | actuator/health 엔드포인트 활용 |
| 취약점 스캔 | CI에서 Trivy/Grype 자동 실행 |
마치며
Spring Boot Docker 이미지 최적화는 Layered JAR로 캐시 효율을 높이고, Jib으로 Dockerfile 없는 빌드를 자동화하며, 멀티 아키텍처 지원으로 ARM/AMD64 환경을 모두 커버하는 것이 핵심입니다. JRE-alpine 베이스, 비루트 실행, JVM 컨테이너 튜닝, 보안 스캔까지 적용하면 프로덕션 레벨의 컨테이너 이미지를 구축할 수 있습니다.