Docker 이미지 최적화하기: 용량 줄이기부터 멀티 스테이지 빌드까지
Docker 이미지를 만들 때 소스 코드와 개발 도구를 모두 넣으면 이미지는 커지고 배포에 필요한 전송량도 늘어납니다. 최적화의 핵심은 실행에 필요한 파일만 최종 이미지에 포함하고, 변경이 적은 작업의 빌드 캐시를 재사용하는 것입니다.
이 글에서는 기본 원리부터 Dockerfile 개선 예제, Java 애플리케이션의 멀티 스테이지 빌드, 최적화 결과 확인 방법까지 살펴봅니다.
1. Docker 이미지 최적화는 무엇을 개선할까?
| 목표 | 주요 방법 | 실무 효과 |
|---|---|---|
| 이미지 용량 감소 | 런타임 베이스 이미지, 멀티 스테이지 빌드, 불필요한 파일 제외 | 레지스트리 저장량과 신규 노드의 이미지 다운로드 부담 감소 |
| 반복 빌드 시간 감소 | COPY 순서 조정, 의존성 캐시, BuildKit 캐시 마운트 | 소스 수정 후 CI 빌드 대기 시간 감소 |
| 공격 표면 감소 | 불필요한 패키지 제거, 베이스 이미지 업데이트 | 관리해야 할 소프트웨어 구성 요소 감소 |
| 배포 재현성 개선 | 의존성 버전 관리, 이미지 digest 고정 | 검증한 이미지와 배포 이미지의 일치 여부 확인 가능 |
이미지 용량은 주로 저장 공간과 이미지 전송에 영향을 줍니다. 실행 중 CPU·메모리 사용량은 애플리케이션 동작과 JVM 등의 런타임 설정을 별도로 확인해야 합니다. 다운로드가 줄어도 애플리케이션 초기화 시간이 그대로라면 전체 기동 시간의 개선은 제한적일 수 있습니다.
2. 먼저 이미지 용량과 레이어를 확인하기
최적화 전에는 현재 이미지의 크기와 큰 레이어를 기록합니다. 다음 명령은 myapp:before 이미지가 로컬에 있다는 전제입니다.
docker image ls myapp
docker image inspect myapp:before --format '{{.Size}}'
docker history --no-trunc myapp:before
docker system df -v
docker image ls: 이미지 크기를 빠르게 비교합니다.docker image inspect: 이미지 크기를 바이트 단위로 확인합니다.docker history: 어떤 빌드 단계에서 큰 레이어가 만들어졌는지 확인합니다.docker system df -v: 공유 레이어와 이미지·컨테이너·캐시의 디스크 사용 현황을 확인합니다.
여러 이미지가 베이스 레이어를 공유하므로 이미지 크기를 단순히 합산한 값과 실제 디스크 사용량은 다를 수 있습니다. 레지스트리의 압축된 전송 크기도 로컬 이미지 크기와 구분해야 합니다.
3. 실행 목적에 맞는 베이스 이미지 선택하기
빌드 단계에는 컴파일러와 빌드 도구가 필요하지만 실행 단계에는 필요하지 않을 수 있습니다. Java 서비스라면 빌드용 JDK와 실행용 JRE를 분리하는 방식부터 검토할 수 있습니다. Docker도 빌드 환경과 운영 실행 환경에 적합한 베이스 이미지를 각각 선택하도록 안내합니다. :chatgpt-content-reference{index="0"}
| 유형 | 특징 | 확인할 사항 |
|---|---|---|
| 일반 배포판 기반 | 패키지 설치와 문제 분석이 비교적 편리함 | 실행에 불필요한 도구가 포함되는지 확인 |
| slim / minimal | 같은 계열에서 구성 요소를 줄인 이미지 | 인증서, 로케일, 네이티브 라이브러리 의존성 확인 |
| Alpine 기반 | 작은 기본 구성, musl libc 사용 | glibc 전제 바이너리와 네이티브 모듈의 호환성 검증 |
| Distroless / scratch | 실행에 필요한 구성만 포함하거나 빈 기반에서 시작 | 셸·패키지 관리자 부재, 인증서와 동적 라이브러리 준비 여부 확인 |
기존 WAS를 컨테이너로 옮긴다면 이미지 계열을 바꾸기 전에 Java 버전, JDBC 드라이버, JNI 라이브러리와 운영 도구 의존성을 확인합니다. 먼저 기존 실행 환경에서 불필요한 파일을 줄인 뒤 베이스 이미지 변경을 검증하면 원인을 구분하기 쉽습니다.
4. .dockerignore로 불필요한 파일 제외하기
.dockerignore는 빌드 컨텍스트 루트에 작성합니다. Git 이력, 로컬 로그, 개발 환경 파일을 제외하면 빌드에 전달되는 파일을 줄일 수 있습니다. COPY . .를 사용할 때 불필요한 파일이 이미지에 들어가는 것도 방지합니다. 다만 제외한 파일은 빌드 중 COPY할 수 없습니다. :chatgpt-content-reference{index="1"}
소스를 컨테이너 안에서 빌드하는 Java 프로젝트 예제
.git
.idea
.vscode
target
*.log
.env
.env.*
*.pem
*.key
tmp
backup
이 예제는 컨테이너 안에서 Maven 빌드를 하므로 로컬의 target 디렉터리를 제외합니다. 반대로 Jenkins에서 미리 만든 target/app.jar를 이미지에 복사한다면 target을 제외하면 안 됩니다.
.dockerignore는 빌드 컨텍스트의 파일을 제외합니다. 베이스 이미지에 이미 포함된 파일이나 RUN 명령으로 다운로드한 파일을 제거하지는 않습니다.5. 설치와 정리는 같은 RUN에서 수행하기
Docker 이미지의 파일 변경은 레이어에 기록됩니다. 앞선 레이어에 저장한 파일을 다음 레이어에서 삭제해도 이전 레이어의 데이터는 남습니다. 다운로드와 정리를 같은 RUN에서 수행하면 임시 파일이 최종 레이어에 남는 것을 피할 수 있습니다.
개선 전: 설치와 정리가 분리된 예제
FROM ubuntu:24.04
RUN apt-get update
RUN apt-get install -y --no-install-recommends ca-certificates
RUN rm -rf /var/lib/apt/lists/*
개선 후: 설치와 정리를 하나의 단계로 구성
FROM ubuntu:24.04
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
--no-install-recommends는 추천 패키지의 자동 설치를 줄입니다. 필요한 기능은 직접 확인해야 하며, 인증서나 런타임 라이브러리처럼 실제 실행에 필요한 패키지까지 제거하면 안 됩니다.
모든 RUN을 하나로 합치는 것이 목표는 아닙니다. 임시 파일의 생성과 정리는 묶되, 서로 다른 작업은 캐시 재사용과 유지보수를 고려해 구분합니다.
6. COPY 순서로 의존성 캐시 유지하기
소스 전체를 먼저 복사한 뒤 의존성을 설치하면 소스만 수정해도 의존성 설치 단계의 캐시가 무효화될 수 있습니다. 의존성 정의 파일을 먼저 복사하고 설치한 다음 소스를 복사하면 불필요한 재설치를 줄일 수 있습니다. :chatgpt-content-reference{index="2"}
Node.js 예제: 개선 전
FROM node:24-bookworm-slim
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
USER node
CMD ["node", "server.js"]
Node.js 예제: 개선 후
FROM node:24-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
이 예제는 package-lock.json이 있고 별도의 컴파일 과정 없이 server.js를 실행하는 프로젝트를 전제로 합니다. .dockerignore에는 로컬 node_modules를 포함해야 합니다. TypeScript나 프런트엔드 빌드처럼 개발 의존성이 필요한 프로젝트는 빌드 단계에서 해당 의존성을 설치하고 최종 실행 단계와 분리합니다.
7. 멀티 스테이지 빌드로 빌드 도구 제외하기
멀티 스테이지 빌드는 하나의 Dockerfile에 여러 FROM 단계를 정의하고, 최종 단계에 필요한 산출물만 복사하는 방식입니다. Maven과 JDK를 빌드 단계에 두고 JAR만 실행 단계에 복사하면 빌드 도구와 소스 코드가 최종 이미지에 포함되지 않습니다. :chatgpt-content-reference{index="3"}
실행 가능한 JAR를 만드는 Java 프로젝트 예제
아래 예제는 pom.xml과 src가 있는 단일 모듈 프로젝트이며, Java 21에서 실행 가능한 JAR를 생성한다고 가정합니다. Maven의 finalName 설정으로 산출물 이름을 app.jar로 맞춥니다.
<build>
<finalName>app</finalName>
</build>
기존 build 요소가 있으면 finalName만 추가합니다. 실행 가능한 JAR 패키징에 필요한 플러그인 설정은 프로젝트 구성을 유지해야 합니다.
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /workspace
COPY pom.xml ./
RUN --mount=type=cache,target=/root/.m2 \
mvn -B dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
mvn -B package
FROM eclipse-temurin:21-jre-jammy AS runtime
WORKDIR /app
COPY --from=build --chown=10001:10001 \
/workspace/target/app.jar /app/app.jar
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
- build 단계: Maven과 JDK로 빌드 및 테스트를 수행합니다.
- runtime 단계: JRE와 app.jar만 포함합니다.
- COPY --from: 다른 단계에서 만든 산출물만 가져옵니다.
- 캐시 마운트: Maven 다운로드 파일을 빌드 간 재사용합니다. 캐시 자체는 최종 이미지에 복사하지 않습니다.
- USER: 기본 실행 사용자를 UID 10001로 설정합니다. 애플리케이션이 파일을 쓰는 경로는 별도로 권한을 준비해야 합니다.
일반 WAR 파일은
java -jar로 실행할 수 없습니다. WAR 배포형 서비스는 실행 단계에 필요한 WAS를 포함하고, 빌드 단계에서 만든 WAR를 해당 WAS의 배포 경로로 복사하는 구조로 작성합니다.BuildKit 캐시 마운트가 비어 있어도 빌드가 성공하도록 구성해야 합니다. Maven의 사전 다운로드 단계가 모든 프로젝트의 의존성을 완전히 확보하는 것은 아니므로 실제 package 단계에서 추가 다운로드가 발생할 수 있습니다.
8. 이미지 버전과 비밀정보 관리하기
태그 고정과 digest 고정 구분
버전 태그를 지정해도 발행자가 같은 태그의 이미지를 갱신할 수 있습니다. 정확히 동일한 베이스 이미지를 사용해야 한다면 검증한 digest를 FROM에 지정합니다. digest를 고정한 경우 보안 패치를 반영하려면 새 digest로 명시적으로 갱신하고 다시 검증해야 합니다.
인증정보를 이미지에 넣지 않기
저장소 비밀번호나 토큰을 Dockerfile의 ARG·ENV로 전달하지 않습니다. 빌드에 인증정보가 필요하면 BuildKit의 secret mount를 사용합니다. Docker 공식 문서도 빌드 비밀정보 전달에 secret mount를 안내합니다. :chatgpt-content-reference{index="4"}
앞선 Java 예제에서 사설 Maven 저장소용 settings.xml이 필요한 경우, 각 Maven RUN에 다음 형태의 secret mount를 적용할 수 있습니다.
RUN --mount=type=cache,target=/root/.m2 \
--mount=type=secret,id=maven_settings,required=true \
mvn -B -s /run/secrets/maven_settings package
docker build \
--secret id=maven_settings,src=./settings.xml \
-t myapp:optimized .
사전 의존성 다운로드 단계에도 같은 secret을 마운트해야 합니다. settings.xml은 일반 COPY 대상에서 제외하고, 명령 출력이나 생성 파일에 인증정보가 기록되지 않는지도 확인합니다.
9. 최적화 전후 결과 검증하기
기존 Dockerfile을 Dockerfile.before, 개선한 파일을 Dockerfile로 준비한 경우 다음처럼 비교합니다. 같은 소스와 같은 플랫폼을 사용해야 차이를 해석하기 쉽습니다.
docker build -f Dockerfile.before -t myapp:before .
docker build -f Dockerfile -t myapp:optimized .
docker image ls myapp
docker image inspect myapp:before --format '{{.Size}}'
docker image inspect myapp:optimized --format '{{.Size}}'
docker history --no-trunc myapp:optimized
docker image inspect myapp:optimized \
--format '{{.Config.User}}'
크기 감소율은 (기존 크기 - 개선 크기) / 기존 크기 × 100으로 계산합니다. 예를 들어 1,000MB에서 400MB로 줄었다면 60% 감소입니다. 이는 계산 예시이며 실제 감소 폭은 베이스 이미지와 애플리케이션 구성에 따라 달라집니다.
애플리케이션 실행 확인
docker run -d \
--name myapp-check \
-p 18080:8080 \
myapp:optimized
docker logs myapp-check
docker inspect myapp-check \
--format '{{.State.Status}}'
위 포트 예제는 애플리케이션이 컨테이너의 0.0.0.0:8080에서 요청을 받는 경우입니다. 컨테이너가 running이어도 서비스가 정상이라는 뜻은 아니므로 실제 HTTP 경로, DB 연결, 인증서, 파일 쓰기와 종료 처리까지 확인합니다.
docker stop myapp-check
docker rm myapp-check
반복 빌드 시간 확인
time docker build --progress=plain \
-t myapp:optimized .
time docker build --progress=plain \
-t myapp:optimized .
첫 빌드와 재빌드 시간을 구분하고, 소스만 변경했을 때 의존성 단계가 캐시를 사용하는지 확인합니다. 이미지 크기 개선과 캐시 효과는 서로 다른 지표입니다.
10. Jenkins·Harbor·Kubernetes 환경에 적용하기
GitLab 소스를 Jenkins에서 빌드하고 Harbor를 통해 Kubernetes에 배포하는 환경이라면 다음 항목을 함께 확인합니다.
- Jenkins: 의존성 파일과 소스 COPY 순서를 분리합니다. 매번 새 빌더를 사용하는 경우에는 외부 빌드 캐시 구성을 검토합니다.
- Harbor: 실행 산출물 중심의 이미지를 저장하고, 배포 버전과 digest를 기록합니다.
- Kubernetes: 신규 노드에서의 이미지 다운로드 시간과 애플리케이션 준비 완료 시간을 따로 측정합니다.
- 보안 점검: 크기 비교와 함께 취약점 검사 결과, 실행 사용자와 포함 패키지를 검토합니다.
- 폐쇄망: 베이스 이미지뿐 아니라 Maven·npm 의존성과 Dockerfile frontend 등 빌드에 필요한 구성 요소도 내부에서 확보합니다.
멀티 스테이지 빌드나 캐시 마운트만으로 오프라인 빌드가 가능해지는 것은 아닙니다. 외부 다운로드가 필요한 단계가 남아 있는지 실제 폐쇄망 조건에서 확인해야 합니다.
11. 자주 하는 실수
| 실수 | 문제 | 개선 방법 |
|---|---|---|
| COPY . .로 모든 파일 복사 | 로그·로컬 의존성·환경 파일이 포함될 수 있음 | .dockerignore와 명시적인 COPY 사용 |
| 다음 RUN에서 대용량 파일 삭제 | 이전 레이어의 데이터가 남음 | 생성과 삭제를 같은 RUN에서 처리 |
| 운영 이미지에 Maven·컴파일러 포함 | 실행에 불필요한 구성 증가 | 빌드 단계와 실행 단계 분리 |
| 무조건 Alpine으로 변경 | 네이티브 라이브러리 호환 문제가 발생할 수 있음 | 런타임 의존성과 실제 서비스 동작 검증 |
| 매번 --no-cache 사용 | 반복 빌드의 캐시 효과 상실 | 일반 빌드는 캐시 활용, 갱신 검증 시 필요에 따라 사용 |
| docker system prune으로 이미지 최적화 시도 | 로컬 공간을 정리해도 이미지 자체의 내용은 바뀌지 않음 | Dockerfile 수정 후 이미지 재빌드 |
실무에서는 현재 크기를 기록한 다음 불필요한 파일 제외 → 멀티 스테이지 분리 → COPY 순서 개선 → 실행 검증 순서로 적용하면 효과와 문제 원인을 확인하기 쉽습니다.
공식문서 및 참고자료
'지식 공유 > ETC' 카테고리의 다른 글
| Dell Switch 설정 가이드 - VLAN, Access, Trunk, 포트 설정과 조회 명령어 (0) | 2026.10.01 |
|---|---|
| WildFly 9 배포 시 Unsupported major.minor version 52.0 오류 원인과 해결 방향 (0) | 2026.09.28 |
| 폐쇄망 파일전송 망연계 환경에서 A 서버에서 C 서버로 nc 테스트 (0) | 2026.08.03 |
| WildFly·JBoss WAR 배포 시 WFLYSRV0153 PARSE 오류 원인과 해결 방법 (0) | 2026.07.16 |
| WebtoB·JEUS 대신 WildFly를 선택하는 이유 (0) | 2026.07.14 |
