Junior: питання на співбесіді з теми «BuildKit і збирання образів»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
BuildKit - рушій збирання образів, що замінив старий (legacy) збирач. У Docker Engine він використовується за замовчуванням з версії 23.0 (у Docker Desktop - ще раніше), тож у сучасному Docker docker build - це вже BuildKit.
Що він змінив:
1. Паралельне збирання. BuildKit будує граф залежностей між етапами й виконує незалежні частини одночасно. У multi-stage збиранні етапи для Composer і для npm виконуються паралельно, а не по черзі.
2. Пропуск непотрібних етапів. Етапи, від яких не залежить цільовий етап, не збираються взагалі. Старий збирач виконував усі етапи послідовно.
3. Розумніший кеш:
- cache mounts (
RUN --mount=type=cache) - кеш пакетних менеджерів між збираннями, не потрапляючи в шари образу; - експорт і імпорт кешу в реєстр, локальний каталог чи кеш GitHub Actions - корисно для CI, де кожне збирання на «чистій» машині.
4. Секрети й SSH під час збирання - --mount=type=secret, --mount=type=ssh: доступ до приватних репозиторіїв і токенів без потрапляння в шари.
5. Мультиплатформні образи через docker buildx - одна команда збирає образ для amd64 і arm64.
6. Сучасний синтаксис Dockerfile: heredoc-и в RUN і COPY, COPY --link, COPY --chmod, перевірки (docker build --check).
7. Атестації - SBOM і provenance прикріплюються до образу під час збирання.
docker build і docker buildx build: у сучасному Docker docker build - псевдонім для docker buildx build з налаштованим за замовчуванням збирачем. buildx дає змогу створювати окремі збирачі (наприклад, драйвер docker-container для мультиплатформних збирань і експорту кешу).
Корисний рядок на початку Dockerfile:
# syntax=docker/dockerfile:1
Він каже BuildKit завантажити актуальну стабільну версію фронтенду Dockerfile - нові можливості синтаксису доступні без оновлення самого Docker.
Вивід збирання у BuildKit компактний; для повного логу кожного кроку - --progress=plain.
Контекст збирання - набір файлів, які Docker передає збирачу. У команді docker build . крапка - це контекст: поточний каталог з усім вмістом. Інструкції COPY і ADD бачать лише файли з контексту.
Проблема без .dockerignore: у контекст потрапляє все - vendor, node_modules, .git, логи, локальна база SQLite, .env:
- повільне збирання - сотні мегабайтів передаються збирачу щоразу;
- ламається кеш:
COPY . .вважає зміненими будь-які файли, включно з логами й.git, - шар і всі наступні збираються заново; - витік секретів:
.envз паролями чи ключі можуть опинитися в образі й потрапити в реєстр; - неправильні залежності: локальний
vendor(зібраний під macOS, з dev-пакетами) перезаписує той, що встановлено в образі.
.dockerignore для Laravel-проєкту:
.git
.github
.env
.env.*
!.env.example
node_modules
vendor
public/build
public/hot
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
bootstrap/cache/*.php
tests
*.log
docker-compose*.yml
Синтаксис схожий на .gitignore: шаблони, ** для будь-якої глибини, ! - виняток з виключення.
Чому краще виключати, ніж копіювати вибірково: навіть з точними COPY (COPY app/ app/) контекст усе одно передається збирачу повністю - .dockerignore зменшує саму передачу.
Як перевірити розмір контексту: BuildKit показує його у виводі збирання (transferring context: 2.3MB). Сотні мегабайтів - сигнал, що щось не виключено.
Нюанси:
.dockerignoreшукається в корені контексту (або поруч з Dockerfile у виглядіDockerfile.dockerignoreдля кількох Dockerfile в одному репозиторії);vendorіnode_modulesвстановлюються всередині збирання (composer install,npm ci) - тоді вони збираються під Linux образу й лише з потрібними залежностями;- виключені файли недоступні навіть для
COPY- якщо файл потрібен у збиранні, його не можна виключати.
ARG - змінна часу збирання. Доступна лише під час docker build, у запущеному контейнері її немає:
ARG PHP_VERSION=8.5
FROM php:${PHP_VERSION}-fpm-alpine
ARG APP_VERSION=dev
RUN echo "Building version $APP_VERSION"
docker build --build-arg APP_VERSION=1.4.2 .
ENV - змінна середовища образу. Діє і під час збирання (у наступних інструкціях), і в кожному запущеному контейнері:
ENV APP_ENV=production \
PHP_OPCACHE_VALIDATE_TIMESTAMPS=0
Значення за замовчуванням можна перевизначити при запуску: docker run -e APP_ENV=staging ....
Типові поєднання:
ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION} # перенести значення збирання в середовище контейнера
Особливості ARG:
- область видимості:
ARG, оголошений доFROM, доступний лише в рядкуFROM. Щоб використати його всередині етапу, треба повторитиARG PHP_VERSIONпісляFROM; - кожен етап multi-stage збирання має свої
ARG; - вбудовані змінні BuildKit:
TARGETPLATFORM,TARGETARCH,BUILDPLATFORM- для мультиплатформних збирань.
Головна пастка - секрети:
ARG GITHUB_TOKEN
RUN composer config github-oauth.github.com $GITHUB_TOKEN && composer install
Значення ARG зберігається в історії образу (docker history показує команди з підставленими значеннями), а ENV - ще й у метаданих образу й кожному контейнері. Будь-хто з доступом до образу прочитає секрет. docker build --check попереджає про це правилом SecretsUsedInArgOrEnv. Для секретів - RUN --mount=type=secret.
Що куди класти:
ARG- параметри збирання: версії базових образів і інструментів, прапорці («встановити розширення для розробки»), версія релізу;ENV- налаштування середовища, однакові для всіх запусків образу (шляхи, параметри PHP);- конфігурація конкретного середовища (база, ключі, URL) - не в образ, а при запуску: змінні оточення,
.env, секрети оркестратора. Один образ має працювати і на staging, і на продакшені.
Офіційні PHP-образи мають варіанти на Alpine Linux (php:8.5-fpm-alpine) і на Debian (php:8.5-fpm на повному Debian, а для інших технологій часто ще й -slim).
Alpine:
- маленький розмір - базова система кілька мегабайтів, образ PHP - десятки мегабайтів замість сотень;
- менше пакетів - менше потенційних вразливостей у сканері;
- але інша стандартна бібліотека C - musl замість glibc.
Наслідки musl, через які обирають Debian:
- бінарні пакети під glibc не працюють: готові збірки деяких розширень, інструментів (наприклад, частина бінарників для Node.js-модулів, браузери для генерації PDF) - доводиться збирати з вихідного коду, і збирання стає довшим;
- відмінності в поведінці: робота з DNS (наприклад, обробка
searchуresolv.conf), локалі, деякі функції форматування - інколи дають несподівані помилки, які важко відтворити; - продуктивність: у частині навантажень (виділення пам'яті, багатопотоковість) musl помітно повільніший за glibc;
- налагодження: менше звичних інструментів, інша пакетна система (
apkзамістьapt).
Debian (slim):
- стандартний glibc - сумісність з більшістю бінарних пакетів і розширень;
- більший розмір (але з multi-stage збиранням і чисткою - прийнятний);
- передбачувана поведінка, звична для більшості серверних систем.
Як обрати:
- Alpine - коли важливий розмір і застосунок не залежить від нативних бінарників під glibc; добре перевірена конфігурація;
- Debian - коли потрібні нестандартні розширення, бінарні інструменти (wkhtmltopdf, Chromium, драйвери баз даних), коли команда стикалася з «дивними» помилками на musl;
- distroless / мінімальні образи - для скомпільованих застосунків (Go); для PHP менш практичні.
Незалежно від вибору:
- фіксувати версію: не
php:8.5-fpm-alpine, а конкретну мінорну версію чи дайджест - інакше нове збирання тихо підтягне іншу версію PHP або системних бібліотек; - оновлювати свідомо й регулярно - заради виправлень безпеки;
- не порівнювати лише розмір: різниця у 100 МБ рідко важлива, а година налагодження проблеми musl - помітна.
Докладніше в документації: Найкращі практики: вибір базового образу
Чому розмір має значення: довше завантаження при деплої й масштабуванні, більше місця в реєстрі й на серверах, більше пакетів - більше потенційних вразливостей.
Як знайти, що займає місце:
docker image ls myapp # загальний розмір
docker history myapp:1.4.2 # розмір кожного шару й команда, що його створила
dive myapp:1.4.2 # інтерактивний перегляд шарів і файлів
dive показує вміст кожного шару, які файли додано, змінено чи «видалено» в ньому, і оцінює «втрачене» місце.
Типові причини великого образу:
1. Видалення в окремому шарі. Шар неможливо зменшити наступним шаром - видалений файл просто приховується, але лишається в образі:
# погано: 300 МБ кешу лишаться в першому шарі
RUN apt-get update && apt-get install -y build-essential
RUN apt-get purge -y build-essential && rm -rf /var/lib/apt/lists/*
# добре: встановлення й чистка в одному RUN
RUN apt-get update \
&& apt-get install -y --no-install-recommends libzip-dev \
&& rm -rf /var/lib/apt/lists/*
2. Інструменти збирання в фінальному образі - компілятори, -dev-пакети, Composer, Node.js. Ліки - multi-stage: збирати в одному етапі, копіювати лише результат у фінальний.
3. Залежності для розробки: composer install без --no-dev, node_modules після збирання фронтенду (у фінальний образ потрібен лише public/build).
4. Зайве в контексті збирання: .git, тести, локальні vendor і node_modules через COPY . . без .dockerignore.
5. Кеші пакетних менеджерів: кеш Composer, npm, apk, apt - чистити в тому самому RUN або використовувати cache mounts (RUN --mount=type=cache), що взагалі не потрапляють в образ.
6. Рекомендовані пакети: apt-get install без --no-install-recommends тягне багато непотрібного.
7. Великий базовий образ: повний Debian замість slim/Alpine, якщо застосунку не потрібна сумісність, яку дає повний образ.
Корисні звички для PHP-образу: composer install --no-dev --optimize-autoloader --no-scripts на етапі залежностей, розширення - через docker-php-ext-install з видаленням -dev-пакетів у тому самому шарі, фронтенд - окремим етапом на образі Node.
Межа оптимізації: зменшувати розмір має сенс, доки це не шкодить читабельності Dockerfile і швидкості налагодження.