Деякі збірки Docker повільні з поважних причин: ви компілюєте велетня, завантажуєте пів-інтернету або виконуєте криптографію в великому масштабі. Але більшість «повільних збірок» — самі собі на зло. Типовий сценарій: ви витрачаєте час на ту саму роботу при кожному коміті, на кожному ноутбуці, на кожному CI-ранері, назавжди.
BuildKit може це виправити — якщо ви перестанете думати про кеш як «Docker пам’ятає речі» і почнете ставитися до нього як до ланцюга постачання. Це польовий посібник з кешування, який витримує тиск продакшену: що вимірювати, що змінювати і що призведе до протилежного ефекту.
Ментальна модель: BuildKit кеш — не магія, а адреси
Класичні збірки Docker (старий билдер) працювали як стос шарів: кожна інструкція створювала знімок файлової системи. Якщо інструкція та її вхідні дані не змінювалися, Docker міг повторно використати шар. Це твердження досі в основному вірне, але BuildKit змінив механіку й зробив кеш набагато виразнішим.
BuildKit розглядає вашу збірку як граф операцій. Кожна операція продукує вихід, і цей вихід може кешуватися ключем, похідним від входів (файли, args, оточення, базові образи і іноді метадані). Якщо ключ збігається, BuildKit може пропустити роботу і повторно використати результат.
У цьому останньому реченні ховається підступ: кеш корисний лише настільки, наскільки корисні ваші ключі. Якщо випадково включити в ключ «сьогоднішній таймштамп», ви отримаєте промах кешу щоразу. Якщо включити «весь репозиторій» як вхід на ранньому кроці, ви інвалідируєте весь світ, коли хтось змінить README.
Що означає «справді прискорює»
Є три окремі проблеми швидкості, які люди зводять в одну скаргу:
- Повторна робота на одній машині: багаторазова локальна перебудова того самого Dockerfile.
- Повторна робота на різних машинах: ноутбуки, ефермерні CI-ранери, автоскейловані флотилії збірок.
- Повільні кроки навіть при cache hit: великі контексти, повільні завантаження образів, декомпресія і «встановлення залежностей», яке ніколи не кешується.
BuildKit кеш вирішує всі три — але лише якщо ви оберете правильний механізм:
- Layer cache (класичний): підходить для імутабельних кроків зі стабільними входами.
- Inline cache metadata: зберігає інформацію про кеш всередині маніфесту образу, щоб інші билдери могли її використовувати.
- External/remote cache: експортує кеш у реєстр/локально/артефакт, щоб ефермерні билдери не починали з нуля.
- Cache mounts (
RUN --mount=type=cache): прискорюють інсталяцію залежностей без випікання кешу в кінцевий образ.
Операційне правило: якщо ви не можете пояснити, чому крок кешується, він не кешується навмисно. Це просто тимчасове везіння.
Парафраз ідеї Вернера Фогельса (CTO Amazon): «Усе ламається, весь час». Промахи кешу — це режим відмови. Проєктуйте відповідно.
Цікаві факти та коротка історія (чому це важливо)
- BuildKit починався як окремий проєкт для заміни старого билдера Docker на графовий рушій, що дав змогу паралелізувати та розширити можливості кешування.
- Класичний шаровий кеш Docker передував BuildKit і був тісно пов’язаний із «одна інструкція = один шар». Ця модель проста, але погано виражає тимчасовий кеш.
- BuildKit може виконувати незалежні кроки паралельно (наприклад, тягнути базові образи й одночасно передавати контекст), тому логи виглядають інакше, а час може покращитися без змін у Dockerfile.
.dockerignoreстаріший за BuildKit, але став критичнішим із ростом репозиторіїв: великі контексти не лише гальмують збірки, вони отруюють ключі кешу через зміни входів.- Inline cache metadata — прагматичний хаґ: зберегти підказки кешу в образі, щоб наступний билд міг використовувати шари навіть на іншій машині.
- BuildKit cache mounts — філософський зсув: «кеш як concern під час збірки», а не «кеш, випечений в образ». Це різниця між швидкими збірками і роздутими образами.
- Multi-stage builds змінили поведінку кешування в командах: ви можете ізолювати дорогі тулчейни в стадіях збірки й тримати рантайм-стадію стабільною та дружньою до кешу.
- Експорт віддаленого кешу став необхідним, коли CI перейшов на ефермерні ранери та автоскейл, де локальний диск кешу зникає після запуску.
Жарт №1: кеш Docker — як офісна парковка: всі думають, що в них заброньоване місце, поки не настане понеділок.
Швидка діагностика: знайти вузьке місце за 10 хвилин
Порядок триажу, що економить час. Не починайте «оптимізувати Dockerfile», доки не з’ясуєте, хто справжній ворог.
1) Чи увімкнений взагалі BuildKit?
Якщо у вас новіша версія Docker, зазвичай так, але «зазвичай» — не план. Старий билдер поводиться інакше і позбавлений ключових можливостей, як-от cache mounts.
2) Чи великий або нестабільний контекст збірки?
Якщо ви відправляєте гігабайти, ви будете повільні навіть із ідеальним кешем. А якщо контекст змінюється при кожному коміті (логи, артефакти збірки, vendor), ключі кешу блукають.
3) Чи отримуєте ви cache hits там, де очікуєте?
Подивіться вивід: CACHED має з’являтися на дорогих кроках. Якщо крок інсталяції залежностей виконується щоразу, ось ваша ціль.
4) Чи CI стартує «з холодного» кожного разу?
Ефермерні ранери означають, що локальний кеш зник. Без віддаленого експорту/імпорту кешу ви перебудовуєте з нуля, незалежно від того, наскільки «дружній до кешу» ваш Dockerfile.
5) Чи заблоковані ви мережею або CPU?
Завантаження залежностей, притягування базових образів і оновлення індексів пакетів — мережеві операції. Компіляції — CPU-важкі. Вирішення різниться.
6) Чи секрети/SSH спричиняють промахи кешу?
Секрети за дизайном не входять у ключ кешу, але спосіб, яким ви підключаєте приватні залежності, часто змінює команди або вводить недетермінованість, що руйнує повторне використання кешу.
7) Чи випадково ви ламаєте кеш?
Типові винуватці: ADD . занадто рано, непіновані пакети, змінні build args, таймстемпи та «очищення», що змінює mtime файлів у неочікуваний спосіб.
Практичні завдання: команди, виводи, рішення (12+)
Це не «іграшкові» команди. Це ті команди, які ви запускаєте, коли пайплайн збірки горить, і потрібно вирішити, що змінювати далі.
Завдання 1: Переконатися, що BuildKit увімкнений (і який билдер використовується)
cr0x@server:~$ docker build --progress=plain -t demo:bk .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 1.12kB done
#2 [internal] load metadata for docker.io/library/alpine:3.19
#2 DONE 0.8s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/3] FROM docker.io/library/alpine:3.19
#4 resolve docker.io/library/alpine:3.19 done
#4 DONE 0.0s
Що це означає: Нумеровані кроки й «internal» фази — це вивід у стилі BuildKit. Якщо ви бачите старий стиль «Step 1/…», ви не використовуєте BuildKit.
Рішення: Якщо вивід старий, увімкніть BuildKit через змінну середовища або конфіг демона перед тим, як щось робити. Інакше ви будете оптимізувати не той рушій.
Завдання 2: Надрукувати версію Docker і можливості сервера
cr0x@server:~$ docker version
Client: Docker Engine - Community
Version: 26.1.3
API version: 1.45
Server: Docker Engine - Community
Engine:
Version: 26.1.3
API version: 1.45 (minimum version 1.24)
Experimental: false
Що це означає: Можливості BuildKit залежать від версії рушія/buildx. Дуже старі рушії можуть бути «майже BuildKit», але без ключових функцій.
Рішення: Якщо ви відстаєте на кілька великих версій — оновіть першим ділом. Виправлення кешування на старому рушії — як налаштовувати карбюратор у машині, яка потребує заміни двигуна.
Завдання 3: Перевірити наявність buildx і поточний билдер
cr0x@server:~$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default docker
default default running v0.12.5 linux/amd64,linux/arm64
Що це означає: У вас є buildx і інстанс билдера. Версія BuildKit важлива для деяких експортів/імпортів кешу.
Рішення: Якщо buildx відсутній або билдер зламаний — виправте це. Віддалене кешування простіше з buildx.
Завдання 4: Виміряти розмір контексту збірки (мовчазний вбивця)
cr0x@server:~$ docker build --no-cache --progress=plain -t demo:ctx .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 2.01kB done
#2 [internal] load .dockerignore
#2 transferring context: 2B done
#3 [internal] load build context
#3 transferring context: 812.4MB 12.3s done
#3 DONE 12.4s
Що це означає: 812MB передачі контексту. Навіть якщо все кешується, ви витратили 12 секунд, перш ніж збірка почала робити реальну роботу.
Рішення: Виправте .dockerignore і/або використайте більш вузький шлях контексту. Не погоджуйтеся з «в мене на машині нормально»; CI платитиме більше.
Завдання 5: Перевірити, що в контексті збірки (швидка перевірка)
cr0x@server:~$ tar -czf - . | wc -c
853224921
Що це означає: Ваш контекст ~853MB у стисненому вигляді. Зазвичай це артефакти збірки, node_modules, virtualenv або тестові звіти, що просочилися всередину.
Рішення: Додайте виключення або збирайте з підкаталогу, що містить лише те, що потрібно образу.
Завдання 6: Перевірити hits кешу крок за кроком
cr0x@server:~$ docker build --progress=plain -t demo:cachecheck .
#7 [2/6] COPY package.json package-lock.json ./
#7 CACHED
#8 [3/6] RUN npm ci
#8 CACHED
#9 [4/6] COPY . .
#9 DONE 0.9s
#10 [5/6] RUN npm test
#10 DONE 34.6s
Що це означає: Інсталяція залежностей кешується; тести — ні (і, мабуть, не повинні). Ви відокремили «стабільні входи» від «змінних входів».
Рішення: Якщо дорогий крок інсталяції не кешується, змініть структуру Dockerfile або використайте cache mounts.
Завдання 7: Показати використання кешу на хості
cr0x@server:~$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 41 6 9.12GB 6.03GB (66%)
Containers 12 2 311MB 243MB (78%)
Local Volumes 8 3 1.04GB 618MB (59%)
Build Cache 145 0 3.87GB 3.87GB
Що це означає: Кеш збірки існує і має чималий розмір; «ACTIVE 0» натякає, що він не зафіксований поточними збірками. Цей кеш можуть агресивно чистити скрипти.
Рішення: Якщо кеш продовжує зникати, припиніть запуск «docker system prune -a» у CI-образах або на спільних ранерах без розуміння наслідків.
Завдання 8: Перевірити використання диска билдера BuildKit
cr0x@server:~$ docker buildx du
ID RECLAIMABLE SIZE LAST ACCESSED
m5p8xg7n0f3m2b0c6v2h7w2n1 true 1.2GB 2 hours ago
u1k7tq9p4y2c9s8a1b0n6v3z5 true 842MB 3 days ago
total: 2.0GB
Що це означає: Buildx має власну звітність. Якщо це порожнє на CI, у вас немає постійного кешу між запусками.
Рішення: Для ефермерних билдера плануйте віддалений експорт/імпорт кешу.
Завдання 9: Довести причину інвалідизації кешу (build args)
cr0x@server:~$ docker build --progress=plain --build-arg BUILD_ID=123 -t demo:arg .
#6 [2/5] RUN echo "build id: 123" > /build-id.txt
#6 DONE 0.2s
cr0x@server:~$ docker build --progress=plain --build-arg BUILD_ID=124 -t demo:arg .
#6 [2/5] RUN echo "build id: 124" > /build-id.txt
#6 DONE 0.2s
Що це означає: Цей крок ніколи не буде кешуватися при різних значеннях BUILD_ID. Якщо цей аргумент використовується рано, він інвалідирує все після нього.
Рішення: Перенесіть мінливі args в кінець або припиніть вбудовувати ідентифікатори збірки в шари файлової системи, якщо це не потрібно.
Завдання 10: Підтвердити, що витяг базового образу — вузьке місце
cr0x@server:~$ docker pull ubuntu:22.04
22.04: Pulling from library/ubuntu
Digest: sha256:4f2...
Status: Image is up to date for ubuntu:22.04
docker.io/library/ubuntu:22.04
Що це означає: «up to date» вказує, що образ уже присутній. Якщо pulls повільні і часті в CI, можливо, відсутній місцевий mirror реєстру або кеш поблизу ранерів.
Рішення: Якщо CI завжди тягне образи, розгляньте кешування базових образів на ранерах або використання реєстру ближче до ранерів.
Завдання 11: Збирати з явним експортом/імпортом кешу в локальний каталог
cr0x@server:~$ docker buildx build --progress=plain \
--cache-to type=local,dest=/tmp/bkcache,mode=max \
--cache-from type=local,src=/tmp/bkcache \
-t demo:localcache --load .
#11 [4/7] RUN apt-get update && apt-get install -y build-essential
#11 DONE 39.4s
#12 exporting cache to client directory
#12 DONE 0.6s
Що це означає: Ви експортували повторно використовуваний кеш у /tmp/bkcache. При наступному запуску дорогі кроки повинні відображатися як CACHED.
Рішення: Якщо локальний кеш допомагає, але CI все ще повільний, потрібен віддалений експорт кешу (реєстр/артефакт), а не лише локальний.
Завдання 12: Перевірити кеш на другому запуску (доказ, не відчуття)
cr0x@server:~$ docker buildx build --progress=plain \
--cache-from type=local,src=/tmp/bkcache \
-t demo:localcache --load .
#11 [4/7] RUN apt-get update && apt-get install -y build-essential
#11 CACHED
#13 exporting to docker image format
#13 DONE 1.1s
Що це означає: Дорогий apt-крок кешується. Ви довели, що механізм працює.
Рішення: Тепер можна інвестувати в віддалений кеш без ризику, бо ви знаєте, що Dockerfile кешується.
Завдання 13: Виявити недетермінованість під час встановлення пакетів
cr0x@server:~$ docker build --progress=plain -t demo:apt .
#9 [3/6] RUN apt-get update && apt-get install -y curl
#9 DONE 18.7s
cr0x@server:~$ docker build --progress=plain -t demo:apt .
#9 [3/6] RUN apt-get update && apt-get install -y curl
#9 DONE 19.4s
Що це означає: Він перебудував двічі; так може бути, якщо щось раніше інвалідовало шар, або ви використовували --no-cache, або вхідні файли файлової системи змінилися.
Рішення: Переконайтеся, що крок RUN apt-get ... йде після стабільних входів. Також розгляньте фіксацію версій пакетів або використання базового образу з потрібними інструментами вже всередині.
Завдання 14: Визначити «ADD .» занадто рано (тест інвалідизації кешу)
cr0x@server:~$ git diff --name-only HEAD~1
README.md
cr0x@server:~$ docker build --progress=plain -t demo:readme .
#6 [2/6] COPY . .
#6 DONE 0.8s
#7 [3/6] RUN npm ci
#7 DONE 45.1s
Що це означає: Зміна README викликала COPY . ., що інвалідувало npm ci, бо ви скопіювали весь репозиторій перед встановленням залежностей.
Рішення: Спочатку копіюйте лише манифести залежностей; решту — пізніше. Це не «дрібна оптимізація»; це різниця між 20s перебудовою і 2-хвилинною перебудовою.
Патерни Dockerfile, які роблять кеш реальним
BuildKit винагороджує дисципліну. Якщо ваш Dockerfile — як ящик для мотлоху, ваш кеш поводитиметься так само: повний, дорогий і не міститиме того, що вам потрібно.
Патерн 1: Розділяйте маніфести залежностей від коду
Це класичне виправлення, бо це найпоширеніша помилка. Ви хочете, щоб інсталяція залежностей була ключована по lockfile, а не по всіх файлах репозиторію.
cr0x@server:~$ cat Dockerfile
# syntax=docker/dockerfile:1.7
FROM node:20-bookworm AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
FROM node:20-bookworm-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
CMD ["node","dist/server.js"]
Чому це працює: дорогий крок npm ci залежить головним чином від package-lock.json. Зміна README.md не має перевстановлювати весь світ.
Патерн 2: Переносьте мінливі кроки наприкінці
Усе, що змінюється при кожній збірці — ідентифікатори, git SHA, штампування версії — має бути наприкінці. Інакше ви інвалідируєте весь downstream кеш.
Погано:
cr0x@server:~$ cat Dockerfile.bad
FROM alpine:3.19
ARG GIT_SHA
RUN echo "$GIT_SHA" > /git-sha.txt
RUN apk add --no-cache curl
Добре:
cr0x@server:~$ cat Dockerfile.good
FROM alpine:3.19
RUN apk add --no-cache curl
ARG GIT_SHA
RUN echo "$GIT_SHA" > /git-sha.txt
Чому це працює: Ви зберігаєте стабільні шари повторно використовуваними. Ваш штамп усе ще в образі, але він інвалідовує лише сам себе.
Патерн 3: Multi-stage builds як межі кешу
Multi-stage builds — це не лише про менші рантайм-образи. Вони дозволяють ізолювати «шум тулчейну» від «стабільності рантайму».
- Стадія збірки: компілятори, пакетні менеджери, кеші, заголовки.
- Стадія рантайму: компактна, стабільна, менше рухомих частин, менше інвалідизацій кешу.
Патерн 4: Розумно обирайте теги базових образів
Якщо ви використовуєте «плаваючі» теги, наприклад latest, ваш базовий образ може змінитися без редагування Dockerfile. Це інвалідизація кешу і проблема відтворюваності.
Використовуйте явні теги або, в середовищах з високими вимогами, фіксуйте образи за digest. Це політичний вибір: зручність проти відтворюваності.
Патерн 5: Мінімізуйте хаос від apt-get update
apt-get update часто стає джерелом непередбачуваної поведінки кешу. Не через те, що його не можна кешувати, а через те, що його зазвичай поміщають у шари, які інвалідуються незв’язаними змінами.
Також: завжди поєднуйте update і install в один шар. Розділені шари викликають застарілі індекси і загадкові 404 при відлагодженні.
Патерн 6: Тримайте контекст збірки чистим
Якщо ви не контролюєте свій контекст, ви не контролюєте свої ключі кешу. Використовуйте .dockerignore агресивно: артефакти збірки, папки залежностей, логи, звіти тестів, локальні env-файли і все, що виробляє CI.
Жарт №2: якщо ваш контекст збірки включає node_modules, ваш демон Docker майже займається кросфітом — піднімає важкі речі повторно без сенсу.
Віддалений кеш, що переживає CI: реєстр, локальний та ранери
Локальне кешування приємне. Віддалене кешування — те, що змушує команди припинити скаржитися в Slack. Якщо ваш CI-ранер ефермерний, він прокидається з амнезією кожного запуску. Це не моральна помилка — так проєктують ці системи.
Inline cache: ділитися підказками кешу через образ
Inline cache зберігає метадані кешу в конфігу образу, щоб подальші збірки могли тягнути образ і повторно використовувати шари. Це «мінімально життєздатний» шлях кешування між машинами.
Приклад збірки (push образу опущений; тут важлива механіка):
cr0x@server:~$ docker buildx build --progress=plain \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t demo:inlinecache --load .
#15 exporting config sha256:4c1d...
#15 DONE 0.4s
#16 writing image sha256:0f9a...
#16 DONE 0.8s
Що це означає: Образ тепер несе метадані кешу. Інший билдер може використати їх із --cache-from, вказавши посилання на той образ.
Рішення: Використовуйте inline cache, коли ви вже пушите образи і хочете простий шлях кешування. Це не завжди достатньо, але це надійний базовий рівень.
Реєстровий кеш: експортувати кеш окремо (потужніше)
BuildKit може експортувати кеш у посилання реєстру. Це часто краще за inline cache, бо може зберігати більше деталей і не вимагає «промоції» образу лише для використання його кешу.
cr0x@server:~$ docker buildx build --progress=plain \
--cache-to type=registry,ref=registry.internal/demo:buildcache,mode=max \
--cache-from type=registry,ref=registry.internal/demo:buildcache \
-t registry.internal/demo:app --push .
#18 exporting cache to registry
#18 DONE 2.7s
Що це означає: Кеш зберігається як OCI-артефакт у вашому реєстрі. Наступна збірка імпортує його навіть на свіжому ранері.
Рішення: Якщо CI ефермерний і збірки повільні, зазвичай це правильний крок. Компроміс — зростання використання сховища в реєстрі й іноді питання з правами доступу.
Локальний каталог кешу: добре для повторного використання на одному ранері
Експорт локального кешу підходить, коли у вас є стійкий ранер із робочим простором, що переживає запуски, або коли ви можете підключити персистентний том.
Це також гарний крок «доведі, що працює», перед тим як розбиратися з auth реєстру й політиками CI.
Вибір режиму кешу
mode=min: менший кеш, менше проміжних результатів. Підходить, коли зберігання обмежене.mode=max: повніший кеш, кращі показники попадань. Підходить, коли потрібна швидкість і є місце для зберігання.
На практиці: почніть із mode=max у CI на тиждень, спостерігайте за зростанням реєстру та показниками попадань, потім вирішіть, чи потрібно обмежувати.
Cache mounts, які справді прискорюють інсталяцію залежностей
Кеш шарів грубий інструмент. Менеджери залежностей тонкі. Вони хочуть каталог кешу, що зберігається між запусками, але ви не хочете випікати цей кеш у кінцевий образ. Cache mounts BuildKit — правильний інструмент.
npm / yarn / pnpm
Приклад npm показаний раніше. Суть: змонтуйте каталог кешу в /root/.npm (або у шлях кешу користувача) під час інсталяції.
apt
Можна кешувати apt-списки та архіви. Це корисно, якщо у вас повторні інсталяції між збірками і мережа CI не ідеальна. Це не панацея; метадані apt часто змінюються.
cr0x@server:~$ cat Dockerfile.aptcache
# syntax=docker/dockerfile:1.7
FROM ubuntu:22.04
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt/lists \
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
Операційна нотатка: кешування /var/lib/apt/lists може прискорити процес, але також може маскувати зміни в репозиторіях у неочікуваний спосіб при відлагодженні доступності пакетів. Використовуйте свідомо.
pip
Python-білди — класичні порушники: колеса завантажуються кожного разу, компіляція відбувається кожного разу, і хтось врешті «вирішує» це, копіюючи весь venv у образ (не робіть так).
cr0x@server:~$ cat Dockerfile.pipcache
# syntax=docker/dockerfile:1.7
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
COPY . .
CMD ["python","-m","app"]
Go
У Go є два ключові кеші: кеш модулів для завантажень і кеш збірки. BuildKit може зберігати їх без забруднення рантайм-образу.
cr0x@server:~$ cat Dockerfile.go
# syntax=docker/dockerfile:1.7
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
go build -o /out/app ./cmd/app
FROM gcr.io/distroless/base-debian12
COPY --from=build /out/app /app
CMD ["/app"]
Чому це важливо: кеш завантажень модулів зберігає мережевий трафік; кеш збірки зберігає CPU. Різні вузькі місця, один і той самий механізм.
Секрети, SSH і чому ваш кеш зникає
Приватні залежності — це місце, де «все працює локально» гине. Люди обходять це через ARG TOKEN=... і випадково випікають секрети в шари (погано) або ламають кеш так, що проблема з’являється лише в CI (також погано).
Використовуйте BuildKit secrets замість ARG для облікових даних
Секрети, змонтовані під час збірки, не зберігаються в шарах образу. Вони також не стають частиною ключа кешу. Це й перевага, і пастка: крок може бути кешований навіть якщо секрет змінився, тож переконайтеся, що вихід детермінований.
cr0x@server:~$ cat Dockerfile.secrets
# syntax=docker/dockerfile:1.7
FROM alpine:3.19
RUN apk add --no-cache git openssh-client
RUN --mount=type=secret,id=git_token \
sh -c 'TOKEN=$(cat /run/secrets/git_token) && echo "token length: ${#TOKEN}" > /tmp/token-info'
Рішення: Якщо потрібні приватні git-клони — використовуйте --mount=type=ssh з переспрямованим агентом або deploy-ключем, а не токен у ARG.
SSH mounts: безпечніші, але слід пам’ятати про відтворюваність
SSH mounts уникають випікання секретів. Вони також вводять новий режим відмови: ваша збірка стає залежною від мережі й стану віддаленого репозиторію. Фіксуйте коміти/теги для детермінованості, інакше кеш «правильно» інвалідовується змінами upstream.
Три корпоративні історії з фронту кешування
1) Інцидент через хибне припущення: «CI кешує Docker-шари за замовчуванням»
Середня компанія мігрувала CI з довгоживучих VM на ефермерні ранери. Міграція була представлена як плюс: чисті середовища, менше флейкових збірок, менше обслуговування дисків. Все правда. Але хтось припустив, що шаровий кеш Docker «просто буде» як на старих VM.
В перший день після міграції час збірок утраївся. Не трохи повільніше — настільки, що вікна деплою пропускали затвердження змін. Інженери відповіли як інженери: паралелізували задачі, додали більше ранерів, вклали гроші. Флот збірок виріс; збірки лишилися повільними.
Справжня причина була нудною. Старі VM мали розігріті кеші Docker. Нові ранери стартували порожніми кожного разу. Кожна збірка тягла базові образи, завантажувала залежності і перекомпілювала той самий код. Кеш існував, але випаровувався після кожної задачі.
Виправлення також було нудним: віддалений експорт/імпорт кешу через buildx з посиланням у реєстрі. За ніч час збірок знову повернувся до попереднього рівня. «Оптимізація» не була хитрим трюком у Dockerfile. Це було визнання інфраструктурної реальності: ефермерні ранери потребують зовнішнього стану, якщо ви хочете повторного використання.
2) Оптимізація, що обернулася проти: «Закешувати все в образі, щоб зробити збірки швидшими»
Інша організація мала збірку, яка виконувала pip install і довго працювала на ноутбуках розробників. Хтось вирішив «вирішити» це, скопіювавши весь pip-кеш і артефакти збірки в шар образу після інсталяції. Це спрацювало — в вузькому сенсі. Перебудови були швидкі на тій машині.
Потім образ почав роздуватися. Він став несумісним: двоє інженерів отримували різні образи з одного й того самого коміту, бо кеші містили платформозалежні колеса і залишки збірки. QA виявив «працює лише в стагінгу», що не відтворювалося локально.
Безпека підключилася, бо кеш містив завантажені артефакти, що не відслідковувалися в lockfile, ускладнюючи аналіз походження. В результаті компанія отримала швидку збірку і повільний процес реагування на інциденти. Це некорисний компроміс.
Відкат був болючим. Вони перейшли на BuildKit cache mounts для pip, тримали рантайм-образи компактними і впровадили політику фіксації залежностей. Збірки лишилися швидкими, а артефакти — детермінованими для відлагодження.
3) Нудна, але правильна практика, яка врятувала ситуацію: «Ключі кешу базуються на lockfile, а не на стані репозиторію»
Під час напруженого кварталу одна команда випустила багато дрібних змін: конфіг, текст, feature flags. Їхні сервісні збірки були Node-базовані і важкі по залежностях, але пайплайн залишався стабільним.
Причина була не в геройстві. Місяць тому хтось рефакторнув Dockerfile у службах: спочатку копіювати лише маніфести залежностей, запускати інсталяцію, а вже потім копіювати код додатку. Також ввели правило: зміни в lockfile вимагають окремого рев’ю.
Тож коли прийшли дрібні не-кодні зміни, більшість збірок потрапила в кеш на етапі інсталяції залежностей і базових шарів. CI все одно будував і тестував, але не перезавантажував інтернет. Черга деплоїв залишалася короткою навіть при великому обсязі комітів.
Ця практика не вражає на демо. Вона не потрапляє на слайд. Але вона запобігла передбачуваному режиму відмови: «дрібна зміна, велика вартість збірки». В операціях нудне часто — найкраща похвала.
Поширені помилки: симптом → корінь → виправлення
1) Симптом: «Кожна збірка повторно виконує інсталяцію залежностей»
Корінь: Ви копіюєте весь репозиторій перед інсталяцією залежностей, тому будь-яка зміна файлу інвалідовує шар.
Виправлення: Копіюйте лише package-lock.json/requirements.txt/go.sum спочатку, виконайте інсталяцію/завантаження, потім копіюйте решту.
2) Симптом: «CI завжди повільний, локально — нормально»
Корінь: CI-ранери ефермерні; локальна машина має розігрітий кеш.
Виправлення: Використовуйте docker buildx build з --cache-to/--cache-from (реєстр або сховище артефактів). Inline cache допомагає, але не завжди достатньо.
3) Симптом: «Передача контексту займає вічність»
Корінь: Величезний контекст (node_modules, dist, логи, .git) або нестабільні файли включені.
Виправлення: Звужуйте .dockerignore. Збирайтеся з більш вузького каталогу. Не надсилайте всесвіт демону.
4) Симптом: «Кеш працює локально, а в CI промахи навіть з віддаленим кешем»
Корінь: Різні build args/платформи/таргети або різні digest базових образів. Ключі кешу розходяться.
Виправлення: Уніфікуйте build args, платформу (--platform) і таргети. Фіксуйте базові образи. Переконайтеся, що CI імпортує той самий кеш, який експортує.
5) Симптом: «Кеш працює поки хтось не запустить cleanup job»
Корінь: Агресивне прибирання видаляє кеш збірки (docker system prune -a), або ранери скидають сховище.
Виправлення: Припиніть бездумно знищувати кеши. Використовуйте цілеспрямовані політики очищення і покладайтеся на віддалений кеш для CI, якщо диски ранерів нестійкі.
6) Симптом: «Збірка повільна, навіть коли все позначено як CACHED»
Корінь: Ви витрачаєте час на тягнення базових образів, експорт образів, компресію шарів або завантаження в демон.
Виправлення: Вимірюйте: шукайте час у «exporting», «writing image» та pulls. Розгляньте використання --output type=registry у CI замість --load, якщо локальний образ не потрібен.
7) Симптом: «Випадкові промахи кешу»
Корінь: Недетерміновані команди (apt-get update без стабільного порядку, непіновані залежності), таймстемпи у виходах або згенеровані файли, включені рано.
Виправлення: За можливості зробіть кроки детермінованими, фіксуйте версії і ізолюйте згенеровані артефакти до пізніших стадій.
8) Симптом: «Ми увімкнули inline cache і нічого не змінилося»
Корінь: Ви фактично не імпортували кеш (--cache-from), або образ недоступний/не витягнутий билдом, або ви будуєте іншу платформу.
Виправлення: У CI явно імпортуйте кеш. Перевірте логи, що кроки позначаються як CACHED і що билдер має доступ до посилання кешу.
Чеклісти / покроковий план
Чекліст A: Прискорити локальні збірки (одна машина)
- Увімкніть BuildKit і використовуйте
--progress=plainдля бачення поведінки кешу. - Позрізайте контекст збірки до мінімуму за допомогою
.dockerignore. - Реструктуруйте Dockerfile: копіюйте манифести → інсталюйте залежності → копіюйте код.
- Використовуйте cache mounts для менеджерів залежностей (
npm,pip,go, принагідноapt). - Перенесіть мінливі args/штампи в кінець.
- Запустіть дві підряд збірки і підтвердіть, що дорогі кроки позначені як
CACHED.
Чекліст B: Прискорити CI-збірки (епермерні ранери)
- Переконайтеся, що CI використовує buildx і вивід BuildKit (не legacy).
- Оберіть бекенд віддаленого кешу: реєстр зазвичай найпростіший з операційної точки зору.
- Додайте
--cache-toі--cache-fromу CI-збірки. - Уніфікуйте
--platformміж CI-джобами; не міксуйте amd64 і arm64 кеші, якщо це не навмисно. - Фіксуйте базові образи за стабільними тегами (або digest там, де потрібно).
- Слідкуйте за часом експорту образів; віддавайте перевагу пушу безпосередньо з buildx замість
--loadу CI. - Встановіть політику зберігання/очищення кешу в реєстрі; неконтрольований ріст кешу — повільний віддалений збій.
Чекліст C: Коли стоїть займатися оптимізацією
- Якщо ваша збірка <30 секунд і трапляється рідко — не починайте кампанію з кешування.
- Якщо збірка блокує мерджі, деплои або вирішення інцидентів — ставте кешування як роботу з надійності.
- Якщо інсталяція залежностей виконується щоразу — виправте це першочергово; це найвищий ROI у більшості стеків.
FAQ
1) Чому зміна README викликає повну перебудову?
Тому що ви скопіювали весь репозиторій в образ перед дорогими кроками. Ключі кешу включають файли. Копіюйте менше, пізніше.
2) Чи те саме кешування BuildKit і кеш шарів Docker?
Шарове кешування — один з механізмів. BuildKit узагальнює збірки в граф і додає cache mounts, віддалений експорт/імпорт кешу та покращену паралелізацію.
3) Чи завжди треба використовувати --mount=type=cache?
Використовуйте його для кешів менеджерів залежностей і кешів компілятора. Не застосовуйте, щоб приховувати недетермінованість або «прискорювати тести» повторним використанням застарілих результатів.
4) У чому різниця між inline cache і registry cache?
Inline cache зберігає метадані кешу в самому образі. Registry cache експортує кеш як окремий артефакт. Registry cache гнучкіший і ефективніший для CI.
5) Мій CI використовує docker build. Чи потрібен buildx?
Деякі переваги можна отримати з docker build, якщо BuildKit увімкнений, але buildx значно полегшує віддалене кешування і мультиплатформні збірки.
6) Чому мої попадання кешу зникають після очищення Docker?
Бо хтось чистить кеш збірки або ранер — ефермерний. Виправте політику очищення або покладайтесь на віддалений кеш замість локального стану.
7) Чи використання секретів відключає кешування?
Самі секрети не є частиною ключа кешу. Але команди, які використовують секрети, можуть бути недетермінованими або залежати від змінних зовнішніх джерел, що й викликає перебудову.
8) Чи можна ділитися кешем між архітектурами (amd64 і arm64)?
Безпосередньо — ні. Ключі кешу включають платформу. Ви можете зберігати кеші для декількох платформ під тим самим посиланням, але вони будуть окремими записами.
9) Чому експорт образу повільний, навіть коли все CACHED?
Бо експорт усе одно має зібрати шари, стиснути і записати/запушити їх. Якщо CI не потребує локального образу, пуште прямо й уникайте --load.
10) Коли варто фіксувати базові образи по digest?
Коли відтворюваність і контроль ланцюга постачань важливіші за зручність. Digest зменшує несподівані інвалідизації кешу і «той самий Dockerfile — різний образ» ситуації.
Висновок: наступні кроки, які окуповуються
Зробіть це нудним. Нудне — швидке.
- Запустіть збірку з
--progress=plainі занотуйте, який крок повільний і чи вінCACHED. - Спочатку виправте розмір контексту збірки. Це податок, який ви платите перед тим, як кеш отримає слово.
- Реструктуруйте Dockerfile так, щоб інсталяції залежностей залежали від lockfile, а не від усього репозиторію.
- Додайте cache mounts для менеджерів залежностей, щоб припинити перезавантаження та повторну компіляцію.
- Якщо CI ефермерний — реалізуйте віддалений експорт/імпорт кешу. Інакше ви оптимізуєте кеш, який випаровується після успіху.
- Уніфікуйте build args, платформу і політику базових образів, щоб ключі кешу збігалися в різних середовищах.
Якщо ви зробите ці шість речей, ви припините «оптимізувати збірки Docker» і почнете керувати системою збірки, яка поводиться як продакшен: передбачувано, вимірювано і швидко з правильних причин.