Spring Docker 이미지 최적화

왜 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 컨테이너 튜닝, 보안 스캔까지 적용하면 프로덕션 레벨의 컨테이너 이미지를 구축할 수 있습니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux