infra
Platform

모듈 맵

[Docker] 사용하지 않는 이미지 정리와 최적의 태그 아카이빙 전략

0 / 27 완료

펼치기
0 / 27 완료0%

도커 & 컨테이너 · 08 / 27

[Docker] 사용하지 않는 이미지 정리와 최적의 태그 아카이빙 전략

디스크 가득 찬 CI 서버를 살리는 이미지 정리 기술, 롤백 가능한 태그 전략, 폐쇄망 아카이빙까지 실무 이미지 운영 전체를 다룹니다

🚨INCIDENT ALERT
HIGH

새벽 3시 22분, CI 서버에서 빌드 파이프라인이 멈췄다는 알림이 왔습니다. 에러 메시지는 no space left on device 한 줄뿐이었습니다.

빌드마다 생긴 이미지와 캐시가 정리되지 않은 채 쌓였고, 50GB 디스크는 조용히 가득 찼습니다. 어떤 이미지를 지워도 되는지, 어떤 태그로 롤백할 수 있는지, 폐쇄망에는 이미지를 어떻게 옮길지 정해두지 않으면 이미지도 운영 장애의 원인이 됩니다.

Docker 이미지 운영 — 정리·태그 전략·아카이빙

새벽 3시 22분, 슬랙 알림이 울렸습니다. CI 서버에서 빌드 파이프라인이 죽었습니다. 에러 메시지는 딱 한 줄이었습니다 — no space left on device. 빌드마다 생성되던 이미지들이 정리되지 않은 채 쌓였고, 50GB 디스크는 조용히 가득 찼습니다. 로그는 못 쓰고, 새 이미지는 빌드가 안 되고, 서비스 배포는 전면 중단이었습니다. 이 상황을 겪고 나서야 "이미지 운영"이 개발만큼 중요하다는 걸 알게 됩니다. 이미지를 어떻게 쌓이게 둘 것인지, 어떤 기준으로 지울 것인지, 태그는 어떻게 관리할 것인지 — 이 챕터는 그 실무를 다룹니다.


이번 챕터에서 배울 것

CI 서버 디스크가 꽉 차거나 롤백이 안 되는 상황을 예방하는 실무 이미지 운영 기술을 익힙니다. 정리·태그·아카이빙 세 축을 모두 다룹니다.

  • 1docker system df와 docker system df -v로 Docker 디스크 사용 구조를 파악할 수 있다
  • 2dangling 이미지와 unused 이미지의 차이를 이해하고 prune과 prune -a의 영향 범위를 구분할 수 있다
  • 3image → volume → system 순서로 데이터 손실 없이 안전하게 공간을 회수할 수 있다
  • 4latest를 금지하고 git SHA와 날짜를 조합한 immutable tag 원칙으로 이미지 태그 전략을 세울 수 있다
  • 5docker image prune --filter until=72h로 CI cron 자동 정리를 설정할 수 있다
  • 6docker save / load로 이미지를 아카이빙하여 폐쇄망 전달과 백업에 활용할 수 있다
실습 환경 준비

이미 Docker를 사용 중인 환경이라면 system df 출력에서 실제 사용량을 바로 확인할 수 있습니다. 이미지가 없는 깨끗한 환경이라면 실습용 이미지 pull 단계를 먼저 진행하세요.

Docker 버전 확인 (20.10 이상 권장)
docker --version
현재 디스크 사용 상태 미리 확인
docker system df
실습용 이미지 여러 개 pull (dangling 이미지 실습 준비)
docker pull nginx:1.24 && docker pull nginx:1.25 && docker pull alpine:3.18
실습용 디렉토리 생성
mkdir -p ~/image-ops-lab && cd ~/image-ops-lab
실습 후 상태 재확인 (삭제 전 필수)
docker system df -v

system prune은 백업·대상 확인·명시적 승인 후 별도 실행

💡개념

docker system df — 디스크 사용 구조를 한눈에 파악하기

CI 서버 디스크가 갑자기 꽉 찼습니다. 어디서 공간을 잡아먹고 있는지 모른 채 무작정 파일을 지우면 실행 중인 컨테이너나 중요한 볼륨 데이터까지 날릴 수 있습니다. docker system df는 Docker가 사용하는 공간을 유형별로 정리해서 보여주는 진단 명령입니다. 먼저 전체 그림을 파악하고, 그다음 어디를 정리할지 판단해야 안전합니다. 이 ConceptBlock에서는 docker system df-v 플래그의 출력을 읽는 방법과 각 항목의 의미를 다룹니다.

docker system df — 이미지, 컨테이너, 볼륨, 빌드 캐시 사용량확대

docker system df 기본 출력 읽기

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part3/exam_8 && cd /tmp/docker/part3/exam_8

docker system df

출력 예시:

TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          23        4         8.2GB     6.1GB (74%)
Containers      7         2         124MB     98MB (79%)
Local Volumes   5         3         2.3GB     800MB (34%)
Build Cache     -         -         1.1GB     1.1GB

각 컬럼의 의미:

컬럼의미
TOTALDocker가 알고 있는 전체 수량
ACTIVE현재 실행 중인 컨테이너에서 사용 중인 수량
SIZE해당 유형이 차지하는 디스크 크기
RECLAIMABLE지금 당장 정리해도 안전한 공간 (괄호 안은 비율)

RECLAIMABLE이 높을수록 정리 효과가 크고 위험도는 낮습니다. 위 예시에서는 이미지에서만 6.1GB를 회수할 수 있습니다.

docker system df -v — 항목별 상세 조회

로컬 터미널
docker system df -v

출력 예시 (일부):

Images space usage:

REPOSITORY    TAG       IMAGE ID       CREATED        SIZE      SHARED SIZE   UNIQUE SIZE   CONTAINERS
nginx         1.25      a6bd71249a7e   2 weeks ago    187MB     72.8MB        114MB         0
nginx         1.24      a8758716bb6a   4 months ago   187MB     72.8MB        114MB         0
<none>        <none>    d1a364dc548d   3 days ago     187MB     72.8MB        114MB         0
alpine        3.18      c1afedef4846   1 month ago    7.34MB    0B            7.34MB        0

Containers space usage:
...

Local Volumes space usage:
...

<none>:<none> 항목이 dangling 이미지입니다. TAG도 없고 REPOSITORY도 없으며 CONTAINERS가 0입니다. 이것들이 가장 먼저 정리할 대상입니다.

-v 출력에서 확인할 핵심 정보:

  • CONTAINERS 컬럼이 0인 이미지 — 어떤 컨테이너도 이 이미지를 사용하지 않음
  • UNIQUE SIZE — 이 이미지만 차지하는 고유 레이어 크기 (삭제 시 실제 회수량)
  • SHARED SIZE — 다른 이미지와 공유하는 레이어 크기 (삭제해도 회수 안 됨)
🔍실행 후 확인할 것
  • docker system df 출력에서 Images, Containers, Local Volumes, Build Cache 항목을 구분했는가?
  • RECLAIMABLE 컬럼에서 지금 회수 가능한 공간을 확인했는가?
  • docker system df -v 출력에서 <none>:<none> dangling 이미지를 찾을 수 있는가?
  • CONTAINERS 컬럼이 0인 이미지와 사용 중인 이미지를 구분했는가?
💡개념

이미지를 정리하면 실제로 무슨 일이 — 확인부터 용량 회수까지 5단계

docker image prune -a 한 줄을 쳤는데 "몇 GB 회수했다"는 메시지와 달리 df -h는 거의 그대로일 때가 있습니다. 이미지 정리는 "이미지를 지운다"는 단일 동작이 아니라 확인 → 식별 → 로컬 prune → 레지스트리 GC → 실제 용량 회수라는 5단계의 결과입니다. 각 단계가 무엇을 하고 어디서 막히는지 알면 "지웠는데 왜 안 줄지", "이건 왜 안 지워지지"를 단계로 좁혀 진단할 수 있습니다.

TEXT
[호스트]  docker system df -v   /   docker images
   │
   ① 확인 — 태그·다이제스트·용량 파악        (SIZE·SHARED/UNIQUE·CONTAINERS 컬럼)
   │
   ② 식별 — 지워도 되는 것 골라내기          (<none>=dangling / CONTAINERS=0=unused)
   │
   ③ 로컬 prune — 참조 없는 이미지의 태그 해제  (prune=dangling / prune -a=unused)
   │     → 레이어는 아직 남을 수 있음(다른 이미지가 참조 중)
   │
   ④ 레지스트리 정리 — 원격 저장소 GC         (매니페스트 삭제 → garbage-collect 실행)
   │     → GC를 돌리기 전엔 원격 용량이 안 줄어듦
   │
   ⑤ 용량 회수 — 참조가 0이 된 레이어만 실제 삭제 (UNIQUE 레이어만 디스크에서 사라짐)
   ▼
[결과]  회수량 = 삭제 대상의 UNIQUE SIZE 합 (docker images의 SIZE 컬럼 합이 아님)

각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:

단계하는 일여기서 막히면
① 확인docker system df -v로 이미지별 SIZE·SHARED/UNIQUE·CONTAINERS를, docker images --digests로 태그·다이제스트를 본다이 단계를 건너뛰고 무작정 지우면 실행 중 서비스가 쓰는 이미지·롤백용 이전 버전까지 날림
② 식별태그 없는 <none>은 dangling, CONTAINERS가 0인데 태그만 있는 건 unused. 둘을 구분한다dangling과 unused를 혼동하면 prune -a로 필요한 태그 이미지까지 삭제
③ 로컬 prunedocker image prune은 dangling만, -a는 unused까지 태그를 떼고 참조를 끊는다컨테이너가 참조 중이면 image is being used by ... container로 삭제 실패 — 컨테이너부터 정리
④ 레지스트리 GC원격은 태그·매니페스트 삭제만으론 공간이 안 준다. registry garbage-collect(또는 ECR·GHCR retention)로 blob을 실제 회수GC를 안 돌리면 레지스트리 디스크가 계속 참 — 로컬만 지우고 원격은 방치되는 흔한 사각지대
⑤ 용량 회수overlay2는 레이어를 내용 해시로 공유 저장 → 참조가 0이 된 UNIQUE 레이어만 디스크에서 사라진다공유 베이스 레이어를 남은 이미지가 붙잡고 있으면 "지웠는데 df가 그대로" — 빌드 캐시는 builder prune로 따로 비워야 함

"이미지를 지웠다"와 "디스크가 줄었다"는 다른 사건입니다 — ③은 태그·참조를 끊는 단계이고, 실제 바이트가 사라지는 건 참조가 0이 된 ⑤에서만 일어납니다. 그래서 회수량을 추정할 땐 docker images의 SIZE 합이 아니라 삭제 대상들의 UNIQUE SIZE 합으로 계산하고, 원격 용량은 레지스트리 GC(④)를 따로 확인해야 합니다. docker system df -v 전후 비교로 어느 단계에서 기대와 어긋났는지 좁히는 것이 진단의 핵심입니다.


1dangling 이미지를 만들어 prune 영향 범위를 직접 확인

dangling 이미지와 unused 이미지를 혼동하면 필요한 이미지까지 지우거나, 반대로 지워야 할 이미지를 남길 수 있습니다. 같은 태그로 두 번 빌드해 dangling을 만들고, docker images -f dangling=true<none> 이미지가 생겼는지 확인합니다.

로컬 터미널
# 임시 Dockerfile 생성
cat > /tmp/Dockerfile.test << 'EOF'
FROM alpine:3.18
RUN echo "version 1" > /version.txt
EOF

# 같은 태그로 두 번 빌드 — 첫 번째 이미지가 dangling이 됨
docker build -t test-dangling:latest -f /tmp/Dockerfile.test /tmp
docker build -t test-dangling:latest --no-cache -f /tmp/Dockerfile.test /tmp

# dangling 이미지 확인 (TAG가 <none>인 항목)
docker images -f dangling=true
# REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
# <none>       <none>    a1b2c3d4e5f6   10 seconds ago   7.5MB
docker images -f dangling=true
🔍실행 후 확인할 것
  • docker images -f dangling=true 결과에 TAG가 <none>인 항목이 생겼는지 본다 — 생겼으면 dangling 재현 성공(같은 태그 재빌드로 옛 이미지의 태그가 떨어진 것)
  • docker image prune -f(다음 단계) 후 그 <none> 이미지만 사라지고 태그 있는 이미지는 그대로인지 확인 — 태그 이미지가 사라지면 -a를 잘못 붙인 것
  • prune -a 는 '사용 중이지 않은 태그 이미지'까지 지운다 — 실행 중/중지된 컨테이너가 참조하는 이미지는 두 경우 모두 보존되는지 docker ps -a로 교차 확인
  • 회수 용량(Total reclaimed space)을 보고 무엇이 지워졌는지 가늠한다 — 예상보다 크면 -a나 --volumes가 섞였는지 명령을 재확인

docker image prune — dangling만 삭제

로컬 터미널
# dangling 이미지만 삭제 (태그 있는 이미지는 건드리지 않음)
docker image prune -f

# 출력:
# Deleted Images:
# deleted: sha256:a1b2c3d4e5f6...
# Total reclaimed space: 7.5MB

prune 단독 실행은 안전합니다. 태그가 붙어 있는 이미지는 하나도 삭제되지 않습니다.

docker image prune -a — unused 이미지 전체 삭제

로컬 터미널
# 현재 컨테이너에서 사용 중이지 않은 이미지를 모두 삭제
# (태그 있어도 사용 중인 컨테이너가 없으면 삭제됨)
docker image prune -a -f

# 출력:
# Deleted Images:
# untagged: nginx:1.24
# deleted: sha256:a8758716bb6a...
# untagged: nginx:1.25
# deleted: sha256:a6bd71249a7e...
# Total reclaimed space: 6.1GB

-a는 강력하지만 다음 빌드 시 베이스 이미지를 다시 pull해야 하는 비용이 생깁니다. CI 서버에서는 유용하지만 개발 머신에서는 신중하게 사용해야 합니다.

두 명령의 차이 정리

명령어삭제 대상태그 있는 이미지사용 중 이미지
docker image prunedangling 이미지만건드리지 않음건드리지 않음
docker image prune -a사용 중이지 않은 모든 이미지삭제됨건드리지 않음

두 경우 모두 현재 실행 중이거나 중지된 컨테이너에서 사용 중인 이미지는 삭제하지 않습니다.


💡개념

안전한 정리 순서 — 데이터 손실 없이 공간 회수하기

docker system prune -a --volumes를 한 번에 실행하면 가장 많은 공간을 회수할 수 있습니다. 하지만 이 명령은 볼륨 데이터까지 삭제합니다. 데이터베이스 볼륨이 포함되어 있다면 데이터 전체를 잃을 수 있습니다. 안전한 정리는 단계적으로 진행하고, 각 단계에서 무엇이 사라지는지 확인한 다음 넘어가야 합니다. 이 ConceptBlock에서는 위험도가 낮은 순서부터 정리하는 4단계 전략을 다룹니다.

안전한 정리 순서 — 이미지 → 컨테이너 → 볼륨 단계별 접근확대

1단계: 이미지 정리 (가장 안전)

로컬 터미널
# dangling 이미지만 삭제 (가장 안전)
docker image prune -f

# 72시간 이상 된 미사용 이미지 삭제 (필터 활용)
docker image prune -a -f --filter "until=72h"

이미지는 컨테이너가 삭제된 뒤에도 남는 캐시 역할을 합니다. 정리해도 필요하면 다시 pull할 수 있어 위험도가 낮습니다.

2단계: 중지된 컨테이너 정리

로컬 터미널
# 중지된 컨테이너 목록 확인
docker ps -a --filter status=exited

# 중지된 컨테이너 모두 삭제
docker container prune -f

실행 중인 컨테이너는 건드리지 않습니다. 중지된 컨테이너의 로그와 파일시스템 레이어만 삭제됩니다.

3단계: 사용하지 않는 볼륨 정리 (신중하게)

로컬 터미널
# 어떤 컨테이너에도 연결되지 않은 볼륨 목록 확인 (먼저 확인!)
docker volume ls -f dangling=true

# 볼륨 이름과 용도를 파악한 뒤 삭제 결정

볼륨 정리는 가장 위험한 단계입니다. 데이터베이스 볼륨, 업로드 파일 볼륨이 포함될 수 있습니다. 반드시 docker volume ls -f dangling=true로 목록을 확인하고, 용도를 파악한 뒤 삭제하세요.

4단계: 빌드 캐시 정리

Docker
# 빌드 캐시만 삭제 (이미지/컨테이너/볼륨은 건드리지 않음)
docker builder prune -f

# 24시간 이상 된 빌드 캐시만 삭제
docker builder prune --filter "until=24h" -f

한 번에 정리할 때 — system prune

--volumes 플래그는 명시적으로 지정해야만 볼륨을 삭제합니다. 프로덕션 서버나 개발 DB 볼륨이 있는 환경에서는 절대 -a --volumes를 무심코 실행하지 마세요.

정리 단계별 위험도 요약

단계명령어위험도복구 가능 여부
이미지docker image prune낮음가능 (재pull)
이미지 전체docker image prune -a낮음가능 (재pull)
컨테이너docker container prune낮음불가 (로그 손실 가능)
볼륨docker volume prune높음불가 (데이터 영구 삭제)
전체docker system prune -a --volumes매우 높음불가

실습: 이미지 태그 전략 — latest 금지, immutable tag 설계

태그 전략이 없으면 롤백이 불가능해집니다. 새 버전이 배포된 뒤 latest만 있다면 이전 버전을 찾을 방법이 없습니다.

latest 태그의 문제

Docker
# CI에서 매 빌드마다 latest로 push하는 잘못된 패턴
docker build -t myapp:latest .
docker push myapp:latest

# 문제: 오늘의 latest와 어제의 latest는 다른 이미지
# docker pull myapp:latest  ← 항상 가장 최신 이미지를 가져옴
# 특정 시점으로 롤백하려면 어떤 이미지인지 알 수가 없음

git SHA + 날짜 조합 태그 — immutable tag

로컬 터미널
# git SHA 추출 (단축 7자리)
GIT_SHA=$(git rev-parse --short HEAD)

# 날짜 (YYYYMMDD 형식)
BUILD_DATE=$(date +%Y%m%d)

# 이미지 태그 조합
IMAGE_TAG="${BUILD_DATE}-${GIT_SHA}"

# 빌드 및 태그
docker build -t myapp:${IMAGE_TAG} .

# 예: myapp:20240510-a3f7c91

# 레지스트리 push
docker push myapp:${IMAGE_TAG}

# 참고용으로 latest도 함께 push (운영에서는 SHA 태그만 사용)
docker tag myapp:${IMAGE_TAG} myapp:latest
docker push myapp:latest

latest는 "현재 stable" 참조용으로만 유지하고, 실제 배포와 롤백은 반드시 20240510-a3f7c91처럼 고정된 태그로 합니다.

태그 전략 비교

로컬 터미널
# 나쁜 패턴 — 무엇이 배포됐는지 알 수 없음
myapp:latest

# 좋은 패턴 — 언제, 어떤 커밋인지 명확
myapp:20240510-a3f7c91

# 더 구체적인 패턴 — 브랜치까지 포함
myapp:main-20240510-a3f7c91

# 시맨틱 버저닝 + SHA 조합
myapp:v2.3.1-a3f7c91

태그로 롤백하기

로컬 터미널
# 배포된 버전 확인 (태그 목록)
docker images myapp --format "table {{.Tag}}\t{{.CreatedAt}}\t{{.ID}}"

# 특정 시점으로 롤백
docker run -d --name myapp myapp:20240508-b9e2d14

# 또는 레지스트리에서 이전 태그 pull
docker pull myapp:20240508-b9e2d14

💡개념

CI cron 자동 정리 — 이미지가 쌓이지 않는 파이프라인 만들기

이미지 정리를 수동으로 하면 결국 새벽 3시 경보가 옵니다. CI 서버는 매 빌드마다 이미지를 생성하고, 아무도 정리하지 않으면 디스크는 선형으로 증가합니다. 자동화된 정리 정책이 있어야 이 문제를 예방할 수 있습니다. 이 ConceptBlock에서는 GitHub Actions, Jenkins 등 CI 파이프라인과 시스템 cron에서 이미지 자동 정리를 설정하는 방법을 다룹니다.

CI cron 자동 정리 — 파이프라인과 스케줄 기반 이미지 정리확대

--filter until 플래그로 안전하게 정리

docker image prune -a -f는 모든 미사용 이미지를 삭제합니다. --filter "until=72h"를 추가하면 72시간 이내에 생성된 이미지는 보존하고 그보다 오래된 이미지만 삭제합니다.

로컬 터미널
# 72시간 이상 된 미사용 이미지 삭제
docker image prune -a -f --filter "until=72h"

# 24시간 이상 된 dangling 이미지 삭제
docker image prune -f --filter "until=24h"

# 라벨 기준 필터링 — 특정 프로젝트 이미지만 정리
docker image prune -a -f --filter "label=project=myapp"

GitHub Actions에서 자동 정리

YAML
# .github/workflows/cleanup.yml
name: Docker Image Cleanup

on:
  schedule:
    - cron: '0 2 * * *'   # 매일 새벽 2시 실행
  workflow_dispatch:        # 수동 실행도 허용

jobs:
  cleanup:
    runs-on: self-hosted    # CI 서버 러너 (self-hosted)
    steps:
      - name: Remove unused images older than 72h
        run: |
          docker image prune -a -f --filter "until=72h"
          docker builder prune -f --filter "until=24h"
          
      - name: Show disk status after cleanup
        run: docker system df

시스템 cron으로 자동 정리 (Linux 서버)

로컬 터미널
# crontab -e 로 추가
# 매일 새벽 3시에 72시간 이상 된 미사용 이미지 정리
0 3 * * * /usr/bin/docker image prune -a -f --filter "until=72h" >> /var/log/docker-cleanup.log 2>&1

# 매주 일요일 새벽 4시에 빌드 캐시 정리
0 4 * * 0 /usr/bin/docker builder prune -f >> /var/log/docker-cleanup.log 2>&1

Jenkins 파이프라인 마지막 단계에 정리 추가

GROOVY
// Jenkinsfile (선언형 파이프라인)
pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'docker build -t myapp:${BUILD_DATE}-${GIT_COMMIT[0..6]} .'
            }
        }
        stage('Push') {
            steps {
                sh 'docker push myapp:${BUILD_DATE}-${GIT_COMMIT[0..6]}'
            }
        }
    }
    post {
        always {
            // 빌드 완료 후 dangling 이미지 정리
            sh 'docker image prune -f'
        }
        cleanup {
            // 워크스페이스 정리
            cleanWs()
        }
    }
}

post { always { } } 블록에 docker image prune -f를 넣으면 빌드 성공·실패에 관계없이 항상 dangling 이미지를 정리합니다.


실습: docker save / load로 이미지 아카이빙

인터넷이 차단된 폐쇄망 환경, 오프라인 납품, 이미지 백업이 필요할 때 docker savedocker load를 사용합니다.

이미지를 tar 파일로 저장

로컬 터미널
# 단일 이미지 저장
docker save -o nginx-alpine.tar nginx:alpine

# 저장된 파일 확인
ls -lh nginx-alpine.tar
# -rw------- 1 user user 8.1M ... nginx-alpine.tar

# gzip으로 압축하여 크기 줄이기
docker save nginx:alpine | gzip > nginx-alpine.tar.gz
ls -lh nginx-alpine.tar.gz
# -rw-r--r-- 1 user user 3.2M ... nginx-alpine.tar.gz  (약 60% 압축)

여러 이미지를 하나의 파일로 묶기

로컬 터미널
# nginx와 redis를 하나의 tar에 저장
docker save -o app-bundle.tar nginx:alpine redis:7-alpine

# gzip 압축까지
docker save nginx:alpine redis:7-alpine | gzip > app-bundle.tar.gz

# 파일 전송 (scp 예시)
scp app-bundle.tar.gz deploy@192.168.1.100:/tmp/

다른 호스트에서 이미지 복원

로컬 터미널
# tar 파일에서 이미지 로드
docker load -i nginx-alpine.tar

# 출력:
# Loaded image: nginx:alpine

# gzip 압축 파일 로드
docker load -i nginx-alpine.tar.gz
# 또는 파이프 사용
gunzip -c nginx-alpine.tar.gz | docker load

# 복원 확인
docker images nginx
# REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
# nginx        alpine    c1afedef4846   2 weeks ago   41.4MB

이미지 무결성 검증 (폐쇄망 납품 시)

로컬 터미널
# 전송 전: 이미지 다이제스트(sha256) 기록
docker inspect nginx:alpine --format '{{.Id}}'
# sha256:c1afedef4846b6c6a93f62f4db7af...

# 전송 후 복원된 이미지의 다이제스트와 비교
docker inspect nginx:alpine --format '{{.Id}}'
# 동일한 sha256이면 전송 중 손상 없음

빌드 또는 pull 중 no space left on device 에러가 발생했습니다. CI 파이프라인이 멈춘 상태에서 빠르게 공간을 확보해야 합니다.

즉시 진단

로컬 터미널
# Docker 공간 사용 현황 파악
docker system df

# 호스트 디스크 전체 상태 확인
df -h /var/lib/docker

빠른 공간 확보 (순서대로)

로컬 터미널
# 1단계: dangling 이미지 삭제 (가장 안전, 즉시 실행)
docker image prune -f

# 2단계: 중지된 컨테이너 삭제
docker container prune -f

# 3단계: 빌드 캐시 삭제
docker builder prune -f

# 공간 확인
docker system df && df -h /var/lib/docker

빠른 효과가 필요할 때 (볼륨 제외)

위험 명령어확인 없이 미사용 이미지·컨테이너·네트워크·빌드 캐시를 삭제합니다.

Docker 미사용 리소스 긴급 정리

안전한 실행 조건: CI 서버 디스크 부족처럼 긴급 공간 확보가 필요하고 볼륨 삭제가 포함되지 않는 경우에만 실행합니다.

실행 전 반드시 확인

  • docker system df로 회수 가능 공간을 확인했다
  • 실행 중인 서비스에 필요한 이미지가 영향을 받지 않는지 확인했다
  • 볼륨 삭제가 포함되지 않는 명령임을 확인했다
docker system prune -a -f

위 항목을 모두 확인한 후 복사할 수 있습니다

이미지, 컨테이너, 네트워크, 빌드 캐시를 한 번에 삭제합니다. 볼륨은 건드리지 않으므로 --volumes를 붙이지 않으면 데이터는 안전합니다.

근본 원인 해결 — 예방 정리 설정

로컬 터미널
# crontab -e 에 추가 (매일 새벽 3시)
0 3 * * * /usr/bin/docker image prune -a -f --filter "until=72h"

이 에러가 다시 발생하지 않도록 CI 서버에 정기 정리 cron을 반드시 추가하세요.

증상

로컬에서 태그를 붙이고 push했는데, 레지스트리에서 해당 이미지를 찾을 수 없습니다.

로컬 터미널
docker tag myapp:latest myapp:v1.0.0
docker push myapp:v1.0.0
# Using default tag: latest
# The push refers to repository [docker.io/library/myapp]
# denied: requested access to the resource is denied

원인 진단

로컬 터미널
# 현재 이미지 태그 확인
docker images | grep myapp
# myapp    v1.0.0    abc123    2 min ago   512MB
# → 레지스트리 prefix 없이 로컬 이름만 있음

# docker.io/library/ 는 Docker Hub 공식 이미지 경로
# 개인/조직 이미지는 docker.io/<username>/<image> 형식이어야 함

해결

로컬 터미널
# 올바른 태그 형식으로 다시 태깅
# Docker Hub: docker.io/<username>/<image>:<tag>
docker tag myapp:v1.0.0 myusername/myapp:v1.0.0
docker push myusername/myapp:v1.0.0

# GHCR: ghcr.io/<org>/<image>:<tag>
docker tag myapp:v1.0.0 ghcr.io/myorg/myapp:v1.0.0
docker push ghcr.io/myorg/myapp:v1.0.0

# ECR: <account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>
docker tag myapp:v1.0.0 123456789.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:v1.0.0
docker push 123456789.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:v1.0.0

증상

오프라인 환경에 이미지를 전달하려고 docker save로 내보내고 docker load로 불러왔는데, 이미지 이름이 사라집니다.

로컬 터미널
# 내보내기
docker save abc123 > myapp.tar  # 이미지 ID로 저장

# 다른 서버에서 불러오기
docker load < myapp.tar
# Loaded image ID: sha256:abc123...

docker images
# REPOSITORY   TAG       IMAGE ID
# <none>       <none>    abc123...  ← 이름이 없음

원인

docker save에 이미지 ID만 지정하면 이름/태그 정보가 포함되지 않습니다. 이름과 태그는 이미지 메타데이터에 저장되는데, ID만으로 저장하면 그 정보가 빠집니다.

해결

로컬 터미널
# 올바른 방법: 이미지 이름:태그 형식으로 저장
docker save myapp:v1.0.0 > myapp-v1.tar

# 불러오기
docker load < myapp-v1.tar
# Loaded image: myapp:v1.0.0

# 여러 이미지를 한 파일에 저장
docker save myapp:v1.0.0 myapp:v1.1.0 > myapp-versions.tar

# 또는 gzip 압축
docker save myapp:v1.0.0 | gzip > myapp-v1.tar.gz
docker load < myapp-v1.tar.gz

심화 — 태그 아래의 저장 구조를 알아야 정리가 예측된다

💡개념

심화: 태그 위에 있는 것 — 다이제스트와 내용 주소 저장(content-addressable)

"SHA를 넣은 태그를 쓰면 불변"이라고 배웠지만, 사실 태그는 언제든 다른 이미지로 옮길 수 있는 가변 포인터입니다. myapp:20240510-a3f7c91도 누군가 같은 태그로 다시 push하면 가리키는 대상이 바뀝니다. 진짜로 변하지 않는 참조와, 디스크가 예측을 벗어나는 이유는 모두 태그 아래의 저장 구조에 있습니다.

  • 다이제스트가 진짜 불변 주소입니다. 이미지 매니페스트를 sha256으로 해시한 값이 다이제스트(sha256:...)이고, 내용이 1비트라도 다르면 다이제스트가 달라집니다. docker pull myapp@sha256:abc... 처럼 다이제스트로 pull하면 태그가 어디를 가리키든 정확히 그 이미지를 받습니다. 쿠버네티스 매니페스트에 다이제스트를 박아 두는 것이 SHA 태그보다 한 단계 강한 고정이고, docker inspect --format 로 RepoDigests를 확인합니다.
  • 레이어도 내용 주소로 저장·중복 제거됩니다. overlay2는 각 레이어를 그 내용의 해시로 식별해 /var/lib/docker 에 한 번만 저장합니다. 서로 다른 이미지 10개가 같은 node:20-alpine 베이스를 쓰면 그 레이어는 디스크에 하나뿐입니다. 이 중복 제거가 docker system df -v 의 SHARED SIZE와 UNIQUE SIZE 구분의 정체입니다.
  • 그래서 '지운 크기'와 '회수된 크기'가 다릅니다. 이미지를 지워도 그 레이어를 다른 이미지가 아직 참조하면 디스크에서 사라지지 않습니다. 실제로 회수되는 건 그 이미지만의 UNIQUE SIZE뿐이라, 목록의 SIZE 합을 회수량으로 기대하면 어긋납니다.
  • 한계로 이어집니다. 회수량을 추정할 때는 SIZE 컬럼의 합이 아니라 삭제 대상들의 UNIQUE SIZE 합으로 계산해야 하고, 공통 베이스 이미지를 표준화해 공유 레이어를 늘리면 전체 디스크 사용이 오히려 작아집니다.

상황: 디스크가 90%라 급히 docker image prune -a 를 돌렸습니다. docker images 목록에서 지워진 이미지들의 SIZE 합은 30GB는 돼 보였는데, 결과는 Total reclaimed space: 3.8GB 였고 df -h /var/lib/docker 도 거의 그대로입니다. 이미지를 그렇게 많이 지웠는데 왜 이것밖에 안 줄어드는지 답답합니다.

원인: overlay2는 레이어를 내용 주소로 중복 제거해 저장합니다. 지운 이미지 대부분이 같은 베이스 레이어(공통 OS·런타임 레이어)를 공유했고, 그 레이어는 아직 남아 있는 다른 이미지들이 참조 중이라 삭제되지 않았습니다. docker images 의 SIZE 컬럼은 각 이미지의 논리적 크기라 공유분이 중복 집계되므로, 실제 회수되는 UNIQUE 레이어 합보다 훨씬 크게 보였을 뿐입니다.

진단: docker system df -v 로 삭제했던 이미지들의 SHARED SIZE와 UNIQUE SIZE를 봅니다. UNIQUE SIZE 합이 작으면 대부분이 공유 레이어였다는 뜻이고, 그 값이 곧 실제 회수 가능량입니다. df -h /var/lib/docker 전후 비교로 교차 확인합니다.

해결: 더 확보하려면 (1) 공유 베이스 레이어를 붙잡고 있는 '남은' 이미지·중지 컨테이너까지 정리 대상에 넣거나, (2) docker builder prune 으로 BuildKit 빌드 캐시를 비웁니다 — 빌드 캐시는 이미지 prune이 건드리지 않아 따로 쌓입니다. 예방으로는 회수량을 SIZE 합이 아니라 UNIQUE SIZE 합으로 추정하고, 팀 이미지의 베이스를 표준화해 공유 레이어를 늘립니다.


💼
실무 맥락
현업 패턴

CI/CD 파이프라인에서 이미지 정리 정책 설정하는 실무

인프라 엔지니어나 DevOps 엔지니어로 일하면 "CI 서버 디스크 관리"가 반복적인 운영 업무가 됩니다. 빌드가 빠를수록 이미지는 더 빨리 쌓이고, 팀이 커질수록 문제가 심해집니다.

실무에서 적용하는 이미지 운영 정책의 핵심은 세 가지입니다.

첫째, 태그 규약을 팀 전체가 따르도록 Makefile이나 CI 스크립트에 고정합니다. make build를 실행하면 자동으로 $(date +%Y%m%d)-$(git rev-parse --short HEAD) 형식의 태그가 붙도록 합니다. 개발자가 수동으로 태그를 결정하면 규칙이 흐트러집니다.

둘째, 정리 cron은 CI 서버 초기 세팅 시 반드시 포함합니다. 나중에 추가하려면 이미 터진 디스크 사고를 수습하면서 해야 합니다. ansible-playbook이나 terraform으로 서버를 프로비저닝할 때 crontab 설정을 같이 넣으면 빠뜨리지 않습니다.

셋째, 레지스트리 retention policy를 함께 설정합니다. ECR(AWS), GCR(GCP), GHCR(GitHub)은 모두 이미지 보관 정책을 지원합니다. "최근 10개만 보관", "30일 이상 된 이미지 삭제" 같은 정책을 레지스트리에서도 적용하면 CI 서버와 레지스트리 양쪽에서 불필요한 이미지가 쌓이는 것을 막을 수 있습니다.

로컬 터미널
# AWS ECR 예시 — 30일 이상 된 이미지 자동 삭제 정책
aws ecr put-lifecycle-policy \
  --repository-name myapp \
  --lifecycle-policy-text '{
    "rules": [{
      "rulePriority": 1,
      "description": "Keep last 10 images",
      "selection": {
        "tagStatus": "any",
        "countType": "imageCountMoreThan",
        "countNumber": 10
      },
      "action": { "type": "expire" }
    }]
  }'

이미지 운영 정책이 없으면 인프라 엔지니어는 주기적으로 수동 정리를 해야 하고, 결국 어느 날 새벽에 파이프라인 장애로 불려 나오게 됩니다. 처음 설정할 때 10분을 쓰면 그 이후의 새벽 비상 대응을 막을 수 있습니다.


명령어·단축키 빠른 참조

이 모듈에서 다룬 이미지·디스크 정리와 태그·전송 명령을 실전 옵션과 함께 모았습니다. prune 계열은 삭제 명령이므로 --filter로 범위를 좁히는 예를 함께 실었습니다.

명령어/단축키용도자주 쓰는 예
docker system dfDocker 디스크 사용량 요약(이미지/컨테이너/볼륨/캐시)docker system df -v (항목별 상세)
docker images -f dangling=true태그 없는 <none> 이미지 조회docker images -f dangling=true
docker image prune미사용 이미지 정리docker image prune -a -f --filter "until=72h"
docker container prune종료된 컨테이너 정리docker container prune -f
docker volume prune미사용 볼륨 정리(데이터 삭제 주의)docker volume prune -f
docker builder prune빌드 캐시 정리docker builder prune -f --filter "until=24h"
docker system prune미사용 리소스 일괄 정리docker system prune -a (미사용 이미지 전체)
docker tag배포용 레지스트리 경로·버전 태그 부여docker tag myapp:latest ghcr.io/org/myapp:v1.0.0
docker push레지스트리에 이미지 업로드docker push myapp:20240508-b9e2d14
docker pull특정 불변 태그로 이미지 내려받기docker pull myapp:20240508-b9e2d14
docker save이미지를 tar로 아카이브(폐쇄망 이전)docker save nginx:alpine | gzip > nginx.tar.gz
docker loadtar 아카이브에서 이미지 복원docker load -i nginx-alpine.tar
docker images --format태그·생성일·ID를 표로 출력docker images myapp --format "table {{.Tag}}\t{{.CreatedAt}}"

관련 모듈로 더 깊이:

다음 모듈에서는 컨테이너의 휘발성을 다루고, Named Volume과 Bind Mount로 데이터가 컨테이너 삭제 후에도 유지되도록 설계합니다.

지식 확인

퀴즈 — 8문제

Q1

`docker system df`와 `docker system df -v`의 차이는 무엇인가요?

Q2

CI 파이프라인에서 이미지 태그로 `latest` 대신 `git SHA + 날짜` 조합을 사용해야 하는 핵심 이유는?

Q3

`docker image prune`과 `docker image prune -a`의 삭제 범위 차이로 옳은 것은?

Q4

같은 태그(myapp:latest)로 docker build를 두 번 실행하면 첫 번째 이미지는 어떻게 되는가?

Q5

운영 서버에서 디스크가 부족해 docker image prune -a를 실행하려 한다. 가장 주의할 점은?

Q6

docker system df가 이미지 RECLAIMABLE 80%를 보여준다. 다음 조치로 가장 적절한 것은?

Q7

[심화] 배포를 특정 이미지에 '진짜로' 고정하고 싶다. myapp:20240510-a3f7c91 처럼 날짜+SHA 태그를 쓰는 것보다 더 강하게 고정하는 방법은?

Q8

[심화] docker image prune -a를 실행하니 목록 이미지의 SIZE 합은 30GB였는데 Total reclaimed space는 3.8GB뿐이고 디스크도 거의 안 줄었다. 직접 원인은?

0 / 8 답변

🧪 실습으로 확인하기

Docker Compose 멀티 서비스 구성

초급

docker-compose.yml로 nginx + 앱 컨테이너를 함께 정의하고, 서비스 간 통신과 볼륨 마운트를 구성한다.

35📋 4단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요