아침 배포 직후 운영 서버가 어제와 다른 nginx:latest 이미지를 내려받았습니다. 코드 변경은 없었는데 컨테이너 안의 패키지 버전이 달라져 장애 원인을 재현하기 어려웠습니다.
이미지는 레이어로 저장되고 태그는 언제든 다른 이미지를 가리킬 수 있습니다. Docker Hub에서 무엇을 pull했는지, 어떤 태그를 신뢰할지, 디스크가 찰 때 무엇을 지워도 되는지 모르면 배포와 정리가 모두 위험해집니다.
Docker 이미지 구조와 Registry
Docker의 강력함은 단순히 컨테이너를 실행하는 것에 그치지 않습니다. 이미지의 레이어 아키텍처를 이해하면 왜 docker pull이 빠른지, 왜 동일 베이스 이미지를 쓰는 컨테이너들이 디스크를 공유하는지 이해할 수 있습니다. 이 챕터에서는 이미지의 내부 구조부터 레지스트리 활용까지 살펴봅니다.
앞 모듈에서 pull/build/tag/push 워크플로우를 익혔다면, 이번엔 이미지가 내부적으로 어떻게 저장되는지, 레지스트리 생태계를 어떻게 활용하는지 깊이 들어갑니다.
- 1Union Filesystem(OverlayFS)과 Copy-on-Write로 레이어가 디스크에 저장되는 방식을 설명할 수 있다
- 2Docker Hub, GitHub Container Registry(GHCR), 사설 Registry를 비교해 선택할 수 있다
- 3이미지 다이제스트(digest)와 immutable 태그 전략으로 :latest를 쓰면 안 되는 이유를 설명할 수 있다
- 4docker system df와 prune으로 디스크 공간을 관리할 수 있다
push 실습은 Docker Hub 계정이 필요합니다. 계정이 없으면 pull/inspect 실습만으로도 충분합니다.
docker info | grep 'Server Version'https://hub.docker.com — push 실습 시 필요
docker logindocker pull ubuntu:22.04 && docker pull alpine:3.19레이어 아키텍처와 UnionFS — 이미지가 저장되는 방식
docker pull로 이미지를 받을 때 줄마다 'Pull complete', 'Already exists'가 번갈아 표시됩니다. 이게 레이어입니다. 이 구조를 이해하면 왜 같은 Alpine 이미지를 기반으로 한 이미지 10개가 디스크를 중복해서 차지하지 않는지, 컨테이너를 100개 실행해도 이미지가 100배로 늘어나지 않는지 설명됩니다. Dockerfile에서 명령어 순서가 빌드 속도에 영향을 주는 이유도 레이어 캐시 구조에서 나옵니다. 이 ConceptBlock에서는 레이어가 실제 디스크에 어떻게 저장되는지와 Copy-on-Write 전략을 다룹니다.
확대
레이어(Layer)란?
Docker 이미지는 여러 개의 읽기 전용 레이어가 쌓인 구조입니다. 각 레이어는 Dockerfile의 한 명령어(RUN, COPY, ADD 등)가 만들어내는 파일시스템 변경 사항의 스냅샷입니다.
위에서 아래로 쌓인 레이어 구조입니다 (위가 최신).
- 컨테이너 레이어 (읽기/쓰기) ← docker run 시 생성
- 레이어 4:
COPY . /app(앱 소스코드) — 읽기 전용 - 레이어 3:
RUN pip install -r requirements.txt— 읽기 전용 - 레이어 2:
RUN apt-get install python3— 읽기 전용 - 레이어 1:
FROM ubuntu:22.04(베이스 이미지) — 읽기 전용
각 레이어는 SHA256 해시로 식별되며, Docker는 이 해시를 사용해 레이어를 캐시하고 재사용합니다.
UnionFS와 Copy-on-Write
Docker는 OverlayFS(또는 overlay2)라는 Union 파일시스템을 사용합니다. UnionFS는 여러 디렉토리(레이어)를 하나의 가상 파일시스템으로 병합하여 보여주는 기술입니다.
컨테이너에서 파일 읽기:
1. 최상위 쓰기 레이어 확인
2. 없으면 아래 레이어 순서대로 탐색
3. 처음 발견된 파일 반환
컨테이너에서 파일 수정(Copy-on-Write):
1. 해당 파일을 하위 읽기 전용 레이어에서 쓰기 레이어로 복사
2. 쓰기 레이어에서 파일 수정
3. 이후 읽기 시 쓰기 레이어의 파일이 우선
레이어 공유의 장점
동일한 베이스 이미지를 사용하는 여러 이미지는 하위 레이어를 공유합니다:
| 레이어 | 이미지 A (python-app) | 이미지 B (node-app) |
|---|---|---|
| 4 | COPY python-app | COPY node-app |
| 3 | pip install | npm install |
| 2 | python3 설치 | nodejs 설치 |
| 1 | ubuntu:22.04 | ubuntu:22.04 (← 공유!) |
ubuntu:22.04 레이어는 디스크에 한 번만 저장되며, 여러 이미지가 이를 공유합니다. 이는 디스크 절약과 pull 속도 향상으로 이어집니다.
OverlayFS 실제 구조 확인
Docker의 레이어는 호스트 파일시스템에서 직접 확인할 수 있습니다. overlay2 드라이버가 각 레이어를 별도 디렉토리로 관리하는 방식을 보면, 왜 동일 베이스 이미지를 쓰는 수십 개의 컨테이너가 디스크를 거의 추가로 차지하지 않는지 이해됩니다.
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part2/exam_6 && cd /tmp/docker/part2/exam_6
# overlay2 드라이버 사용 확인
docker info | grep "Storage Driver"
# Storage Driver: overlay2
# 이미지 레이어 실제 저장 위치
sudo ls /var/lib/docker/overlay2/
# 특정 이미지의 레이어 ID 확인
docker image inspect nginx --format '{{json .RootFS.Layers}}'
Docker Hub와 이미지 태그 관리
docker pull nginx를 실행하면 이미지가 내려옵니다. 그런데 이 nginx 이미지를 누가 관리하는지, 보안 패치는 제때 이루어지는지 확인하셨나요? Docker Hub에는 공식 이미지부터 누구나 올릴 수 있는 커뮤니티 이미지까지 수백만 개가 혼재합니다. 이름만 보고 신뢰하면 악의적으로 패키징된 이미지를 내려받을 수 있고, :latest 태그를 그대로 프로덕션에 사용하면 오늘과 내일의 배포 결과가 달라지는 상황을 만납니다. "어제까지 잘 됐는데 오늘 배포만 실패한다"는 문제의 절반은 태그 관리 부재에서 비롯됩니다. 이 ConceptBlock에서는 Docker Hub 이미지 신뢰 등급과 태그 전략을 다룹니다.
확대
Docker Hub 이미지 종류
Docker Hub(hub.docker.com)는 세계 최대의 컨테이너 이미지 레지스트리입니다. 이미지는 신뢰 수준에 따라 구분됩니다.
공식 이미지 (Official Images)
Docker Inc.와 해당 소프트웨어 커뮤니티가 직접 관리합니다. 이미지 이름에 슬래시(/)가 없으며 Docker Hub에서 파란 "Official Image" 배지를 확인할 수 있습니다.
# 공식 이미지 예시 (사용자명 없음)
docker pull nginx # Nginx 공식 이미지
docker pull postgres # PostgreSQL 공식 이미지
docker pull python # Python 공식 이미지
docker pull node # Node.js 공식 이미지
docker pull ubuntu # Ubuntu 공식 이미지
docker pull alpine # Alpine Linux 공식 이미지
검증된 퍼블리셔 이미지 (Verified Publisher)
Docker가 신원을 확인한 기업/조직이 제공합니다.
# 검증된 퍼블리셔 이미지 (조직명/이미지명)
docker pull elastic/elasticsearch:8.11.0
docker pull bitnami/postgresql:15
커뮤니티 이미지
누구나 올릴 수 있는 이미지입니다. 사용 전 반드시 Dockerfile과 관리 상태를 검토해야 합니다.
# 커뮤니티 이미지 (사용자명/이미지명)
docker pull myusername/myapp:latest
태그(Tag) 관리의 중요성
태그는 이미지의 특정 버전을 식별하는 레이블입니다. 이미지 전체 참조 형식은 다음과 같습니다:
[레지스트리호스트:포트/][사용자명/]이미지명[:태그]
예시:
nginx # Docker Hub 공식, latest 태그
nginx:1.25.3 # Docker Hub 공식, 특정 버전
myuser/myapp:v2.1 # Docker Hub 커뮤니티
registry.example.com/myapp:1.0.0 # 프라이빗 레지스트리
gcr.io/google-containers/pause:3.9 # Google Container Registry
:latest 태그의 위험성
:latest는 고정된 버전이 아닙니다. 누군가 새 이미지를 push할 때마다 가리키는 대상이 바뀝니다.
# 위험한 패턴 — 언제 이미지가 바뀔지 모름
FROM python:latest # ❌ 프로덕션에서 절대 금지
# 안전한 패턴 — 정확한 버전 고정
FROM python:3.11.7-slim-bookworm # ✅ 재현 가능한 빌드
:latest는 단순히 "가장 최근에 push된 이미지"를 가리키는 일반 태그입니다. 특별한 의미가 없으며, 이미지 관리자가 새 버전을 push할 때마다 변경될 수 있습니다.
태그 전략 예시
CI/CD 파이프라인에서 이미지를 빌드할 때는 버전 번호, Git SHA, 환경명을 조합해 태그를 붙입니다.
# 시맨틱 버저닝 기반 태그 전략
docker tag myapp:latest myapp:2.1.3 # 정확한 버전
docker tag myapp:latest myapp:2.1 # 마이너 버전
docker tag myapp:latest myapp:2 # 메이저 버전
docker tag myapp:latest myapp:stable # 안정화 채널
docker tag myapp:latest myapp:latest # 최신 (개발용)
# Git 커밋 해시 기반 태그 (CI/CD에서 많이 사용)
GIT_SHA=$(git rev-parse --short HEAD)
docker tag myapp:latest myapp:$GIT_SHA
Alpine vs slim vs 기본 이미지
공식 이미지는 보통 여러 변형(variant)을 제공합니다:
| 변형 | 특징 | 크기 |
|---|---|---|
python:3.11 | Debian 기반, 모든 패키지 포함 | ~900MB |
python:3.11-slim | Debian 기반, 최소 패키지 | ~125MB |
python:3.11-alpine | Alpine Linux, musl libc | ~50MB |
Alpine은 크기가 작지만 musl libc 사용으로 일부 Python 패키지 빌드에 문제가 생길 수 있습니다. 일반적으로 slim 변형이 크기와 호환성의 균형이 좋습니다.
목표
Docker Hub에서 다양한 이미지를 pull하고, 로컬 이미지를 효율적으로 관리하는 방법을 익힙니다.
이 실습은 Docker Hub와 로컬 이미지를 다루는 순수 명령어 실습입니다. 별도 파일 생성 없이 바로 진행할 수 있습니다.
실습
1단계: 이미지 검색
실습 전 디렉토리와 예제 파일을 먼저 준비합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part1/exam_3 && cd /tmp/docker/part1/exam_3
# 이미지 태그 목록 확인용 스크립트 생성
cat > list-tags.sh << 'EOF'
#!/bin/bash
IMAGE=${1:-nginx}
echo "=== Docker Hub 이미지 태그 목록: $IMAGE ==="
curl -s "https://registry.hub.docker.com/v2/repositories/library/${IMAGE}/tags/?page_size=10" \
| python3 -c "import sys,json; [print(t['name']) for t in json.load(sys.stdin)['results']]" 2>/dev/null \
|| echo "curl/python3 없는 환경에서는 docker search ${IMAGE} 사용"
EOF
chmod +x list-tags.sh
이제 실습을 진행합니다.
# Docker Hub에서 이미지 검색
docker search nginx
# NAME DESCRIPTION STARS OFFICIAL
# nginx Official build of Nginx. 19000+ [OK]
# unit Official build of NGINX Unit. 108 [OK]
# ...
# 공식 이미지만 필터링
docker search --filter is-official=true nginx
# 별점 100개 이상 이미지 필터링
docker search --filter stars=100 python
2단계: 이미지 pull
특정 버전 태그나 SHA256 다이제스트로 pull하면 동일한 이미지를 재현할 수 있습니다.
# 기본 pull (latest 태그)
docker pull ubuntu
# 특정 버전 태그로 pull
docker pull ubuntu:20.04
docker pull ubuntu:22.04
# 특정 다이제스트(SHA256)로 pull (가장 안전한 방법)
docker pull ubuntu@sha256:b6b83d3c331794420340093eb706a6f152d9c1be1f6377a2e1f7b9b9a1d6e6c2
# 멀티플랫폼 이미지: 특정 아키텍처 명시
docker pull --platform linux/arm64 nginx:1.25
3단계: 로컬 이미지 목록 확인
pull한 이미지는 로컬에 캐시됩니다. 태그, ID, 크기를 확인하고 필요 없는 이미지는 정리합니다.
# 기본 목록
docker images
# REPOSITORY TAG IMAGE ID CREATED SIZE
# ubuntu 22.04 c6b84b685f35 2 weeks ago 77.8MB
# ubuntu 20.04 1c5c8d0b973a 3 months ago 72.8MB
# nginx latest a6bd71f48f68 4 days ago 187MB
# 모든 이미지 (중간 레이어 포함)
docker images -a
# 특정 이미지만 필터링
docker images ubuntu
# 이미지 ID만 출력
docker images -q
# 포맷 지정 출력
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
4단계: 이미지 상세 정보 확인
# 전체 메타데이터 출력 (JSON)
docker image inspect ubuntu:22.04
# 레이어 구조만 확인
docker image inspect ubuntu:22.04 --format '{{json .RootFS.Layers}}' | python3 -m json.tool
# 환경 변수 확인
docker image inspect nginx --format '{{json .Config.Env}}'
# 노출 포트 확인
docker image inspect nginx --format '{{json .Config.ExposedPorts}}'
# 이미지 히스토리 (각 레이어 생성 명령어)
docker history nginx
# IMAGE CREATED CREATED BY SIZE
# a6bd71f48f68 4 days ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
# ...
5단계: 이미지 태그 추가
# 이미지에 새 태그 추가 (이미지 복사가 아닌 참조 추가)
docker tag ubuntu:22.04 my-ubuntu:latest
docker tag ubuntu:22.04 registry.mycompany.com/base/ubuntu:22.04
# 태그 확인
docker images ubuntu
docker images my-ubuntu
- pull 단계 먼저: docker pull 출력에서 "Already exists"와 "Pull complete"를 구분 — Already exists는 로컬에 이미 있는 공유 레이어, Pull complete는 새로 내려받은 레이어
- 레이어 압축률 판단: docker image inspect의 Size(바이트)와 실제 다운로드 크기를 비교 — 압축률이 원본 대비 50% 이하면 레이어 캐싱 효율 양호. 레이어가 5개 이상이고 총 크기가 1GB 넘으면 Dockerfile 최적화 검토 시점
- Already exists + 레이어 수 조합 해석: 동일 Alpine 기반 이미지 여러 개 pull 시 하위 레이어가 Already exists이면 → 디스크 공유 동작 중. 모두 Pull complete이면 → 베이스 이미지가 달라 공유 없음
push와 pull이 레지스트리와 주고받는 대화 — 인증부터 매니페스트 커밋까지
docker push myuser/app:1.0 한 줄에도 Layer already exists가 섞여 나오고, 마지막에 digest: sha256:...가 찍힙니다. docker pull은 이 대화를 거의 반대로 진행합니다. push/pull이 레지스트리와 어떤 단계로 주고받는지 알면 401과 403이 무엇이 다른지, 왜 코드 한 줄만 바꾼 재배포는 작은 레이어만 오가는지, 태그와 다이제스트가 어디서 갈리는지를 단계로 짚을 수 있습니다.
[내 PC] docker push myuser/app:1.0
│
① 로그인·토큰 획득 (docker login → 레지스트리가 저장소 push 토큰 발급)
│
② 준비: 매니페스트·레이어 목록 (로컬 이미지의 config + 레이어 digest 목록 산출)
│
③ 레이어 존재 확인 (각 레이어 digest를 레지스트리에 질의)
│ → 이미 있음: "Layer already exists" (업로드 생략)
│ → 없음: 다음 단계로
│
④ 없는 레이어만 업로드 (blob 업로드 → 레지스트리가 digest 재계산·검증)
│
⑤ 매니페스트 커밋 (config + 레이어 목록을 태그 이름으로 등록 → 매니페스트 digest 확정)
▼
[레지스트리] myuser/app:1.0 태그가 방금 만든 매니페스트 digest 를 가리킴
※ pull은 역방향: ⑤ 매니페스트 조회 → ③ 로컬에 없는 레이어 판별 → ④ 다운로드
각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① 로그인·토큰 | docker login으로 자격 증명을 넘기면 레지스트리가 해당 저장소용 토큰을 발급. 토큰에 read/write 범위가 담김 | 로그인 안 함·토큰 만료 → 401 Unauthorized (누구인지 증명 실패) |
| ② 준비 | 올릴 이미지의 config와 레이어 digest 목록을 만든다. 이름이 레지스트리/계정/저장소:태그 형식이어야 함 | 계정·레지스트리 주소 누락 → denied: requested access to the resource is denied |
| ③ 존재 확인 | 각 레이어 digest가 레지스트리에 이미 있는지 물어봐, 있으면 업로드를 건너뜀 | 같은 베이스를 쓰는 재배포는 대부분 Layer already exists — 바뀐 상위 레이어만 남아 전송량 급감 |
| ④ 업로드 | 없는 레이어 blob만 올리고, 레지스트리가 받은 내용의 digest를 다시 계산해 무결성 검증 | 인증은 됐지만 이 저장소 쓰기 권한 없음 → 403 Forbidden / 네트워크 끊김 → 업로드 재시도 |
| ⑤ 매니페스트 커밋 | 레이어가 다 올라간 뒤 config와 레이어 목록을 묶은 매니페스트를 태그로 등록. 이 매니페스트를 해시한 값이 이미지의 다이제스트 | 레이어 일부 누락 상태로 커밋 → manifest blob unknown |
핵심은 401과 403의 갈림입니다 — 401은 "누구인지 모름"(①의 로그인·토큰 문제), 403은 "누구인지는 알지만 이 저장소에 쓸 권한이 없음"(④의 권한 문제)입니다. 로그인부터 다시 할지, 권한을 받을지가 여기서 갈립니다. 태그 vs 다이제스트도 ⑤에서 갈립니다 — 태그(:1.0)는 매니페스트에 붙이는 가변 이름이라 같은 태그를 다시 push하면 새 매니페스트 digest를 가리키도록 바뀌지만, @sha256:... 다이제스트는 매니페스트 내용 그 자체의 해시라 언제나 같은 실체를 가리킵니다. 재배포가 빠른 이유는 ③에서 안 바뀐 레이어를 전부 건너뛰고 바뀐 상위 레이어와 작은 매니페스트만 오가기 때문이고, pull이 manifest unknown으로 실패하면 대칭적으로 ⑤(그 태그의 매니페스트가 레지스트리에 없음)를 의심하면 됩니다.
목표
Docker Hub 계정에 로그인하고, 로컬에서 빌드한 이미지를 Docker Hub에 push합니다.
사전 조건
- Docker Hub 계정 (hub.docker.com에서 무료 가입)
- 로컬에 빌드된 이미지
실습
1단계: Docker Hub 로그인
# 대화형 로그인
docker login
# 사용자명과 패스워드 직접 지정 (CI/CD에서 사용)
docker login -u myusername -p mypassword
# Access Token 사용 (패스워드 대신 권장)
# Docker Hub → Account Settings → Security → New Access Token
docker login -u myusername --password-stdin << EOF
dckr_pat_xxxxxxxxxxxxxxxxxxxx
EOF
# 프라이빗 레지스트리 로그인
docker login registry.mycompany.com
2단계: 간단한 이미지 빌드
# 테스트용 Dockerfile 생성
mkdir -p /tmp/myapp && cat > /tmp/myapp/Dockerfile << 'EOF'
FROM alpine:3.18
RUN echo "Hello from my custom image" > /hello.txt
CMD ["cat", "/hello.txt"]
EOF
# 이미지 빌드 (myusername을 실제 Docker Hub 계정명으로 교체)
docker build -t myusername/hello-docker:1.0 /tmp/myapp
3단계: 이미지 push
# Docker Hub에 push
docker push myusername/hello-docker:1.0
# 여러 태그 동시 push
docker tag myusername/hello-docker:1.0 myusername/hello-docker:latest
docker push myusername/hello-docker:latest
# push 결과 확인 (Docker Hub 웹사이트에서도 확인 가능)
# https://hub.docker.com/r/myusername/hello-docker
4단계: push한 이미지 확인
# 로컬 이미지 삭제
docker rmi myusername/hello-docker:1.0
docker rmi myusername/hello-docker:latest
# Docker Hub에서 다시 pull
docker pull myusername/hello-docker:1.0
# 실행 확인
docker run --rm myusername/hello-docker:1.0
# Hello from my custom image
5단계: 로그아웃
# Docker Hub 로그아웃
docker logout
# 프라이빗 레지스트리 로그아웃
docker logout registry.mycompany.com
목표
이미지 빌드와 업데이트 과정에서 쌓이는 불필요한 이미지를 정리하여 디스크 공간을 확보합니다.
Dangling 이미지란?
docker build를 반복 실행하면 이전 빌드의 이미지가 태그 없이 남습니다. 이런 이미지를 Dangling 이미지라고 합니다.
# 첫 번째 빌드
docker build -t myapp:latest .
# → myapp:latest 생성 (이미지 ID: abc123)
# 코드 수정 후 두 번째 빌드
docker build -t myapp:latest .
# → myapp:latest는 새 이미지 (ID: def456)를 가리킴
# → abc123 이미지는 태그 없이 남음 (Dangling)
# Dangling 이미지 확인
docker images -f dangling=true
# REPOSITORY TAG IMAGE ID CREATED SIZE
# <none> <none> abc123... 10 minutes ago 200MB
디스크 사용량 확인 및 정리
# Docker가 사용하는 디스크 공간 전체 확인
docker system df
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 10 3 2.5GB 1.8GB (72%)
# Containers 5 2 100MB 50MB (50%)
# Local Volumes 3 2 500MB 100MB (20%)
# Build Cache 30 0 800MB 800MB
# 상세 내용 확인
docker system df -v
# Dangling 이미지만 삭제
docker image prune
# 사용하지 않는 이미지 모두 삭제 (실행 중 컨테이너와 무관한 이미지)
docker image prune -a
# 개별 이미지 삭제
docker rmi nginx:1.24 # 특정 태그 삭제
docker rmi abc123def456 # 이미지 ID로 삭제
# 사용하지 않는 이미지 한 번에 삭제 (태그 없는 것만)
docker rmi $(docker images -f "dangling=true" -q)
Docker 리소스와 볼륨 일괄 정리
안전한 실행 조건: 실습 환경이며 삭제 대상 볼륨에 데이터베이스나 업로드 파일이 없음을 확인한 경우에만 실행합니다.
실행 전 반드시 확인
- docker system df -v로 삭제 범위를 확인했다
- docker volume ls로 보존해야 할 볼륨이 없는지 확인했다
- --volumes가 데이터 삭제를 포함한다는 점을 이해했다
docker system prune -a --volumes위 항목을 모두 확인한 후 복사할 수 있습니다
참조 중인 이미지 강제 삭제
안전한 실행 조건: 이미지 ID와 참조 컨테이너를 확인했고 다시 pull 또는 build 가능한 경우에만 실행합니다.
실행 전 반드시 확인
- docker ps -a --filter ancestor=abc123def456로 참조 컨테이너를 확인했다
- 삭제 대상 이미지가 운영 롤백에 필요한 태그가 아님을 확인했다
- 이미지를 다시 받을 수 있는 레지스트리 또는 Dockerfile이 있다
docker rmi -f abc123def456위 항목을 모두 확인한 후 복사할 수 있습니다
정기적인 정리 자동화
# cron으로 매일 새벽 3시에 정리 실행 (dangling 이미지만)
# crontab -e 에 추가:
0 3 * * * docker image prune -f >> /var/log/docker-prune.log 2>&1
# 또는 사용량 임계값 기반 정리 스크립트
#!/bin/bash
THRESHOLD=80 # 디스크 사용률 80% 초과 시 정리
USAGE=$(df /var/lib/docker | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USAGE" -gt "$THRESHOLD" ]; then
echo "디스크 사용률 ${USAGE}% — Docker 이미지 정리 시작"
docker image prune -af
fi
문제 상황
$ docker pull python:3.11
Error response from daemon: failed to register layer: Error processing tar file
(exit status 1): write /usr/lib/...: no space left on device
또는
$ docker build -t myapp .
error: failed to solve: failed to create symlink ...:
no space left on device
원인 분석
Docker 이미지, 컨테이너, 볼륨, 빌드 캐시는 기본적으로 /var/lib/docker에 저장됩니다. 이 경로가 속한 파티션의 공간이 부족하면 위 에러가 발생합니다.
진단
# 전체 디스크 사용량 확인
df -h /var/lib/docker
# Filesystem Size Used Avail Use% Mounted on
# /dev/sda1 50G 48G 1.2G 98% /
# Docker별 사용량 확인
docker system df
# 가장 큰 이미지 확인
docker images --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" | sort -rh | head -10
해결 방법 (긴급 순서대로)
1단계: 즉시 공간 확보
# 중지된 컨테이너 모두 삭제
docker container prune -f
# Dangling 이미지 삭제
docker image prune -f
# 전체 미사용 리소스 삭제
빌드 캐시 강제 삭제
안전한 실행 조건: 디스크 부족 대응 중이고 캐시를 재생성해도 되는 경우에만 실행합니다.
실행 전 반드시 확인
- docker system df로 Build Cache 용량을 확인했다
- 현재 진행 중인 빌드가 없음을 확인했다
- 다음 빌드에서 캐시 미스로 시간이 늘어날 수 있음을 이해했다
docker builder prune -af위 항목을 모두 확인한 후 복사할 수 있습니다
전체 미사용 Docker 리소스 강제 정리
안전한 실행 조건: 디스크 부족 대응 중이며 삭제 범위를 먼저 확인한 경우에만 실행합니다.
실행 전 반드시 확인
- docker system df로 Docker 디스크 사용량을 확인했다
- 실행 중인 컨테이너에서 사용하는 이미지는 삭제되지 않는다는 범위를 이해했다
- 필요한 로컬 빌드 이미지가 다시 빌드 또는 pull 가능하다
docker system prune -af위 항목을 모두 확인한 후 복사할 수 있습니다
2단계: Docker 데이터 경로 변경 (근본 해결)
파티션 자체가 작다면 더 큰 파티션으로 Docker 데이터를 이동합니다.
# Docker 데몬 설정 파일 편집
sudo nano /etc/docker/daemon.json
# 다음 내용 추가 (새 데이터 경로)
{
"data-root": "/data/docker"
}
# 새 디렉토리에 기존 데이터 복사
sudo systemctl stop docker
sudo mkdir -p /data/docker
sudo rsync -aP /var/lib/docker/ /data/docker/
# Docker 재시작
sudo systemctl start docker
# 확인
docker info | grep "Docker Root Dir"
3단계: .dockerignore로 빌드 컨텍스트 크기 감소
빌드 컨텍스트가 크면 Docker 데몬에 불필요한 파일까지 전송되어 시간과 공간을 낭비합니다. .dockerignore로 제외할 파일을 명시합니다.
# 빌드 컨텍스트 크기 확인 (build 시 "Sending build context" 메시지)
# Sending build context to Docker daemon 500MB ← 너무 크면 문제
# .dockerignore 파일 생성
cat > .dockerignore << 'EOF'
node_modules
.git
*.log
dist
coverage
.env
*.md
.DS_Store
EOF
문제 상황
$ docker pull myapp:production
Error response from daemon: manifest for myapp:production not found:
manifest unknown: manifest unknown
또는
$ docker pull mycompany/myapp:v2.5
Error response from daemon: pull access denied for mycompany/myapp,
repository does not exist or may require 'docker login'
원인 분석
| 에러 메시지 | 원인 |
|---|---|
manifest unknown | 지정한 태그가 레지스트리에 없음 |
pull access denied | 인증 필요(private 이미지) 또는 이미지 자체가 없음 |
repository does not exist | 이미지 이름 오타 |
해결 방법
태그 오타 확인
# Docker Hub에서 사용 가능한 태그 목록 확인 (API 사용)
curl -s "https://hub.docker.com/v2/repositories/library/python/tags/?page_size=20" \
| python3 -m json.tool | grep '"name"'
# 또는 웹 브라우저에서 확인
# https://hub.docker.com/_/python/tags
Private 레지스트리 인증
# 로그인 후 재시도
docker login registry.mycompany.com
docker pull registry.mycompany.com/myapp:v2.5
아키텍처 불일치 확인
# ARM64 호스트에서 AMD64 전용 이미지 pull 시도 시 에러
# --platform으로 명시적 지정
docker pull --platform linux/amd64 myapp:v2.5
로컬에서 이미지 존재 여부 확인
# 로컬 이미지 중 특정 이름 검색
docker images | grep myapp
# 이미지가 로컬에 있는지 확인 후 pull
docker image inspect myapp:v2.5 2>/dev/null && echo "로컬에 존재" || echo "pull 필요"
심화 — 레지스트리는 콘텐츠 주소 저장소다
심화: 하나의 태그가 여러 이미지를 가리킨다 — 매니페스트와 멀티아치
docker pull nginx가 하는 일을 '이미지 파일 하나를 통째로 내려받는 것'으로 이해하면, 아키텍처 불일치 같은 함정 앞에서 막힙니다. 레지스트리가 이미지를 어떻게 쪼개 보관하는지 보면 태그 뒤에 숨은 구조가 드러납니다.
- 이미지 = 매니페스트 + blob: 레지스트리는 콘텐츠 주소(content-addressable) 저장소입니다. 이미지는 매니페스트(설정과 레이어 다이제스트 목록)와 blob(실제 레이어 데이터)으로 나뉘어 저장됩니다.
pull은 매니페스트를 먼저 받고, 로컬에 없는 레이어 blob만 내려받습니다.push도 마찬가지라, 레지스트리에 이미 있는 레이어는Layer already exists로 건너뜁니다 — 그래서 코드 한 줄만 바꾼 재배포는 작은 상위 레이어만 오갑니다. - 태그는 매니페스트 리스트일 수 있다:
nginx같은 태그는 흔히 단일 이미지가 아니라 매니페스트 리스트(멀티아치 index) 를 가리킵니다. 그 안에linux/amd64,linux/arm64등 아키텍처별 이미지가 묶여 있고, pull하는 클라이언트가 자기 아키텍처에 맞는 매니페스트를 골라 받습니다. 그래서 같은 태그라도 x86 서버와 Apple Silicon 맥에서 다른 실체가 내려옵니다. - 다이제스트는 그 실체를 불변으로 고정한다: 태그는 사람이 붙인 가변 이름이라 같은 태그에 다른 이미지가 다시 push될 수 있지만,
@sha256:...다이제스트는 매니페스트 내용을 해시한 값이라 항상 정확히 그 실체를 가리킵니다. 멀티아치 태그를 다이제스트로 핀하면 특정 아키텍처의 특정 이미지까지 못 박을 수 있습니다.
이 구조를 알면 "push가 왜 빠른가", "같은 태그인데 왜 서버마다 다른 게 받아지나"가 한 번에 설명됩니다.
상황: Apple Silicon(arm64) 맥에서 docker build로 이미지를 만들어 레지스트리에 푸시했습니다. 그런데 amd64 운영 서버에서 그 이미지를 pull해 실행하니 exec format error가 나거나, 컨테이너가 아무 로그도 없이 즉시 종료됩니다. 맥에서는 잘 돌던 이미지라 더 당황스럽습니다.
원인: exec format error는 CPU 아키텍처가 맞지 않는 바이너리를 실행하려 할 때 커널이 내는 오류입니다. arm64 맥에서 아무 옵션 없이 빌드하면 arm64 전용 이미지가 만들어지는데, 이를 amd64 서버에서 실행하면 명령어 집합이 달라 바이너리가 돌지 않습니다. 태그는 하나였지만 그 안에 amd64용 이미지가 없었던 것입니다.
진단: docker inspect --format '{{.Architecture}} {{.Os}}' <image>로 이미지의 아키텍처를 확인합니다. arm64로 찍히는데 서버는 x86_64(uname -m)라면 불일치가 확정입니다. 레지스트리의 매니페스트를 docker buildx imagetools inspect <image>로 보면 어떤 플랫폼이 들어 있는지 목록으로 나옵니다.
해결: docker buildx build --platform linux/amd64,linux/arm64 --push로 멀티아치 이미지를 만들어 매니페스트 리스트로 푸시하면, 서버는 자기 아키텍처(amd64)에 맞는 이미지를 자동으로 받습니다. 배포 대상이 amd64 하나뿐이라면 최소한 --platform linux/amd64로 빌드해 아키텍처를 맞춥니다. 로컬에서 급히 확인만 할 때는 실행 측에서 --platform으로 강제할 수 있지만, 이 경우 QEMU 에뮬레이션으로 느려질 수 있으니 운영 이미지는 대상 아키텍처로 실제 빌드하는 것이 원칙입니다.
엔터프라이즈 이미지 관리 체계
현업에서는 Docker Hub 공개 이미지를 그대로 프로덕션에서 사용하지 않고, 조직 내부 레지스트리를 통해 관리합니다.
프라이빗 레지스트리 구성
많은 기업들이 다음 중 하나를 사용합니다:
| 레지스트리 | 특징 | 적합 환경 |
|---|---|---|
| Docker Hub (private) | 가장 간단 | 소규모 팀 |
| AWS ECR | AWS 통합 | AWS 인프라 |
| Google Artifact Registry | GCP 통합 | GCP 인프라 |
| Harbor | 오픈소스 자체 호스팅 | 온프레미스 |
| GitLab Container Registry | GitLab 통합 | GitLab 사용 조직 |
이미지 보안 스캐닝
외부 이미지를 프로덕션에 그대로 올리면 알려진 CVE를 함께 배포하는 셈입니다. 빌드 단계에서 취약점을 자동으로 검사합니다.
# Docker Scout (Docker 공식 보안 스캐닝)
docker scout cves nginx:1.25.3
# Trivy (오픈소스 취약점 스캐너)
trivy image nginx:1.25.3
# 취약점 발견 시 자동으로 빌드 실패시키는 CI/CD 설정 예시
# .github/workflows/security.yml
이미지 라이프사이클 정책
실제 팀에서는 이미지 버전을 언제 삭제할지 정책을 수립합니다:
CI/CD에서의 이미지 빌드와 push
코드가 main 브랜치에 머지될 때마다 이미지를 자동으로 빌드하고 레지스트리에 올리는 것이 표준 패턴입니다. 커밋 SHA를 태그로 쓰면 배포 이력을 그대로 추적할 수 있습니다.
# GitHub Actions 예시: 코드 push 시 이미지 빌드 및 push
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push
run: |
IMAGE_TAG=${{ github.sha }}
docker build -t $ECR_REGISTRY/myapp:$IMAGE_TAG .
docker push $ECR_REGISTRY/myapp:$IMAGE_TAG
# main 브랜치는 latest도 함께 태그
docker tag $ECR_REGISTRY/myapp:$IMAGE_TAG $ECR_REGISTRY/myapp:latest
docker push $ECR_REGISTRY/myapp:latest
이러한 전략들은 애플리케이션 배포의 안정성과 재현성을 보장하며, 보안 취약점을 조기에 발견할 수 있게 합니다.
이미지 이름의 완전한 구조 — registry/namespace/repo:tag@digest
로컬에서 docker build -t myapp:1.0 .으로 이미지를 만든 뒤 docker push myapp:1.0을 실행하면 "denied: access forbidden" 에러가 납니다. 회사 ECR이나 사설 레지스트리에 올리려면 이미지 이름 앞에 레지스트리 주소가 포함되어야 하는데, myapp:1.0은 Docker Hub의 개인 계정을 가정하기 때문입니다. 또 nginx라는 이름은 실제로는 docker.io/library/nginx:latest가 생략된 형태이고, AWS ECR 이미지는 123456.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:latest처럼 보입니다. 이 규칙을 모르면 push 실패, pull 실패, 이미지 이름 혼동이 반복됩니다. 이 ConceptBlock에서는 이미지 이름의 전체 구성과 다이제스트로 불변성을 보장하는 방법을 다룹니다.
확대
이미지 이름의 전체 형식
Docker 이미지 이름은 여러 부분으로 구성되며, 생략 시 기본값이 채워집니다.
전체 형식:
[레지스트리호스트[:포트]/][네임스페이스/]저장소[:태그][@다이제스트]
예시:
docker.io/library/nginx:1.25@sha256:abc123...def456
↑ ↑ ↑ ↑ ↑
레지스트리 네임스페이스 저장소 태그 다이제스트
# 생략 시 기본값 채우기
nginx = docker.io/library/nginx:latest
nginx:alpine = docker.io/library/nginx:alpine
myuser/myapp = docker.io/myuser/myapp:latest
myuser/myapp:v1.0 = docker.io/myuser/myapp:v1.0
# 다른 레지스트리 사용
ghcr.io/myorg/myapp:latest # GitHub Container Registry
123456.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:latest # AWS ECR
asia-northeast1-docker.pkg.dev/project/repo/myapp:latest # GCP Artifact Registry
myregistry.internal:5000/myapp:v2 # 사설 레지스트리
태그 vs 다이제스트 — 가변 vs 불변
태그는 편하지만 언제든 덮어쓸 수 있어 재현성이 깨질 수 있습니다. 다이제스트는 SHA256 해시이므로 항상 동일한 이미지를 가리킵니다.
# 태그: 가변 (같은 태그가 언제든 다른 이미지를 가리킬 수 있음)
docker pull nginx:latest
# 오늘의 latest와 내일의 latest가 다를 수 있음!
# 다이제스트: 불변 (SHA256 해시, 항상 동일한 이미지 보장)
docker pull nginx@sha256:a0bcfe...8d12c
# 언제 어디서 pull해도 완전히 동일한 이미지
# 현재 이미지의 다이제스트 확인
docker image inspect nginx:latest --format '{{.RepoDigests}}'
# [nginx@sha256:a0bcfe...8d12c]
# 다이제스트로 pull (프로덕션 환경 권장)
docker pull nginx@sha256:a0bcfe...8d12c
사설 레지스트리 이미지 관리
로컬에서 빌드한 이미지를 사설 레지스트리에 push할 때는 이미지 이름에 레지스트리 주소를 포함한 태그를 먼저 추가합니다.
# 로컬에서 빌드한 이미지를 사설 레지스트리에 push
# 1. 이미지에 레지스트리 주소 포함한 태그 추가
docker tag myapp:latest myregistry.example.com:5000/myapp:latest
docker tag myapp:latest myregistry.example.com:5000/myapp:v1.2.3
# 2. 레지스트리에 push
docker push myregistry.example.com:5000/myapp:latest
docker push myregistry.example.com:5000/myapp:v1.2.3
# 3. 다른 서버에서 pull
docker pull myregistry.example.com:5000/myapp:v1.2.3
# 4. 이미지 목록 API 확인
curl http://myregistry.example.com:5000/v2/_catalog
# {"repositories":["myapp","nginx","redis"]}
curl http://myregistry.example.com:5000/v2/myapp/tags/list
# {"name":"myapp","tags":["latest","v1.2.3","v1.2.2"]}
이미지 정리와 디스크 관리 — docker system df와 prune
CI 빌드 서버를 3개월째 운영하다 보면 어느 날 아침 파이프라인 전체가 멈춥니다. docker build 도중 "no space left on device" 에러가 납니다. 원인은 매번 빌드할 때마다 생성되는 dangling 이미지가 아무도 정리하지 않은 채 쌓인 것입니다. 개발 노트북에서도 Docker를 몇 달 쓰면 디스크 수 GB가 Docker 이미지와 빌드 캐시로 채워집니다. docker system df를 처음 실행했을 때 "회수 가능 공간 3.1GB"가 표시되면 당황하기 마련입니다. 정리 명령어는 여러 종류가 있고 영향 범위가 다르므로, 순서와 범위를 알고 써야 실수로 운영 데이터를 지우는 사고를 막을 수 있습니다. 이 ConceptBlock에서는 Docker 디스크 사용량을 파악하고 안전하게 정리하는 방법을 다룹니다.
확대
디스크 사용량 파악
Docker를 오래 사용하면 이미지, 컨테이너, 볼륨, 빌드 캐시가 쌓여 디스크를 차지합니다.
# Docker 전체 디스크 사용량 확인
docker system df
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 12 3 4.2GB 3.1GB (73%) ← 회수 가능
# Containers 5 2 12MB 1kB (0%)
# Local Volumes 8 3 2.1GB 800MB (38%)
# Build Cache 23 0 1.5GB 1.5GB ← 전부 회수 가능
# 상세 목록
docker system df -v
dangling 이미지가 생기는 이유
# 같은 태그로 재빌드하면 이전 이미지가 <none>:<none> 으로 남음
docker build -t myapp:latest . # 첫 번째 빌드 → abc123
docker build -t myapp:latest . # 재빌드 → def456 (새 이미지)
# abc123은 태그를 잃고 <none>:<none>으로 전락
docker images
# REPOSITORY TAG IMAGE ID SIZE
# myapp latest def456 200MB ← 현재 이미지
# <none> <none> abc123 200MB ← dangling 이미지!
단계별 정리 방법
영향 범위가 작은 것부터 순서대로 실행합니다. 운영 환경에서는 prune -a나 --volumes 사용 전에 반드시 어떤 데이터가 삭제되는지 확인해야 합니다.
# 1. dangling 이미지만 삭제 (가장 안전)
docker image prune
# WARNING! This will remove all dangling images.
# 삭제할 공간: 1.2GB
# 2. 사용 중이지 않은 이미지 전체 삭제
docker image prune -a
# 실행 중인 컨테이너에서 사용 중인 이미지는 삭제 안 됨
# 3. 미사용 컨테이너 삭제
docker container prune
# 4. 미사용 볼륨 삭제 (주의: 데이터 영구 삭제!)
# 5. 한 번에 전체 정리
볼륨 포함 Docker 전체 정리
안전한 실행 조건: 삭제 대상이 실습/임시 리소스뿐이고 볼륨 백업이 끝난 경우에만 실행합니다.
실행 전 반드시 확인
- docker volume ls -f dangling=true로 삭제 후보 볼륨을 확인했다
- 볼륨 이름과 용도를 확인했다
- 보존할 데이터의 백업 또는 재생성 방법이 있다
docker system prune -a --volumes위 항목을 모두 확인한 후 복사할 수 있습니다
자동 정리 설정
--filter 옵션으로 기간이나 라벨을 지정하면 원하는 범위만 정리할 수 있습니다. CI 빌드 서버에는 cron으로 등록해 두면 디스크가 차는 것을 예방할 수 있습니다.
# 특정 날짜 이전 이미지만 삭제
docker image prune -a --filter "until=72h" # 72시간 이상 미사용
# 특정 라벨이 있는 이미지만 삭제
docker image prune --filter "label=stage=dev"
# cron으로 주기적 자동 정리 (매주 일요일 새벽 3시)
정리
핵심 명령어 요약
이 모듈에서 다룬 이미지 관련 명령어를 한 곳에 정리합니다.
# 이미지 pull
docker pull ubuntu:22.04
# 로컬 이미지 목록
docker images
docker images -a # 중간 레이어 포함
# 이미지 상세 확인
docker image inspect ubuntu:22.04
docker history nginx
# 태그 추가
docker tag myapp:1.0 registry.example.com/myapp:1.0
# Docker Hub 로그인/push
docker login
docker push myuser/myapp:1.0
# 이미지 삭제 및 정리
docker rmi nginx:old
docker image prune # dangling만
docker image prune -a # 미사용 전체
docker system df # 디스크 사용량 확인
명령어·단축키 빠른 참조
이 모듈에서 다룬 이미지 pull·검사·태그·정리 명령을 실전 옵션과 함께 모았습니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
docker pull | 태그·다이제스트·플랫폼 지정 내려받기 | docker pull ubuntu:22.04 / docker pull --platform linux/arm64 nginx:1.25 |
docker search | Docker Hub 이미지 검색·필터 | docker search --filter is-official=true nginx / --filter stars=100 python |
docker images | 로컬 이미지 목록·필터 | docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" / -f dangling=true |
docker image inspect | 레이어·환경·다이제스트 확인 | docker image inspect nginx --format '{{.RepoDigests}}' |
docker history | 레이어별 생성 명령·크기 보기 | docker history nginx |
docker tag | 레지스트리 주소 포함 태그 부여 | docker tag myapp:1.0 registry.example.com/myapp:1.0 |
docker login / push | 인증 후 이미지 업로드 | echo "$PW" | docker login -u user --password-stdin → docker push user/app:1.0 |
docker system df | Docker 디스크 사용·회수 가능량 | docker system df -v |
docker image prune | dangling/미사용 이미지 정리 | docker image prune / docker image prune -a --filter "until=72h" |
docker rmi | 특정 이미지·태그 삭제 | docker rmi nginx:old / docker rmi $(docker images -f dangling=true -q) |
docker info | 스토리지 드라이버·데이터 경로 확인 | docker info | grep "Storage Driver" |
관련 모듈로 더 깊이:
- 백엔드 애플리케이션의 컨테이너 이미지 빌드와 작성 요령 — pull한 이미지를 넘어 FROM/RUN/COPY로 직접 이미지를 빌드하는 법
- 포트 바인딩 오류 방지와 컨테이너의 핵심 상태 변화 — 이미지에서 컨테이너가 생성·실행되는 상태 전환의 큰 그림
- 사내 프라이빗 레지스트리 구축과 안전한 이미지 관리 방법 — Docker Hub를 넘어 사내 프라이빗 레지스트리를 운영·인증하는 법
다음 챕터 예고
다음 모듈에서는 직접 Dockerfile을 작성하여 애플리케이션 이미지를 빌드하는 방법을 학습합니다. FROM, RUN, COPY, CMD 등 핵심 명령어와 레이어 캐시 최적화를 통해 빠르고 작은 이미지를 만드는 기술을 다룹니다.