Skip to main content

Як працює 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.

Оптимізація порядку інструкцій

dockerfile
# НЕПРАВИЛЬНО: 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:

dockerfile
# syntax=docker/dockerfile:1.7 FROM python:3.13-slim WORKDIR /app COPY requirements.txt . RUN --mount=type=cache,target=/root/.cache/pip \ 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:

bash
# Перший білд: пишемо 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

bash
# Перебудувати усе з нуля 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

dockerfile
# НЕПРАВИЛЬНО: 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

dockerfile
COPY . . # інвалідує editor save у будь-якому файлі

Для dev-середовищ бери bind mount при run-time. Для CI-білдів прийми, що source-зміни інвалідують пізніші шари і дизайнь навколо (deps спочатку).

Забути, що RUN не дивиться всередину команди

dockerfile
RUN curl https://example.com/installer.sh | sh # Та сама RUN-string назавжди; ніколи не освіжається, навіть якщо installer.sh змінюється.

Cache-key для RUN це літеральна команда. Щоб форсувати перевиконання, зміни string якось:

dockerfile
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

bash
# Використання 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

dockerfile
# syntax=docker/dockerfile:1.7 FROM node:22-alpine AS deps WORKDIR /app COPY package*.json ./ RUN --mount=type=cache,target=/root/.npm \ npm ci FROM deps AS build COPY . . RUN npm run build FROM node:22-alpine WORKDIR /app COPY --from=build /app/dist ./dist COPY --from=deps /app/node_modules ./node_modules USER node CMD ["node", "dist/server.js"]
  • Стейдж deps інвалідує лише при зміні package*.json.
  • npm cache mount переживає між білдами.
  • Source-зміни перезапускають лише build-стейдж.

CI-shared cache через registry

yaml
# .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-час падає драматично.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів