Главная/Dockerfile/Урок

5.5. Build cache

Цели

После этого материала вы сможете:

  • назвать правило инвалидации кэша для каждой инструкции;
  • упорядочить инструкции так, чтобы изменение кода не пересобирало зависимости;
  • измерить эффект оптимизации и доказать его числами;
  • прочитать вывод сборки и определить, какая инструкция сломала кэш;
  • объяснить, почему кэш иногда не срабатывает при внешне одинаковых условиях;
  • осознанно использовать --no-cache и точечную инвалидацию.

Предварительные знания

Ключевые термины

ТерминОбъяснение
cache keyИдентификатор, по которому BuildKit ищет готовый результат инструкции
инвалидацияСитуация, когда кэш не подходит и инструкция выполняется заново
каскадная инвалидацияПересборка всех инструкций после изменившейся
cache hit / cache missПопадание и промах кэша
CACHEDМетка в выводе сборки, означающая использование кэша

Теория

Как работает кэш

Для каждой инструкции BuildKit вычисляет cache key и ищет результат с таким ключом. Если находит — инструкция не выполняется, берётся готовый слой.

Ключевое свойство: cache key включает ключ предыдущей инструкции. Отсюда каскадная инвалидация — если сломался кэш на шаге 3, шаги 4, 5 и все последующие тоже выполнятся заново, даже если сами не изменились.

text
   FROM python:3.13-slim      ключ = хэш(базовый образ)              ✓ CACHED
   WORKDIR /app               ключ = хэш(предыдущий + "WORKDIR /app") ✓ CACHED
   COPY requirements.txt .    ключ = хэш(предыдущий + содержимое)     ✓ CACHED
   RUN pip install ...        ключ = хэш(предыдущий + текст команды)  ✓ CACHED
   COPY . .                   ключ = хэш(предыдущий + содержимое)     ✗ изменился код
   CMD [...]                  ключ = хэш(предыдущий + ...)            ✗ каскад

Из этого следует главный принцип оптимизации: редко меняющееся размещайте раньше, часто меняющееся — позже.

Правила инвалидации по инструкциям

ИнструкцияЧто входит в cache key
FROMDigest базового образа. Смена тега на тот же образ кэш не ломает
RUNТолько текст команды. Содержимое скачиваемых файлов не учитывается
COPY, ADDСодержимое файлов, права доступа, владелец; путь назначения
ENV, ARG, LABEL, WORKDIR, USER, EXPOSEТекст инструкции
ARG (использованный)Значение аргумента — если он подставляется в инструкцию

Строка про RUN — источник самой частой ошибки в понимании кэша.

Почему RUN кэшируется по тексту команды

BuildKit не знает, что делает команда. Он сравнивает только строку.

dockerfile
RUN pip install -r requirements.txt

Если строка не изменилась, инструкция берётся из кэша — независимо от того, что изменился requirements.txt. Кэш сломается только потому, что предыдущая инструкция COPY requirements.txt . заметила изменение файла и инвалидировалась каскадно.

Уберите COPY перед RUN — и обновление зависимостей перестанет применяться:

dockerfile
# ОШИБКА: pip install закэширован навсегда
RUN pip install fastapi==0.141.1

При смене версии в этой строке кэш сломается. А вот RUN apt-get upgrade или RUN pip install -r /mnt/requirements.txt (через монтирование) закэшируются и будут возвращать устаревший результат месяцами.

Отсюда практическое правило: если результат RUN зависит от внешних данных, эта зависимость должна быть выражена через COPY или через ключ кэша явно.

Каскадная инвалидация на практике

Типичный неоптимальный Dockerfile:

dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY . .                                    ← изменение ЛЮБОГО файла
RUN pip install -r requirements.txt         ← пересобирается всегда
CMD ["python", "app.py"]

Изменили одну строку в app.py — переустанавливаются все зависимости.

Оптимальный вариант:

dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .                     ← меняется редко
RUN pip install -r requirements.txt         ← кэшируется
COPY . .                                    ← меняется часто
CMD ["python", "app.py"]

Теперь изменение кода инвалидирует только последние две инструкции.

Приём называется «разделение по частоте изменений» и применяется во всех языках: package.json до npm install, go.mod до go mod download, pyproject.toml до установки зависимостей.

Что ломает кэш неожиданно

ПричинаПочему
Изменились права доступа файлаВходят в cache key COPY
Изменился владелец файлаТо же
Новый файл в контексте под COPY . .Меняется содержимое копируемого набора
Изменилось значение ARG, используемого в инструкцииВходит в ключ
Обновился базовый образМеняется его digest
Другая машина, пустой кэшЛокальный кэш не переносится — см. раздел 16
Изменился порядок строк в DockerfileКлюч включает текст инструкции

Первые две — частая причина «на моей машине кэш работает, а в CI нет»: git clone выставляет одинаковые права, а локально они могли измениться.

Многострочные инструкции и кэш

dockerfile
RUN apt-get update \
 && apt-get install -y curl \
 && rm -rf /var/lib/apt/lists/*

Cache key считается по всему тексту. Добавление одного пакета инвалидирует инструкцию целиком — и это правильно: иначе получился бы cache busting (урок 5.3).

Отсюда компромисс: объединение команд экономит слои и предотвращает устаревание, но делает инвалидацию грубее. Для долгих операций (компиляция) имеет смысл выделять стабильную часть в отдельную инструкцию.


Внутренний механизм

Чем BuildKit отличается от legacy builder

Legacy builder проверял кэш линейно: шёл по инструкциям сверху вниз и останавливался на первом промахе.

BuildKit строит граф зависимостей. Из этого следуют два практических отличия:

Параллельная сборка. Независимые стадии multi-stage собираются одновременно.

Пропуск ненужного. Если стадия не используется в финальном образе, она не собирается вовсе.

Проверить, что используется BuildKit:

bash
docker build --help | grep -q 'buildx' && echo "BuildKit"

С Docker Engine 23.0 BuildKit — сборщик по умолчанию, legacy builder объявлен устаревшим.

Где хранится кэш

Кэш BuildKit хранится отдельно от образов:

bash
docker system df
text
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Build Cache     183       0         4.271GB   4.271GB

Он не удаляется командой docker image prune и часто оказывается главным потребителем места (урок 3.4).


Команды и примеры

Подготовка

bash
mkdir -p /tmp/cache-demo/app && cd /tmp/cache-demo

cat > requirements.txt <<'EOF'
fastapi==0.141.1
uvicorn[standard]==0.52.0
pydantic==2.13.4
httpx==0.28.1
EOF

cat > app/main.py <<'PY'
print("версия 1")
PY

cat > .dockerignore <<'EOF'
Dockerfile*
.dockerignore
__pycache__
*.pyc
EOF

Плохой порядок инструкций

bash
cat > Dockerfile.bad <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "app/main.py"]
EOF

echo "--- первая сборка (наполняем кэш) ---"
time docker build -q -f Dockerfile.bad -t cache:bad . > /dev/null
text
real	0m28.412s

Теперь изменим одну строку кода:

bash
echo 'print("версия 2")' > app/main.py

echo "--- пересборка после изменения кода ---"
time docker build -f Dockerfile.bad -t cache:bad . 2>&1 | grep -E 'CACHED|\[[0-9]/[0-9]\]'
text
 => CACHED [2/4] WORKDIR /app
 => [3/4] COPY . .
 => [4/4] RUN pip install --no-cache-dir -r requirements.txt

real	0m24.183s

24 секунды на изменение одной строки. Зависимости переустановлены заново, хотя requirements.txt не менялся.

Хороший порядок

bash
cat > Dockerfile.good <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
CMD ["python", "app/main.py"]
EOF

echo "--- первая сборка ---"
time docker build -q -f Dockerfile.good -t cache:good . > /dev/null

echo 'print("версия 3")' > app/main.py

echo "--- пересборка после изменения кода ---"
time docker build -f Dockerfile.good -t cache:good . 2>&1 | grep -E 'CACHED|\[[0-9]/[0-9]\]'
text
--- первая сборка ---
real	0m27.914s
--- пересборка после изменения кода ---
 => CACHED [2/5] WORKDIR /app
 => CACHED [3/5] COPY requirements.txt .
 => CACHED [4/5] RUN pip install --no-cache-dir -r requirements.txt
 => [5/5] COPY app/ ./app/

real	0m0.612s

0.6 секунды против 24. Разница в 40 раз, достигнутая перестановкой двух строк.

Установка зависимостей взята из кэша, потому что requirements.txt не изменился, а инструкция COPY app/ идёт после неё.

Проверка, что изменение зависимостей всё ещё применяется

Оптимизация не должна ломать обновление:

bash
echo "python-multipart==0.0.20" >> requirements.txt

docker build -f Dockerfile.good -t cache:good . 2>&1 | grep -E 'CACHED|RUN pip'
text
 => CACHED [2/5] WORKDIR /app
 => [3/5] COPY requirements.txt .
 => [4/5] RUN pip install --no-cache-dir -r requirements.txt

COPY requirements.txt заметил изменение и каскадно инвалидировал установку. Обновление применилось.

Убедимся:

bash
docker run --rm cache:good python -c "import multipart; print('пакет установлен')"
text
пакет установлен

RUN кэшируется по тексту команды

Демонстрация того, почему это важно.

bash
mkdir -p /tmp/run-cache && cd /tmp/run-cache

cat > Dockerfile.date <<'EOF'
FROM alpine:3.21
RUN date +%s%N > /build-time.txt
CMD ["cat", "/build-time.txt"]
EOF

docker build -q -f Dockerfile.date -t rc:1 . > /dev/null
FIRST="$(docker run --rm rc:1)"
sleep 2
docker build -q -f Dockerfile.date -t rc:1 . > /dev/null
SECOND="$(docker run --rm rc:1)"

echo "первая сборка:  $FIRST"
echo "вторая сборка:  $SECOND"
[ "$FIRST" = "$SECOND" ] && echo "→ значения совпали: инструкция взята из кэша"
text
первая сборка:  1785481203847291000
вторая сборка:  1785481203847291000
→ значения совпали: инструкция взята из кэша

Команда date возвращает разное значение при каждом запуске, но она не выполнялась — BuildKit сравнил текст и взял результат из кэша.

Практическое следствие: инструкция вида

dockerfile
RUN apt-get update && apt-get upgrade -y

выполнится один раз и будет возвращать результат месячной давности, пока текст не изменится. Обновления безопасности применяться не будут.

Решение — либо фиксировать версии явно (тогда изменение версии меняет текст), либо периодически пересобирать с --no-cache.

Точечная инвалидация через ARG

Приём, позволяющий сломать кэш начиная с определённой инструкции:

bash
cat > Dockerfile.bust <<'EOF'
FROM alpine:3.21
RUN echo "стабильная часть" > /stable.txt

ARG CACHE_BUST=0
RUN echo "обновляемая часть, bust=${CACHE_BUST}" > /volatile.txt

CMD ["sh", "-c", "cat /stable.txt /volatile.txt"]
EOF

docker build -q -f Dockerfile.bust -t bust:1 . > /dev/null
docker run --rm bust:1

echo "--- сборка с другим значением ARG ---"
docker build -f Dockerfile.bust --build-arg CACHE_BUST=1 -t bust:1 . 2>&1 \
    | grep -E 'CACHED|RUN echo'
docker run --rm bust:1
text
стабильная часть
обновляемая часть, bust=0
--- сборка с другим значением ARG ---
 => CACHED [2/3] RUN echo "стабильная часть" > /stable.txt
 => [3/3] RUN echo "обновляемая часть, bust=1" > /volatile.txt
стабильная часть
обновляемая часть, bust=1

Первая инструкция взята из кэша, вторая пересобрана. Значение ARG вошло в её cache key.

Типичное применение — принудительное обновление пакетов раз в неделю:

bash
docker build --build-arg CACHE_BUST="$(date +%Y-%W)" -t app .

Номер недели меняется раз в семь дней, и в этот момент инструкции после ARG пересобираются.

Права доступа ломают кэш

bash
cd /tmp/cache-demo
chmod 644 app/main.py
docker build -q -f Dockerfile.good -t cache:good . > /dev/null

echo "--- меняем ТОЛЬКО права, содержимое то же ---"
chmod 755 app/main.py
docker build -f Dockerfile.good -t cache:good . 2>&1 | grep -E 'CACHED|COPY app'
text
--- меняем ТОЛЬКО права, содержимое то же ---
 => CACHED [3/5] COPY requirements.txt .
 => CACHED [4/5] RUN pip install --no-cache-dir -r requirements.txt
 => [5/5] COPY app/ ./app/

Содержимое файла не изменилось, но слой пересобрался: права входят в cache key.

Это объясняет расхождения между локальной сборкой и CI. Локально права могли измениться после запуска скрипта, а git clone в CI выставляет их по умолчанию — cache key отличается.

bash
chmod 644 app/main.py

Диагностика: кто сломал кэш

Подробный вывод показывает, какие инструкции выполнялись:

bash
cd /tmp/cache-demo
echo 'print("версия 4")' > app/main.py

docker build --progress=plain -f Dockerfile.good -t cache:good . 2>&1 \
    | grep -E '^#[0-9]+ (\[|CACHED|DONE)' | head -12

Более читаемо — подсчёт:

bash
analyze_cache() {
    local dockerfile="$1" tag="$2"
    local out
    out="$(docker build -f "$dockerfile" -t "$tag" . 2>&1)"
    local cached total
    cached="$(grep -c 'CACHED' <<< "$out" || echo 0)"
    total="$(grep -cE '=> (CACHED )?\[[0-9]+/[0-9]+\]' <<< "$out" || echo 0)"
    printf '  кэш: %s из %s инструкций\n' "$cached" "$total"
    echo "  пересобраны:"
    grep -E '=> \[[0-9]+/[0-9]+\]' <<< "$out" | sed 's/^/    /' | head -5
}

echo 'print("версия 5")' > app/main.py
echo "=== Dockerfile.good ==="
analyze_cache Dockerfile.good cache:good

echo 'print("версия 6")' > app/main.py
echo "=== Dockerfile.bad ==="
analyze_cache Dockerfile.bad cache:bad
text
=== Dockerfile.good ===
  кэш: 3 из 4 инструкций
  пересобраны:
     => [5/5] COPY app/ ./app/
=== Dockerfile.bad ===
  кэш: 1 из 3 инструкций
  пересобраны:
     => [3/4] COPY . .
     => [4/4] RUN pip install --no-cache-dir -r requirements.txt

Видно, какая инструкция инвалидировалась первой — она и есть точка каскада. Всё после неё пересобирается независимо от собственных изменений.

Отключение кэша

bash
# полностью без кэша
docker build --no-cache -t cache:fresh -f Dockerfile.good . 2>&1 | grep -c CACHED || echo "0 попаданий"

# обновить базовый образ перед сборкой
docker build --pull -t cache:fresh -f Dockerfile.good . 2>&1 | tail -1
ФлагДействиеКогда нужен
--no-cacheИгнорировать кэш полностьюПроверка воспроизводимости, подозрение на устаревший кэш
--pullОбновить базовый образПериодически, для получения обновлений безопасности
--no-cache-filter=stageОтключить кэш для конкретной стадииТочечная пересборка в multi-stage

Пример последнего:

bash
cat > Dockerfile.stages <<'EOF'
FROM python:3.13-slim AS deps
RUN echo "стадия зависимостей" > /deps.txt

FROM python:3.13-slim AS final
COPY --from=deps /deps.txt /deps.txt
RUN echo "финальная стадия" > /final.txt
CMD ["cat", "/deps.txt", "/final.txt"]
EOF

docker build -q -f Dockerfile.stages -t stages:1 . > /dev/null
echo "--- пересборка только стадии final ---"
docker build --no-cache-filter=final -f Dockerfile.stages -t stages:1 . 2>&1 \
    | grep -E 'CACHED|\[final|\[deps' | head -4
text
--- пересборка только стадии final ---
 => CACHED [deps 2/2] RUN echo "стадия зависимостей" > /deps.txt
 => [final 2/3] COPY --from=deps /deps.txt /deps.txt
 => [final 3/3] RUN echo "финальная стадия" > /final.txt

Стадия deps взята из кэша, final пересобрана.

Управление размером кэша

bash
docker system df --format 'table {{.Type}}\t{{.Size}}\t{{.Reclaimable}}' | grep -E 'TYPE|Build'
text
TYPE           SIZE      RECLAIMABLE
Build Cache    2.847GB   2.847GB

Очистка:

bash
# записи старше недели
docker builder prune --filter 'until=168h' -f

# ограничить общий размер
docker builder prune --reserved-space 5GB -f

Подробно — в разделе 13.

Уборка

bash
cd /tmp
docker rmi -f cache:bad cache:good cache:fresh rc:1 bust:1 stages:1 2>/dev/null || true
rm -rf /tmp/cache-demo /tmp/run-cache

Практическое упражнение

Задание. Дан Dockerfile, где сборка при изменении одной строки кода занимает больше минуты. Найдите три причины и исправьте, измерив эффект каждого исправления по отдельности.

dockerfile
FROM python:3.13-slim

RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential curl
RUN rm -rf /var/lib/apt/lists/*

COPY . /app
WORKDIR /app

RUN pip install --no-cache-dir -r requirements.txt
RUN python -m compileall /app

CMD ["python", "/app/app/main.py"]

Требуемый результат: пересборка при изменении кода менее чем за 3 секунды. Для каждого исправления приведите замер до и после.

Подсказки

Подсказка 1

Основная проблема — порядок: COPY . /app стоит до установки зависимостей.

Подсказка 2

Три инструкции RUN с apt-get можно объединить — это уменьшит образ и защитит от cache busting.

Подсказка 3

Отсутствие .dockerignore означает, что кэш ломается от файлов, не имеющих отношения к сборке.

Решение

Сначала выполните задание самостоятельно.

Показать решение
bash
#!/usr/bin/env bash
# cache-optimize.sh — пошаговая оптимизация кэша с измерениями.
set -uo pipefail

WORK="$(mktemp -d)"
trap 'docker rmi -f $(docker images -q --filter "reference=opt:*") >/dev/null 2>&1 || true;
      rm -rf "$WORK"' EXIT
cd "$WORK"

mkdir -p app __pycache__ .git
cat > requirements.txt <<'EOF'
fastapi==0.141.1
uvicorn[standard]==0.52.0
pydantic==2.13.4
EOF
echo "print('версия 1')" > app/main.py
dd if=/dev/urandom of=.git/pack.bin bs=1M count=20 status=none
dd if=/dev/urandom of=__pycache__/x.pyc bs=1K count=50 status=none

# ── Вариант 0: исходный ──
cat > Dockerfile.v0 <<'EOF'
FROM python:3.13-slim
RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential curl
RUN rm -rf /var/lib/apt/lists/*
COPY . /app
WORKDIR /app
RUN pip install --no-cache-dir -r requirements.txt
RUN python -m compileall /app
CMD ["python", "/app/app/main.py"]
EOF

# ── Вариант 1: объединён apt-get ──
cat > Dockerfile.v1 <<'EOF'
FROM python:3.13-slim
RUN apt-get update \
 && apt-get install -y --no-install-recommends build-essential curl \
 && rm -rf /var/lib/apt/lists/*
COPY . /app
WORKDIR /app
RUN pip install --no-cache-dir -r requirements.txt
RUN python -m compileall /app
CMD ["python", "/app/app/main.py"]
EOF

# ── Вариант 2: + правильный порядок COPY ──
cat > Dockerfile.v2 <<'EOF'
FROM python:3.13-slim
RUN apt-get update \
 && apt-get install -y --no-install-recommends build-essential curl \
 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
RUN python -m compileall /app/app
CMD ["python", "/app/app/main.py"]
EOF

measure() {
    local df="$1" tag="$2" label="$3" n="$4"
    # первая сборка — наполнить кэш
    docker build -q -f "$df" -t "$tag" . > /dev/null 2>&1
    # изменить код
    echo "print('версия $n')" > app/main.py
    # замерить пересборку
    local s e
    s="$(date +%s.%N)"
    docker build -q -f "$df" -t "$tag" . > /dev/null 2>&1
    e="$(date +%s.%N)"
    printf '  %-42s %6.2f c   образ %s\n' "$label" \
        "$(awk -v a="$s" -v b="$e" 'BEGIN{print b-a}')" \
        "$(docker images "$tag" --format '{{.Size}}')"
}

echo "═══ Пересборка после изменения одной строки кода ═══"
measure Dockerfile.v0 opt:v0 "v0: исходный"                    2
measure Dockerfile.v1 opt:v1 "v1: + объединён apt-get"         3
measure Dockerfile.v2 opt:v2 "v2: + правильный порядок COPY"   4

echo
echo "── добавляем .dockerignore ──"
cat > .dockerignore <<'EOF'
.git
__pycache__
*.pyc
Dockerfile*
.dockerignore
EOF

measure Dockerfile.v2 opt:v3 "v3: + .dockerignore"             5

echo
echo "═══ Проверка: обновление зависимостей всё ещё применяется ═══"
echo "httpx==0.28.1" >> requirements.txt
docker build -f Dockerfile.v2 -t opt:v3 . 2>&1 | grep -E 'CACHED|RUN pip' | head -3
docker run --rm opt:v3 python -c "import httpx; print('  новый пакет установлен')" 2>/dev/null

Ожидаемый вывод:

text
═══ Пересборка после изменения одной строки кода ═══
  v0: исходный                                62.41 c   образ 428MB
  v1: + объединён apt-get                     41.18 c   образ 397MB
  v2: + правильный порядок COPY                2.84 c   образ 397MB
  
── добавляем .dockerignore ──
  v3: + .dockerignore                          1.12 c   образ 376MB

═══ Проверка: обновление зависимостей всё ещё применяется ═══
 => CACHED [2/6] RUN apt-get update  && apt-get install -y ...
 => [4/6] COPY requirements.txt .
 => [5/6] RUN pip install --no-cache-dir -r requirements.txt
  новый пакет установлен

Разбор трёх исправлений.

Исправление 1 — объединение apt-get. Дало 21 секунду и 31 MB. Секунды — за счёт того, что три инструкции стали одной и слой берётся из кэша целиком. Мегабайты — потому что индекс apt перестал оставаться в отдельном слое (урок 5.3).

Исправление 2 — порядок COPY. Дало главный выигрыш: 41 секунда → 2.8. Причина в каскадной инвалидации: COPY . /app до pip install означает, что изменение любого файла пересобирает установку зависимостей. После перестановки pip install зависит только от requirements.txt.

Исправление 3 — .dockerignore. Дало ещё 1.7 секунды и 21 MB. Каталоги .git и __pycache__ перестали передаваться в контекст и попадать в образ через COPY.

Итог: 62 секунды → 1.1 секунды, в 56 раз быстрее. Образ уменьшился с 428 до 376 MB.

Что осталось неоптимальным. В образе всё ещё присутствует build-essential — около 180 MB компилятора, не нужного при работе приложения. Это решается multi-stage build (урок 5.7), где компиляторы остаются в стадии сборки.

Ключевая проверка, которую нельзя пропускать. Последний блок доказывает, что оптимизация не сломала обновление зависимостей: добавление пакета в requirements.txt каскадно инвалидировало pip install, и пакет установился. Оптимизация кэша, при которой обновления перестают применяться, — не оптимизация, а скрытая поломка.

Проверка результата

bash
mkdir -p /tmp/vfy/app && cd /tmp/vfy
echo "fastapi==0.141.1" > requirements.txt
echo "print(1)" > app/main.py
printf 'FROM python:3.13-slim\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY app/ ./app/\nCMD ["python","app/main.py"]\n' > Dockerfile

docker build -q -t vfy:1 . > /dev/null
echo "print(2)" > app/main.py
docker build -t vfy:1 . 2>&1 | grep -c CACHED

docker rmi -f vfy:1 > /dev/null; cd /tmp && rm -rf /tmp/vfy

Ожидается 3 попадания в кэш — пересобирается только последний COPY.

Типичные ошибки

ОшибкаПричинаИсправление
COPY . . до установки зависимостейПорядок кажется неважнымКаскадная инвалидация; копировать файл зависимостей отдельно
Ожидание, что RUN pip install заметит изменение файлаКажется логичнымRUN кэшируется по тексту команды; зависимость выражается через COPY
RUN apt-get upgrade в DockerfileХотят свежие пакетыИнструкция закэшируется навсегда; использовать --no-cache периодически
Кэш не работает в CI, хотя локально работаетРазные права доступа файловПрава входят в cache key; проверить git clone против локального состояния
Нет .dockerignoreНе задумывалисьПосторонние файлы инвалидируют COPY . .
--no-cache при каждой сборкеБорьба с непонятным поведениемРазобраться в причине; полное отключение кэша делает сборку долгой
Слишком мелкое дробление RUNКажется, что кэшируется лучшеБольше слоёв и риск cache busting; объединять связанное
Слишком крупное объединение RUNКажется, что слоёв меньшеОдно изменение пересобирает всё; выделять стабильную часть
Оптимизация сломала обновление зависимостейНе проверилиПосле оптимизации убедиться, что изменение файла зависимостей применяется

Контрольные вопросы

На понимание:

  1. Почему изменение инструкции в середине Dockerfile пересобирает и все последующие?
  2. Что входит в cache key инструкции RUN и чего там нет?
  3. Почему RUN apt-get update && apt-get upgrade -y не приносит обновлений при повторных сборках?
  4. Почему изменение прав доступа файла ломает кэш COPY?
  5. Почему объединение команд в одном RUN делает инвалидацию грубее?

На применение:

  1. Как упорядочить инструкции, чтобы изменение кода не пересобирало зависимости?
  2. Как принудительно пересобрать всё начиная с определённой инструкции?
  3. Как определить, какая инструкция первой сломала кэш?

На диагностику:

  1. Сборка в CI занимает 5 минут при неизменных зависимостях, локально — 3 секунды. Что проверить?
  2. После оптимизации кэша обновление пакета в requirements.txt перестало применяться. Что произошло?

Краткое резюме

  1. Cache key инструкции включает ключ предыдущей — отсюда каскадная инвалидация.
  2. Главный принцип: редко меняющееся раньше, часто меняющееся позже.
  3. RUN кэшируется по тексту команды; содержимое внешних данных не учитывается.
  4. Зависимость RUN от файлов выражается через предшествующий COPY.
  5. COPY учитывает содержимое, права доступа и владельца.
  6. Разделение по частоте изменений даёт ускорение в десятки раз.
  7. Разные права файлов — частая причина расхождения кэша между локальной машиной и CI.
  8. ARG в cache key позволяет точечно инвалидировать часть сборки.
  9. --no-cache-filter=<стадия> пересобирает конкретную стадию multi-stage.
  10. После оптимизации обязательно проверьте, что обновление зависимостей всё ещё применяется.

Официальные источники

ИсточникСсылкаЧто подтверждает
Docker build cachehttps://docs.docker.com/build/cache/Механизм cache key, каскадная инвалидация, правила для каждой инструкции
Optimize cache usagehttps://docs.docker.com/build/cache/optimize/Порядок инструкций, разделение по частоте изменений
Cache invalidationhttps://docs.docker.com/build/cache/invalidation/Что именно ломает кэш: содержимое, права, ARG
Building best practiceshttps://docs.docker.com/build/building/best-practices/Рекомендации по упорядочиванию инструкций
docker build referencehttps://docs.docker.com/reference/cli/docker/buildx/build/Флаги --no-cache, --pull, --no-cache-filter, --progress
docker builder prunehttps://docs.docker.com/reference/cli/docker/builder/prune/Управление размером кэша, --reserved-space (бывш. --keep-storage), фильтр until
BuildKithttps://docs.docker.com/build/buildkit/Граф зависимостей, параллельная сборка стадий

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → BuildKit
Главное оглавление

Markdown на GitHub ↗