도커 이미지 용량을 줄이려고 docker image prune 을 돌렸는데 Total reclaimed space: 0B 한 줄만 돌아와서 무엇을 잘못한 건지 막막했던 적이 있으신가요?
안녕하세요? 정리하는 개발자 워니즈입니다. 이번 시간에는 비어 있는 Docker 에 흔한 개발 상태를 일부러 만들어두고 정리 명령을 하나씩 돌려서 각 명령이 실제로 무엇을 지우고 무엇을 남기는지 숫자로 재본 결과를 정리해보려고 합니다.
한 줄 요약 도커 이미지 용량은 어떤 prune 명령을 고르느냐보다 어떤 순서로 돌리느냐에 따라 회수량이 달라졌습니다. 종료된 컨테이너, 이미지, 빌드 캐시, 볼륨 순서로 풀어야 앞 단계가 붙잡고 있던 몫이 뒤 단계에서 풀려납니다.
- 무엇을 쟀나 macOS Docker Desktop(Engine 29.8.0)에서 이미지 5종을 받고 같은 태그로 3회 재빌드하고 컨테이너 4개와 볼륨 2개를 만든 뒤 prune 명령 5개가 각각 회수한 도커 이미지 용량을 쟀습니다
- 예상과 달랐던 것 같은 태그로 세 번 빌드했는데 dangling 이미지가 0개였고 그래서 기본
image prune은 0B 를 회수했습니다 - 그래서 바꾼 순서 이미지를 먼저 지우자 빌드 캐시 회수 가능량이 36.38MB 에서 98.08MB 로 늘었고, 볼륨은 컨테이너가 남아 있는 동안
-a를 붙여도 0B 였습니다
지금 내 PC 의 도커 이미지 용량이 어느 칸에 얼마나 쌓였는지는 아래 명령 한 줄로 먼저 확인할 수 있습니다.
docker system df
이번 재현 환경에서 컨테이너까지 만든 직후 찍은 출력은 이랬는데요, RECLAIMABLE 열이 지금 지워도 되는 몫이라서 이 열부터 보시면 됩니다.
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 6 3 687.1MB 335.3MB (48%)
Containers 4 2 98.3kB 12.29kB (12%)
Local Volumes 2 2 0B 0B
Build Cache 13 0 98.08MB 36.38MB
목차
- 도커 이미지 용량은 어디에 쌓이는가
- 재현 실험은 빈 Docker 에서 이렇게 만들었습니다
- 1단계 지우기 전에 system df 로 센다
- 기본 image prune 은 왜 0B 를 돌려줬을까요?
- 2단계 종료된 컨테이너부터 풀고 이미지를 지운다
- 3단계 빌드 캐시를 비우고 볼륨은 마지막에 본다
- 볼륨은 왜 -a 를 붙여도 하나도 안 지워졌을까요?
- system prune 한 줄로 끝내면 안 되나요?
- 이번 재현으로 말할 수 없는 것
- 정리와 필자의 결론
1. 도커 이미지 용량은 어디에 쌓이는가
흔히 도커 이미지 용량이라고 뭉뚱그려 부르지만 Docker 가 디스크를 쓰는 곳은 네 칸으로 나뉘어 있고, docker system df 가 이 네 칸을 Images, Containers, Local Volumes, Build Cache 로 따로 보여줍니다(Docker 공식 문서 docker system df).
| 칸 | 무엇이 들어가나 | 지우는 명령 |
|---|---|---|
| Images | pull 로 받거나 직접 빌드한 이미지 | docker image prune |
| Containers | 컨테이너마다 이미지 위에 얹힌 쓰기 계층 | docker container prune |
| Local Volumes | 볼륨에 쌓인 데이터 | docker volume prune |
| Build Cache | BuildKit 이 빌드 중에 남긴 중간 산물 | docker builder prune |
칸마다 지우는 명령이 따로 있다는 점이 이 글 전체의 출발점입니다. 디스크가 부족할 때 이미지 칸만 보고 명령을 고르면 나머지 세 칸에 쌓인 몫은 손도 대지 못한 채 그대로 남습니다.
표를 읽을 때는 SIZE 열보다 RECLAIMABLE 열을 먼저 보는 편이 좋은데, SIZE 는 그 칸이 지금 차지하는 총량이고 RECLAIMABLE 은 그중 지금 당장 지워도 되는 몫이라서 도커 이미지 용량을 얼마나 되찾을 수 있을지 기대값을 잡는 숫자는 뒤쪽이기 때문입니다.

2. 재현 실험은 빈 Docker 에서 이렇게 만들었습니다
도커 정리 글에 실린 숫자는 보통 글쓴이 PC 한 대의 상태라서 독자가 자기 환경과 맞춰볼 방법이 없습니다. 그래서 이번에는 네 칸이 전부 0B 인 빈 Docker 에서 출발해 누구나 같은 상태를 다시 만들 수 있는 재현 실험으로 도커 이미지 용량의 변화를 쟀습니다.
| 항목 | 값 |
|---|---|
| 실행일 | 2026-09-25 |
| Docker Engine(Server) | 29.8.0 |
| 스토리지 드라이버 | overlayfs |
| 이미지 저장소 | containerd(driver-type io.containerd.snapshotter.v1) |
| 호스트 | macOS, Docker Desktop |
| 시작 상태 | 이미지, 컨테이너, 볼륨, 빌드 캐시 전부 0B |
재현은 아래 세 단계로 진행했고 각 단계가 끝날 때마다 docker system df 를 찍어서 네 칸이 어떻게 바뀌는지 남겼습니다.
- 흔히 쓰는 이미지 5종을 받았습니다. alpine:3.20, nginx:1.27-alpine, node:20-slim, python:3.12-slim, redis:7-alpine 입니다
- python:3.12-slim 위에 requests 를 설치하는 작은 이미지를
demo-app:latest라는 같은 태그로 세 번 빌드했고, 빌드마다--build-arg V값만 1, 2, 3 으로 바꿨습니다 - 컨테이너 4개를 만들었습니다. nginx 와 redis 는 실행 상태로 두고 redis 에는
cachedata라는 이름 붙은 볼륨을 달았고, alpine 은 한 번 실행하고 끝나는 컨테이너를 두 개 만들면서 그중 하나에-v /scratch로 익명 볼륨을 붙였습니다
빌드에 쓴 Dockerfile 은 아래 다섯 줄이고, Dockerfile 문법이 낯설다면 Dockerfile 의 이해 를 먼저 보시면 읽기 편합니다.
FROM python:3.12-slim
ARG V=1
RUN pip install --no-cache-dir requests==2.31.0 && echo "build $V" > /build.txt
COPY app.py /app/app.py
CMD ["python", "/app/app.py"]
같은 태그로 세 번 빌드하고 dangling 이미지를 세는 부분은 아래와 같은데, ctx 는 위 Dockerfile 과 한 줄짜리 app.py 가 들어 있는 빌드 디렉터리입니다.
for v in 1 2 3; do
docker build -q -t demo-app:latest --build-arg V=$v ctx
done
docker images -f dangling=true
3. 1단계 지우기 전에 system df 로 센다
첫 단계는 아무것도 지우지 않고 도커 이미지 용량이 어느 칸에 얼마나 있는지 세는 것인데요, 컨테이너까지 만든 직후 docker system df 를 돌리자 아래 출력이 나왔습니다(macOS Docker Desktop, Engine 29.8.0, 2026-09-25 실행).
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 6 3 687.1MB 335.3MB (48%)
Containers 4 2 98.3kB 12.29kB (12%)
Local Volumes 2 2 0B 0B
Build Cache 13 0 98.08MB 36.38MB
재현 단계마다 같은 명령을 찍어서 네 칸 중 크게 움직인 이미지와 빌드 캐시만 모아보면 아래 표처럼 됩니다.
| 시점 | 이미지 | 이미지 회수 가능 | 빌드 캐시 | 캐시 회수 가능 |
|---|---|---|---|---|
| 빈 상태 | 0개, 0B | 0B | 0개, 0B | 0B |
| 이미지 5종 pull | 5개, 669.2MB | 669.2MB | 0개, 0B | 0B |
| 같은 태그 3회 재빌드 | 6개, 687.1MB | 484.4MB | 13개, 98.08MB | 36.38MB |
| 컨테이너 4개 생성 | 6개, 687.1MB | 335.3MB | 13개, 98.08MB | 36.38MB |
직접 확인 1. 실행 중인 컨테이너는 nginx 와 redis 두 개뿐인데 Images 의 ACTIVE 는 3 으로 찍혔고, 남은 하나는 이미 종료된 컨테이너 두 개가 쓰고 있던 alpine 이었습니다. 종료된 컨테이너도 자기가 만들어진 이미지를 사용 중으로 붙잡고 있다는 뜻입니다. 컨테이너를 만든 뒤 이미지 회수 가능량이 484.4MB 에서 335.3MB 로 줄어든 것도 같은 이유로 읽힙니다.
반대로 컨테이너 칸 자체의 회수 가능량은 12.29kB 로 사실상 0 에 가까웠으니, 종료된 컨테이너는 그 자체로는 거의 공간을 차지하지 않으면서 이미지를 붙잡는 방식으로 도커 이미지 용량을 늘리고 있었던 셈입니다.

4. 기본 image prune 은 왜 0B 를 돌려줬을까요?
이 환경에는 지울 dangling 이미지가 처음부터 하나도 없었기 때문입니다. 공식 문서에 따르면 docker image prune 은 기본으로 dangling 이미지만 지우고 -a 를 붙여야 컨테이너가 참조하지 않는 이미지까지 지우는데, 기본 명령의 대상이 비어 있으니 회수량도 0B 일 수밖에 없었습니다(Docker 공식 문서 docker image prune).
직접 확인 2. 같은 태그로 세 번 빌드한 직후 docker images -f dangling=true 는 머리글 한 줄만 찍고 목록이 비어 있었고, 이미지 개수도 받은 5종에 demo-app:latest 하나를 더한 6개라서 앞선 두 번의 빌드 결과는 목록에 별도 이미지로 남지 않았습니다. 이 상태에서 docker image prune -f 를 돌리자 결과는 Total reclaimed space: 0B 였고 네 칸의 숫자도 한 자리도 바뀌지 않았습니다.
같은 태그로 다시 빌드하면 이전 이미지가 태그를 잃고 dangling 으로 남는다는 설명이 흔한데 이번 환경에서는 그 현상이 나타나지 않았습니다. 이 환경은 containerd 이미지 저장소를 쓰고 있었고, dangling 목록의 머리글도 IMAGE ID DISK USAGE CONTENT SIZE EXTRA 로 기존에 익숙한 형식과 달랐습니다(Docker Desktop containerd image store 문서).
기본
image prune이 0B 를 돌려주는 것은 명령이 고장 난 것이 아니라 지울 dangling 이미지가 없다는 뜻입니다. 돌리기 전에docker images -f dangling=true로 대상이 있는지부터 보면 헛걸음을 줄일 수 있습니다.
다만 이 관찰을 Docker 29 부터 항상 그렇다는 규칙으로 읽으면 곤란한데요, 필자가 확인한 것은 containerd 저장소를 쓰는 이 한 환경이고 기존 방식 저장소를 쓰는 서버에서는 같은 태그 재빌드가 dangling 을 남길 수 있어서 비교 실험 없이는 말할 수 없습니다. 앞선 두 번의 빌드 결과가 어디로 갔는지도 df 숫자만으로는 가를 수 없어서 미확인으로 남겨둡니다.
5. 2단계 종료된 컨테이너부터 풀고 이미지를 지운다
이번 재현에서 도커 이미지 용량이 실제로 크게 빠진 명령은 docker image prune -a 였고, 같은 상태에서 이 명령을 돌리자 Total reclaimed space: 328.7MB 가 찍혔습니다.
| 항목 | 정리 전 | image prune -a 후 |
|---|---|---|
| 이미지 개수 | 6개 | 3개 |
| 이미지 SIZE | 687.1MB | 149.2MB |
| 이미지 회수 가능 | 335.3MB | 0B |
| 빌드 캐시 회수 가능 | 36.38MB | 98.08MB |
직접 확인 3. 지워진 이미지는 어떤 컨테이너도 쓰지 않던 node:20-slim, python:3.12-slim, demo-app 세 개였고 남은 세 개는 nginx 와 redis 와 alpine 이었습니다. alpine 은 실행 중인 컨테이너가 하나도 없는데도 종료된 컨테이너 두 개가 붙잡고 있어서 -a 로도 지워지지 않았습니다.
그래서 2단계는 종료된 컨테이너를 먼저 정리하고 이미지를 지우는 순서입니다. 공식 문서 설명대로라면 종료된 컨테이너가 사라진 뒤에는 alpine 도 참조가 끊겨 -a 의 대상이 되지만 이 순서로 한 번 더 돌려서 alpine 이 빠지는지는 이번에 따로 재지 않았습니다.
docker ps -a --filter status=exited
docker container prune -f
docker image prune -a -f
docker system df
docker container prune 은 종료된 컨테이너를 전부 지우므로 다시 켜서 쓸 컨테이너가 섞여 있는지 첫 줄로 먼저 확인하고, -a 로 지운 이미지는 다음에 필요할 때 다시 받아야 하니 받기 어려운 이미지가 섞여 있다면 그것부터 빼두는 편이 안전합니다.
직접 확인 4. 숫자 하나가 맞지 않았는데, prune 이 보고한 회수량은 328.7MB 였지만 df 의 이미지 SIZE 는 687.1MB 에서 149.2MB 로 537.9MB 가 줄었습니다. 정리 전 RECLAIMABLE 열이 보여준 335.3MB 는 prune 보고값과 가까웠고 두 숫자가 왜 다른지는 이번에 원인을 확인하지 못했으니, 정리 계획을 세울 때는 prune 보고값과 df 전후 차이를 둘 다 적어두는 쪽이 나중에 헷갈리지 않습니다.
이미지를 처음부터 작게 만드는 방법은 Dockerfile 최적화 에서 레이어 캐싱과 베이스 이미지 선택을 다뤘는데, 지우는 것보다 덜 쌓는 쪽이 도커 이미지 용량 관리에서는 오래가는 방법이라고 생각합니다.
6. 3단계 빌드 캐시를 비우고 볼륨은 마지막에 본다
마지막 단계는 앞 단계가 풀어준 몫을 지우는 것인데요, 이미지를 정리한 직후 docker builder prune -f 를 돌리자 Total: 98.08MB 가 찍혔고 빌드 캐시 칸은 13개, 98.08MB 에서 0개, 0B 가 됐습니다(Docker 공식 문서 docker builder prune).
직접 확인 5. 이 글에서 순서를 강조하는 이유가 여기 있는데, 이미지를 지우기 전 빌드 캐시의 회수 가능량은 36.38MB 였지만 image prune -a 뒤에는 98.08MB 로 약 2.7배가 됐습니다. 이미지가 남아 있는 동안에는 빌드 캐시 일부가 회수 대상에서 빠져 있었다는 뜻이고, 도커 이미지 용량을 정리하는 순서가 뒤 칸의 회수량까지 바꾼 셈입니다.
| 시점 | 빌드 캐시 전체 | 회수 가능 |
|---|---|---|
| 이미지 정리 전 | 13개, 98.08MB | 36.38MB |
| image prune -a 직후 | 13개, 98.08MB | 98.08MB |
| builder prune 직후 | 0개, 0B | 0B |
반대 순서로 빌드 캐시를 먼저 비웠다면 df 의 회수 가능 열 기준으로 36.38MB 정도만 빠졌을 것 같은데요, 이 순서를 실제로 돌려보지는 않았으니 기대값으로만 읽어주시면 되고 이미지가 캐시를 붙잡는 내부 구조도 문서에서 확인하지 못해 미확인으로 둡니다.
공식 문서는 builder prune 이 기본으로 dangling 캐시만 지우고 -a 를 붙이면 사용하지 않는 캐시 전부를 지운다고 설명합니다. 이번 재현에서는 -a 없이 돌렸는데도 이미지를 먼저 지운 뒤라서 98.08MB 전부가 빠졌고, -a 를 붙였을 때 결과가 어떻게 달라지는지는 재지 않았습니다.
빌드 캐시는 도커 이미지 용량을 이루는 칸 가운데 잃는 것이 다음 빌드 시간뿐인 칸이라 주기적으로 돌려도 고민할 거리가 적고, CI 에서 빌드를 자주 태우는 환경이라면 예전에 정리한 Docker Build Pipeline 구성하기 처럼 빌드가 도는 자리에 이 명령을 같이 두는 방식을 생각해볼 수 있습니다.

7. 볼륨은 왜 -a 를 붙여도 하나도 안 지워졌을까요?
볼륨 두 개가 모두 컨테이너에 참조되고 있었기 때문입니다. 공식 문서는 docker volume prune 이 어떤 컨테이너도 참조하지 않는 볼륨만 지우고 기본으로는 익명 볼륨만 대상으로 삼으며, -a 를 붙이면 이름 붙은 볼륨까지 대상을 넓힌다고 설명합니다(Docker 공식 문서 docker volume prune).
직접 확인 6. 이미지와 빌드 캐시를 정리한 뒤 docker volume prune -f 와 docker volume prune -a -f 를 차례로 돌렸는데 둘 다 Total reclaimed space: 0B 였고, docker volume ls 에는 두 볼륨이 이름 그대로 남아 있었습니다.
| 볼륨 | 종류 | 참조하는 컨테이너 | volume prune | volume prune -a |
|---|---|---|---|---|
| cachedata | 이름 붙음 | redis 컨테이너(실행 중) | 남음 | 남음 |
| 61b2e0f02c17 로 시작하는 해시 | 익명 | 이름 없는 alpine 컨테이너(종료) | 남음 | 남음 |
-a 는 대상 범위를 이름 붙은 볼륨까지 넓힐 뿐이고 사용 중인 볼륨을 지우는 옵션이 아닙니다. 익명 볼륨은 컨테이너가 이미 종료됐는데도 그 컨테이너가 남아 있다는 이유만으로 지워지지 않았고 이것이 볼륨 정리를 컨테이너 정리 뒤에 두는 이유입니다.
직접 확인 7. 실험을 원상 복구하면서 컨테이너를 전부 지우고 docker system prune -a -f --volumes 를 돌렸더니 볼륨은 2개에서 1개로 줄었습니다. 남은 하나의 이름은 이번 출력에 찍지 않아서 확인하지 못했는데, 공식 문서대로 --volumes 가 익명 볼륨만 지운다면 이름 붙은 cachedata 쪽일 것 같습니다.
볼륨은 네 칸 중 유일하게 지우면 데이터가 사라지는 칸이라서 도커 이미지 용량을 되찾겠다고 한 번에 밀어버리기보다 목록을 눈으로 보고 하나씩 판단하는 편이 낫고, 볼륨의 종류와 마운트 방식은 예전에 정리한 Volume 의 관리 에 더 자세히 있습니다.

8. system prune 한 줄로 끝내면 안 되나요?
끝낼 수는 있지만 이번 재현에서 드러난 차이를 보려면 나눠 돌리는 쪽이 맞다고 봅니다. docker system prune 은 종료된 컨테이너와 사용하지 않는 네트워크와 dangling 이미지와 빌드 캐시를 한 번에 지우고 -a 와 --volumes 로 범위를 넓히는 명령입니다(Docker 공식 문서 Prune unused Docker objects).
| 명령 | 지우는 대상(공식 문서) | 이번 재현 회수량 |
|---|---|---|
| docker image prune | dangling 이미지 | 0B |
| docker image prune -a | 컨테이너가 참조하지 않는 이미지 전부 | 328.7MB |
| docker builder prune | 빌드 캐시(기본은 dangling) | 98.08MB(이미지 정리 뒤) |
| docker volume prune | 참조 없는 익명 볼륨 | 0B |
| docker volume prune -a | 참조 없는 볼륨 전부 | 0B |
| docker system prune | 위 대상을 묶어서 | 개별 회수량으로는 재지 않음 |
한 줄로 돌리면 도커 이미지 용량이 어느 칸에서 얼마나 빠졌는지가 출력 하나로 뭉쳐서 다음에 같은 상황이 왔을 때 무엇이 원인이었는지 되짚기 어렵습니다. --volumes 를 붙여도 이름 붙은 볼륨은 남는다는 점도 7장에서 본 대로라서, 한 줄이면 다 지워진다는 기대 자체가 맞지 않습니다.
필자는 도커 이미지 용량을 정리할 때 단계마다 docker system df 를 한 번씩 찍어 네 칸의 변화를 남기는 쪽을 택했는데, 명령 하나가 늘어날 뿐이지만 3장의 표 같은 기록이 쌓이면 자기 환경에서 어느 칸이 주로 불어나는지가 보이기 시작합니다.

9. 이번 재현으로 말할 수 없는 것
재현 실험은 조건을 통제한 대신 범위가 좁아서, 확인하지 못한 것을 지우지 않고 아래에 그대로 적어둡니다.
| 항목 | 상태 | 이유 |
|---|---|---|
| 기존 방식 이미지 저장소에서도 dangling 이 0개인가 | 미확인 | containerd 저장소 한 환경만 돌렸습니다 |
| 빌드 캐시를 먼저 지우면 회수량이 얼마인가 | 미확인 | 반대 순서는 실행하지 않았고 df 기대값만 있습니다 |
| 종료된 컨테이너를 지운 뒤 alpine 이 -a 로 빠지는가 | 미확인 | 문서상 동작이고 따로 재지 않았습니다 |
| prune 보고값 328.7MB 와 df 감소 537.9MB 의 차이 | 원인 미확인 | 두 숫자를 기록만 했습니다 |
| builder prune 에 -a 를 붙이면 달라지는가 | 미확인 | -a 없이만 돌렸습니다 |
| macOS 쪽 Docker Desktop 가상 디스크 파일이 같이 줄어드는가 | 미확인 | 호스트 파일 크기를 재지 않았습니다 |
규모도 작았습니다. 이미지 5종에 수백 MB 수준의 실험이라 이미지가 수십 개 쌓인 환경에서 칸별 비율이 같게 나온다고 말할 근거는 없고, 이 글이 도커 이미지 용량 정리에서 말할 수 있는 것은 칸 사이의 관계와 지우는 순서까지입니다.
10. 정리: 누구에게 맞고 누구에게는 굳이인가, 그리고 필자는 이렇게 봅니다
이 정리 순서가 잘 맞는 쪽은 Docker Desktop 으로 개발하면서 빌드를 자주 돌리고 한 번 띄우고 끝나는 컨테이너가 쌓이는 환경인데, 종료된 컨테이너가 이미지를 붙잡고 그 이미지가 빌드 캐시를 붙잡는 구조가 이번 재현과 같다면 순서만 바꿔도 도커 이미지 용량의 회수량이 달라집니다.
굳이인 쪽도 있어서, 작업이 끝나면 머신째 사라지는 CI 러너라면 정리 순서를 고민할 이유가 적고 기존 방식 저장소를 쓰는 서버라면 이 글의 0B 관찰을 그대로 옮기지 말고 dangling 목록부터 세어보는 것이 먼저일 것 같습니다.
필자의 결론
도커 이미지 용량 정리는 명령을 아는 문제가 아니라 붙잡힌 순서대로 푸는 문제라고 봅니다. 기본 image prune 의 0B 도, 빌드 캐시의 2.7배도, -a 를 붙여도 남은 볼륨도 전부 앞 칸이 뒤 칸을 붙잡고 있어서 생긴 숫자였습니다.
그래서 필자는 정리 순서를 종료된 컨테이너, 이미지, 빌드 캐시, 볼륨으로 고정하고 단계마다 docker system df 를 찍어 남기기로 했습니다. 반대 순서 실험과 기존 방식 저장소와의 비교는 아직 하지 않았는데, 앞의 것은 같은 스크립트에서 순서만 바꾸면 되지만 뒤의 것은 containerd 가 아닌 환경을 따로 준비해야 해서 이번에는 한 환경의 숫자를 있는 그대로 남기는 데서 멈췄습니다.