Як працює build cache в Docker і як ним керувати?
Docker build cache це різниця між 60-секундним rebuild і 2-секундним. Знання, як рахується cache key і як тримати його валідним, це найбільший скіл для швидких Dockerfile.
Теорія
TL;DR
- Після кожної інструкції Docker зберігає отриманий шар у cache.
- На rebuild Docker рахує cache key для кожної інструкції. Збігається → перевикористовує шар; не збігається → перевиконує і інвалідує усе нижче.
- Компоненти cache key:
- Digest попереднього шару (ланцюг важить)
- Сам текст інструкції
- Для
COPYіADD: digest кожного файлу, що копіюється - Для
RUN: лише command string. Docker НЕ перевіряє, що команда робить.
- Порядок важить: ставь стабільні дорогі кроки високо; волатильні часто-мінливі низько.
- BuildKit cache mount (
RUN --mount=type=cache,target=/path) персистить cache між білдами, не стаючи частиною шару. --no-cacheбудує усе з нуля.
Як працює cache-інвалідація
FROM alpine:3.21 ← кешується, якщо alpine:3.21 не змінилася
WORKDIR /app ← кешується, якщо FROM не змінилася
COPY package.json ./ ← кешується, якщо байти package.json не змінилися
RUN npm ci ← кешується, якщо попередній крок hit
COPY src/ ./src/ ← інвалідує, якщо будь-який файл у src/ змінився
CMD ["node", "server.js"] ← кешується, якщо попередній крок hitКлючовий інсайт: Docker хешує вміст файлу для COPY/ADD, але не для RUN-output. RUN apt-get install curl cache-hit, навіть якщо upstream apt має нову версію curl.
Оптимізація порядку інструкцій
# НЕПРАВИЛЬНО: source копіюється до встановлення deps
FROM node:22-alpine
WORKDIR /app
COPY . . # будь-яка зміна файлу інвалідує усе нижче
RUN npm ci --omit=dev # перезапускається на кожну зміну коду
CMD ["node", "server.js"]
# ПРАВИЛЬНО: deps спочатку, source в кінці
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./ # міняється тільки при зміні deps
RUN npm ci --omit=dev # кешується, поки package*.json не змінився
COPY . . # міняється при зміні source; лише це перезапускається
CMD ["node", "server.js"]Для типового застосунку зі стабільними deps це перетворює rebuild з 60 секунд (неправильно) на 2 секунди (правильно).
BuildKit cache mount
З BuildKit (дефолт у сучасному Docker) можна змонтувати cache-директорію, що персистить між білдами без становлення частиною image:
# syntax=docker/dockerfile:1.7
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN \
pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]Pip wheel-cache живе поза шаром. Білд 2 з тим самим requirements.txt перевикористовує wheels, навіть якщо сам шар перебудовано. Шар лишається чистим; wheels кешовано.
Поширені cache-mount targets:
- pip:
/root/.cache/pip - npm:
/root/.npm - apt:
/var/cache/aptі/var/lib/apt/listsзsharing=locked - Go modules:
/go/pkg/mod - Cargo:
/usr/local/cargo/registry
Шарування cache між білдами (CI)
З BuildKit + docker buildx можна експортувати і імпортувати cache у registry, тож CI-білди перевикористовують cache між runner:
# Перший білд: пишемо cache у registry
docker buildx build \
--cache-to type=registry,ref=myreg/myapp:cache,mode=max \
--cache-from type=registry,ref=myreg/myapp:cache \
-t myreg/myapp:1.0 \
--push .
# Наступні білди (інший runner) читають з того самого cache
docker buildx build \
--cache-from type=registry,ref=myreg/myapp:cache \
-t myreg/myapp:1.1 \
--push .Холодний runner стартує таким же теплим, як останній успішний білд. Масивне CI-прискорення для проектів з важкими build-кроками.
Обхід cache
# Перебудувати усе з нуля
docker build --no-cache -t myapp .
# Освіжити лише FROM (re-pull base-image)
docker build --pull -t myapp .
# Обидва
docker build --pull --no-cache -t myapp .
# Інвалідувати з конкретної інструкції далі (BuildKit)
# Бери build-arg, чиє значення міняється: --build-arg BUILD_REV=$(date +%s)Типові помилки
COPY . . перед RUN install
Згадано вище. Найпоширеніший cache-killer.
Розкласти apt-get update і apt-get install у окремі RUN
# НЕПРАВИЛЬНО: update може cache hit, поки install тягне stale package list
RUN apt-get update
RUN apt-get install -y --no-install-recommends curl
# ПРАВИЛЬНО: тримай їх в одному RUN, щоб завжди йшли разом
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*Якщо apt-get update кешовано і apt-get install крутиться, можеш встановити з stale package-list, пакети можуть бути відсутні.
Монтування source-коду, що тригерить cache-інвалідацію на кожному save
COPY . . # інвалідує editor save у будь-якому файліДля dev-середовищ бери bind mount при run-time. Для CI-білдів прийми, що source-зміни інвалідують пізніші шари і дизайнь навколо (deps спочатку).
Забути, що RUN не дивиться всередину команди
RUN curl https://example.com/installer.sh | sh
# Та сама RUN-string назавжди; ніколи не освіжається, навіть якщо installer.sh змінюється.Cache-key для RUN це літеральна команда. Щоб форсувати перевиконання, зміни string якось:
ARG INSTALLER_SHA="abc123..."
RUN curl https://example.com/installer.sh -o /tmp/i.sh && \
echo "$INSTALLER_SHA /tmp/i.sh" | sha256sum -c && \
sh /tmp/i.sh
# Тепер зміна INSTALLER_SHA інвалідує цей шар.Inspecting і управління cache
# Використання cache
docker system df # high-level
docker buildx du # деталі build cache
# Prune build cache
docker builder prune # інтерактивно
docker builder prune -af # усе, безумовно
docker builder prune --filter 'until=72h' # старше 3 днів
# Що BuildKit вважав кешованим
DOCKER_BUILDKIT=1 docker build --progress=plain -t myapp .
# Output показує CACHED для hit, RUN для missРеальне застосування
- Локальний dev: шар install-deps кешовано → 2-секундні rebuild на зміни коду. Множник продуктивності.
- CI:
--cache-from registry, щоб принести cache попереднього білду на свіжий runner. Скорочує 10-хвилинні білди до 90 секунд. - Cache mount для package manager: pip/npm/apt cache персистить між білдами без роздуття image.
- Build-ферми (Bazel-style): cache шиппиться як registry-артефакт; багато builder ділять один cache.
Питання для поглиблення
Q: Чому мій CI-білд ніколи не hit cache, навіть коли нічого не змінилось?
A: Кожен CI-runner стартує чистим, без локального cache. Бери --cache-from, щоб читати cache з registry, що переживає прогони.
Q: Яка різниця між BuildKit cache mount і image layer?
A: Шари це частина image. Cache mount ні, вони живуть у окремому cache, монтуються при build-time. Mount це як тримати build-time cache (npm packages, pip wheels) без роздуття фінального image файлами, що були потрібні лише для компіляції.
Q: Як інвалідувати лише другу половину Dockerfile?
A: Додай ARG CACHEBUST=1 у потрібному місці і передай --build-arg CACHEBUST=$(date +%s). Наступний білд побачить різне значення і інвалідує звідти вниз.
Q: Чи --pull інвалідує усе?
A: Лише якщо base-image реально має новий digest. --pull пере-перевіряє FROM, але якщо node:22-alpine резолвиться у той самий digest, що і минулого разу, FROM лишається кешованим, як і усе після нього.
Q: (Senior) Як налаштувати cache-from у matrix-білді GitHub Actions?
A: Бери docker/build-push-action@v5 з cache-from: type=gha і cache-to: type=gha,mode=max. GitHub Actions дає вбудований cache backend per repo. Для агресивнішого cross-job шарування бери type=registry,ref=ghcr.io/myorg/myapp:cache. Уникай type=local у CI, runner ефемерні.
Приклади
Оптимальний Node Dockerfile
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN \
npm ci
FROM deps AS build
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]- Стейдж
depsінвалідує лише при змініpackage*.json. - npm cache mount переживає між білдами.
- Source-зміни перезапускають лише
build-стейдж.
CI-shared cache через registry
# .github/workflows/build.yml
- uses: docker/build-push-action@v5
with:
push: true
tags: myorg/myapp:${{ github.sha }}
cache-from: type=registry,ref=myorg/myapp:cache
cache-to: type=registry,ref=myorg/myapp:cache,mode=maxПерший прогін наповнює myorg/myapp:cache. Кожен наступний прогін на будь-якому runner перевикористовує. Build-час падає драматично.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів