Пошук уроків, статей та іншого контенту
Розберіться з шарами образів, повторним використанням даних і кешуванням під час створення та завантаження образів.
Docker-образ складається з послідовності незмінних файлових шарів. Кожен шар містить зміни файлової системи порівняно з попереднім станом:
додані файли;
змінені файли;
видалені файли, представлені спеціальними маркерами видалення;
метадані, пов’язані з відповідною інструкцією.
Під час запуску контейнера Docker додає поверх образу окремий записуваний шар контейнера. Образ при цьому не змінюється.
Спрощена структура образу:
базовий образ
└── шар встановлення системних пакетів
└── шар копіювання файлів залежностей
└── шар встановлення залежностей
└── шар копіювання коду застосункуШари є незмінними та можуть використовуватися повторно різними образами й контейнерами.
Наприклад, якщо два образи використовують однаковий базовий образ node:22-alpine, Docker не зберігатиме його двічі. Обидва образи посилатимуться на ті самі шари.
Розглянемо Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]Під час створення образу Docker обробляє інструкції послідовно:
FROM додає шари базового образу.
WORKDIR встановлює робочий каталог.
Перша COPY додає файли опису залежностей.
RUN npm ci створює шар із встановленими залежностями.
Друга COPY додає код застосунку.
CMD визначає команду запуску контейнера.
Не кожна інструкція обов’язково створює окремий файловий шар. Зокрема, CMD, ENTRYPOINT, ENV та WORKDIR переважно змінюють конфігурацію образу. Проте всі інструкції впливають на результат побудови та на перевірку кешу.
Щоб переглянути шари образу, використовуйте:
docker image history my-node-appПриклад виводу:
IMAGE CREATED CREATED BY SIZE
... seconds ago COPY . . 12kB
... seconds ago RUN npm ci 84MB
... seconds ago COPY package*.json ./ 2kB
... seconds ago WORKDIR /app 0B
... days ago /bin/sh -c #(nop) CMD [...] 0B
... days ago /bin/sh -c #(nop) FROM ... 0BФактичний вивід залежить від Dockerfile та версії Docker.
docker buildПід час побудови образу Docker намагається використати вже створені результати попередніх побудов. Якщо для певного кроку знайдено відповідний кеш, Docker не виконує цей крок повторно.
Під час перевірки Docker враховує:
базовий образ;
текст інструкції;
файли, які використовуються в COPY або ADD;
попередні шари;
деякі параметри побудови.
Коли кеш більше не можна використати для певного кроку, цей крок виконується заново. Наступні кроки також зазвичай виконуються заново, оскільки вони залежать від нового результату попереднього кроку.
Нехай Dockerfile має такий вигляд:
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
CMD ["node", "server.js"]Якщо змінити лише server.js, Docker може повторно використати FROM, WORKDIR і, можливо, знайти кеш для COPY . . лише якщо набір файлів не змінився. Але оскільки COPY . . включає server.js, цей крок стане недійсним. RUN npm ci також буде виконано знову, хоча файли залежностей не змінювалися.
Це зайва робота. Краще спочатку копіювати файли залежностей, а код — окремим кроком:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]Тепер зміна server.js не змінює шар із package.json і package-lock.json. Docker повторно використає кеш для RUN npm ci та виконає лише наступний COPY.
Створімо невеликий Node.js-застосунок.
package.json:
{
"name": "docker-cache-example",
"version": "1.0.0",
"private": true,
"dependencies": {
"express": "^5.1.0"
}
}server.js:
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
app.get("/", (_request, response) => {
response.send("Привіт із Docker");
});
app.listen(port, () => {
console.log(`Сервер запущено на порту ${port}`);
});Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]Для цього прикладу спочатку потрібно створити файл блокування залежностей:
npm install --package-lock-onlyПобудуйте образ:
docker build -t docker-cache-example .Під час першої побудови Docker виконає всі кроки. Запустіть побудову ще раз:
docker build -t docker-cache-example .Більшість кроків буде взято з кешу. Якщо змінити тільки текст відповіді в server.js і знову виконати docker build, кроки копіювання файлів залежностей та npm ci можуть бути повторно використані.
Запустіть контейнер:
docker run --rm -p 3000:3000 docker-cache-exampleПісля цього застосунок буде доступний на порту 3000 хоста.
Порядок інструкцій впливає на ефективність кешу. Часто змінювані файли варто копіювати пізніше, а стабільні файли — раніше.
Невдалий варіант:
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ciТут будь-яка зміна в контексті, наприклад у файлі документації, може змінити результат COPY . . і змусити Docker виконати npm ci повторно.
Кращий варіант:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .Переваги:
залежності перевстановлюються лише після зміни файлів залежностей;
зміни вихідного коду не руйнують кеш npm ci;
повторні побудови виконуються швидше.
Цей принцип можна застосовувати до інших менеджерів пакетів:
спочатку копіювати package.json і lock-файл для Node.js;
спочатку копіювати requirements.txt для Python;
спочатку копіювати pom.xml або build.gradle для Java;
спочатку копіювати файли проєкту, необхідні для відновлення залежностей у .NET.
.dockerignoreКоманда:
docker build -t docker-cache-example .передає Docker поточний каталог як контекст побудови. Файли з контексту можуть використовуватися в інструкціях COPY і ADD.
Якщо контекст містить зайві файли, це може:
збільшити обсяг даних, які передаються Docker;
уповільнити побудову;
змінювати кеш інструкції COPY . . без потреби;
випадково додати секрети або локальні залежності до образу.
Створіть .dockerignore:
node_modules
.git
.env
npm-debug.log
Dockerfile
.dockerignoreФайли, виключені через .dockerignore, не потрапляють до контексту побудови. Винятки залежать від структури проєкту, тому список потрібно підлаштовувати під конкретний застосунок.
Docker повторно використовує шари на кількох рівнях.
Якщо два образи мають однаковий базовий образ або однакові результати попередніх кроків, спільні шари можуть зберігатися один раз.
Попередній результат docker build може бути використаний під час наступної побудови, якщо відповідний крок не змінився.
Коли Docker завантажує образ із реєстру, він перевіряє, які шари вже є локально. Відсутні шари завантажуються, а наявні повторно не передаються.
Наприклад, якщо локально вже є базові шари node:22-alpine, завантаження іншого образу на цій самій базі може потребувати лише його додаткових шарів.
Перелік локальних образів:
docker image lsОрієнтовний розмір образу:
docker image inspect docker-cache-exampleКоманда docker image inspect повертає JSON із метаданими образу, зокрема посиланнями на його шари та конфігурацією.
Якщо файл додано в одному шарі, а в наступному видалено, його дані можуть залишатися в попередньому шарі. Видалення робить файл невидимим у підсумковій файловій системі контейнера, але не обов’язково зменшує розмір уже створеного шару.
Неоптимальний варіант:
FROM alpine:3.22
COPY large-archive.tar /tmp/large-archive.tar
RUN tar -xf /tmp/large-archive.tar -C /opt/app \
&& rm /tmp/large-archive.tarАрхів потрапляє в шар COPY. Наступний шар видаляє файл із поточного стану файлової системи, але дані архіву можуть залишатися в образі.
Якщо файл потрібен лише під час одного кроку, краще виконати створення та видалення в одній інструкції:
FROM alpine:3.22
COPY large-archive.tar /tmp/large-archive.tar
RUN tar -xf /tmp/large-archive.tar -C /opt/app \
&& rm /tmp/large-archive.tarУ цьому прикладі проблема з архівом у COPY все одно залишається: він уже був доданий окремим шаром. Щоб не додавати непотрібний проміжний файл до фінального образу, для складніших сценаріїв застосовують окремий етап побудови та копіюють до фінального образу лише потрібний результат.
Головний принцип: операції зі створенням і видаленням тимчасових файлів краще об’єднувати в одному RUN, але це не виправляє файли, які вже були додані попередньою інструкцією COPY.
RUN і зовнішні зміниDocker може повторно використати кеш для інструкції RUN, якщо сама інструкція та попередній стан образу не змінилися. Docker не обов’язково знає, що зовнішній ресурс змінився.
Наприклад:
FROM alpine:3.22
RUN apk add --no-cache curlЯкщо цей крок уже був закешований, наступна побудова може використати старий результат, навіть якщо в репозиторії пакетів з’явилася новіша версія, дозволена вказаними обмеженнями.
Щоб навмисно не використовувати кеш:
docker build --no-cache -t docker-cache-example .Щоб примусово отримати новішу версію базового образу:
docker build --pull -t docker-cache-example .Ці параметри мають різне призначення:
--no-cache не використовує кеш для кроків побудови;
--pull перевіряє наявність новішої версії базового образу;
разом вони змушують побудову почати з актуального базового образу та виконати кроки заново.
Тег, наприклад node:22-alpine, є змінним покажчиком. З часом він може посилатися на інший образ, тому для відтворюваних побудов важливо контролювати версії базових образів.
Навіть якщо тег не змінився у вашому Dockerfile, локально може залишатися стара версія базового образу. Параметр --pull дає змогу перевірити оновлення перед побудовою:
docker build --pull -t docker-cache-example .Повне оновлення залежить від того, як у проєкті зафіксовані версії пакетів і базового образу. Lock-файли допомагають зменшити відмінності під час повторних побудов, але не замінюють контроль версії самого базового образу.
Під час docker pull образ завантажується не як один монолітний файл. Реєстр зберігає його шари окремо.
Якщо локально вже присутні деякі шари, Docker не завантажує їх повторно. Це особливо корисно, коли кілька образів використовують одну базу:
docker pull node:22-alpine
docker pull інший-образ-на-node-alpineУ другому випадку спільні шари можуть бути взяті з локального сховища, а завантажаться лише відмінності.
Так само під час завантаження нової версії образу Docker повторно використовує незмінені шари. Якщо змінився лише шар із кодом застосунку, базові шари та шари залежностей можуть не завантажуватися повторно.
Старі образи, проміжні шари та кеш побудови можуть займати місце на диску.
Переглянути використання диска Docker:
docker system dfОчистити невикористаний кеш побудови:
docker builder pruneПеред очищенням Docker попросить підтвердження. Не слід автоматично видаляти весь кеш у робочому середовищі: це звільняє місце, але наступні побудови виконуватимуться повільніше.
COPY . .
RUN npm ciЗміна будь-якого файлу, який потрапляє до контексту, може змусити повторно встановлювати залежності.
Краще розділяти файли залежностей і код:
COPY package.json package-lock.json ./
RUN npm ci
COPY . ..dockerignoreДо контексту можуть потрапити:
node_modules;
каталог .git;
файли .env;
журнали;
локальні результати побудови.
Це збільшує контекст і може додати конфіденційні дані до образу.
Інструкція RUN rm ... не видаляє дані з уже створеного шару. Вона лише додає новий результат, у якому файл більше не видимий.
--no-cacheПостійна побудова з --no-cache не дає Docker повторно використовувати незмінні шари та суттєво сповільнює процес.
Якщо стабільні залежності та код застосунку копіюються одним кроком, кеш буде інвалідований частіше, ніж потрібно.
Docker-образ складається з послідовності незмінних шарів.
Шари можуть повторно використовуватися різними образами та побудовами.
Зміна одного кроку зазвичай робить недійсним кеш цього та наступних кроків.
Стабільні файли, зокрема файли залежностей, варто копіювати раніше.
Код, який часто змінюється, краще копіювати ближче до кінця Dockerfile.
.dockerignore зменшує контекст побудови та допомагає зберігати кеш.
Видалення файлу в наступному шарі не обов’язково прибирає його дані з попереднього шару.
--no-cache вимикає кеш побудови, а --pull перевіряє нову версію базового образу.
Під час docker pull Docker завантажує лише ті шари, яких ще немає локально.