이미지와 레이어 — 왜 빌드가 빨랐다 느렸다 하나
이미지는 통짜 파일이 아니라 겹겹이 쌓인 레이어다. 이 사실 하나가 빌드 속도·이미지 크기·배포 시간을 전부 설명한다.
1. 레이어는 파일시스템의 변경분이다
FROM alpine:3.20 # 레이어 1 — 베이스
RUN apk add --no-cache curl # 레이어 2 — curl 설치로 바뀐 파일들
COPY app /app # 레이어 3 — 추가된 파일
각 레이어는 '이전 상태 대비 무엇이 바뀌었나' 만 담는다
- 추가된 파일
- 수정된 파일 (통째로 다시 담긴다)
- 삭제된 파일 (whiteout 이라는 표식으로 기록된다)
레이어는 읽기 전용이고, 내용이 정해지면 그 해시가 곧 식별자다
2. 겹쳐서 하나로 보인다 — union filesystem
┌─────────────────────┐ ← 컨테이너 쓰기 레이어 (읽기·쓰기)
├─────────────────────┤
│ 레이어 3 /app │ ← 이미지 레이어 (읽기 전용)
├─────────────────────┤
│ 레이어 2 curl │
├─────────────────────┤
│ 레이어 1 alpine │
└─────────────────────┘
↓ 겹쳐 보면
하나의 파일시스템으로 보인다
리눅스에서는 보통 OverlayFS 가 이 일을 한다.
copy-up — 쓰기가 일어나는 순간
-
읽기 — 위에서부터 찾아 처음 발견한 것을 준다
-
쓰기 — 읽기 전용 레이어의 파일을 고치려 하면
- 그 파일을 쓰기 레이어로 '통째로 복사한 뒤' 고친다 (copy-up)
-
1GB 짜리 파일의 1바이트만 고쳐도 1GB 가 복사된다
-
컨테이너 안에서 큰 파일을 수정하는 워크로드는 느리고 디스크를 먹는다
-
그런 데이터는 볼륨에 둔다 (union 을 거치지 않는다)
3. 이미지가 공유된다는 것의 의미
같은 베이스를 쓰는 이미지 10개
- 베이스 레이어는 디스크에 한 벌만 있다
- 컨테이너 100개를 띄워도 읽기 전용 부분은 공유된다
레지스트리에서 받을 때도 마찬가지다 이미 가진 레이어는 건너뛴다 "이미지가 2GB 인데 배포는 금방 끝난다" 의 이유
그래서 베이스 이미지를 통일하는 것이 실제 이득이 된다. 팀마다 다른 베이스를 쓰면 노드마다 레이어가 중복 저장되고 pull 시간도 늘어난다.
4. 빌드 캐시 — 무엇이 캐시를 깨는가
여기가 빌드가 빨랐다 느렸다 하는 이유다.
-
COPY · ADD — 파일 메타데이터로 체크섬을 계산해 비교한다
- mtime(수정 시각)은 계산에 넣지 않는다
- touch 만 해서는 캐시가 깨지지 않는다
-
RUN — '명령 문자열' 만 본다
- RUN apt-get update 로 실제 무엇이 바뀌었는지는 보지 않는다
- 문자열이 같으면 몇 달 전 캐시를 그대로 쓴다
그리고 가장 중요한 것 — 캐시가 한 번 깨지면 그 아래 모든 레이어가 다시 실행된다
그래서 순서가 전부다
# ✗ 나쁜 순서 — 소스가 한 줄만 바뀌어도 의존성을 다시 받는다
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
# ✓ 좋은 순서 — 의존성 목록이 바뀔 때만 다시 받는다
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci # ← 여기까지는 대부분 캐시 적중
COPY . .
RUN npm run build
원칙 — 자주 바뀌는 것을 뒤에 둔다
| 거의 안 바뀜 | 베이스 이미지 · 시스템 패키지 | |
|---|---|---|
| 가끔 바뀜 | 의존성 목록 (package.json · pom.xml · go.mod) | |
| 자주 바뀜 | 애플리케이션 소스 |
RUN apt-get update 의 함정
# ✗ update 가 캐시되면 낡은 인덱스로 install 한다
RUN apt-get update
RUN apt-get install -y curl
# ✓ 한 레이어로 묶는다 (cache busting)
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
update 와 install 이 다른 레이어면
- install 줄만 바뀔 때 update 는 캐시를 쓴다
- 몇 달 전 패키지 인덱스로 설치를 시도한다 → 404 나 낡은 버전
그리고 rm -rf 를 '같은 레이어' 에서 해야 의미가 있다 다음 레이어에서 지우면 앞 레이어에 이미 담겨 있어 크기가 안 준다
5. 지운다고 작아지지 않는다
RUN wget big-file.tar.gz && tar xf big-file.tar.gz
RUN rm big-file.tar.gz # ← 크기가 안 줄어든다
레이어는 추가만 된다 삭제는 '지웠다는 표식(whiteout)' 을 추가하는 것이다
- 앞 레이어의 실제 데이터는 이미지 안에 그대로 남아 있다
같은 이유로
- 앞 레이어에 넣은 비밀 파일은 지워도 이미지에서 꺼낼 수 있다
- docker history · 레이어 추출로 그대로 나온다
- ARG 로 넘긴 토큰이 RUN 명령에 찍히면 히스토리에 남는다
비밀은 애초에 레이어에 넣지 않는다 → BuildKit 시크릿 마운트 (03편)
6. 태그와 다이제스트
docker pull nginx:1.27
docker pull nginx@sha256:abc123...
-
태그 — 사람이 붙이는 이름. 언제든 다른 이미지를 가리킬 수 있다
-
다이제스트 — 내용의 해시. 절대 안 바뀐다
-
nginx:1.27 이 어제와 오늘 다른 이미지일 수 있다
- 보안 패치로 다시 빌드해 같은 태그로 올렸다면 그렇다
latest 는 "최신" 이라는 뜻이 아니다
latest 는 그냥 기본 태그 이름이다 아무도 올리지 않으면 몇 년 전 이미지가 latest 일 수 있다
운영에서 latest 를 쓰면
-
노드마다 다른 이미지가 떠 있을 수 있다
-
롤백할 대상이 없다 (무엇으로 돌아갈지 모른다)
-
재현이 안 된다
-
배포는 다이제스트나 불변 태그(커밋 SHA·빌드 번호)로 고정한다
imagePullPolicy 도 여기에 엮인다 태그가 latest 면 기본이 Always (매번 확인) 그 외에는 IfNotPresent (있으면 안 받는다)
- 불변 태그를 쓰면 pull 이 줄고 배포가 빨라진다
7. 멀티 아키텍처
같은 태그가 여러 아키텍처를 가리킬 수 있다 (manifest list)
- linux/amd64 · linux/arm64 · …
클라이언트가 자기 플랫폼에 맞는 것을 골라 받는다
실무에서 걸리는 것
- 애플 실리콘에서 빌드한 이미지를 x86 서버에 올리면 안 돈다
- exec format error
- buildx 로 멀티 아키텍처를 함께 빌드해 올린다
- docker buildx build --platform linux/amd64,linux/arm64 --push .
- 에뮬레이션 빌드(QEMU)는 매우 느리다 — 네이티브 러너를 쓰는 편이 낫다
8. 크기를 확인하는 법
docker images # 이미지 크기
docker history myapp:1.0 # 레이어별 크기와 만든 명령
docker system df # 전체 사용량 (이미지·컨테이너·볼륨·캐시)
docker system df -v # 상세
docker history 로 '어느 명령이 몇 MB 를 더했는지' 가 바로 보인다 크기 문제를 잡을 때 가장 먼저 여는 명령이다
디스크가 찼을 때
docker system prune 정지 컨테이너·미사용 네트워크·dangling 이미지
docker system prune -a 쓰지 않는 이미지 전부 ← 신중하게
docker builder prune 빌드 캐시 (의외로 여기가 가장 크다)
한눈에 정리
- 레이어 — 파일시스템 변경분. 추가만 된다. 삭제는 표식일 뿐
- union fs — 겹쳐서 하나로 보인다. 쓰기 시 copy-up (큰 파일은 볼륨으로)
- 공유 — 같은 레이어는 한 벌만 저장·전송된다 → 베이스 통일이 이득
- 캐시 — COPY/ADD 는 파일 메타데이터 체크섬(mtime 제외)
- RUN 은 명령 문자열만 본다
- 한 번 깨지면 아래 전부 다시 실행된다
- 순서 — 자주 바뀌는 것을 뒤로. 의존성 목록 먼저 COPY
- apt-get — update 와 install 은 같은 레이어에. 정리도 같은 레이어에
- 지워도 안 준다 — 앞 레이어에 남는다. 비밀은 애초에 넣지 않는다
- 태그 — latest 는 최신이 아니다. 배포는 다이제스트·불변 태그로
- 멀티 아치 — manifest list. 애플 실리콘 빌드를 x86 에 올리면 안 돈다
- 확인 — docker history · docker system df · builder prune
출처 — Docker Docs: "Build cache" · "Cache invalidation" · "Best practices for writing Dockerfiles" · "Multi-platform builds" · OCI Image Specification