Infra & Network

[Docker | Part 2] Dockerfile + Multi-stage Build

hrbds 2026. 4. 28. 14:08

목차

  • WHY — 왜 Dockerfile을 알아야 하는가
  • Dockerfile 핵심 문법
  • 레이어 캐시 전략
  • CMD vs ENTRYPOINT
  • Spring Boot 앱 이미지 만들기
  • Multi-stage Build
  • .dockerignore
  • 전체 요약

1. WHY — 왜 Dockerfile을 알아야 하는가

Part 1에서 redis:7.2를 pull해서 컨테이너로 띄웠다. 그건 누군가 만들어둔 이미지를 가져다 쓴 것이다.

아리 팬 커뮤니티의 카드 뽑기 서비스, 채팅 서비스를 K8s에 올리려면? 내 Spring Boot 앱을 직접 이미지로 만들어야 한다. Dockerfile을 모르면 그게 불가능하다.

Dockerfile (설계도)
    │
    │ docker build
    ▼
Image (결과물)
    │
    │ docker run
    ▼
Container (실행)

2. Dockerfile 핵심 문법

명령어 역할

FROM Base 이미지 지정
RUN 이미지 빌드 시 실행 (레이어 생성)
COPY 로컬 파일을 이미지 안으로 복사
ENV 환경변수 설정
ARG 빌드 시점에만 쓰는 변수
EXPOSE 컨테이너가 사용할 포트 선언
WORKDIR 작업 디렉토리 설정
CMD 컨테이너 시작 시 실행할 기본 명령어 (덮어쓰기 가능)
ENTRYPOINT 고정 실행 명령어 (덮어쓰기 불가)

기본 Dockerfile 예시

FROM ubuntu:22.04

RUN apt-get update && apt-get install -y curl

WORKDIR /app
COPY ari.txt .

ENV DOG_NAME=Ari
ENV DOG_AGE=11

CMD ["cat", "/app/ari.txt"]

빌드 및 실행:

docker build -t ari-hello .
docker run --rm ari-hello
# 아리는 11살 믹스견, 최고의 강아지입니다!

3. 레이어 캐시 전략

Dockerfile 명령어 한 줄이 레이어 하나다. 특정 레이어가 바뀌면 그 아래 레이어는 전부 캐시가 무효화된다.

나쁜 예 — 매번 패키지를 재설치

FROM ubuntu:22.04
COPY ari.txt .              ← 자주 바뀜
RUN apt-get install curl    ← 매번 재실행됨 (27초 낭비)

 

좋은 예 — 캐시 최대 활용

FROM ubuntu:22.04
RUN apt-get install curl    ← 잘 안 바뀜 → CACHED (0초)
COPY ari.txt .              ← 자주 바뀜 → 여기만 재실행

원칙: 자주 바뀌는 것은 아래에, 잘 안 바뀌는 것은 위에 Spring Boot 앱 기준으로는 라이브러리 설치는 위에, 내 코드 COPY는 아래에 배치한다.

 

실제로 확인한 결과:

# 첫 빌드
[2/4] RUN apt-get install curl   27.5s

# 두 번째 빌드 (코드만 변경)
[2/4] CACHED RUN apt-get install curl   0.0s  ← 캐시 재사용

4. CMD vs ENTRYPOINT

둘 다 컨테이너 시작 시 실행되는 명령어지만 동작 방식이 다르다.

CMD ENTRYPOINT

역할 기본 명령어 고정 명령어
docker run 시 덮어쓰기 가능 불가
실무 사용 기본값 지정 항상 같은 실행 파일
ENTRYPOINT ["cat"]
CMD ["/app/ari.txt"]
# CMD 기본값 사용
docker run --rm ari-hello
# 아리는 11살 믹스견!

# CMD를 덮어써서 다른 파일 출력
docker run --rm ari-hello /etc/hostname
# 148efbe34f9f

docker run 이미지명 뒤에 붙이는 인자는 CMD를 대체한다. ENTRYPOINT ["cat"]는 항상 고정이고, 인자만 바뀐다.

Spring Boot 앱 기준 실무 패턴

ENTRYPOINT ["java", "-jar", "app.jar"]

java -jar는 항상 고정이므로 ENTRYPOINT로 지정한다.


5. Spring Boot 앱 이미지 만들기 (단순 방식)

로컬에서 gradle로 빌드 후 jar를 COPY하는 방식이다.

./gradlew build
build/libs/
├── ari-0.0.1-SNAPSHOT-plain.jar  ← 의존성 없는 jar (사용 안 함)
└── ari-0.0.1-SNAPSHOT.jar        ← 실행 가능한 fat jar (이걸 씀)
FROM eclipse-temurin:17-jre-jammy

WORKDIR /app
COPY build/libs/ari-0.0.1-SNAPSHOT.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]
docker build -t ari-app .
docker run -d --name ari-app -p 8080:8080 ari-app
curl http://localhost:8080/health
# Ari is alive! 🐶

6. Multi-stage Build

단순 방식의 문제점

로컬에서 gradle로 빌드하면:

  • 로컬에 Java, Gradle이 설치되어 있어야 함
  • CI/CD 서버에도 동일한 환경 구성 필요
  • 빌드 환경이 표준화되지 않음

Multi-stage Build 구조

Stage 1 (builder)
└── gradle:8-jdk17 (1.24GB)
    소스코드 빌드 → jar 생성
         │
         │ jar 파일만 꺼냄
         ▼
Stage 2 (runner)
└── eclipse-temurin:17-jre-jammy
    jar만 실행
    
최종 이미지 = 401MB (빌드 환경 1.24GB는 버려짐)
# Stage 1: 빌드
FROM gradle:8-jdk17 AS builder
WORKDIR /app
COPY . .
RUN gradle build --no-daemon -x test

# Stage 2: 실행
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=builder /app/build/libs/ari-0.0.1-SNAPSHOT.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]

JDK vs JRE

JDK JRE

포함 내용 컴파일러 + 실행 환경 실행 환경만
사용 위치 Stage 1 (빌드) Stage 2 (운영)
크기 작음
보안 공격 면적 큼 공격 면적 작음

운영 환경에서는 JRE만 포함하는 것이 표준이다. 컴파일이 필요 없고, 이미지 크기와 보안 측면에서 모두 유리하다.

Multi-stage Build의 진짜 가치

개발자 git push
       │
       ▼
GitHub Actions
       │
       ├── docker build
       │   (Dockerfile 안에서 gradle build → jar 생성)
       │
       ├── docker push (Registry)
       │
       └── kubectl apply (K8s 배포)

Dockerfile 하나에 빌드 환경까지 포함되어 있으므로, 개발자는 소스코드만 push하면 이후는 전부 자동이다. 로컬에 Java, Gradle이 없어도 빌드 가능하다.


7. .dockerignore

docker build 시 빌드 컨텍스트에 포함되는 파일을 제한한다. 불필요한 파일이 포함되면 빌드가 느려지고 이미지가 커진다.

# .dockerignore
.gradle
build
.gitignore
*.md
HELP.md
.git

적용 전후 빌드 컨텍스트 크기 비교:

# .dockerignore 적용 전
transferring context: 456.08kB

# .dockerignore 적용 후
transferring context: 5.06kB   ← 약 90배 감소

build/ 폴더(컴파일된 클래스, jar)와 .gradle/(캐시)가 제외된 덕분이다.


8. 전체 요약

Dockerfile 핵심 원칙
├── 레이어 캐시: 자주 바뀌는 것은 아래에, 안 바뀌는 것은 위에
├── ENTRYPOINT: 고정 실행 파일 지정 (java -jar)
├── CMD: 기본 인자 지정 (docker run 시 덮어쓰기 가능)
└── .dockerignore: 불필요한 파일 제외

Multi-stage Build 핵심 원칙
├── Stage 1: JDK + 빌드 도구 (최종 이미지에 포함 안 됨)
├── Stage 2: JRE만 (운영 환경 표준)
├── 소스코드가 최종 이미지에 없음 → 보안
└── 로컬 환경 없이 빌드 가능 → CI/CD 연동

 

최종 Dockerfile

# Stage 1: 빌드
FROM gradle:8-jdk17 AS builder
WORKDIR /app
COPY . .
RUN gradle build --no-daemon -x test

# Stage 2: 실행
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]