스토리지 드라이버

스토리지 드라이버

이 페이지에서는 overlay2 같은 클래식 스토리지 드라이버에 대해 설명해요. Docker Engine 29.0 이상을 새로 설치하면 기본적으로 containerd 이미지 저장소를 사용하는데, 여기서는 클래식 스토리지 드라이버 대신 스냅샷터(snapshotters)를 사용해요. 이 페이지의 개념은 이미지 레이어가 어떻게 동작하는지 이해하는 데 여전히 도움이 되지만, 명령어와 예시는 여러분 시스템의 이미지 저장 방식과 다를 수 있어요. 운영 관련 지침은 containerd 이미지 저장소 문서를 참고하세요.

스토리지 드라이버를 효과적으로 사용하려면 Docker가 이미지를 어떻게 빌드하고 저장하는지, 그리고 컨테이너가 이 이미지를 어떻게 사용하는지 아는 것이 중요해요. 이 정보를 바탕으로 애플리케이션 데이터를 영속화하는 최선의 방법을 선택하고 성능 문제를 피할 수 있어요.

출처: 공식문서

본문

스토리지 드라이버와 Docker 볼륨

Docker는 스토리지 드라이버를 사용해서 이미지 레이어를 저장하고, 컨테이너의 쓰기 가능한 레이어에 데이터를 저장해요. 컨테이너의 쓰기 가능한 레이어는 컨테이너가 삭제되면 함께 사라지지만, 런타임에 생성되는 임시 데이터를 저장하기에는 적합해요. 스토리지 드라이버는 공간 효율성에 최적화되어 있지만, 스토리지 드라이버에 따라 쓰기 속도가 네이티브 파일 시스템 성능보다 낮을 수 있어요. 특히 copy-on-write 파일 시스템을 사용하는 스토리지 드라이버의 경우 더 그렇죠. 데이터베이스 저장소처럼 쓰기 집약적인 애플리케이션은 성능 오버헤드의 영향을 받는데, 특히 읽기 전용 레이어에 기존 데이터가 있는 경우에 두드러져요.

쓰기 집약적인 데이터, 컨테이너 수명을 넘어서 유지해야 하는 데이터, 여러 컨테이너 간에 공유해야 하는 데이터는 Docker 볼륨을 사용하세요. 볼륨을 사용해 데이터를 영속화하고 성능을 개선하는 방법은 볼륨 섹션을 참고하세요.

이미지와 레이어

Docker 이미지는 일련의 레이어로 구성되어 있어요. 각 레이어는 이미지의 Dockerfile에 있는 하나의 명령어를 나타내요. 마지막 레이어를 제외한 모든 레이어는 읽기 전용이에요. 다음 Dockerfile을 살펴볼까요?

# syntax=docker/dockerfile:1

FROM ubuntu:22.04
LABEL org.opencontainers.image.authors="[email protected]"
COPY . /app
RUN make /app
RUN rm -r $HOME/.cache
CMD python /app/app.py

이 Dockerfile에는 네 개의 명령어가 있어요. 파일 시스템을 수정하는 명령어는 새 레이어를 만들어요. FROM 문은 ubuntu:22.04 이미지에서 레이어를 만들어 내요. LABEL 명령어는 이미지의 메타데이터만 수정하고 새 레이어를 만들지 않아요. COPY 명령어는 Docker 클라이언트의 현재 디렉토리에서 파일을 추가해요. 첫 번째 RUN 명령어는 make 명령어로 애플리케이션을 빌드하고 결과를 새 레이어에 기록해요. 두 번째 RUN 명령어는 캐시 디렉토리를 제거하고 결과를 새 레이어에 기록해요. 마지막으로 CMD 명령어는 컨테이너에서 실행할 명령어를 지정하는데, 이 역시 메타데이터만 수정하므로 이미지 레이어를 만들지 않아요.

각 레이어는 이전 레이어와의 차이점 집합일 뿐이에요. 파일을 추가하거나 제거하는 것 모두 새 레이어를 만든다는 점을 기억하세요. 위 예시에서 $HOME/.cache 디렉토리가 제거되지만, 이전 레이어에는 여전히 존재해서 이미지 전체 크기에 포함돼요. 효율적인 이미지를 위한 Dockerfile 작성 모범 사례멀티 스테이지 빌드 사용 섹션을 참고하세요.

레이어는 서로 위에 쌓여요. 새 컨테이너를 만들면 기본 레이어 위에 새 쓰기 가능한 레이어가 추가돼요. 이 레이어를 보통 "컨테이너 레이어"라고 불러요. 실행 중인 컨테이너에 대한 모든 변경 사항(새 파일 쓰기, 기존 파일 수정, 파일 삭제)은 이 얇은 쓰기 가능한 컨테이너 레이어에 기록돼요. 아래 다이어그램은 ubuntu:15.04 이미지를 기반으로 한 컨테이너를 보여줘요.

스토리지 드라이버는 이러한 레이어들이 서로 상호작용하는 방식의 세부 사항을 처리해요. 다양한 스토리지 드라이버가 있으며, 각각 상황에 따라 장단점이 있어요.

컨테이너와 레이어

컨테이너와 이미지의 가장 큰 차이점은 최상위 쓰기 가능한 레이어예요. 컨테이너에 새 데이터를 추가하거나 기존 데이터를 수정하는 모든 쓰기는 이 쓰기 가능한 레이어에 저장돼요. 컨테이너가 삭제되면 쓰기 가능한 레이어도 함께 삭제되고, 기본 이미지는 변경되지 않은 채로 남아요.

각 컨테이너는 자체 쓰기 가능한 컨테이너 레이어를 가지고 있고 모든 변경 사항이 이 레이어에 저장되므로, 여러 컨테이너가 동일한 기본 이미지를 공유하면서도 각자 고유한 데이터 상태를 가질 수 있어요. 아래 다이어그램은 여러 컨테이너가 동일한 Ubuntu 15.04 이미지를 공유하는 모습을 보여줘요.

Docker는 스토리지 드라이버를 사용해서 이미지 레이어와 쓰기 가능한 컨테이너 레이어의 내용을 관리해요. 각 스토리지 드라이버는 구현 방식이 다르지만, 모든 드라이버는 스택형 이미지 레이어와 copy-on-write(CoW) 전략을 사용해요.

참고: 여러 컨테이너가 정확히 동일한 데이터를 공유해야 한다면 Docker 볼륨을 사용하세요. 볼륨에 대한 자세한 내용은 볼륨 섹션을 참고하세요.

디스크에서의 컨테이너 크기

실행 중인 컨테이너의 대략적인 크기를 확인하려면 docker ps -s 명령어를 사용할 수 있어요. 크기와 관련된 두 가지 열이 있어요.

  • size: 각 컨테이너의 쓰기 가능한 레이어에 사용되는 데이터(디스크 기준) 양이에요.
  • virtual size: 컨테이너가 사용하는 읽기 전용 이미지 데이터와 컨테이너의 쓰기 가능한 레이어 size를 합친 양이에요.

여러 컨테이너가 읽기 전용 이미지 데이터의 일부 또는 전체를 공유할 수 있어요. 동일한 이미지에서 시작된 두 컨테이너는 읽기 전용 데이터의 100%를 공유하고, 공통 레이어가 있는 다른 이미지의 두 컨테이너는 해당 공통 레이어를 공유해요. 따라서 virtual size를 단순히 합산할 수 없어요. 이렇게 하면 총 디스크 사용량을 상당히 과대평가하게 돼요.

디스크에서 실행 중인 모든 컨테이너가 사용하는 총 디스크 공간은 각 컨테이너의 sizevirtual size 값의 조합이에요. 정확히 동일한 이미지에서 여러 컨테이너를 시작한 경우, 이 컨테이너들의 총 디스크 크기는 컨테이너 size의 합에 이미지 하나의 크기(virtual size - size)를 더한 값이에요.

여기에 더해 컨테이너가 디스크 공간을 차지하는 추가적인 방법도 있어요:

  • 로깅 드라이버가 저장하는 로그 파일에 사용되는 디스크 공간. 컨테이너가 많은 양의 로그 데이터를 생성하고 로그 로테이션이 구성되지 않은 경우 무시할 수 없는 크기가 될 수 있어요.
  • 컨테이너가 사용하는 볼륨과 바인드 마운트.
  • 일반적으로 작은 컨테이너 구성 파일에 사용되는 디스크 공간.
  • 스와핑이 활성화된 경우 디스크에 기록되는 메모리.
  • 실험적인 체크포인트/복원 기능을 사용하는 경우 체크포인트.

copy-on-write(CoW) 전략

Copy-on-write는 최대 효율을 위해 파일을 공유하고 복사하는 전략이에요. 이미지의 하위 레이어에 파일이나 디렉토리가 있고 다른 레이어(쓰기 가능한 레이어 포함)가 읽기 액세스가 필요하다면 기존 파일을 그냥 사용해요. 다른 레이어가 파일을 수정해야 하는 첫 번째 시점(이미지 빌드 또는 컨테이너 실행 중)에 파일이 해당 레이어로 복사되어 수정돼요. 이렇게 하면 I/O와 각 후속 레이어의 크기를 최소화할 수 있어요. 이러한 장점을 아래에서 자세히 설명할게요.

공유는 더 작은 이미지를 만든다

docker pull로 저장소에서 이미지를 가져오거나 로컬에 아직 없는 이미지에서 컨테이너를 만들 때, 각 레이어는 별도로 가져와져서 Docker의 로컬 저장 영역(보통 Linux 호스트의 /var/lib/docker/)에 저장돼요. 다음 예시에서 레이어가 가져와지는 것을 볼 수 있어요.

$ docker pull ubuntu:22.04
22.04: Pulling from library/ubuntu
f476d66f5408: Pull complete
8882c27f669e: Pull complete
d9af21273955: Pull complete
f5029279ec12: Pull complete
Digest: sha256:6120be6a2b7ce665d0cbddc3ce6eae60fe94637c6a66985312d1f02f63cc0bcd
Status: Downloaded newer image for ubuntu:22.04
docker.io/library/ubuntu:22.04

각 레이어는 Docker 호스트의 로컬 저장 영역 안에 있는 자체 디렉토리에 저장돼요. 파일 시스템에서 레이어를 확인하려면 /var/lib/docker/<storage-driver>의 내용을 나열해 보세요. 이 예시에서는 overlay2 스토리지 드라이버를 사용해요.

$ ls /var/lib/docker/overlay2
16802227a96c24dcbeab5b37821e2b67a9f921749cd9a2e386d5a6d5bc6fc6d3
377d73dbb466e0bc7c9ee23166771b35ebdbe02ef17753d79fd3571d4ce659d7
3f02d96212b03e3383160d31d7c6aeca750d2d8a1879965b89fe8146594c453d
ec1ec45792908e90484f7e629330666e7eee599f08729c93890a7205a6ba35f5
l

디렉토리 이름은 레이어 ID와 일치하지 않아요.

이제 두 개의 서로 다른 Dockerfile이 있다고 상상해 보세요. 첫 번째로 acme/my-base-image:1.0이라는 이미지를 만들어요.

# syntax=docker/dockerfile:1
FROM alpine
RUN apk add --no-cache bash

두 번째는 acme/my-base-image:1.0을 기반으로 하지만 추가 레이어가 있어요.

# syntax=docker/dockerfile:1
FROM acme/my-base-image:1.0
COPY . /app
RUN chmod +x /app/hello.sh
CMD /app/hello.sh

두 번째 이미지에는 첫 번째 이미지의 모든 레이어에 더해 COPYRUN 명령어로 생성된 새 레이어와 읽기-쓰기 컨테이너 레이어가 포함돼요. Docker는 이미 첫 번째 이미지의 모든 레이어를 가지고 있으므로 다시 가져올 필요가 없어요. 두 이미지는 공통된 레이어를 공유해요.

두 Dockerfile에서 이미지를 빌드하면 docker image lsdocker image history 명령어를 사용해서 공유 레이어의 암호화 ID가 동일한지 확인할 수 있어요.

  1. 새 디렉토리 cow-test/를 만들고 이동하세요.

  2. cow-test/ 안에 hello.sh라는 새 파일을 만들고 다음 내용을 넣으세요.

#!/usr/bin/env bash
echo "Hello world"
  1. 위의 첫 번째 Dockerfile 내용을 Dockerfile.base라는 새 파일에 복사하세요.

  2. 위의 두 번째 Dockerfile 내용을 Dockerfile이라는 새 파일에 복사하세요.

  3. cow-test/ 디렉토리 안에서 첫 번째 이미지를 빌드하세요. 명령어 끝에 있는 .을 잊지 마세요. 이는 PATH를 설정해서 Docker가 이미지에 추가해야 할 파일을 찾을 위치를 알려줘요.

$ docker build -t acme/my-base-image:1.0 -f Dockerfile.base .
[+] Building 6.0s (11/11) FINISHED
=> [internal] load build definition from Dockerfile.base 0.4s
=> => transferring dockerfile: 116B 0.0s
=> [internal] load .dockerignore 0.3s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1 1.5s
=> [auth] docker/dockerfile:pull token for registry-1.docker.io 0.0s
=> CACHED docker-image://docker.io/docker/dockerfile:1@sha256:9e2c9eca7367393aecc68795c671... 0.0s
=> [internal] load .dockerignore 0.0s
=> [internal] load build definition from Dockerfile.base 0.0s
=> [internal] load metadata for docker.io/library/alpine:latest 0.0s
=> CACHED [1/2] FROM docker.io/library/alpine 0.0s
=> [2/2] RUN apk add --no-cache bash 3.1s
=> exporting to image 0.2s
=> => exporting layers 0.2s
=> => writing image sha256:da3cf8df55ee9777ddcd5afc40fffc3ead816bda99430bad2257de4459625eaa 0.0s
=> => naming to docker.io/acme/my-base-image:1.0 0.0s
  1. 두 번째 이미지를 빌드하세요.
$ docker build -t acme/my-final-image:1.0 -f Dockerfile .

[+] Building 3.6s (12/12) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 156B 0.0s
=> [internal] load .dockerignore 0.1s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1 0.5s
=> CACHED docker-image://docker.io/docker/dockerfile:1@sha256:9e2c9eca7367393aecc68795c671... 0.0s
=> [internal] load .dockerignore 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> [internal] load metadata for docker.io/acme/my-base-image:1.0 0.0s
=> [internal] load build context 0.2s
=> => transferring context: 340B 0.0s
=> [1/3] FROM docker.io/acme/my-base-image:1.0 0.2s
=> [2/3] COPY . /app 0.1s
=> [3/3] RUN chmod +x /app/hello.sh 0.4s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:8bd85c42fa7ff6b33902ada7dcefaaae112bf5673873a089d73583b0074313dd 0.0s
=> => naming to docker.io/acme/my-final-image:1.0 0.0s
  1. 이미지 크기를 확인해 보세요.
$ docker image ls

REPOSITORY TAG IMAGE ID CREATED SIZE
acme/my-final-image 1.0 8bd85c42fa7f About a minute ago 7.75MB
acme/my-base-image 1.0 da3cf8df55ee 2 minutes ago 7.75MB
  1. 각 이미지의 히스토리를 확인해 보세요.
$ docker image history acme/my-base-image:1.0

IMAGE CREATED CREATED BY SIZE COMMENT
da3cf8df55ee 5 minutes ago RUN /bin/sh -c apk add --no-cache bash # bui… 2.15MB buildkit.dockerfile.v0
<missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 7 weeks ago /bin/sh -c #(nop) ADD file:f278386b0cef68136… 5.6MB

일부 단계는 크기가 없고(0B) 메타데이터만 변경하는데, 이미지 레이어를 만들지 않고 메타데이터 자체 외에는 크기를 차지하지 않아요. 위 출력은 이 이미지가 2개의 이미지 레이어로 구성되어 있음을 보여줘요.

$ docker image history acme/my-final-image:1.0

IMAGE CREATED CREATED BY SIZE COMMENT
8bd85c42fa7f 3 minutes ago CMD ["/bin/sh" "-c" "/app/hello.sh"] 0B buildkit.dockerfile.v0
<missing> 3 minutes ago RUN /bin/sh -c chmod +x /app/hello.sh # buil… 39B buildkit.dockerfile.v0
<missing> 3 minutes ago COPY . /app # buildkit 222B buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c apk add --no-cache bash # bui… 2.15MB buildkit.dockerfile.v0
<missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 7 weeks ago /bin/sh -c #(nop) ADD file:f278386b0cef68136… 5.6MB

첫 번째 이미지의 모든 단계가 최종 이미지에도 포함되어 있는 것을 볼 수 있어요. 최종 이미지에는 첫 번째 이미지의 두 레이어와 두 번째 이미지에서 추가된 두 레이어가 포함돼요.

docker history 출력의 <missing> 줄은 해당 단계가 다른 시스템에서 빌드되었거나 Docker Hub에서 가져온 alpine 이미지의 일부이거나 BuildKit 빌더로 빌드되었음을 나타내요. BuildKit 이전에는 "클래식" 빌더가 캐싱 목적으로 각 단계마다 새 "중간" 이미지를 생성했고, IMAGE 열에 해당 이미지의 ID가 표시되었어요.

BuildKit은 자체 캐싱 메커니즘을 사용하며 더 이상 캐싱을 위해 중간 이미지가 필요하지 않아요. BuildKit의 다른 개선 사항에 대해 자세히 알아보려면 BuildKit 문서를 참고하세요.

  1. 각 이미지의 레이어를 확인해 보세요.

docker image inspect 명령어를 사용해서 각 이미지 레이어의 암호화 ID를 확인할 수 있어요.

$ docker image inspect --format "{{json .RootFS.Layers}}" acme/my-base-image:1.0
[
 "sha256:72e830a4dff5f0d5225cdc0a320e85ab1ce06ea5673acfe8d83a7645cbd0e9cf",
 "sha256:07b4a9068b6af337e8b8f1f1dae3dd14185b2c0003a9a1f0a6fd2587495b204a"
]
$ docker image inspect --format "{{json .RootFS.Layers}}" acme/my-final-image:1.0
[
 "sha256:72e830a4dff5f0d5225cdc0a320e85ab1ce06ea5673acfe8d83a7645cbd0e9cf",
 "sha256:07b4a9068b6af337e8b8f1f1dae3dd14185b2c0003a9a1f0a6fd2587495b204a",
 "sha256:cc644054967e516db4689b5282ee98e4bc4b11ea2255c9630309f559ab96562e",
 "sha256:e84fb818852626e89a09f5143dbc31fe7f0e0a6a24cd8d2eb68062b904337af4"
]

첫 번째 두 레이어가 두 이미지 모두에서 동일하다는 것을 볼 수 있어요. 두 번째 이미지는 두 개의 추가 레이어를 더해요. 공유된 이미지 레이어는 /var/lib/docker/에 한 번만 저장되며 이미지 레지스트리로 푸시하거나 풀할 때도 공유돼요. 따라서 공유 이미지 레이어는 네트워크 대역폭과 저장 공간을 줄일 수 있어요.

팁: Docker 명령어 출력은 --format 옵션으로 서식을 지정하세요.

위 예시에서는 docker image inspect 명령어에 --format 옵션을 사용해서 레이어 ID를 JSON 배열로 확인했어요. Docker 명령어의 --format 옵션은 awksed 같은 추가 도구 없이 출력에서 특정 정보를 추출하고 서식을 지정할 수 있는 강력한 기능이에요. --format 플래그를 사용한 출력 서식 지정에 대해 자세히 알아보려면 format command and log output 섹션을 참고하세요. 가독성을 위해 jq 유틸리티로 JSON 출력을 예쁘게 출력했어요.

복사는 컨테이너를 효율적으로 만든다

컨테이너를 시작하면 다른 레이어 위에 얇은 쓰기 가능한 컨테이너 레이어가 추가돼요. 컨테이너가 파일 시스템에 가하는 모든 변경 사항은 여기에 저장돼요. 컨테이너가 변경하지 않는 파일은 이 쓰기 가능한 레이어로 복사되지 않아요. 즉, 쓰기 가능한 레이어는 가능한 한 작게 유지돼요.

컨테이너에서 기존 파일을 수정하면 스토리지 드라이버가 copy-on-write 작업을 수행해요. 구체적인 단계는 특정 스토리지 드라이버에 따라 달라져요. overlay2 드라이버의 경우 copy-on-write 작업은 대략 다음과 같은 순서로 진행돼요.

  • 업데이트할 파일을 이미지 레이어에서 검색해요. 이 과정은 최신 레이어에서 시작해서 기본 레이어까지 한 번에 한 레이어씩 내려가요. 결과를 찾으면 캐시에 추가해서 이후 작업 속도를 높여요.
  • 찾은 첫 번째 파일 복사본에 대해 copy_up 작업을 수행해서 파일을 컨테이너의 쓰기 가능한 레이어로 복사해요.
  • 이 파일 복사본에 모든 수정 사항이 적용되며, 컨테이너는 하위 레이어에 존재하는 읽기 전용 복사본을 볼 수 없어요.

Btrfs, ZFS 및 기타 드라이버는 copy-on-write를 다르게 처리해요. 이러한 드라이버의 방법에 대한 자세한 내용은 각각의 상세 설명에서 더 읽을 수 있어요.

데이터를 많이 쓰는 컨테이너는 그렇지 않은 컨테이너보다 더 많은 공간을 사용해요. 대부분의 쓰기 작업이 컨테이너의 얇은 쓰기 가능한 최상위 레이어에 새 공간을 소비하기 때문이에요. 파일 권한이나 소유권 변경과 같은 파일 메타데이터 변경도 copy_up 작업을 초래할 수 있으므로 파일이 쓰기 가능한 레이어로 복제된다는 점을 기억하세요.

팁: 쓰기 집약적인 애플리케이션에는 볼륨을 사용하세요.

쓰기 집약적인 애플리케이션의 데이터를 컨테이너에 저장하지 마세요. 예를 들어 쓰기 집약적인 데이터베이스는 특히 읽기 전용 레이어에 기존 데이터가 있는 경우 문제가 되는 것으로 알려져 있어요.

대신 Docker 볼륨을 사용하세요. 볼륨은 실행 중인 컨테이너와 독립적이며 I/O에 효율적이도록 설계되었어요. 또한 볼륨은 컨테이너 간에 공유할 수 있고 컨테이너의 쓰기 가능한 레이어 크기를 늘리지 않아요. 볼륨에 대한 자세한 내용은 볼륨 사용 섹션을 참고하세요.

copy_up 작업은 눈에 띄는 성능 오버헤드를 발생시킬 수 있어요. 이 오버헤드는 사용 중인 스토리지 드라이버에 따라 달라져요. 큰 파일, 많은 레이어, 깊은 디렉토리 트리는 그 영향을 더 눈에 띄게 만들 수 있어요. 각 copy_up 작업이 특정 파일이 처음 수정될 때만 발생한다는 사실이 이런 영향을 완화해 줘요.

copy-on-write가 어떻게 동작하는지 확인하기 위해, 다음 절차에서는 앞서 빌드한 acme/my-final-image:1.0 이미지를 기반으로 5개의 컨테이너를 실행하고 얼마나 많은 공간을 차지하는지 살펴봐요.

  1. Docker 호스트의 터미널에서 다음 docker run 명령어를 실행하세요. 끝에 있는 문자열은 각 컨테이너의 ID예요.
$ docker run -dit --name my_container_1 acme/my-final-image:1.0 bash \
 && docker run -dit --name my_container_2 acme/my-final-image:1.0 bash \
 && docker run -dit --name my_container_3 acme/my-final-image:1.0 bash \
 && docker run -dit --name my_container_4 acme/my-final-image:1.0 bash \
 && docker run -dit --name my_container_5 acme/my-final-image:1.0 bash

40ebdd7634162eb42bdb1ba76a395095527e9c0aa40348e6c325bd0aa289423c
a5ff32e2b551168b9498870faf16c9cd0af820edf8a5c157f7b80da59d01a107
3ed3c1a10430e09f253704116965b01ca920202d52f3bf381fbb833b8ae356bc
939b3bf9e7ece24bcffec57d974c939da2bdcc6a5077b5459c897c1e2fa37a39
cddae31c314fbab3f7eabeb9b26733838187abc9a2ed53f97bd5b04cd7984a5a
  1. --size 옵션과 함께 docker ps 명령어를 실행해서 5개 컨테이너가 실행 중인지 확인하고 각 컨테이너의 크기를 확인하세요.
$ docker ps --size --format "table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Size}}"

CONTAINER ID IMAGE NAMES SIZE
cddae31c314f acme/my-final-image:1.0 my_container_5 0B (virtual 7.75MB)
939b3bf9e7ec acme/my-final-image:1.0 my_container_4 0B (virtual 7.75MB)
3ed3c1a10430 acme/my-final-image:1.0 my_container_3 0B (virtual 7.75MB)
a5ff32e2b551 acme/my-final-image:1.0 my_container_2 0B (virtual 7.75MB)
40ebdd763416 acme/my-final-image:1.0 my_container_1 0B (virtual 7.75MB)

위 출력은 모든 컨테이너가 이미지의 읽기 전용 레이어(7.75MB)를 공유하지만, 컨테이너 파일 시스템에 기록된 데이터가 없으므로 추가 저장 공간이 사용되지 않았음을 보여줘요.

고급: 컨테이너에 사용되는 메타데이터 및 로그 저장

참고: 이 단계는 Linux 머신이 필요하며 Docker 데몬의 파일 저장소에 액세스해야 하므로 Docker Desktop에서는 작동하지 않아요.

docker ps의 출력은 컨테이너의 쓰기 가능한 레이어가 소비하는 디스크 공간에 대한 정보를 제공하지만, 각 컨테이너에 저장된 메타데이터와 로그 파일에 대한 정보는 포함하지 않아요.

Docker 데몬의 저장 위치(기본값 /var/lib/docker)를 탐색하면 더 자세한 정보를 얻을 수 있어요.

$ sudo du -sh /var/lib/docker/containers/*

36K /var/lib/docker/containers/3ed3c1a10430e09f253704116965b01ca920202d52f3bf381fbb833b8ae356bc
36K /var/lib/docker/containers/40ebdd7634162eb42bdb1ba76a395095527e9c0aa40348e6c325bd0aa289423c
36K /var/lib/docker/containers/939b3bf9e7ece24bcffec57d974c939da2bdcc6a5077b5459c897c1e2fa37a39
36K /var/lib/docker/containers/a5ff32e2b551168b9498870faf16c9cd0af820edf8a5c157f7b80da59d01a107
36K /var/lib/docker/containers/cddae31c314fbab3f7eabeb9b26733838187abc9a2ed53f97bd5b04cd7984a5a

각 컨테이너는 파일 시스템에서 36k의 공간만 차지해요.

  1. 컨테이너별 저장 공간

이를 확인하기 위해 다음 명령어를 실행해서 my_container_1, my_container_2, my_container_3 컨테이너의 쓰기 가능한 레이어에 있는 파일에 'hello'라는 단어를 써 보세요.

$ for i in {1..3}; do docker exec my_container_$i sh -c 'printf hello > /out.txt'; done

이후 docker ps 명령어를 다시 실행하면 해당 컨테이너들이 각각 5바이트를 소비하는 것을 볼 수 있어요. 이 데이터는 각 컨테이너에 고유하며 공유되지 않아요. 컨테이너의 읽기 전용 레이어는 영향을 받지 않으며 모든 컨테이너가 계속 공유해요.

$ docker ps --size --format "table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Size}}"

CONTAINER ID IMAGE NAMES SIZE
cddae31c314f acme/my-final-image:1.0 my_container_5 0B (virtual 7.75MB)
939b3bf9e7ec acme/my-final-image:1.0 my_container_4 0B (virtual 7.75MB)
3ed3c1a10430 acme/my-final-image:1.0 my_container_3 5B (virtual 7.75MB)
a5ff32e2b551 acme/my-final-image:1.0 my_container_2 5B (virtual 7.75MB)
40ebdd763416 acme/my-final-image:1.0 my_container_1 5B (virtual 7.75MB)

앞선 예시들은 copy-on-write 파일 시스템이 어떻게 컨테이너를 효율적으로 만드는지 보여줘요. copy-on-write는 공간을 절약할 뿐만 아니라 컨테이너 시작 시간도 줄여줘요. 컨테이너(또는 동일한 이미지에서 여러 컨테이너)를 만들 때 Docker는 얇은 쓰기 가능한 컨테이너 레이어만 만들면 돼요.

Docker가 새 컨테이너를 만들 때마다 기본 이미지 스택 전체를 복사해야 한다면 컨테이너 생성 시간과 디스크 공간 사용량이 크게 증가할 거예요. 이는 가상 머신이 가상 머신당 하나 이상의 가상 디스크를 사용하는 방식과 유사해요. vfs 스토리지 드라이버는 CoW 파일 시스템이나 다른 최적화를 제공하지 않아요. 이 스토리지 드라이버를 사용하면 각 컨테이너에 대해 이미지 데이터의 전체 복사본이 만들어져요.

더 알아보기 (Learn more)