Learning Platform
Глоссарий Troubleshooting
Урок 07.02 · 22 мин
Средний
dockerhistorydiveimage-sizedebug

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}}"
TIP

Колонка 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-slim145MB
RUN apt-get install -y build-essential libpq-dev curl wget git312MB
RUN pip install pandas numpy scipy scikit-learn psycopg2285MB
RUN curl ... && tar xzf45MB
COPY . /app25MB

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.

Image efficiency: что съедает место
Типичные источники wasted space в DE-образах
apt build deps200-400MBbuild-essential, libpq-dev, gcc -- нужны для компиляции, не для runtime. В multi-stage идут только в builder, в runtime -- runtime-only deps.
pip cache50-150MB~/.cache/pip/ -- скачанные wheel'ы для возможной переустановки. В Docker это бесполезно (caching через --mount=type=cache). Решение: pip install --no-cache-dir.
apt lists / archives20-50MB/var/lib/apt/lists и /var/cache/apt/archives. apt-get update создаёт списки, install качает .deb. После -- rm -rf обоих директорий.
git / build artifacts100MB+Если COPY . без .dockerignore -- попадает .git, node_modules, __pycache__, .venv, тесты, фикстуры. Использовать .dockerignore (см. урок 06.05).

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, потом оптимизируй и сравни.


Проверка знанийKnowledge check
В docker history образа есть слой с RUN curl -O huge-dataset.tar.gz, размер 500MB. В следующем слое RUN rm huge-dataset.tar.gz, размер 0B (но whiteout). Какой будет реальный размер образа и что показывает dive в Wasted Space?
ОтветAnswer
Образ будет включать все 500MB слоя curl, потому что слой создаётся ПОСЛЕ выполнения инструкции -- 500MB файла попадает в snapshot слоя, и удаление в следующем слое создаёт только whiteout-маркер, не вычитая данные. Dive покажет wasted space ~500MB и низкий efficiency score (10-20%). Решение: объединить curl и rm в один RUN (RUN curl -O ... && tar xzf ... && rm ...) -- тогда snapshot создаётся ПОСЛЕ rm и архив в образ не попадает.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что показывает колонка SIZE в выводе docker history image:tag?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4