Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює build cache в Docker і як ним керувати?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Docker build cache** зберігає результат кожної Dockerfile-інструкції, тож ідентичні інструкції на rebuild перевикористовують попередній шар. Cache key = digest попереднього шару + текст інструкції + (для COPY/ADD) digest input-файлів. ```dockerfile FROM node:22-alpine COPY package*.json ./ # кешується, поки package*.json не змінився RUN npm ci # кешується, поки попередній крок не перебудовано COPY . . # інвалідується будь-якою зміною коду ``` **Головне:** упорядкуй від стабільного до волатильного. Як крок промахнувся повз cache, кожен крок після теж промах. З BuildKit `RUN --mount=type=cache` тримає cache *між* білдами без запікання у шари. `--no-cache` обходить все.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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-час падає драматично.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.