Middle: питання на співбесіді з теми «Образи й Dockerfile»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
Multi-stage build - кілька етапів FROM в одному Dockerfile. Проміжні етапи містять інструменти для збирання, а фінальний образ копіює з них лише результат.
# Етап 1: фронтенд
FROM node:22-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Етап 2: залежності PHP
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist
# Етап 3: фінальний образ
FROM php:8.4-fpm-alpine
WORKDIR /var/www
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
COPY . .
Що це дає:
- Менший образ. У фінальному немає Node.js,
node_modules, Composer, компіляторів - лише те, що потрібно для роботи. Різниця буває в сотні мегабайтів. - Менша поверхня атаки. Менше програм - менше вразливостей і менше інструментів для нападника, який потрапив у контейнер.
- Паралельне збирання. BuildKit збирає незалежні етапи одночасно, а невикористані пропускає.
- Один Dockerfile для всього: окремий етап
devз Xdebug і інструментами,testдля CI,production- збирати потрібний через--target.
docker build --target production -t myapp .
Пастка: копіюючи з етапу збирання, легко забути щось потрібне під час роботи (розширення PHP, системну бібліотеку). Перевіряйте фінальний образ запуском тестів чи smoke-тестом саме в ньому.
Обидві інструкції задають, що запускається в контейнері, але по-різному поводяться з аргументами docker run:
CMD- команда за замовчуванням. Якщо при запуску передати команду, вона повністю замінитьCMD.ENTRYPOINT- основний виконуваний файл. Аргументиdocker run(абоCMD) дописуються до нього як параметри.
ENTRYPOINT ["php", "artisan"]
CMD ["serve", "--host=0.0.0.0"]
docker run myapp # php artisan serve --host=0.0.0.0
docker run myapp queue:work # php artisan queue:work
docker run myapp migrate --force # php artisan migrate --force
Типовий сценарій - скрипт-обгортка як ENTRYPOINT, що готує середовище й передає керування команді:
#!/bin/sh
set -e
php artisan config:cache
exec "$@" # запустити CMD - і зробити його PID 1
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["php-fpm"]
Exec-форма проти shell-форми:
CMD ["php-fpm"](JSON-масив, exec-форма) - процес запускається напряму й отримує сигнали.CMD php-fpm(shell-форма) - запускається через/bin/sh -c, і PID 1 - це оболонка. СигналSIGTERMприdocker stopотримує shell, а не застосунок, тож контейнер не завершується коректно і через 10 секунд його вбиваютьSIGKILL.
Тому в обох інструкціях - exec-форма, а в скрипті-обгортці - exec "$@".
ENTRYPOINT можна перевизначити при запуску: docker run --entrypoint sh myapp.
Поширене непорозуміння: EXPOSE 80 «відкриває порт». Насправді EXPOSE нічого не відкриває - це документація в метаданих образу: «застосунок у контейнері слухає цей порт».
EXPOSE 8080
Що EXPOSE робить:
- записує порт у метадані образу (
docker image inspect→ExposedPorts); - слугує підказкою для людей і інструментів;
- використовується прапорцем
-P(--publish-all): Docker опублікує всі задекларовані порти на випадкові порти хоста.
Що EXPOSE не робить:
- не публікує порт на хості - без
-pконтейнер недоступний ззовні, зEXPOSEчи без; - не обмежує доступ: інші контейнери в тій самій мережі можуть звертатися до будь-якого порту, на якому слухає процес, - навіть незадекларованого;
- не змушує застосунок слухати цей порт - це має налаштувати сам застосунок.
Публікація порту - при запуску:
docker run -p 8080:80 nginx # хост:8080 → контейнер:80, на всіх інтерфейсах хоста
docker run -p 127.0.0.1:8080:80 nginx # лише локально
# compose
services:
web:
ports:
- "8080:80"
Пастка безпеки з -p 3306:3306: опублікований порт доступний на всіх інтерфейсах хоста (0.0.0.0), тобто з інтернету, якщо сервер має публічну адресу. Docker додає правила в iptables так, що вони можуть обійти налаштування фаєрвола хоста (ufw). Базу й Redis не публікують зовсім - сервіси звертаються до них через мережу Docker - або публікують лише на 127.0.0.1.
Протокол: за замовчуванням TCP; для UDP - EXPOSE 53/udp і -p 53:53/udp.
Чи писати EXPOSE взагалі: так - як документацію: той, хто запускає образ, одразу бачить, який порт слухає застосунок. Але покладатися на нього як на механізм безпеки чи мережі не можна.
Docker за замовчуванням знає лише, чи працює процес. Процес може бути живим, але застосунок - завислим: зайняті всі воркери, втрачено з'єднання з базою, вичерпано пам'ять. HEALTHCHECK дає змогу перевіряти справжню готовність.
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsS http://localhost/up || exit 1
Параметри:
--interval- як часто перевіряти (за замовчуванням 30 с);--timeout- скільки чекати на відповідь;--start-period- час на запуск, протягом якого невдачі не рахуються (прогрів, міграції);--start-interval- частіші перевірки протягом стартового періоду, щоб швидше стати «здоровим»;--retries- скільки невдач поспіль до статусуunhealthy.
Команда повертає 0 - здоровий, 1 - нездоровий. Статус видно в docker ps (healthy, unhealthy, starting).
Хто використовує статус:
- Compose:
depends_onзcondition: service_healthy- сервіс стартує, коли залежність справді готова; - Swarm замінює нездорові контейнери і не перемикає трафік на новий контейнер під час оновлення, доки той не здоровий;
- платформи деплою (Dokploy, Coolify, Kamal) чекають на здоровий контейнер перед перемиканням трафіку.
Сам Docker Engine не перезапускає нездоровий контейнер - статус лише позначається. Перезапуск робить оркестратор чи власна логіка.
Яка перевірка добра:
- легка й швидка: запит на
/up(у Laravel 11+ такий маршрут є за замовчуванням), а не тяжка сторінка з запитами до бази; - перевіряє застосунок, а не лише порт: відповідь саме від PHP, а не лише від Nginx;
- глибина - свідомий вибір: перевірка бази в healthcheck означає, що короткочасна недоступність бази позначить нездоровими всі контейнери застосунку - і оркестратор почне їх перезапускати, погіршуючи ситуацію. Часто краще перевіряти лише сам застосунок;
- наявні утиліти: у мінімальних образах немає
curl- тодіwget -q --spider, вбудована команда застосунку чи невеликий бінарник.
Для воркерів без HTTP (черги, планувальник) - перевірка процесу чи файлу-маркера, який воркер оновлює («серцебиття»).
Перевизначити чи вимкнути - у Compose: healthcheck: у сервісі чи disable: true.
Шари образу незмінні. Кожна інструкція додає новий шар поверх попередніх, і видалення файлу в пізнішому шарі не прибирає його з попереднього - лише позначає як видалений (так званий whiteout-файл).
RUN curl -o /tmp/sdk.tar.gz https://example.com/sdk.tar.gz # шар +500 МБ
RUN tar -xzf /tmp/sdk.tar.gz -C /opt && rm /tmp/sdk.tar.gz # архів «видалено»
У контейнері /tmp/sdk.tar.gz не видно, але 500 МБ залишилися в першому шарі. Образ завантажується з усіма шарами - і з видаленими файлами теж.
Як працює файлова система контейнера (OverlayFS):
- шари образу - лише для читання, складені один на одного;
- контейнер отримує тонкий записуваний шар зверху;
- зміна файлу з нижнього шару - копіювання при записі (copy-on-write): файл копіюється у верхній шар і змінюється там;
- видалення - позначка у верхньому шарі, що ховає файл нижче.
Як правильно - створення і видалення в одному шарі:
RUN curl -o /tmp/sdk.tar.gz https://example.com/sdk.tar.gz \
&& tar -xzf /tmp/sdk.tar.gz -C /opt \
&& rm /tmp/sdk.tar.gz
Або, краще, multi-stage build: усе тимчасове - у стадії збирання, у фінальний образ копіюється лише результат:
FROM debian:bookworm-slim AS sdk
RUN ... завантажити й розпакувати
FROM php:8.5-fpm
COPY --from=sdk /opt/sdk /opt/sdk
Типові «невидимі» витрати:
apt-get updateв одномуRUN, а очищення/var/lib/apt/lists- в іншому;COPY . ., а потімRUN rm -rf tests node_modules- краще не копіювати їх через.dockerignore;RUN chown -R www-data /appпісляCOPY- повна копія всіх файлів у новому шарі (замість цього -COPY --chown);- секрети: файл з ключем, скопійований і потім видалений, лишається в шарі - його можна дістати з образу. Для секретів -
RUN --mount=type=secret.
Записуваний шар контейнера теж варто тримати малим: логи, кеш, завантажені файли - у томах чи зовнішніх сховищах. Запис у шар контейнера повільніший (copy-on-write) і зникає разом з контейнером.