이미지 레이어는 불변이다. 만들어진 이미지를 사후에 "압축"하거나 "합치는" 정식 기능은 없다. 크기를 줄이려면 다시 빌드하거나, 파일 시스템만 남기고 새 이미지를 만드는 수밖에 없다.
중요한 점은 중간 레이어에서 지운 파일도 이미지 크기에 남는다는 것이다. 한 RUN 에서 받은 파일을 다음 RUN 에서 지우면, 앞 레이어에 원본이 그대로 있고 뒤 레이어에 삭제 표시만 추가된다. 오히려 커진다.
docker history --no-trunc --format '{{.Size}}\t{{.CreatedBy}}' myimage:1.0 | head -20
docker image inspect myimage:1.0 --format '{{len .RootFS.Layers}} layers'
같은 레이어 안에서 받고 쓰고 지운다. 패키지 캐시는 설치와 같은 RUN 에서 정리해야 효과가 있다.
# 나쁨 — 레이어 3개, 캐시가 이미지에 남는다
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# 좋음 — 레이어 1개
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
RHEL 계열은 다음과 같다.
RUN dnf install -y --setopt=install_weak_deps=False curl \
&& dnf clean all && rm -rf /var/cache/dnf
빌드 산출물만 남기는 다단계 빌드가 가장 효과가 크다. 컴파일러와 빌드 의존성이 최종 이미지에 들어가지 않는다.
FROM golang:1.25 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian12
COPY --from=builder /out/app /app
ENTRYPOINT ["/app"]
.dockerignore 를 두어 빌드 컨텍스트 자체를 줄인다. .git 과 테스트 데이터가 통째로 들어가는 일이 흔하다.
.git
node_modules
*.log
tests/fixtures
자주 바뀌지 않는 것부터 위에 둔다. 의존성 설치를 소스 복사보다 앞에 두면 캐시가 살아 빌드 시간이 줄어든다.
컨테이너의 파일 시스템만 꺼내 새 이미지로 만든다. 레이어가 하나로 줄지만 ENV · CMD · ENTRYPOINT · EXPOSE · 작성 이력이 모두 사라지므로 docker import 에서 다시 지정해야 한다.
docker create --name tmp myimage:1.0
docker export tmp | docker import \
--change 'ENTRYPOINT ["/app"]' \
--change 'ENV PATH=/usr/local/bin:/usr/bin:/bin' \
- myimage:squashed
docker rm tmp
docker build --squash 는 데몬의 experimental 기능이며 BuildKit 에서는 동작하지 않는다. 요즘 기본 빌더가 BuildKit 이므로 DOCKER_BUILDKIT=1 과 함께 쓰면 무시되거나 오류가 난다. 굳이 쓰려면 DOCKER_BUILDKIT=0 으로 옛 빌더를 강제해야 하는데, 유지보수되는 방향이 아니므로 다단계 빌드로 대체하는 편이 낫다.
같은 애플리케이션이라도 베이스에 따라 수백 MB 가 갈린다. -slim · alpine · distroless 순으로 작아지지만, alpine 은 musl libc 를 쓰므로 glibc 를 전제로 한 바이너리와 파이썬 휠이 동작하지 않을 수 있다. 데이터 플랫폼 클라이언트처럼 네이티브 라이브러리를 쓰는 이미지는 -slim 이 안전하다.