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

Питання на співбесіді з Docker

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

100 питань

Xdebug - налагоджувач PHP: точки зупинки, покрокове виконання, перегляд змінних. У Docker головна складність - мережева: Xdebug сам ініціює з'єднання з IDE (порт 9003), а IDE працює на хості, поза контейнером.

Мінімальна конфігурація PHP у контейнері:

[xdebug]
xdebug.mode=debug,develop
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.start_with_request=trigger   ; лише за запитом, а не на кожен

host.docker.internal - ім'я, що вказує на хост із контейнера. На Docker Desktop (macOS, Windows) воно працює автоматично; на Linux його треба додати явно:

services:
  app:
    extra_hosts:
      - "host.docker.internal:host-gateway"

У Laravel Sail це вже налаштовано: досить змінної в .env - SAIL_XDEBUG_MODE=develop,debug,coverage - і перезапуску контейнерів. Для CLI-команд - sail debug artisan ....

Налаштування IDE (PhpStorm):

  • слухати вхідні з'єднання налагодження на порту 9003;
  • відображення шляхів (path mappings): код у контейнері лежить у /var/www/html, а на хості - у ~/projects/app. Без відображення IDE отримує з'єднання, але не може зіставити файли й не зупиняється на точках;
  • ім'я сервера (PHP_IDE_CONFIG=serverName=...) збігається з налаштуваннями сервера в IDE.

Чому з'єднання не приходить - чек-лист:

  1. Xdebug не ввімкнено: php -v у контейнері має показувати Xdebug, php -i | grep xdebug.mode;
  2. немає тригера при start_with_request=trigger - потрібне розширення браузера Xdebug Helper чи параметр XDEBUG_TRIGGER;
  3. неправильний client_host - особливо на Linux без host-gateway;
  4. фаєрвол хоста блокує вхідний порт 9003;
  5. IDE не слухає чи слухає інший порт (старий Xdebug 2 використовував 9000);
  6. журнал Xdebug показує причину: xdebug.log=/tmp/xdebug.log - там видно спробу з'єднання і помилку.

Продуктивність: Xdebug помітно сповільнює PHP навіть без активного налагодження. Тому start_with_request=trigger чи окремий образ/профіль з Xdebug, а не постійно ввімкнений режим. У продакшені Xdebug не встановлюють узагалі.

Докладніше в документації: Laravel Sail: налагодження з Xdebug

Ресурси Compose (контейнери, мережі, томи) належать проєкту. Назва проєкту - префікс усіх ресурсів: myapp-app-1, myapp_default, myapp_pgdata. За замовчуванням назва - ім'я каталогу з compose.yaml; явно задається полем name: у файлі, змінною COMPOSE_PROJECT_NAME чи прапорцем -p.

Рівні «скидання» - від м'якого до радикального:

docker compose restart app            # перезапустити процес, контейнер той самий
docker compose up -d --force-recreate # перестворити контейнери (записуваний шар скинуто)
docker compose up -d --build          # перезібрати образи й перестворити
docker compose down                   # видалити контейнери й мережі проєкту, ТОМИ ЛИШАЮТЬСЯ
docker compose down -v                # + іменовані томи проєкту: дані бази ЗНИКНУТЬ
docker compose down --rmi local       # + образи, зібрані для проєкту

Головне - розуміти, що де живе:

  • записуваний шар контейнера зникає при перестворенні;
  • іменовані томи (pgdata) переживають down, але не down -v;
  • bind mount - це файли хоста, їх Docker не видаляє ніколи;
  • анонімні томи накопичуються, якщо не видаляти їх разом з контейнерами (down -v чи docker compose rm -v).

Небезпечні звички:

  • docker system prune -a --volumes - чистить усе невикористовуване на машині: томи, образи, мережі всіх проєктів, включно з базами сусідніх проєктів, які зараз просто не запущені. Для чистки одного проєкту - команди docker compose з його назвою;
  • однакова назва проєкту для двох різних каталогів (обидва app/) - вони ділять мережі й томи, і down -v в одному зносить дані іншого. Явне name: у compose.yaml знімає проблему;
  • down -v «за звичкою» при кожному перезапуску - щоразу порожня база й повторні міграції з сидерами.

Безпечне скидання бази розробки:

docker compose exec app php artisan migrate:fresh --seed   # схема й тестові дані
# або точково видалити один том
docker compose down
docker volume rm myapp_pgdata
docker compose up -d

Перед видаленням томів із цінними даними - бекап: docker compose exec -T postgres pg_dump -U postgres app > backup.sql.

Перелік ресурсів проєкту: docker compose ps -a, docker volume ls --filter label=com.docker.compose.project=myapp.

Докладніше в документації: Назва проєкту Compose

Тег - змінне посилання: власник реєстру (чи будь-хто з правом запису) може перемістити app:1.4.2 на інший образ. Дайджест - SHA-256 від маніфесту образу:

docker pull ghcr.io/acme/app@sha256:4f8e0a...b21c
docker image ls --digests
docker buildx imagetools inspect ghcr.io/acme/app:1.4.2   # дайджест і платформи

Той самий дайджест завжди означає ті самі байти. Змінити вміст, не змінивши дайджесту, неможливо.

Що дає деплой за дайджестом:

  • відтворюваність: усі сервери, нові вузли при масштабуванні й перезапуски отримують точно той самий образ;
  • захист від підміни: скомпрометований чи помилково перезаписаний тег у реєстрі не змінить того, що розгортається;
  • точний аудит і відкат: у конфігурації деплою записаний конкретний образ, а не мітка, яка могла змінитися;
  • кешування: якщо дайджест не змінився, завантажувати нічого.

Мультиплатформні образи: для образу з кількома архітектурами (amd64, arm64) тег вказує на індекс (manifest list), а кожна платформа має свій дайджест. Зазвичай фіксують дайджест індексу - і кожен сервер сам обере потрібну архітектуру.

Практичний підхід - тег для людей, дайджест для машин:

  • CI збирає образ з тегами sha-a1b2c3d і 1.4.2, отримує дайджест і записує його в маніфест деплою (Kubernetes, Helm values, Compose-файл);
  • інструменти на кшталт Renovate оновлюють зафіксовані дайджести автоматично pull-request-ами.

Базові образи в Dockerfile теж можна фіксувати дайджестом:

FROM php:8.5-fpm-alpine@sha256:...

Тег лишається для читабельності, але використовується дайджест. Без цього той самий Dockerfile сьогодні й через місяць може зібрати різні образи - з іншою версією PHP чи системних бібліотек.

Ціна: дайджести нечитабельні, а оновлення базового образу (зокрема з виправленнями безпеки) перестає бути автоматичним - його треба робити свідомо, найкраще інструментом автоматичних оновлень залежностей.

Докладніше в документації: docker image pull: завантаження за дайджестом

Образ, що потрапляє на сервер, - результат довгого ланцюжка: базовий образ, системні пакети, залежності Composer і npm, CI, реєстр. Атака на будь-яку ланку (скомпрометований CI, підмінений образ у реєстрі, шкідливий пакет) дає зловмиснику код у продакшені. Захист - перевірювані відомості про образ.

Атестації BuildKit - метадані, прикріплені до образу під час збирання:

docker buildx build --sbom=true --provenance=mode=max -t ghcr.io/acme/app:1.4.2 --push .
  • provenance - як і з чого зібрано образ: репозиторій і коміт, параметри збирання, базові образи, середовище CI. За замовчуванням buildx додає мінімальну provenance-атестацію; mode=max - детальну;
  • SBOM (Software Bill of Materials) - перелік усіх пакетів і бібліотек в образі з версіями.
docker buildx imagetools inspect ghcr.io/acme/app:1.4.2 --format '{{ json .SBOM }}'

Навіщо SBOM: коли виходить критична вразливість у бібліотеці, за SBOM можна за хвилини знайти всі образи, що її містять, замість сканування кожного вручну. Сканери (Docker Scout, Trivy, Grype) використовують SBOM для пошуку CVE.

Підпис образів - криптографічне підтвердження, що образ зібрав саме ваш CI і він не змінювався:

cosign sign ghcr.io/acme/app@sha256:...
cosign verify ghcr.io/acme/app@sha256:... --certificate-identity=... --certificate-oidc-issuer=https://token.actions.githubusercontent.com

Cosign (проєкт Sigstore) підтримує «безключовий» підпис: CI отримує короткоживучий сертифікат через OIDC (наприклад, GitHub Actions), тож не треба зберігати довгоживучий приватний ключ.

Де це перевіряється:

  • політики допуску в Kubernetes (Kyverno, Sigstore policy-controller) - кластер не запустить непідписаний образ чи образ не з вашого репозиторію;
  • CI перед деплоєм - перевірка підпису й відсутності критичних вразливостей у SBOM.

Підписують дайджест, а не тег - тег можна перемістити, а підпис має стосуватися конкретного вмісту.

Рамки: SLSA описує рівні гарантій ланцюжка збирання; provenance BuildKit - крок до вищих рівнів.

Мінімум для невеликого проєкту: збирання лише в CI (не з ноутбуків), SBOM і сканування вразливостей, деплой за дайджестом. Підписи й політики допуску - наступний крок.

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

Між «запускаю docker compose up по SSH» і «маємо кластер Kubernetes» є проміжний клас інструментів - PaaS для самостійного хостингу поверх Docker.

Kamal (від 37signals, творців Basecamp):

  • конфігурація в одному YAML, деплой командою kamal deploy по SSH на звичайні сервери;
  • збирає образ, завантажує в реєстр, запускає на серверах;
  • деплой без простою через власний проксі kamal-proxy: новий контейнер запускається, проходить перевірку стану, і лише тоді на нього перемикається трафік;
  • допоміжні сервіси (accessories) - база, Redis - як окремі контейнери;
  • без вебінтерфейсу й без «панелі» - інструмент командного рядка.

Coolify і Dokploy - самостійно розміщувані аналоги Heroku/Vercel з вебінтерфейсом:

  • деплой з Git-репозиторію чи образу, автоматичні збирання з Dockerfile чи Nixpacks/Buildpacks;
  • TLS-сертифікати, домени, змінні оточення, бази даних «в один клік», резервні копії;
  • кілька серверів (Dokploy використовує Docker Swarm для розподілу між вузлами).

Коли такого інструменту достатньо:

  • один чи кілька серверів, кілька застосунків;
  • команда без окремого DevOps-інженера;
  • потрібні деплой без простою, TLS, перезапуски, базові резервні копії - але не автомасштабування й складні мережеві політики;
  • важлива ціна: свої сервери (Hetzner, DigitalOcean) значно дешевші за керовані платформи.

Обмеження й ризики:

  • панель - теж частина інфраструктури: її треба оновлювати, захищати (доступ до панелі = доступ до всіх застосунків і секретів), резервувати;
  • відмовостійкість обмежена кількістю серверів і можливостями інструменту;
  • «магія» налаштувань: коли щось ламається, доводиться розбиратися, що саме панель згенерувала (Compose-файли, конфігурації проксі);
  • залежність від проєкту: розвиток і підтримка залежать від невеликої команди.

Порівняно з Kubernetes: значно менше понять і супроводу, але й менше гнучкості. Для більшості невеликих і середніх Laravel-застосунків такого рівня достатньо надовго.

Порівняно з керованими PaaS (Laravel Cloud, Render, Fly.io): дешевше й більше контролю, але відповідальність за сервери, оновлення ОС і безпеку - на вас.

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

Контейнери одноразові, а томи - ні: там бази даних, завантажені файли, дані Redis. Резервна копія контейнерної інфраструктури - це насамперед копія томів і конфігурації.

Копія тому через тимчасовий контейнер (з документації Docker):

docker run --rm \
  -v app_storage:/data:ro \
  -v "$PWD":/backup \
  alpine tar czf /backup/storage-$(date +%F).tar.gz -C /data .

Відновлення - зворотна операція в новий том.

Бази даних - не копіювати файли працюючої бази. Копія каталогу даних PostgreSQL чи MySQL під час роботи може бути неузгодженою й непридатною до відновлення. Правильно:

  • логічний дамп засобами бази: docker exec db pg_dump -Fc app > app.dump, mysqldump --single-transaction;
  • фізичні бекапи з WAL/binlog (pgBackRest, WAL-G, Percona XtraBackup) - для великих баз і відновлення на момент часу;
  • або зупинка бази на час копіювання файлів - якщо простій допустимий.

Що ще входить у «відновити все»:

  • конфігурація: Compose-файли, змінні оточення, секрети (у сховищі секретів, а не лише на сервері);
  • образи: у реєстрі за незмінними тегами - щоб розгорнути ту саму версію;
  • файли користувачів: краще одразу в об'єктному сховищі (S3, R2) з версіонуванням, а не в томі на одному сервері;
  • інфраструктура як код: як заново створити сервер, мережу, DNS.

Правило 3-2-1: щонайменше 3 копії, на 2 різних типах носіїв, 1 - поза основною площадкою (інший провайдер чи регіон). Плюс незмінні копії (object lock), які не може видалити зловмисник чи програма-вимагач із доступом до сервера.

Плануючи відновлення, визначають:

  • RPO - скільки даних допустимо втратити (година? хвилина?) - визначає частоту копій;
  • RTO - за скільки часу треба відновитися - визначає підхід (запасний сервер, автоматизація).

Найважливіше - перевіряти відновлення. Регулярно (наприклад, щомісяця) розгортати копію в окремому середовищі й перевіряти, що застосунок працює. Неперевірена резервна копія часто виявляється порожньою, неповною чи без ключа розшифрування саме тоді, коли вона потрібна.

Моніторинг бекапів: сповіщення, якщо копія не створилася чи її розмір різко змінився.

Докладніше в документації: Томи: резервне копіювання й відновлення

Збирання arm64-образу на x86-машині (чи навпаки) через емуляцію QEMU працює «прозоро», але кожна інструкція RUN виконується в емульованому процесорі. Компіляція PHP-розширень (docker-php-ext-install, pecl install), нативних npm-модулів чи npm run build може сповільнитися в 5-20 разів - збирання на хвилини перетворюється на збирання на пів години.

Варіанти прискорення:

1. Нативні збирачі для кожної архітектури. Кожна платформа збирається на своєму залізі, результати об'єднуються в один мультиплатформний образ:

  • buildx з кількома вузлами: docker buildx create --name multi --platform linux/amd64 ssh://amd-builder і --append вузол linux/arm64;
  • CI з матрицею - arm64-раннери (у GitHub Actions доступні) збирають arm64, amd64-раннери - amd64; окремий крок створює індекс (docker buildx imagetools create);
  • хмарні збирачі (Docker Build Cloud та інші) з нативними вузлами обох архітектур.

2. Виконувати незалежні від архітектури кроки на рідній платформі збирача. Збирання фронтенду дає однаковий результат (JS, CSS) для будь-якої архітектури - немає сенсу емулювати його:

# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM node:22-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build                      # виконується нативно один раз

FROM php:8.5-fpm-alpine                # цільова платформа
COPY --from=assets /app/public/build /var/www/html/public/build

--platform=$BUILDPLATFORM змушує етап виконуватися на архітектурі збирача.

3. Крос-компіляція - для мов, що вміють збирати під іншу архітектуру (Go, Rust): етап на $BUILDPLATFORM компілює бінарник для $TARGETOS/$TARGETARCH, а фінальний етап його лише копіює.

Вбудовані змінні BuildKit: BUILDPLATFORM, TARGETPLATFORM, TARGETOS, TARGETARCH, TARGETVARIANT - щоб, наприклад, завантажити бінарник потрібної архітектури:

ARG TARGETARCH
RUN curl -fsSL -o /usr/local/bin/tool "https://example.com/tool-linux-${TARGETARCH}"

Пастки:

  • кеш для кожної платформи окремий - експорт кешу в CI має покривати обидві;
  • залежності Composer з нативними частинами зазвичай немає (PHP-код незалежний від архітектури), тож composer install теж можна винести на $BUILDPLATFORM (з --ignore-platform-reqs чи узгодженими розширеннями);
  • тестування образу кожної архітектури - емуляція під час збирання не гарантує, що розширення поводяться однаково.

Докладніше в документації: Мультиплатформні збирання: крос-компіляція

Відтворюване збирання - з того самого вихідного коду завжди виходить побітово однаковий образ (той самий дайджест), незалежно від того, коли й на якій машині його зібрали.

Навіщо:

  • перевірка ланцюжка постачання: незалежне повторне збирання має дати той самий дайджест - так можна переконатися, що образ у реєстрі справді зібраний з цього коду, а не підмінений;
  • кешування й дедуплікація: однаковий вміст - однаковий дайджест, не потрібно повторно завантажувати;
  • налагодження: відтворити точно той образ, що працює в продакшені.

Що робить збирання невідтворюваним:

1. Мітки часу. Кожен файл у шарі має час зміни, а метадані образу - час створення. Два збирання з різницею в хвилину - різні дайджести.

Рішення - змінна SOURCE_DATE_EPOCH, яку BuildKit використовує як фіксований час (зазвичай - час останнього коміту):

SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) \
docker buildx build --output type=image,name=ghcr.io/acme/app:1.4.2,push=true,rewrite-timestamp=true .

rewrite-timestamp=true переписує й мітки часу файлів у шарах.

2. Плаваючі залежності:

  • базові образи без зафіксованого дайджесту;
  • apt-get install / apk add без версій - пакетні репозиторії змінюються щодня;
  • composer install / npm install без lock-файлів (або npm install замість npm ci);
  • завантаження «останньої версії» інструментів через curl.

3. Недетермінованість інструментів: генерація випадкових значень під час збирання, порядок файлів в архівах, вбудовані дати збирання у фронтенд-бандлі чи версійні файли.

4. Середовище збирання: різні версії BuildKit можуть по-різному формувати шари.

Реалістичний рівень для більшості проєктів:

  • фіксувати все, що визначає вміст (базові образи за дайджестом, lock-файли, версії інструментів);
  • фіксувати час (SOURCE_DATE_EPOCH);
  • збирати лише в CI з однаковим збирачем.

Повна побітова відтворюваність образів з пакетами Debian чи Alpine складна (пакетні репозиторії не зберігають старих версій вічно), тож часто достатньо «функціональної» відтворюваності: та сама поведінка, ті самі версії компонентів, задокументовані в SBOM і provenance-атестації.

Зв'язок з атестаціями: provenance фіксує, з якого коміту й з якими параметрами зібрано образ, - навіть якщо побітової відтворюваності немає, походження перевірюване.

Докладніше в документації: Відтворювані збирання з GitHub Actions

Класичний спосіб виконати кілька команд в одному шарі - довгий ланцюжок через && і \:

RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS icu-dev libzip-dev \
 && docker-php-ext-install intl zip pdo_pgsql \
 && pecl install redis \
 && docker-php-ext-enable redis \
 && apk del .build-deps

Важко читати, легко забути \ чи &&, неможливо коментувати окремі рядки.

Heredoc у RUN (сучасний синтаксис Dockerfile, працює з BuildKit):

# syntax=docker/dockerfile:1
RUN <<EOF
set -eux
apk add --no-cache --virtual .build-deps $PHPIZE_DEPS icu-dev libzip-dev
docker-php-ext-install intl zip pdo_pgsql
# Redis з PECL
pecl install redis
docker-php-ext-enable redis
apk del .build-deps
EOF

Увесь блок виконується одним RUN - тобто одним шаром, як і ланцюжок з &&.

Важливо: set -e. У ланцюжку && помилка будь-якої команди зупиняє виконання. У heredoc команди виконуються як скрипт - без set -e збирання продовжиться після помилки, і проблему помітять лише в образі. set -eux - зупинитися на помилці (e), на невизначених змінних (u) і показувати команди у виводі (x).

Інший інтерпретатор:

RUN <<EOF python3
print("Генерація конфігурації")
EOF

Heredoc у COPY - створити файл прямо в Dockerfile без окремого файлу в репозиторії:

COPY <<EOF /usr/local/etc/php/conf.d/opcache.ini
opcache.enable=1
opcache.validate_timestamps=0
opcache.memory_consumption=256
EOF

Зручно для невеликих конфігураційних файлів, що стосуються лише образу.

Підстановка змінних: у <<EOF змінні збирання (ARG) підставляються; щоб передати текст буквально (зокрема $ для оболонки всередині), - лапки навколо маркера: <<'EOF'.

Коли heredoc не потрібен: одна-дві короткі команди читаються і в звичайному RUN. Для довгих скриптів (понад десяток рядків) краще окремий файл-скрипт у репозиторії, який можна перевірити ShellCheck і викликати з RUN.

Докладніше в документації: Dockerfile: here-documents

OCI (Open Container Initiative) - відкриті специфікації формату образів, середовища запуску й протоколу реєстрів. Завдяки ним образ, зібраний Docker, запускається в containerd, Podman, Kubernetes, а реєстри (Docker Hub, GHCR, ECR) взаємозамінні.

З чого складається образ:

  • шари - архіви змін файлової системи, кожен адресується дайджестом свого вмісту;
  • конфігурація - JSON з налаштуваннями запуску (CMD, ENTRYPOINT, ENV, користувач, порти) і історією збирання;
  • маніфест - перелік: яка конфігурація й які шари складають образ, з їхніми дайджестами;
  • індекс (image index, раніше manifest list) - для мультиплатформних образів: посилання на маніфести для кожної архітектури, а також на атестації (SBOM, provenance).

Дайджест образу - це дайджест маніфесту (чи індексу), тож він однозначно визначає весь вміст.

Мітки (labels) і анотації - метадані образу:

LABEL org.opencontainers.image.source="https://github.com/acme/app" \
      org.opencontainers.image.revision="a1b2c3d" \
      org.opencontainers.image.version="1.4.2" \
      org.opencontainers.image.licenses="MIT"
  • labels зберігаються в конфігурації образу - видно через docker inspect;
  • анотації - поля в маніфесті чи індексі; їх читають реєстри й інструменти, не завантажуючи весь образ:
docker buildx build --annotation "index:org.opencontainers.image.source=https://github.com/acme/app" ...

Навіщо стандартні ключі org.opencontainers.image.*:

  • source - GHCR автоматично пов'язує пакет образу з репозиторієм GitHub, сканери вразливостей і Renovate знаходять, звідки образ;
  • revision, version, created - з образу видно, який коміт і яка версія в ньому, без зовнішніх записів;
  • licenses, vendor, title, description - для каталогів і перевірок відповідності.

Генерувати метадані в CI зручно дією docker/metadata-action: вона створює теги (за гілкою, SHA, версією) і стандартні мітки з контексту репозиторію.

Практичне значення для співбесіди: розуміти, що тег - лише посилання на маніфест, дайджест - незмінний ідентифікатор, мультиплатформний образ - індекс з кількома маніфестами, а атестації - теж частина індексу. Це пояснює більшість «дивних» ситуацій: чому docker pull на Mac і на сервері дає різні дайджести того самого тегу, чому образ з атестаціями показує в реєстрі «unknown/unknown» платформу, і чому видалення тегу не видаляє образ.

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

Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 35 Middle 35 Senior 30

Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії