Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Поясніть поняття Docker layers (шари) та Union File System.». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Docker layers** це read-only delta файлової системи. Кожна Dockerfile-інструкція додає один шар; image це стек шарів. **Union File System** (OverlayFS у сучасному Docker) зливає ці шари у єдину в'юху, плюс один writable-шар зверху для запущеного container. ``` +--------------------------+ | writable layer (container) | <- зміни тут помирають з container +--------------------------+ | layer 4: COPY app/ /app | <- доданий Dockerfile +--------------------------+ | layer 3: RUN npm ci | +--------------------------+ | layer 2: WORKDIR /app | +--------------------------+ | layer 1: FROM node:22 | +--------------------------+ ``` **Головне:** шари дедуплікуються і кешуються. Два image, що поділяють ту саму Node-основу, тримають цю основу один раз. Переупорядкуй Dockerfile так, щоб повільні стабільні інструкції лягли у нижні (кешовані) шари; дешеві швидко мінливі йшли зверху.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Docker layers і Union File System** це те, як Docker перетворює послідовність Dockerfile-інструкцій на один runnable image, тримаючи використання диску і час білду розумними. Уся історія image-кешування стоїть на цьому дизайні. ## Теорія ### TL;DR - Кожна Dockerfile-інструкція зазвичай дає один **шар**: read-only diff файлової системи після виконання інструкції. - **Image** це впорядкований стек шарів + config blob. Шари незмінні і ідентифіковані SHA256-hash контенту. - **Union File System** (OverlayFS у сучасному Docker) зливає ці read-only шари плюс один **writable-шар** у єдину в'юху файлової системи, що бачить container. - Шари **дедуплікуються**: десять image, що поділяють `python:3.13`, тримають цю основу на диску один раз. - Запис всередині запущеного container іде у writable-шар через **copy-on-write (CoW)**. Зникає при видаленні container, persistent-стан належить у volume. - Build cache спрацьовує, коли інструкція має ті самі inputs, що й попередня збірка. Переупорядкуй інструкції так, щоб дешеві, часто мінливі лежали зверху; повільні, стабільні нижче. ### Швидкий приклад ```bash $ docker history nginx:1.27-alpine IMAGE CREATED CREATED BY SIZE 4f06b3e2c0c1 2 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemo… 0B <missing> 2 weeks ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B <missing> 2 weeks ago /bin/sh -c #(nop) EXPOSE 80 0B <missing> 2 weeks ago /bin/sh -c set -x && addgroup -g 101 -S… 8.94MB <missing> 2 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.27.4 0B <missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 4 weeks ago /bin/sh -c #(nop) ADD file:abcd1234… 7.79MB ``` Кожен рядок це один шар. Alpine-основа (`ADD file:...`) знизу; nginx-специфічні шари стекаються зверху. Зроби pull `nginx:1.27-alpine` і `node:22-alpine`, і Alpine-основа поділяється, скачана раз, збережена раз. ### Що таке шар насправді Шар це tarball, що містить: - Файли, додані або змінені інструкцією. - Для видалень: спеціальний whiteout-файл (`.wh.<name>`), що каже union FS «сховай файл від нижніх шарів». Тож якщо `RUN apt-get install vim` додає `/usr/bin/vim`, цей файл лягає у tar шару. Якщо пізніший `RUN rm /usr/bin/vim` його видаляє, у *тому* шарі з'являється whiteout `.wh.vim`, але реальний файл все ще на диску у попередньому шарі. **Розмір image включає все, що ти колись додав, навіть якщо потім видалив.** Шари content-addressed: їхня ідентичність це SHA256 tarball'а. Той самий tarball = той самий шар = збережено один раз. ### Union File System (OverlayFS) Docker за роки підтримував кілька union FS-драйверів (AUFS, btrfs, devicemapper, zfs, OverlayFS). На сучасному Linux (kernel 4.x+) дефолтом є **OverlayFS**, він швидкий, у самому kernel, добре підтримуваний. OverlayFS об'єднує чотири директорії в одну точку монтування: - **lowerdir** одна або кілька read-only директорій (твої шари image, стекнуті). - **upperdir** одна read-write директорія (writable-шар container). - **workdir** scratch-директорія, що kernel використовує внутрішньо. - **merged** єдина в'юха, що container бачить як `/`. ``` +------------------+ | merged/ | <- що бачить container +------------------+ ↑ ↑ ↑ | | | +--------+ +--------+ +--------+ |upperdir| |lowerdir| |lowerdir| | (RW) | |layer N | |layer 1 | +--------+ +--------+ +--------+ ``` Читання спочатку перевіряє upper-шар, провалюється до нижніх шарів. Записи йдуть у upperdir. Модифікація файлу з нижнього шару тригерить **copy-up**: файл копіюється у upperdir, потім модифікується там. Оригінал у нижньому шарі не зачеплений. ### Copy-on-write у дії ```bash # Всередині container на основі alpine: / # cat /etc/hostname a3f9d2b8c1e4 # Цей файл у alpine-шарі (нижній, RO) / # echo new-name > /etc/hostname # Тепер /etc/hostname у writable-шарі (upper). # Копія у alpine-шарі не змінилася, інші container з # того самого image все ще бачать оригінал. ``` Copy-up на файл. Модифікація одного байта 100 MB файлу спочатку копіює всі 100 MB у writable-шар, тому «бази у writable-шарі» працюють погано. Використовуй volume. ### Build cache і перевикористання шарів Коли `docker build` виконує інструкцію, він рахує cache key з: - Digest попереднього шару (щоб ланцюг був детермінованим). - Самої інструкції (текст `RUN` / `COPY`). - Для `COPY` і `ADD`: digest файлів, що копіюються. - Для `RUN`: лише рядка команди. Docker НЕ перевіряє, що команда робить; та сама строка = cache hit, навіть якщо `apt-get` завантажив би нові версії. Якщо cache key збігається з попереднім білдом, Docker перевикористовує існуючий шар. Якщо ні, виконує інструкцію і створює новий шар. **Як тільки крок промахнувся повз кеш, кожен крок після нього також промах.** ### Оптимізація порядку Dockerfile під cache hits ```dockerfile # НЕПРАВИЛЬНО: source копіюється перед встановленням deps FROM node:22-alpine WORKDIR /app COPY . . # будь-яка зміна коду інвалідує все нижче RUN npm ci --omit=dev # перезапускається на кожну зміну коду # ПРАВИЛЬНО: deps встановлено перед копіюванням source FROM node:22-alpine WORKDIR /app COPY package*.json ./ # міняється тільки при зміні deps RUN npm ci --omit=dev # кешовано, поки package.json не міняється COPY . . # міняється при зміні source, тільки це перезапускається ``` Для типового Node-застосунку зі стабільними deps це перетворює 60-секундний rebuild на 2-секундний. Кешований шар `npm ci` перевикористовується, поки `package.json` і `package-lock.json` без змін. ### Наслідки для розміру image ```dockerfile # НЕПРАВИЛЬНО: cleanup у окремому RUN не допомагає RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/* # Шар 2 все ще містить apt-кеш. # Шар 3 лише додає whiteout, кеш на диску. # ПРАВИЛЬНО: cleanup у тому самому RUN, що і install RUN apt-get update && \ apt-get install -y --no-install-recommends curl && \ rm -rf /var/lib/apt/lists/* # Один шар, без кеш-файлів всередині. ``` Whiteout ховають файли від union-в'юхи; не видаляють їх з попередніх шарів. Cleanup має статися у тому самому `RUN`, що створив сміття. ### Типові помилки **Додати файл в одному шарі, видалити в іншому, очікувати менший image** Не працює. 200 MB файл, доданий у шарі 4 і видалений у шарі 5, дає 200 MB image, не нуль. Whiteout лише ховає; байти все ще там. Використовуй multi-stage білди, коли треба тримати щось під час білду, але не у фінальному image. **Перезбирати `npm install` на кожну зміну коду** Класичний симптом: білди займають 60 секунд навіть для однорядкової зміни. Фікс: install перед копіюванням source. Lock-файл має лягти у свій шар, install виконується проти нього, потім source копіюється зверху. **Модифікувати змонтовані файли з нижнього шару, очікуючи безкоштовності** Перший запис у файл з нижнього шару тригерить copy-up всього файлу. Для маленького config-файлу нормально. Для multi-GB файлу бази повільно і безглуздо, використовуй volume, що оминає union FS повністю. **Використовувати `RUN`, щоб клонувати велике репо, а потім видаляти** ```dockerfile # НЕПРАВИЛЬНО: репо живе у шарі назавжди RUN git clone https://github.com/large/repo.git /tmp/r && \ cp /tmp/r/binary /usr/local/bin && \ rm -rf /tmp/r # Шар містить репо + бінар; rm лише додає whiteout. # Чекай, насправді це В одному RUN, тому cleanup ОК. ``` Неочевидний нюанс: якщо все це у одному `RUN`, шар це знімок у КІНЦІ команди, після `rm`. Тому такий патерн ОК. Помилка трапляється, коли кожен крок свій `RUN`. **Зберігати шари image на повільному диску** OverlayFS швидкий, але обмежений сховищем. Старти container на network-mounted Docker root або повільному USB відчуваються жахливо. Тримай `/var/lib/docker` на тому самому швидкому SSD, що і твій kernel. ### Реальне застосування - **Multi-stage білди** канонічний спосіб тримати build-tools поза фінальним image. Stage 1 з компіляторами, stage 2 лише з бінарем; 1.5 GB build-image стає 30 MB runtime image. - **Distroless і scratch image** беруть multi-stage ідею далі. Фінальний стейдж це `FROM scratch` або `FROM gcr.io/distroless/base`, лишаючи лише твій бінар на диску. Без shell, без package manager, near-zero поверхня атаки. - **BuildKit cache mounts** `RUN --mount=type=cache,target=/root/.cache/pip ...` тримає build-cache між білдами без запікання у шар. Шар лишається чистим; кеш живе поза image. - **Layer-aware registry** ECR, GCR, GHCR всі дедуплікують blob'и на рівні registry. Push двох image, що поділяють 90 відсотків шарів, заливає лише нові 10 відсотків. ### Питання для поглиблення **Q:** Чому б не зробити один великий шар на все? **A:** Втратиш кеш. З одним шаром будь-яка зміна перезбирає весь image. З багатьма шарами тільки шари нижче зміни перевикористовуються. Trade-off в overhead на шар (кожен додає bookkeeping); сучасна best practice це приблизно один логічний крок на шар. **Q:** Docker layers і Git commits схожі? **A:** Концептуально схожі (впорядковані diff, що можна стекати), механічно різні. Шари це tarball'и файлової системи; Git зберігає об'єкти, дедупльовані по content hash. Обидва content-addressed і незмінні. OCI image spec насправді запозичив ідеї прямо з того, як працює Git. **Q:** Що таке `docker history --no-trunc <image>`? **A:** Показує кожен шар image з повною інструкцією, що його продукувала. Найкорисніша команда для розуміння, чому image такий великий і де основна вага. Парь з `dive` для інтерактивного inspector'а шарів. **Q:** Чому Alpine дає менші image, ніж Debian або Ubuntu? **A:** Alpine використовує musl замість glibc і busybox замість GNU coreutils. Базовий image приблизно 7 MB проти 30+ MB для Debian slim. Caveat: musl може викликати ледь помітні compat-проблеми з бінарями, що очікують поведінки glibc; використовуй Alpine, коли контролюєш бінарі, Debian-slim, коли ні. **Q:** (Senior) Як BuildKit робить білди швидшими за legacy-builder? **A:** Кількома шляхами. Паралельне виконання стейджів (стейджі multi-stage без залежностей білдяться паралельно). Розумніші cache key (`COPY` інвалідує лише при реальній зміні файлів, не на mtime директорії). Cache mounts, що персистять між білдами без роздування шарів. Frontend-синтаксис (`# syntax=docker/dockerfile:1.7`) дає новим Dockerfile-фічам шиппити без апгрейду daemon. Варто вмикати всюди зараз; це дефолт на Docker 23+. ## Приклади ### Інспекція розмірів шарів через `docker history` ```bash $ docker history --no-trunc node:22-alpine | awk '{print $7, $8}' | head -10 74.4MB /bin/sh -c addgroup -g 1000 node 80.6MB /bin/sh -c apk add --no-cache python3 ... 5.2MB /bin/sh -c #(nop) COPY file:abc... 0B /bin/sh -c #(nop) WORKDIR /home/node ``` Рядок `apk add` це 80 MB. Це шар, який атакувати першим, якщо треба менший image (розглянь тоншу основу або `--no-cache`). ### Демонстрація dedup шарів через `docker pull` ```bash # Перший pull: завантажує все $ docker pull node:22-alpine 22-alpine: Pulling from library/node 9824c27679d3: Pull complete # alpine-основа f52e5f1a8a45: Pull complete # node-бінарі ca7239f1a5a6: Pull complete # node-сетап # Другий pull: ділить alpine-основу $ docker pull python:3.13-alpine 3.13-alpine: Pulling from library/python 9824c27679d3: Already exists # ТА Ж alpine-основа, не перезавантажується 8b1d5c8d2e7f: Pull complete # python-бінарі ``` `9824c27679d3` це Alpine-основа. Два різних image її поділяють, скачана раз, збережена раз на диску. Рядок `Already exists` це OverlayFS dedup у дії. ### Multi-stage, щоб скинути build-шар ```dockerfile # Стейдж 1: повний Node toolchain (~600 MB) FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Стейдж 2: крихітний runtime (~20 MB) FROM nginx:1.27-alpine COPY --from=build /app/dist /usr/share/nginx/html ``` Фінальний image це `nginx:1.27-alpine` (≈20 MB) плюс твої зібрані статичні файли. 600 MB build-стейдж викинуто, він був потрібен для *виробництва* артефакту, не для *запуску*. Це шарова модель, використана на повну: збираєш важко, шиппиш легко.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.