☰ Конспект ← Курс
ЛЕКЦІЯ 3

ОБРАЗ ЯК ШАРИ, DOCKERFILE, КЕШ, БАГАТОЕТАПНА ЗБІРКА

Від інструкцій у тексті до бінарних шарів на диску

ВТФК · Системне програмування · 2026

План лекції

  • Образ = стек шарів (не файл)
  • Dockerfile: інструкції → шари
  • Кеш збірки: чому порядок інструкцій має значення
  • OverlayFS: як шари об'єднуються в одну ФС
  • Тег і digest: ім'я проти вмісту
  • Багатоетапна збірка та scratch
  • Демо: docker history

Фокус: образ = шари

Образ — це не файл, а стек шарів

  • Кожен шар — diff файлової системи відносно попереднього шару
  • Шари незмінні (immutable) й адресуються за вмістом — за SHA-256, не ім'ям
  • Спільний базовий шар (ubuntu) не дублюється між образами — економія диска й мережі
ОБРАЗ = СТЕК НЕЗМІННИХ ШАРІВ RW шар контейнераdocker runCOPY . /appDockerfileRUN npm ciDockerfileFROM node:22базовий образ кеш: незмінені нижні шари не перезбираються

Інструкції Dockerfile: які створюють шар

ІнструкціяШар з файлами?Призначення
FROMтак (базові шари)точка старту — базовий образ
RUNтаквиконує команду, фіксує diff ФС
COPYтакфайли з контексту збірки → образ
ADDтакяк COPY + архіви й URL
WORKDIR ENV ARGні — метаданікаталог, змінні середовища / збірки
CMD ENTRYPOINTні — метаданікоманда при старті контейнера
EXPOSE LABELні — метаданідокументація портів, мітки

Метадані живуть у конфігурації образу й розмір не збільшують

COPY ↔ ADD

COPY
  • Проста, передбачувана копія файлів
  • Лише з контексту збірки
  • Надійно кешується за вмістом
  • Типовий вибір
vs
ADD
  • Копія + автоматичне розпакування tar
  • Уміє тягнути файли за URL
  • URL-джерела кешуються ненадійно
  • Лише коли справді потрібне розпакування

Правило: COPY за замовчуванням, ADD — свідомий виняток

Мінімальний Dockerfile

dockerfile
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl
COPY app.sh /app.sh
CMD ["/app.sh"]

Три змістовні шари: базовий образ, встановлений curl, скопійований файл

Той самий Dockerfile як шари

FROM ubuntu:24.04базові шари · ~80 МБ
RUN apt-get …шар: +curl і залежності
COPY app.shшар: +1 файл
CMDметадані, без шару

docker history покаже кожен із них окремим рядком із розміром

Кеш збірки: чому порядок важливий

  • Docker кешує шар за хешем інструкції та вмісту її вхідних файлів
  • Збіг хеша — шар береться з кешу, інструкція не виконується
  • Один змінений шар інвалідує ВСІ наступні — кеш ламається каскадно
  • Правило: рідкозмінне (залежності) — на початок, мінливе (код) — у кінець

Як Docker вирішує: кеш чи перебудова

  1. 1
    Хеш інструкції + входутекст RUN або вміст файлів COPY/ADD
  2. 2
    Є такий шар у кеші?так — беремо готовий, далі до наступної
  3. 3
    Перша розбіжністьінструкція виконується заново → новий шар
  4. 4
    Каскадусі наступні шари перебудовуються без перевірки
  5. 5
    Висновокчим нижче зміна в Dockerfile — тим дешевша збірка

Кеш шарів: ціна одного рядка не на своєму місці

КЕШ ШАРІВ: ЗМІНА ЛАМАЄ ВСЕ, ЩО НИЖЧЕ ЗМІНИВСЯ ЛИШЕ КОД FROM rust:1.82 CACHED COPY Cargo.toml CACHED RUN cargo fetch CACHED COPY src/ REBUILD RUN cargo build REBUILD 2 c: залежності з кешу COPY src/ СТОЇТЬ ПЕРШИМ FROM rust:1.82 CACHED COPY src/ REBUILD COPY Cargo.toml REBUILD RUN cargo fetch REBUILD RUN cargo build REBUILD 3 хв: усе з нуля щоразу

Правило: спершу те, що змінюється рідко (маніфест залежностей), потім код

Порядок інструкцій і кеш

dockerfile
# погано: COPY усього коду перед install ламає кеш на кожній зміні коду
COPY . .
RUN npm install

# добре: спершу залежності — кешуються, доки package.json не змінився
COPY package*.json .
RUN npm install
COPY . .

COPY package.json окремо від решти коду зберігає кеш npm install

BuildKit і кеш-монтування

  • BuildKit — сучасний бекенд збірки Docker (типовий з версії 23+)
  • --mount=type=cache зберігає каталог МІЖ збірками, навіть коли шар перебудовано
  • Класика: кеш реєстру cargo, npm чи pip, що не потрапляє в шар
  • docker build --target=<stage> зупиняє збірку на проміжному етапі

Кеш-монтування на практиці

dockerfile
FROM rust:1.80 AS build
WORKDIR /src
COPY . .
RUN --mount=type=cache,target=/root/.cargo/registry \
    cargo build --release

Кеш cargo-реєстру переживає перебудову шару, навіть якщо код змінився

Об'єднана файлова система (*OverlayFS*): чотири каталоги

lowerdirшари образу, лише читання; може бути кілька
upperdirшар запису контейнера; порожній на старті
workdirслужбовий каталог для атомарних операцій
mergedоб'єднаний вигляд — корінь ФС для процесу

Запис у контейнер ніколи не змінює образ — лише upperdir

OverlayFS: шари, верхній рівень і whiteout

OVERLAYFS: ШАРИ, ВЕРХНІЙ РІВЕНЬ, WHITEOUT lower ubuntu:24.04 lower apt-get install lower COPY app upper (запис контейнера) read-only read-write merge MERGED · те, що бачить процес /app/main /etc/passwd /tmp/cache (новий) /var/log/old нижні шари спільні для всіх контейнерів образу whiteout: «видалено» лише у верхньому шарі незмінні шаришар записувигляд процесу

Видалення файлу в контейнері не торкається образу — у верхньому шарі з'являється whiteout

Copy-on-write в OverlayFS

  1. 1
    Читанняфайл береться з найвищого шару, де він є
  2. 2
    Зміна файлу з lowerdircopy-up: копія в upperdir, зміна там
  3. 3
    Новий файл (.pixel_was_here)одразу в upperdir, шари образу не чіпає
  4. 4
    Видалення файлу образуwhiteout-позначка в upperdir; місце не звільняється
  5. 5
    docker rmupperdir відкинуто — образ незайманий

Живе демо: docker history

  1. docker build -t demo:1 .
  2. docker history demo:1
  3. Показати розмір кожного шару окремо
  4. docker inspect demo:1 | grep -A5 RootFS — список шарів за хешами

Очікувано: Студенти бачать: "вага" образу — це сума конкретних шарів, і видно, яка саме інструкція додала найбільше мегабайтів

docker inspect: digests і шари

bash
docker inspect demo:1 | grep -A5 RootFS
docker pull nginx@sha256:8f9c...
# digest — SHA-256 маніфесту: незмінний ідентифікатор вмісту,
# на відміну від тегу (latest, 1.27), який можна перепризначити

Digest ≠ тег: тег можна перезаписати, digest — криптографічно незмінний

Тег ↔ digest

Тег (nginx:1.27, latest)
  • Зручне людське ім'я
  • Змінний: власник може перепризначити
  • latest — просто тег за замовчуванням
  • Для розробки й читабельності
vs
Digest (sha256:8f9c…)
  • SHA-256 маніфесту образу
  • Незмінний: інший вміст — інший хеш
  • Гарантує той самий вміст завтра
  • Для production і відтворюваних збірок

docker pull nginx@sha256:… — точний вміст, а не «поточний latest»

Вага образу в цифрах

1.5 ГБ
образ з компілятором усередині
rust:1.80 + вихідний код + кеш
~80 МБ
ubuntu:24.04
без ядра, локалей і документації
~5 МБ
alpine
musl libc + apk + shell
0 Б
scratch
жодного шару — лише твій бінарник

Проблема: інструменти збірки потрібні лише під час збірки, а лишаються назавжди

Багатоетапна збірка (*multi-stage build*)

FROM rust AS buildкомпілятор, cargo, код
cargo build --releaseартефакт: один бінарник
COPY --from=buildпереносимо лише /app
FROM scratchфінальний образ: кілька МБ

Кожен FROM — окремий ізольований етап; проміжні у фінальний образ не потрапляють

Що саме переходить у фінальний образ

БАГАТОЕТАПНА ЗБІРКА: ІНСТРУМЕНТИ ЛИШАЮТЬСЯ ПОЗАДУ ЕТАП 1 · AS build rust:1.82 компілятор, cargo, git src/ Cargo.toml target/ (2.7 GB сміття) target/release/app 1.4 ГБ — і жодного байта в проді COPY --from=build ЕТАП 2 · РУНТАЙМ scratch / distroless ні оболонки, ні пакетів /app усе, що тут є, — один статичний бінарник 4 МБ · нічим зламати менший образ = менша поверхня атаки й швидший запуск

Перевірка на розумність: чи є у фінальному образі компілятор? Якщо так — етапів мало

Багатоетапна збірка

dockerfile
FROM rust:1.80 AS build
WORKDIR /src
COPY . .
RUN cargo build --release

FROM scratch
COPY --from=build /src/target/release/app /app
ENTRYPOINT ["/app"]

Компілятор Rust лишається в build-етапі; у фінальному образі — лише бінарник

Базовий образ для фінального етапу

БазаРозмірShell / libcКоли обирати
scratch0 Бнемає / немаєстатично злінкований бінарник (Rust, Go)
distroless~2–20 МБнемає / glibcдинамічний бінарник без shell
alpine~5 МБsh / muslпотрібен shell або apk для діагностики
ubuntu:24.04~80 МБbash / glibcповна сумісність, apt

musl (alpine) інколи поводиться інакше за glibc — не завжди безкоштовна заміна

Лише інструкції RUN, COPY та ADD створюють шари. Інші інструкції створюють тимчасові проміжні образи й не збільшують розмір збірки.

— Docker Docs, «Best practices for writing Dockerfiles»

Наперед: кілька контейнерів разом

  • Сьогодні — один образ, один контейнер
  • Далі в лабораторних: два контейнери, ізольована мережа, спільний том, секрети
  • Питання наперед: як контейнери знаходять один одного за іменем, а не за IP?

Наперед: мережа між контейнерами (ЛР3)

МЕРЕЖА DOCKER: BRIDGE, VETH-ПАРИ, DNS ЗА ІМЕНАМИ ХОСТ · мережевий простір хоста КОНТЕЙНЕР storage eth0 172.19.0.2 КОНТЕЙНЕР agent eth0 172.19.0.3 docker0 / br-… віртуальний комутатор veth veth DNS 127.0.0.11 storage → 172.19.0.2 звертайся іменем, не IP NAT -p 8080:80 єдина хвіртка назовні усе інше — глухо user-defined мережа = приватний квартал: свій DNS, свій діапазон, назовні — лише через -p

Деталі — на наступній лекції та в ЛР3; тут важливо: імена, а не IP, і -p як єдина хвіртка

Підводні камені: шари, кеш і збірка

  • COPY . . на початку Dockerfile ламає кеш при будь-якій зміні в репозиторії
  • Без .dockerignore контекст збірки роздувається: node_modules, .git
  • scratch несумісний з динамічним лінкуванням — потрібен static linking
  • Багато дрібних RUN замість одного через && множить шари й вагу

Підсумок

  • Образ = стек незмінних, адресованих за вмістом шарів; контейнер додає шар для запису
  • Порядок інструкцій у Dockerfile визначає ефективність кешу збірки
  • Багатоетапна збірка + scratch дають мінімальний образ виконання
  • docker history — інструмент, щоб побачити "вагу" кожної інструкції

🥚 У прикладі OverlayFS новий файл названо .pixel_was_here — він існує лише в upperdir і зникає разом із контейнером. Цей рядок у файловій системі ще трапиться.

VTFK · TERMLINK · L03← → · space · f — на весь екран