컨테이너 학습 노트 목차

이미지와 레이어 — 왜 빌드가 빨랐다 느렸다 하나

이미지는 통짜 파일이 아니라 겹겹이 쌓인 레이어다. 이 사실 하나가 빌드 속도·이미지 크기·배포 시간을 전부 설명한다.


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

컨테이너는 무엇인가 — 결국 프로세스다Dockerfile — 명령어와 그 함정