Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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.

Докладніше в документації: BuildKit

Контекст збирання - набір файлів, які 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 - якщо файл потрібен у збиранні, його не можна виключати.

Докладніше в документації: Контекст збирання: .dockerignore

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 і швидкості налагодження.

Докладніше в документації: dive: аналіз шарів образу