5.5. Build cache
Цели
После этого материала вы сможете:
- назвать правило инвалидации кэша для каждой инструкции;
- упорядочить инструкции так, чтобы изменение кода не пересобирало зависимости;
- измерить эффект оптимизации и доказать его числами;
- прочитать вывод сборки и определить, какая инструкция сломала кэш;
- объяснить, почему кэш иногда не срабатывает при внешне одинаковых условиях;
- осознанно использовать
--no-cacheи точечную инвалидацию.
Предварительные знания
Ключевые термины
| Термин | Объяснение |
|---|---|
cache key | Идентификатор, по которому BuildKit ищет готовый результат инструкции |
инвалидация | Ситуация, когда кэш не подходит и инструкция выполняется заново |
каскадная инвалидация | Пересборка всех инструкций после изменившейся |
cache hit / cache miss | Попадание и промах кэша |
CACHED | Метка в выводе сборки, означающая использование кэша |
Теория
Как работает кэш
Для каждой инструкции BuildKit вычисляет cache key и ищет результат с таким ключом. Если находит — инструкция не выполняется, берётся готовый слой.
Ключевое свойство: cache key включает ключ предыдущей инструкции. Отсюда каскадная инвалидация — если сломался кэш на шаге 3, шаги 4, 5 и все последующие тоже выполнятся заново, даже если сами не изменились.
FROM python:3.13-slim ключ = хэш(базовый образ) ✓ CACHED
WORKDIR /app ключ = хэш(предыдущий + "WORKDIR /app") ✓ CACHED
COPY requirements.txt . ключ = хэш(предыдущий + содержимое) ✓ CACHED
RUN pip install ... ключ = хэш(предыдущий + текст команды) ✓ CACHED
COPY . . ключ = хэш(предыдущий + содержимое) ✗ изменился код
CMD [...] ключ = хэш(предыдущий + ...) ✗ каскад
Из этого следует главный принцип оптимизации: редко меняющееся размещайте раньше, часто меняющееся — позже.
Правила инвалидации по инструкциям
| Инструкция | Что входит в cache key |
|---|---|
FROM | Digest базового образа. Смена тега на тот же образ кэш не ломает |
RUN | Только текст команды. Содержимое скачиваемых файлов не учитывается |
COPY, ADD | Содержимое файлов, права доступа, владелец; путь назначения |
ENV, ARG, LABEL, WORKDIR, USER, EXPOSE | Текст инструкции |
ARG (использованный) | Значение аргумента — если он подставляется в инструкцию |
Строка про RUN — источник самой частой ошибки в понимании кэша.
Почему RUN кэшируется по тексту команды
BuildKit не знает, что делает команда. Он сравнивает только строку.
RUN pip install -r requirements.txt
Если строка не изменилась, инструкция берётся из кэша — независимо от того, что изменился requirements.txt. Кэш сломается только потому, что предыдущая инструкция COPY requirements.txt . заметила изменение файла и инвалидировалась каскадно.
Уберите COPY перед RUN — и обновление зависимостей перестанет применяться:
# ОШИБКА: pip install закэширован навсегда
RUN pip install fastapi==0.141.1
При смене версии в этой строке кэш сломается. А вот RUN apt-get upgrade или RUN pip install -r /mnt/requirements.txt (через монтирование) закэшируются и будут возвращать устаревший результат месяцами.
Отсюда практическое правило: если результат RUN зависит от внешних данных, эта зависимость должна быть выражена через COPY или через ключ кэша явно.
Каскадная инвалидация на практике
Типичный неоптимальный Dockerfile:
FROM python:3.13-slim
WORKDIR /app
COPY . . ← изменение ЛЮБОГО файла
RUN pip install -r requirements.txt ← пересобирается всегда
CMD ["python", "app.py"]
Изменили одну строку в app.py — переустанавливаются все зависимости.
Оптимальный вариант:
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 выставляет одинаковые права, а локально они могли измениться.
Многострочные инструкции и кэш
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:
docker build --help | grep -q 'buildx' && echo "BuildKit"
С Docker Engine 23.0 BuildKit — сборщик по умолчанию, legacy builder объявлен устаревшим.
Где хранится кэш
Кэш BuildKit хранится отдельно от образов:
docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Build Cache 183 0 4.271GB 4.271GB
Он не удаляется командой docker image prune и часто оказывается главным потребителем места (урок 3.4).
Команды и примеры
Подготовка
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
Плохой порядок инструкций
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
real 0m28.412s
Теперь изменим одну строку кода:
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]\]'
=> 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 не менялся.
Хороший порядок
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]\]'
--- первая сборка ---
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/ идёт после неё.
Проверка, что изменение зависимостей всё ещё применяется
Оптимизация не должна ломать обновление:
echo "python-multipart==0.0.20" >> requirements.txt
docker build -f Dockerfile.good -t cache:good . 2>&1 | grep -E 'CACHED|RUN pip'
=> CACHED [2/5] WORKDIR /app
=> [3/5] COPY requirements.txt .
=> [4/5] RUN pip install --no-cache-dir -r requirements.txt
COPY requirements.txt заметил изменение и каскадно инвалидировал установку. Обновление применилось.
Убедимся:
docker run --rm cache:good python -c "import multipart; print('пакет установлен')"
пакет установлен
RUN кэшируется по тексту команды
Демонстрация того, почему это важно.
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 "→ значения совпали: инструкция взята из кэша"
первая сборка: 1785481203847291000
вторая сборка: 1785481203847291000
→ значения совпали: инструкция взята из кэша
Команда date возвращает разное значение при каждом запуске, но она не выполнялась — BuildKit сравнил текст и взял результат из кэша.
Практическое следствие: инструкция вида
RUN apt-get update && apt-get upgrade -y
выполнится один раз и будет возвращать результат месячной давности, пока текст не изменится. Обновления безопасности применяться не будут.
Решение — либо фиксировать версии явно (тогда изменение версии меняет текст), либо периодически пересобирать с --no-cache.
Точечная инвалидация через ARG
Приём, позволяющий сломать кэш начиная с определённой инструкции:
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
стабильная часть
обновляемая часть, bust=0
--- сборка с другим значением ARG ---
=> CACHED [2/3] RUN echo "стабильная часть" > /stable.txt
=> [3/3] RUN echo "обновляемая часть, bust=1" > /volatile.txt
стабильная часть
обновляемая часть, bust=1
Первая инструкция взята из кэша, вторая пересобрана. Значение ARG вошло в её cache key.
Типичное применение — принудительное обновление пакетов раз в неделю:
docker build --build-arg CACHE_BUST="$(date +%Y-%W)" -t app .
Номер недели меняется раз в семь дней, и в этот момент инструкции после ARG пересобираются.
Права доступа ломают кэш
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'
--- меняем ТОЛЬКО права, содержимое то же ---
=> 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 отличается.
chmod 644 app/main.py
Диагностика: кто сломал кэш
Подробный вывод показывает, какие инструкции выполнялись:
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
Более читаемо — подсчёт:
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
=== 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
Видно, какая инструкция инвалидировалась первой — она и есть точка каскада. Всё после неё пересобирается независимо от собственных изменений.
Отключение кэша
# полностью без кэша
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 |
Пример последнего:
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
--- пересборка только стадии 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 пересобрана.
Управление размером кэша
docker system df --format 'table {{.Type}}\t{{.Size}}\t{{.Reclaimable}}' | grep -E 'TYPE|Build'
TYPE SIZE RECLAIMABLE
Build Cache 2.847GB 2.847GB
Очистка:
# записи старше недели
docker builder prune --filter 'until=168h' -f
# ограничить общий размер
docker builder prune --reserved-space 5GB -f
Подробно — в разделе 13.
Уборка
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, где сборка при изменении одной строки кода занимает больше минуты. Найдите три причины и исправьте, измерив эффект каждого исправления по отдельности.
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 означает, что кэш ломается от файлов, не имеющих отношения к сборке.
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/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
Ожидаемый вывод:
═══ Пересборка после изменения одной строки кода ═══
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, и пакет установился. Оптимизация кэша, при которой обновления перестают применяться, — не оптимизация, а скрытая поломка.
Проверка результата
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 | Кажется, что слоёв меньше | Одно изменение пересобирает всё; выделять стабильную часть |
| Оптимизация сломала обновление зависимостей | Не проверили | После оптимизации убедиться, что изменение файла зависимостей применяется |
Контрольные вопросы
На понимание:
- Почему изменение инструкции в середине
Dockerfileпересобирает и все последующие? - Что входит в cache key инструкции
RUNи чего там нет? - Почему
RUN apt-get update && apt-get upgrade -yне приносит обновлений при повторных сборках? - Почему изменение прав доступа файла ломает кэш
COPY? - Почему объединение команд в одном
RUNделает инвалидацию грубее?
На применение:
- Как упорядочить инструкции, чтобы изменение кода не пересобирало зависимости?
- Как принудительно пересобрать всё начиная с определённой инструкции?
- Как определить, какая инструкция первой сломала кэш?
На диагностику:
- Сборка в CI занимает 5 минут при неизменных зависимостях, локально — 3 секунды. Что проверить?
- После оптимизации кэша обновление пакета в
requirements.txtперестало применяться. Что произошло?
Краткое резюме
- Cache key инструкции включает ключ предыдущей — отсюда каскадная инвалидация.
- Главный принцип: редко меняющееся раньше, часто меняющееся позже.
RUNкэшируется по тексту команды; содержимое внешних данных не учитывается.- Зависимость
RUNот файлов выражается через предшествующийCOPY. COPYучитывает содержимое, права доступа и владельца.- Разделение по частоте изменений даёт ускорение в десятки раз.
- Разные права файлов — частая причина расхождения кэша между локальной машиной и CI.
ARGв cache key позволяет точечно инвалидировать часть сборки.--no-cache-filter=<стадия>пересобирает конкретную стадию multi-stage.- После оптимизации обязательно проверьте, что обновление зависимостей всё ещё применяется.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker build cache | https://docs.docker.com/build/cache/ | Механизм cache key, каскадная инвалидация, правила для каждой инструкции |
| Optimize cache usage | https://docs.docker.com/build/cache/optimize/ | Порядок инструкций, разделение по частоте изменений |
| Cache invalidation | https://docs.docker.com/build/cache/invalidation/ | Что именно ломает кэш: содержимое, права, ARG |
| Building best practices | https://docs.docker.com/build/building/best-practices/ | Рекомендации по упорядочиванию инструкций |
| docker build reference | https://docs.docker.com/reference/cli/docker/buildx/build/ | Флаги --no-cache, --pull, --no-cache-filter, --progress |
| docker builder prune | https://docs.docker.com/reference/cli/docker/builder/prune/ | Управление размером кэша, --reserved-space (бывш. --keep-storage), фильтр until |
| BuildKit | https://docs.docker.com/build/buildkit/ | Граф зависимостей, параллельная сборка стадий |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → BuildKit
Главное оглавление