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

Senior: питання на співбесіді з теми «Образи й Dockerfile»

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

5 питань

1. Не запускати від root. За замовчуванням процес у контейнері - root. Вразливість у застосунку тоді дає нападнику root усередині контейнера, а з помилками конфігурації (змонтований Docker-сокет, привілейований режим) - і шлях на хост.

RUN addgroup -S app && adduser -S app -G app
USER app

Для PHP-FPM зазвичай достатньо, щоб воркери працювали від www-data, а записувані каталоги (storage/, bootstrap/cache/) належали цьому користувачу.

2. Мінімальна база. alpine, -slim чи distroless-образи містять менше пакетів - менше CVE і менше інструментів для нападника. Multi-stage build прибирає з фінального образу компілятори й менеджери пакетів.

3. Жодних секретів у шарах. COPY .env чи ARG API_KEY залишають секрет в історії образу, і docker history чи розпакування шарів його покаже - навіть якщо пізніше файл видалено. Секрети - лише під час запуску (змінні оточення, Docker secrets) або під час збирання через --mount=type=secret.

4. Фіксовані версії. FROM php:8.4.13-fpm-alpine замість php:latest; для максимальної відтворюваності - за дайджестом @sha256:.... Плаваючий тег може принести несподівані зміни.

5. Сканування. docker scout cves, Trivy чи Grype у CI знаходять відомі вразливості в пакетах образу. Регулярне перезбирання підтягує оновлення безпеки базового образу.

6. Обмеження під час запуску: файлова система лише для читання (--read-only з окремими томами для запису), --cap-drop=ALL, без --privileged, ніколи не монтувати /var/run/docker.sock у застосунок.

7. .dockerignore - щоб .git, .env, дампи й ключі не потрапили в контекст збирання взагалі.

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

Інколи секрет потрібен під час збирання: токен для приватного Composer- чи npm-репозиторію, ключ для завантаження приватного пакета.

Чому не ARG чи ENV:

ARG COMPOSER_AUTH
RUN composer install

Значення ARG зберігається в метаданих образу й видно через docker history. ENV - тим більше, він лишається у фінальному образі. А COPY auth.json і подальше RM не допоможе: файл лишився в попередньому шарі.

Правильно - секрет-монтування BuildKit: секрет доступний лише під час виконання однієї інструкції RUN, як тимчасовий файл, і не потрапляє в жоден шар чи кеш.

# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=composer_auth,target=/root/.composer/auth.json \
    composer install --no-dev --prefer-dist
docker build --secret id=composer_auth,src=$HOME/.composer/auth.json .
# або зі змінної оточення:
docker build --secret id=composer_auth,env=COMPOSER_AUTH_JSON .

У Docker Compose і GitHub Actions (docker/build-push-action) для цього є власні параметри secrets.

Для SSH-ключів (клонування приватних Git-репозиторіїв під час збирання) - окремий механізм --mount=type=ssh з пересиланням SSH-агента, без копіювання ключа.

Перевірка: docker history --no-trunc і розпакування шарів не мають показувати ні значення, ні файлу секрету. Сканери секретів у CI (gitleaks, trufflehog) можна натравити й на сам образ.

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

Linux визначає права за числовими ідентифікаторами користувача й групи (UID/GID), а не за іменами. Контейнер і хост ділять ядро, тож файл, створений у контейнері процесом з UID 33 (www-data у Debian), на хості належатиме користувачу з UID 33 - яким би не було його ім'я.

Типові симптоми:

  • у розробці з bind mount: застосунок у контейнері (UID 33 чи root) створює файли в storage/ і vendor/ - на хості розробник (UID 1000) не може їх змінити чи видалити без sudo. Або навпаки: контейнер не може писати в каталог, створений на хості;
  • у продакшені: «Permission denied» при записі в storage/logs чи bootstrap/cache, бо файли скопійовано з власником root, а процес працює як www-data.

Рішення в образі:

FROM php:8.5-fpm

COPY --chown=www-data:www-data . /var/www/html

USER www-data
  • COPY --chown - власник одразу при копіюванні, без окремого RUN chown -R (той дублює всі файли в новому шарі);
  • USER - процес працює не від root.

Рішення для розробки - узгодити UID:

ARG UID=1000
ARG GID=1000
RUN groupmod -o -g ${GID} www-data && usermod -o -u ${UID} -g ${GID} www-data
docker compose build --build-arg UID=$(id -u) --build-arg GID=$(id -g)

Тепер www-data у контейнері має той самий UID, що й розробник на хості, - файли з обох сторін належать одній людині. Так робить Laravel Sail (WWWUSER, WWWGROUP).

Альтернативи:

  • docker run --user $(id -u):$(id -g) - запуск від імені користувача хоста (але в образі може не бути такого користувача й домашнього каталогу);
  • іменовані томи замість bind mount для vendor, node_modules - вони живуть у Docker, без конфлікту з хостом;
  • rootless Docker чи userns-remap - root у контейнері відображається на непривілейованого користувача хоста.

На macOS і Windows з Docker Desktop проблема менш помітна: файлова система хоста монтується у віртуальну машину з перетворенням власників. Тому налаштування, що «працює на Mac», ламається на Linux-сервері чи в CI.

Пастка chmod -R 777 - «швидке рішення» проблем з правами, яке дає будь-якому процесу право змінювати код застосунку. Правильно - потрібний власник і мінімальні права (775 для каталогів запису).

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

«Docker» - це набір компонентів, і розуміння їхньої ролі пояснює, чому образи, зібрані Docker, працюють у Kubernetes без Docker.

Стандарти OCI (Open Container Initiative):

  • image spec - формат образу: шари, маніфест, конфігурація;
  • runtime spec - як запустити контейнер з розпакованого образу;
  • distribution spec - протокол реєстрів (push/pull).

Образ, зібраний Docker, - звичайний OCI-образ. Його запускають containerd, CRI-O, Podman, Kubernetes.

Шари виконання:

docker CLI  →  dockerd (Docker Engine)  →  containerd  →  containerd-shim  →  runc  →  процес
  • docker CLI - клієнт; надсилає команди демону через API (сокет /var/run/docker.sock);
  • dockerd - Docker Engine: збирання (BuildKit), мережі, томи, API, Compose-сумісність;
  • containerd - керує життєвим циклом контейнерів і образами (pull, зберігання, знімки файлових систем);
  • runc - низькорівневе середовище виконання OCI: створює namespaces і cgroups і запускає процес;
  • shim - тримає контейнер, коли containerd перезапускається.

Чому це важливо на практиці:

  • Kubernetes з версії 1.24 не використовує Docker Engine напряму - він працює з containerd чи CRI-O через CRI. Образи при цьому ті самі;
  • перезапуск dockerd не обов'язково зупиняє контейнери (опція live-restore);
  • альтернативні runtime: gVisor (runsc) додає ізоляцію ядра в просторі користувача, Kata Containers - легкі віртуальні машини для кожного контейнера. Підключаються до Docker як --runtime;
  • Podman - сумісний з CLI Docker, без центрального демона й з rootless за замовчуванням.

Docker Desktop на macOS і Windows: контейнерам потрібне ядро Linux, тож Docker Desktop запускає легку віртуальну машину з Linux, а docker CLI на хості говорить з демоном у ній. Звідси особливості: повільніший доступ до файлів хоста (bind mount через межу віртуальної машини), окрема пам'ять і CPU, обмежені налаштуваннями VM, host.docker.internal для доступу до хоста.

Архітектура процесора: образ зібрано під конкретну архітектуру (amd64, arm64). На Mac з Apple Silicon образ amd64 запуститься через емуляцію - повільно й інколи з помилками, тому для продакшен-серверів важливі мультиплатформні образи.

Докладніше в документації: Docker: альтернативні середовища виконання

Офіційний образ PHP постачається без php.ini - діють значення за замовчуванням, розраховані на розробку. Для продакшену конфігурацію треба задати явно.

Базова конфігурація:

FROM php:8.5-fpm

RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY docker/php/app.ini $PHP_INI_DIR/conf.d/zz-app.ini
; docker/php/app.ini
expose_php = Off
memory_limit = 256M
upload_max_filesize = 20M
post_max_size = 25M
max_execution_time = 30
date.timezone = Europe/Kyiv

opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.jit = tracing
opcache.jit_buffer_size = 64M

Ключові рішення для контейнера:

  • opcache.validate_timestamps = 0 - PHP не перевіряє зміну файлів на кожен запит. У контейнері код незмінний (новий деплой = новий контейнер), тож перевірка - марна робота. Але для розробки з bind mount потрібне 1, інакше зміни коду не видно;
  • opcache.max_accelerated_files - більше за кількість PHP-файлів проєкту з vendor (у Laravel-проєкті легко десятки тисяч);
  • preload (opcache.preload) - завантажити класи фреймворку в пам'ять при старті FPM. Дає приріст, але вимагає перезапуску при зміні коду й акуратного вибору файлів;
  • JIT помітно допомагає обчислювальним задачам; для типового вебзастосунку, що чекає на базу, ефект невеликий.

PHP-FPM:

; www.conf
pm = static            ; передбачуване використання пам'яті в контейнері з лімітом
pm.max_children = 20   ; ≈ ліміт пам'яті контейнера / пам'ять одного воркера
pm.max_requests = 500  ; перезапуск воркера - захист від витоків пам'яті

У контейнері з жорстким лімітом пам'яті pm = dynamic з великим max_children може перевищити ліміт під навантаженням - і OOM-killer вб'є процеси.

Логи: FPM і PHP мають писати в stderr/stdout (error_log = /proc/self/fd/2, catch_workers_output = yes), щоб їх збирав Docker.

Розділення середовищ: однаковий образ для всіх середовищ, а відмінності (рівень логування, Xdebug) - через змінні оточення чи окрему стадію збирання для розробки, а не різні Dockerfile.

Часовий пояс: date.timezone у PHP і змінна TZ разом з пакетом tzdata в образі - інакше логи, планувальник і дати застосунку можуть «з'їжджати» на UTC.

Докладніше в документації: Docker Hub: офіційний образ PHP