Image history и dive: кто съел место в образе
Сценарий, который Data Engineer встречает регулярно: коллега собрал ETL-образ для Airflow worker’а, образ получился 1.2 GB, deployment теперь pull’ит его 4 минуты при каждом scale-out. Вопрос: где эти гигабайты? docker images показывает только итоговый размер. Чтобы разобраться, нужно копнуть в историю образа и посмотреть, какой слой сколько весит. Для этого есть две команды и один must-have инструмент.
В этом уроке: docker history, инструмент dive, реальный кейс «800MB образ — где 600MB?».
df и du: сколько диска и где именно
docker history: история создания образа
docker history <image> показывает все слои образа с метаданными:
$ docker history python:3.13-slim
IMAGE CREATED CREATED BY SIZE COMMENT
b6f1a3c9d7e2 2 weeks ago CMD ["python3"] 0B buildkit.dockerfile.v0
<missing> 2 weeks ago ENTRYPOINT [] 0B buildkit.dockerfile.v0
<missing> 2 weeks ago RUN /bin/sh -c set -eux; savedAptMark="$(apt 3.21MB buildkit.dockerfile.v0
<missing> 2 weeks ago ENV PYTHON_SHA256=abc... 0B buildkit.dockerfile.v0
<missing> 2 weeks ago ENV PYTHON_VERSION=3.13.1 0B buildkit.dockerfile.v0
<missing> 2 weeks ago ENV LANG=C.UTF-8 0B buildkit.dockerfile.v0
<missing> 2 weeks ago ENV PATH=/usr/local/bin:... 0B buildkit.dockerfile.v0
<missing> 2 weeks ago RUN /bin/sh -c set -eux; apt-get update; ap 7.91MB buildkit.dockerfile.v0
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:abc... in / 75.5MB
Колонки:
- IMAGE — image ID слоя. Только верхний слой имеет «настоящий» ID (полный образ), остальные показаны как
<missing>(это нормально — Docker не хранит intermediate IDs для слоёв в registry). - CREATED BY — команда из Dockerfile, создавшая слой. По ней понятно, что было сделано.
- SIZE — размер именно этого слоя (diff относительно предыдущего). 0B означает что слой содержит только метаданные (ENV, CMD, ENTRYPOINT, EXPOSE, LABEL — не модифицируют файловую систему).
Сумма SIZE = размер образа. Если в одном слое много мегабайт, это и есть кандидат на оптимизацию.
# Полный текст команд (без обрезания)
docker history python:3.13-slim --no-trunc
# Только размеры (для быстрого скана)
docker history python:3.13-slim --format "{{.Size}}\t{{.CreatedBy}}"
Колонка SIZE показывает РАЗМЕР СЛОЯ, не накопительный размер. Если слой добавил 50MB и тут же удалил 40MB через whiteout, SIZE этого слоя будет ~50MB (whiteout-маркер не вычитает данные из lower-слоёв). Чтобы увидеть «эффективный» вклад, нужен dive.
Инструмент dive: интерактивный анализ слоёв
dive (github.com/wagoodman/dive) — must-have для DE, который собирает свои образы. Это TUI (terminal UI), который показывает каждый слой и его содержимое как файловое дерево с размерами.
Установка:
# macOS
brew install dive
# Linux
wget https://github.com/wagoodman/dive/releases/download/v0.13.1/dive_0.13.1_linux_amd64.deb
sudo dpkg -i dive_0.13.1_linux_amd64.deb
# Docker (no install)
alias dive="docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive"
Использование:
dive python:3.13-slim
Откроется UI с двумя панелями:
- Слева: список слоёв с командами Dockerfile и размером.
- Справа: файловое дерево для выбранного слоя. Можно фильтровать (Ctrl+F), переключать вид (Ctrl+A — показать все файлы / только изменённые в этом слое).
Самое полезное: Image Efficiency Score (внизу) и Wasted Space (показывает суммарный размер файлов, которые попали в слой и были удалены в верхних слоях через whiteout, то есть лежат впустую в образе).
│ Image Details │
├──────────────────────────────────────────────────────────────────────────┤
│ Total Image size: 145 MB │
│ Potential wasted space: 23 MB │
│ Image efficiency score: 87 % │
│ │
│ Wasted file list: │
│ /var/cache/apt/archives 18 MB (3 layers) │
│ /var/lib/apt/lists 4 MB (2 layers) │
│ /tmp/build 1 MB (1 layer) │
Efficiency score 90%+ — хорошо. Меньше 80% — есть что оптимизировать (либо неудачные RUN-объединения, либо забытые tmp-файлы).
Реальный кейс: 800MB образ — где 600MB?
Возьмём типичный «жирный» ETL-образ:
# Dockerfile
FROM python:3.13-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
curl \
wget \
git
RUN pip install pandas numpy scipy scikit-learn psycopg2
RUN curl -O https://data.example.com/reference-data.tar.gz && \
tar xzf reference-data.tar.gz
COPY . /app
CMD ["python", "etl.py"]
После сборки:
$ docker build -t etl:v1 .
$ docker images etl
REPOSITORY TAG SIZE
etl v1 812MB
812MB — много. Запускаем dive:
$ dive etl:v1
Видим слои:
| Команда | Размер слоя |
|---|---|
FROM python:3.13-slim | 145MB |
RUN apt-get install -y build-essential libpq-dev curl wget git | 312MB |
RUN pip install pandas numpy scipy scikit-learn psycopg2 | 285MB |
RUN curl ... && tar xzf | 45MB |
COPY . /app | 25MB |
312MB на build-essential — это gcc, g++, make, libc6-dev — нужно для компиляции psycopg2, но не нужно в runtime. После установки pip-пакетов можно было бы apt-get remove -y build-essential — но это бы помогло только если бы было в том же RUN’е (иначе слой 2 уже содержит 312MB файлов, удаление в слое 3 это whiteout).
Решение: разбить на multi-stage build (модуль 8).
Альтернатива без multi-stage — объединить RUN с очисткой:
RUN apt-get update && apt-get install -y \
build-essential libpq-dev curl wget git && \
pip install --no-cache-dir pandas numpy scipy scikit-learn psycopg2 && \
apt-get purge -y build-essential && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
После пересборки:
$ docker images etl
REPOSITORY TAG SIZE
etl v2 412MB # -400MB
Дальше можно ещё оптимизировать через multi-stage (build-stage + runtime-stage), но это уже модуль 8.
docker inspect для быстрых вопросов
Если нужно ответить на одну конкретную вопрос «сколько весит образ», docker inspect:
$ docker inspect etl:v1 --format '{{.Size}}'
812347291
# В человеко-читаемом виде
$ docker images etl:v1 --format '{{.Size}}'
812MB
# Сравнить размеры всех образов
$ docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | sort -k3 -h
Для подсчёта «эффективного» размера (без shared слоёв с другими образами) используется docker system df --verbose:
$ docker system df --verbose
Images space usage:
REPOSITORY TAG IMAGE ID CREATED SIZE SHARED SIZE UNIQUE SIZE
python 3.13-slim abc123 2 weeks ago 145MB 0B 145MB
etl v1 def456 1 hour ago 812MB 145MB 667MB
etl v2 789abc 5 min ago 412MB 145MB 267MB
SHARED SIZE — это слои, шаренные с другими образами. UNIQUE SIZE — только то, что добавил конкретный образ. На диске занимает SHARED + sum(UNIQUE).
Анализ образа без локального pull
Для анализа образа из registry, не скачивая его, есть инструменты:
skopeo— копирование образов между registries + inspect.skopeo inspect docker://ghcr.io/org/etl:v1показывает manifest и slice info.crane— Google’s tool.crane manifest ghcr.io/org/etl:v1 | jqпоказывает структуру.docker manifest inspect— встроенный, показывает manifest:
docker manifest inspect python:3.13-slim
# JSON со списком слоёв и их размерами
Эти инструменты полезны в CI: можно проверить размер образа после push без локального pull.
Попробуй сам
Найди жирный слой в любом локальном образе:
# Скачаем пример
docker pull python:3.13-slim
# Посмотрим историю
docker history python:3.13-slim
# Топ-3 слоёв по размеру
docker history python:3.13-slim --format "{{.Size}}\t{{.CreatedBy}}" | \
sort -h -r | head -3
# Установим dive (если ещё нет)
brew install dive # macOS
# или
alias dive="docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive"
# Запустим интерактивный анализ
dive python:3.13-slim
# Внутри dive: стрелки -- навигация по слоям, Ctrl+F -- фильтр, Ctrl+A -- toggle aggregate view
# Quit: Q или Ctrl+C
Поэкспериментируй: собери свой образ с лишним содержимым (RUN apt install -y build-essential), проверь efficiency score, потом оптимизируй и сравни.