dockerfile 작성

허동빈·2026년 1월 27일

docker

목록 보기
3/8
post-thumbnail

Chapter 03: Dockerfile 작성

Spring Boot 애플리케이션을 Docker 이미지로 빌드하기 위한 Dockerfile 작성.


1. Dockerfile이란?

📖 개념

Dockerfile은 Docker 이미지를 만들기 위한 레시피(설명서)

소스 코드 + Dockerfile → Docker 이미지 → Docker 컨테이너

🎯 목적

항목설명
환경 일관성누가, 언제, 어디서 빌드해도 동일한 이미지
재현 가능성몇 개월 후에도 똑같이 재현 가능
자동화CI/CD 파이프라인에서 자동 빌드
버전 관리Git으로 Dockerfile 변경 이력 추적

🔍 왜 직접 작성하는가?

대안: Spring Boot Buildpacks

# Spring Boot Buildpacks 사용 시
./gradlew bootBuildImage

장점:

  • 자동으로 최적화된 이미지 생성
  • Dockerfile 작성 불필요

단점:

  • ❌ 리소스 제한 커스터마이징 어려움
  • ❌ JVM 옵션 세밀한 조정 어려움
  • ❌ 내부 동작 블랙박스
  • ❌ 이미지 크기가 더 클 수 있음

결론: 직접 작성 선택

  • 리소스 제한 (CPU, Memory) 세밀한 제어
  • JVM 메모리 튜닝 자유도
  • 학습 목적 (내부 동작 이해)

2. 멀티 스테이지 빌드 전략

🏗️ 왜 멀티 스테이지?

단일 스테이지 (비효율적)

FROM gradle:8.5-jdk21

WORKDIR /app
COPY . .
RUN ./gradlew bootJar

CMD ["java", "-jar", "build/libs/*.jar"]

문제점:

  • ❌ 최종 이미지에 Gradle, JDK 포함 (~1.2GB)
  • ❌ 빌드 도구는 실행 시 불필요
  • ❌ 보안 취약점 증가 (불필요한 도구 포함)

멀티 스테이지 (효율적) ✅

# Stage 1: 빌드 (Gradle + JDK)
FROM gradle:8.5-jdk21 AS builder
WORKDIR /app
COPY . .
RUN ./gradlew bootJar

# Stage 2: 실행 (JRE만)
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
CMD ["java", "-jar", "app.jar"]

장점:

  • ✅ 최종 이미지는 JRE만 포함 (~300MB)
  • ✅ 빌드 도구 제외 (보안 향상)
  • ✅ 이미지 크기 75% 감소
  • ✅ 컨테이너 시작 속도 향상

📊 비교

항목단일 스테이지멀티 스테이지
이미지 크기~1.2GB~300MB
JDK 포함✅ (불필요)❌ (JRE만)
Gradle 포함✅ (불필요)
보안낮음높음
빌드 시간동일동일

3. Dockerfile 작성

📝 전체 코드

프로젝트 루트에 Dockerfile 생성:

인텔리제이 폴더에 만들면 됩니다!

macOS

# 프로젝트 루트로 이동
cd ~/Desktop/DevCource_2

# Dockerfile 생성
touch Dockerfile

# 편집기로 열기
nano Dockerfile
# 또는
code Dockerfile  # VS Code 사용 시

Windows (PowerShell)

# 프로젝트 루트로 이동
cd C:\Users\YourName\Desktop\DevCource_2

# Dockerfile 생성
New-Item -Path . -Name "Dockerfile" -ItemType "file"

# 편집기로 열기
notepad Dockerfile
# 또는
code Dockerfile  # VS Code 사용 시

📄 Dockerfile 내용

아래 내용을 복사하여 붙여넣기:

# ====================================
# Stage 1: 빌드 스테이지
# ====================================
FROM gradle:8.5-jdk21 AS builder

# 작업 디렉토리 설정
WORKDIR /app

# Gradle 래퍼와 설정 파일 복사 (레이어 캐싱 최적화)
COPY gradlew .
COPY gradle gradle
COPY build.gradle.kts .
COPY settings.gradle.kts .

# 의존성 다운로드 (레이어 캐싱)
# 소스 코드 변경 시에도 의존성은 재다운로드 안 함
RUN ./gradlew dependencies --no-daemon || return 0

# 소스 코드 복사
COPY src src

# 빌드 수행 (테스트 제외)
RUN ./gradlew bootJar -x test --no-daemon

# ====================================
# Stage 2: 실행 스테이지 (경량화)
# ====================================
FROM eclipse-temurin:21-jre-jammy

# 작업 디렉토리 설정
WORKDIR /app

# 비루트 사용자 생성 (보안 강화)
RUN groupadd -r spring && useradd -r -g spring spring

# 빌드된 JAR 파일 복사 (Stage 1에서)
COPY --from=builder /app/build/libs/*.jar app.jar

# uploads 디렉토리 생성 및 권한 설정
RUN mkdir -p /app/uploads && chown -R spring:spring /app

# 사용자 전환 (root → spring)
USER spring:spring

# JVM 메모리 설정 (AWS EC2 t3.micro 기준)
ENV JAVA_OPTS="-Xms512m -Xmx750m -XX:MaxMetaspaceSize=180m \
               -XX:+UseG1GC \
               -XX:MaxGCPauseMillis=200 \
               -Djava.security.egd=file:/dev/./urandom"

# 애플리케이션 포트 노출
EXPOSE 8080

# 헬스체크 (Spring Boot Actuator)
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1

# 컨테이너 시작 시 실행할 명령
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

저장:

  • macOS nano: Ctrl + XYEnter
  • Windows notepad: Ctrl + S

4. 각 명령어 상세 설명

🔍 Stage 1: 빌드 스테이지

4-1. FROM gradle:8.5-jdk21 AS builder

FROM gradle:8.5-jdk21 AS builder

역할:

  • 베이스 이미지 선택
  • Gradle 8.5 + JDK 21 포함된 공식 이미지

왜 JDK 21?

  • 프로젝트가 Java 21 사용 중 (build.gradle.kts에 명시)
  • JRE만으로는 컴파일 불가능 (빌드 시 JDK 필요)

AS builder란?

  • 이 스테이지에 "builder"라는 이름 부여
  • Stage 2에서 COPY --from=builder로 참조

4-2. WORKDIR /app

WORKDIR /app

역할:

  • 컨테이너 내부 작업 디렉토리를 /app으로 설정
  • 이후 모든 명령어는 /app에서 실행

왜 /app?

  • 리눅스 관례상 애플리케이션은 /app 또는 /opt/app에 배치
  • 절대 경로 사용으로 명확성 확보

4-3. COPY gradlew . + COPY gradle gradle

COPY gradlew .
COPY gradle gradle
COPY build.gradle.kts .
COPY settings.gradle.kts .

역할:

  • Gradle 빌드에 필요한 파일만 먼저 복사

왜 순서가 중요한가? (레이어 캐싱)

Docker는 레이어 캐싱 방식으로 동작:

┌────────────────────────────────┐
│ Layer 1: FROM gradle:8.5-jdk21 │ ← 캐시됨
├────────────────────────────────┤
│ Layer 2: COPY gradlew ...      │ ← gradlew 변경 시에만 재빌드
├────────────────────────────────┤
│ Layer 3: RUN ./gradlew deps    │ ← build.gradle.kts 변경 시에만 재실행
├────────────────────────────────┤
│ Layer 4: COPY src src          │ ← 소스 코드 변경 시에만 재복사
├────────────────────────────────┤
│ Layer 5: RUN ./gradlew bootJar │ ← 소스 변경 시에만 재빌드
└────────────────────────────────┘

효과:

  • 소스 코드만 변경 → Layer 1~3 재사용 (빌드 시간 90% 단축)
  • 의존성 변경 → Layer 1~2 재사용

잘못된 예시 (비효율):

# 안 좋은 방법
COPY . .  # 모든 파일 한 번에 복사
RUN ./gradlew bootJar
# → 소스 한 줄만 바꿔도 전체 재빌드

4-4. RUN ./gradlew dependencies

RUN ./gradlew dependencies --no-daemon || return 0

역할:

  • 의존성 라이브러리만 미리 다운로드
  • 다음 빌드 시 캐시 사용

--no-daemon이란?

  • Gradle 데몬 프로세스 비활성화
  • Docker 빌드에서는 데몬 불필요 (일회성)

|| return 0이란?

  • 실패해도 계속 진행 (일부 프로젝트에서 에러 발생 가능)
  • 주 목적은 캐싱이므로 실패해도 OK

4-5. RUN ./gradlew bootJar -x test

RUN ./gradlew bootJar -x test --no-daemon

역할:

  • Spring Boot 실행 가능한 JAR 파일 생성

-x test란?

  • 테스트 제외 (빌드 시간 단축)
  • CI/CD에서는 별도로 테스트 실행

왜 bootJar?

  • bootJar: 실행 가능한 Fat JAR (모든 의존성 포함)
  • jar: 일반 JAR (실행 불가능)

결과물:

/app/build/libs/Back-0.0.1-SNAPSHOT.jar

🚀 Stage 2: 실행 스테이지

4-6. FROM eclipse-temurin:21-jre-jammy

FROM eclipse-temurin:21-jre-jammy

역할:

  • JRE만 포함된 경량 이미지 사용

왜 eclipse-temurin?

  • AdoptOpenJDK 후속 프로젝트
  • OpenJDK 공식 배포판
  • 안정성 + 보안 패치

왜 JRE? (JDK 아님)

  • 실행만 하면 되므로 컴파일러(javac) 불필요
  • JRE = JDK - (컴파일러 + 개발 도구)
  • 이미지 크기 절반 이상 감소

jammy란?

  • Ubuntu 22.04 LTS 코드명
  • 안정적인 LTS 버전 사용

4-7. RUN groupadd -r spring && useradd -r -g spring spring

RUN groupadd -r spring && useradd -r -g spring spring

역할:

  • 비루트 사용자 spring 생성

왜 root로 실행하면 안 되는가?

┌─────────────────────────────────────────┐
│ Docker 컨테이너 (root 사용자)               │
│   ↓ 보안 취약점 발견                        │
│   ↓ 컨테이너 탈출 (Container Escape)       │
│   ↓                                     │
│ 호스트 시스템 (root 권한 획득) 💥            │
└─────────────────────────────────────────┘

비루트 사용자 사용 시:

┌─────────────────────────────────────────┐
│ Docker 컨테이너 (spring 사용자)             │
│   ↓ 보안 취약점 발견                        │
│   ↓ 컨테이너 탈출 시도                      │
│   ✗ 권한 부족으로 차단 ✅                   │
└─────────────────────────────────────────┘

명령어 분석:

  • groupadd -r spring: spring 그룹 생성 (-r: 시스템 그룹)
  • useradd -r -g spring spring: spring 사용자 생성 및 그룹 추가

4-8. COPY --from=builder

COPY --from=builder /app/build/libs/*.jar app.jar

역할:

  • Stage 1에서 빌드한 JAR 파일만 복사

--from=builder란?

  • 이전 스테이지 builder에서 파일 가져오기
  • Stage 1의 /app/build/libs/*.jar → Stage 2의 /app/app.jar

왜 다른 파일은 복사 안 하나?

  • 소스 코드: 실행 시 불필요
  • Gradle: 실행 시 불필요
  • 빌드 캐시: 실행 시 불필요
  • 오직 JAR 파일만 필요

4-9. RUN mkdir -p /app/uploads

RUN mkdir -p /app/uploads && chown -R spring:spring /app

역할:

  • 이미지 업로드 디렉토리 생성
  • spring 사용자에게 소유권 부여

왜 필요한가?

  • 경매 물품 이미지를 /app/uploads에 저장
  • spring 사용자가 쓰기 권한 필요

chown이란?

  • Change Owner: 소유자 변경
  • -R: Recursive (하위 디렉토리 포함)

4-10. USER spring:spring

USER spring:spring

역할:

  • 이후 모든 명령어를 spring 사용자로 실행

효과:

  • Java 애플리케이션이 비루트 권한으로 실행
  • 보안 Best Practice 준수

4-11. ENV JAVA_OPTS

ENV JAVA_OPTS="-Xms512m -Xmx750m -XX:MaxMetaspaceSize=180m \
               -XX:+UseG1GC \
               -XX:MaxGCPauseMillis=200 \
               -Djava.security.egd=file:/dev/./urandom"

역할:

  • JVM 메모리 및 GC 옵션 설정

각 옵션 설명:

옵션설명
-Xms512m초기 힙 메모리
-Xmx750m최대 힙 메모리 (Docker 980MB 중 75%)
-XX:MaxMetaspaceSize180m메타스페이스 최대 크기
-XX:+UseG1GC-G1 가비지 컬렉터 사용
-XX:MaxGCPauseMillis200GC 일시 정지 목표 시간 (ms)
-Djava.security.egd...난수 생성기 설정 (시작 속도 향상)

왜 G1GC?

  • 낮은 지연 시간 (Low Latency)
  • 대용량 힙에 적합
  • Spring Boot 권장 GC

4-12. EXPOSE 8080

EXPOSE 8080

역할:

  • 컨테이너가 8080 포트를 사용한다고 문서화

주의:

  • 실제로 포트를 여는 것은 아님!
  • docker run -p 8080:8080에서 매핑 필요

왜 필요한가?

  • 다른 개발자에게 어떤 포트를 사용하는지 알림
  • 자동화 도구가 포트 정보 파악

4-13. HEALTHCHECK

HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1

역할:

  • 컨테이너가 정상 동작하는지 자동 확인

옵션 설명:

  • --interval=30s: 30초마다 체크
  • --timeout=3s: 3초 이내 응답 없으면 실패
  • --start-period=60s: 시작 후 60초는 실패해도 OK (앱 시작 시간)
  • --retries=3: 3번 연속 실패 시 unhealthy

CMD 설명:

  • wget --spider: 파일 다운로드 없이 헤더만 확인
  • /actuator/health: Spring Boot Actuator 헬스체크 엔드포인트
  • || exit 1: 실패 시 exit code 1 반환

효과:

  • Docker가 자동으로 unhealthy 컨테이너 재시작
  • 모니터링 도구 연동 가능

4-14. ENTRYPOINT

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

역할:

  • 컨테이너 시작 시 실행할 명령

왜 sh -c?

  • 환경 변수 $JAVA_OPTS 해석을 위해 셸 필요

ENTRYPOINT vs CMD:

  • ENTRYPOINT: 항상 실행 (변경 어려움)
  • CMD: 기본 명령 (docker run 시 덮어쓰기 가능)

결과:

java -Xms512m -Xmx750m ... -jar app.jar

5. .dockerignore 작성

📝 역할

.gitignore와 유사하게 Docker 빌드 시 제외할 파일 지정

🎯 왜 필요한가?

없을 때:

COPY . .
# → 모든 파일 복사 (build/, .git/, node_modules/ 등)
# → 빌드 시간 증가, 이미지 크기 증가

있을 때:

# .dockerignore에 명시
build/
.git/
# → 불필요한 파일 제외
# → 빌드 속도 향상, 이미지 크기 감소

📄 .dockerignore 파일 생성

macOS

cd ~/Desktop/DevCource_2
touch .dockerignore
nano .dockerignore

Windows (PowerShell)

cd C:\Users\YourName\Desktop\DevCource_2
New-Item -Path . -Name ".dockerignore" -ItemType "file"
notepad .dockerignore

📄 .dockerignore 내용

# 빌드 결과물
build/
target/
*.jar
*.war
out/

# Gradle
.gradle/
!gradle/wrapper/gradle-wrapper.jar

# IDE
.idea/
*.iml
*.iws
*.ipr
.vscode/
*.code-workspace

# OS
.DS_Store
Thumbs.db
*.swp
*.swo
*~

# Git
.git/
.gitignore
.gitattributes

# 로그 및 임시 파일
*.log
*.tmp
*.temp
logs/

# 데이터베이스 파일
*.mv.db
*.trace.db
db_*.mv.db

# 업로드 파일
uploads/

# 환경 설정
.env
.env.*

# Docker
Dockerfile
.dockerignore
docker-compose*.yml

# 문서
*.md
!README.md

# 테스트
src/test/

# 기타
node_modules/
npm-debug.log

저장


6. 이미지 빌드 및 검증

🔨 이미지 빌드

macOS

# 프로젝트 루트로 이동
cd ~/Desktop/DevCource_2

# 이미지 빌드
docker build -t auction-app:latest .

# 또는 빌드 진행 상황 상세히 보기
docker build --progress=plain -t auction-app:latest .

Windows (PowerShell)

# 프로젝트 루트로 이동
cd C:\Users\YourName\Desktop\DevCource_2

# 이미지 빌드
docker build -t auction-app:latest .

# 또는 빌드 진행 상황 상세히 보기
docker build --progress=plain -t auction-app:latest .

옵션 설명:

  • -t auction-app:latest: 이미지 이름과 태그 지정
  • .: Dockerfile이 있는 현재 디렉토리
  • --progress=plain: 빌드 로그 상세히 출력 (디버깅용)

빌드 시간:

  • 첫 빌드: 3~5분 (의존성 다운로드)
  • 재빌드 (소스만 변경): 30초~1분 (캐시 활용)

✅ 빌드 검증

1. 이미지 생성 확인

# 이미지 목록 확인
docker images | grep auction-app

# 출력 예시:
# auction-app   latest   1d47fc690ca1   2 minutes ago   320MB

확인 사항:

  • ✅ 이미지 이름: auction-app
  • ✅ 태그: latest
  • ✅ 크기: 약 300~350MB (멀티 스테이지 효과)

2. 이미지 레이어 확인

# 이미지 상세 정보
docker history auction-app:latest

# 출력 예시:
# IMAGE          CREATED         SIZE
# 1d47fc690ca1   2 minutes ago   45MB    ENTRYPOINT
# <missing>      2 minutes ago   0B      HEALTHCHECK
# <missing>      2 minutes ago   0B      EXPOSE 8080
# ...

3. 테스트 실행

# 간단히 실행 테스트 (MySQL/Redis 없이)
docker run --rm auction-app:latest java --version

# 출력 예시:
# openjdk version "21.0.1" 2023-10-17

--rm이란?

  • 컨테이너 종료 시 자동 삭제
  • 테스트 후 정리 자동화

🐛 빌드 에러 해결

에러 1: Permission denied (gradlew)

ERROR: failed to solve: process "/bin/sh -c ./gradlew bootJar" did not complete successfully

원인:

  • gradlew 파일에 실행 권한 없음

해결:

# macOS/Linux
chmod +x gradlew
git add gradlew
git commit -m "Add execute permission to gradlew"

# 다시 빌드
docker build -t auction-app:latest .

에러 2: Gradle 의존성 다운로드 실패

ERROR: Could not resolve dependencies

원인:

  • 네트워크 문제
  • Gradle 저장소 접근 불가

해결:

# 프록시 설정 (회사 네트워크 등)
docker build \
  --build-arg HTTP_PROXY=http://proxy.example.com:8080 \
  -t auction-app:latest .

# 또는 Gradle 캐시 삭제 후 재시도
rm -rf ~/.gradle/caches
docker build -t auction-app:latest .

에러 3: 빌드 시간 너무 오래 걸림

원인:

  • 레이어 캐싱 미활용
  • 모든 의존성 재다운로드

해결:

# Docker BuildKit 활성화 (더 나은 캐싱)
export DOCKER_BUILDKIT=1  # macOS/Linux
$env:DOCKER_BUILDKIT=1    # Windows PowerShell

# 빌드
docker build -t auction-app:latest .

7. 다른 방식과의 비교

🔀 대안 1: Spring Boot Buildpacks

./gradlew bootBuildImage --imageName=auction-app

장점:

  • Dockerfile 작성 불필요
  • 자동 최적화

단점:

  • ❌ JVM 옵션 세밀한 제어 어려움
  • ❌ 리소스 제한 커스터마이징 어려움
  • ❌ 이미지 크기 더 클 수 있음 (~500MB)

🔀 대안 2: Jib (Google)

// build.gradle.kts
plugins {
    id("com.google.cloud.tools.jib") version "3.4.0"
}

jib {
    to {
        image = "auction-app"
    }
}
./gradlew jibDockerBuild

장점:

  • Dockerfile 불필요
  • 레이어 캐싱 최적화
  • Docker 데몬 불필요 (직접 레지스트리 푸시)

단점:

  • ❌ Gradle 플러그인 추가 의존성
  • ❌ 커스터마이징 제한적
  • ❌ 헬스체크 등 Docker 고급 기능 제한

📊 최종 비교

항목Dockerfile (직접)BuildpacksJib
제어력✅ 최고❌ 낮음🟡 중간
학습 곡선🟡 중간✅ 낮음🟡 중간
이미지 크기✅ 300MB❌ 500MB+✅ 300MB
빌드 속도✅ 빠름🟡 보통✅ 빠름
JVM 튜닝✅ 자유❌ 제한🟡 일부 가능
헬스체크✅ 가능❌ 제한❌ 제한

결론: 학습 목적 + 세밀한 제어 필요 → Dockerfile 직접 작성


📚 참고 자료

profile
백엔드 공부

0개의 댓글