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

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

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

100 питань

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: аналіз шарів образу

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-тестом саме в ньому.

Докладніше в документації: Multi-stage builds

Обидві інструкції задають, що запускається в контейнері, але по-різному поводяться з аргументами 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.

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

Поширене непорозуміння: 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 взагалі: так - як документацію: той, хто запускає образ, одразу бачить, який порт слухає застосунок. Але покладатися на нього як на механізм безпеки чи мережі не можна.

Докладніше в документації: Dockerfile: 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.

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

Шари образу незмінні. Кожна інструкція додає новий шар поверх попередніх, і видалення файлу в пізнішому шарі не прибирає його з попереднього - лише позначає як видалений (так званий 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) і зникає разом з контейнером.

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

depends_on: [db] у звичайній формі гарантує лише порядок запуску контейнерів: db стартує раніше за app. Але «контейнер запущено» - не «база приймає з'єднання». PostgreSQL ще кілька секунд ініціалізується, а застосунок уже пробує підключитися й падає.

Рішення - healthcheck + умова:

services:
  db:
    image: postgres:17
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
      interval: 5s
      timeout: 3s
      retries: 10

  app:
    build: .
    depends_on:
      db:
        condition: service_healthy

Тепер app стартує лише тоді, коли healthcheck бази почав проходити.

Інші умови:

  • service_started - поведінка за замовчуванням.
  • service_completed_successfully - дочекатися, поки разовий контейнер завершиться з кодом 0. Зручно для міграцій: окремий сервіс migrate запускається, виконує php artisan migrate --force і завершується, а app стартує після нього.

Але на цьому не зупиняються: depends_on працює лише при старті через Compose. База може перезапуститися пізніше, а на проді застосунок часто запускають оркестратором без Compose. Тому застосунок сам має бути стійким до тимчасової недоступності залежностей: повторні спроби підключення, коректні помилки, а не падіння при першому ж збої.

Healthcheck для самого застосунку (наприклад, на маршрут /up у Laravel) потрібен і проксі, і оркестратору - щоб не відправляти трафік у контейнер, що ще не готовий.

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

За замовчуванням контейнер може використати всю пам'ять і CPU хоста. Один процес з витоком пам'яті здатен покласти весь сервер разом з базою й іншими сервісами.

docker run --memory=512m --cpus=1.5 myapp
services:
  worker:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.5"

Що відбувається при перевищенні пам'яті: ядро Linux (OOM killer) вбиває процес у контейнері. Контейнер завершується з кодом 137 (128 + 9, сигнал SIGKILL), а в docker inspect видно "OOMKilled": true. З restart: unless-stopped він підніметься знову - і, якщо причина не усунена, впаде знову.

CPU-ліміт працює інакше: процес не вбивають, а сповільнюють (throttling) - він отримує не більше вказаної частки процесорного часу.

Нюанси для PHP:

  • memory_limit у PHP і ліміт контейнера - різні речі. Ліміт контейнера рахує всі процеси: майстер PHP-FPM і всі воркери разом. pm.max_children = 20 з піком 100 МБ на воркер - це до 2 ГБ, і ліміт 512 МБ гарантовано закінчиться OOM.
  • Кількість воркерів FPM, Octane чи черг підбирають під ліміт пам'яті контейнера, а не під пам'ять хоста.
  • Воркерам черг ставлять --memory і --max-jobs, щоб вони перезапускалися раніше, ніж їх уб'є ядро.

Моніторинг: docker stats показує поточне споживання. Код виходу 137 у логах і рестарти без явних помилок у застосунку - перша ознака, що контейнеру не вистачає пам'яті.

Докладніше в документації: Обмеження ресурсів

tmpfs - файлова система в оперативній пам'яті. Дані ніколи не потрапляють на диск хоста і зникають при зупинці контейнера.

docker run --tmpfs /tmp:rw,size=64m,mode=1777 myapp
services:
  app:
    tmpfs:
      - /tmp:size=64m
    read_only: true

Три види сховища в Docker:

Том Bind mount tmpfs
де дані область Docker на диску каталог хоста пам'ять
переживає контейнер так так ні
швидкість диск диск (на macOS - повільно) пам'ять
для чого дані, що мають зберігатися код у розробці, конфіги тимчасові й чутливі дані

Коли tmpfs доречний:

  • файлова система лише для читання: read_only: true - сильний захист (зловмисник не запише бекдор у код), але застосунку потрібні тимчасові файли. tmpfs для /tmp, /run і каталогів кешу дає записуване місце без ослаблення захисту;
  • чутливі тимчасові дані, які не повинні потрапити на диск: розшифровані файли, тимчасові ключі;
  • швидкий тимчасовий кеш: скомпільовані шаблони, тимчасові файли обробки - коли їх втрата при перезапуску не страшна;
  • тести: база в tmpfs (/var/lib/postgresql/data у тестовому контейнері) значно прискорює тести з багатьма записами.

Що враховувати:

  • пам'ять: tmpfs займає оперативну пам'ять і рахується в ліміт пам'яті контейнера. Без size можна непомітно з'їсти половину пам'яті хоста; заповнений tmpfs при ліміті - OOM;
  • дані зникають при перезапуску - нічого, що має зберігатися;
  • лише Linux-контейнери і не спільний між контейнерами (кожен має свій);
  • права: mode=1777 для /tmp, інакше непривілейований користувач не зможе писати.

Для Laravel з файловою системою лише для читання: storage/framework/cache, storage/framework/views - у tmpfs чи томі, сесії й кеш - у Redis чи базі, логи - в stderr, завантажені файли - в S3/R2. Тоді коду застосунку взагалі не потрібен запис на диск.

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

Контейнер у циклі перезапусків (Restarting (1) 5 seconds ago) - головний процес падає одразу після старту, а політика --restart запускає його знову.

Крок 1 - логи, включно з попередніми спробами:

docker logs --tail 100 app
docker logs --since 10m app

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

Крок 2 - стан і причина завершення:

docker inspect app --format '{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}}'

OOMKilled=true - перевищено ліміт пам'яті; код 127 - команду не знайдено; 1 - помилка застосунку.

Крок 3 - запустити образ вручну з оболонкою, обійшовши CMD:

docker run --rm -it --entrypoint sh myapp:latest
docker compose run --rm app sh

Усередині - перевірити файли, права, змінні, запустити команду вручну й побачити помилку.

Споживання ресурсів:

docker stats                 # CPU, пам'ять (з лімітом), мережа, диск - у реальному часі
docker stats --no-stream     # одноразовий знімок
docker top app               # процеси всередині контейнера

Що шукати:

  • пам'ять росте й не падає - витік у довгоживучому процесі (воркер черги без --max-jobs, Octane без --max-requests);
  • пам'ять близька до ліміту - скоро буде OOM; врахуйте, що в пам'ять контейнера входить і сторінковий кеш файлів;
  • CPU 100% постійно - нескінченний цикл, активне очікування, надто часте опитування;
  • багато процесів у docker top - PHP-FPM з завеликим max_children чи процеси-«зомбі» (немає init у PID 1).

Події Docker:

docker events --filter container=app --since 1h

Показує die, oom, kill, restart, health_status з часом - видно хронологію.

Для продакшену ручних команд замало: збір метрик (cAdvisor + Prometheus, Beszel, Netdata) і логів у централізоване сховище, сповіщення про перезапуски й OOM.

Мінімальні образи без оболонки (distroless) - для налагодження docker debug, який підключає набір інструментів до запущеного контейнера.

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

Спокуса знайома: зайти в контейнер (docker exec -it app bash), встановити пакет, виправити конфіг - «і все запрацювало». А потім зберегти результат як образ:

docker commit app myapp:fixed

Чому це погана практика:

  • зміни зникнуть: записуваний шар контейнера знищується разом з контейнером. Наступний деплой, перезапуск оркестратором, масштабування - і ручні виправлення втрачено;
  • невідтворюваність: образ з docker commit - «чорна скринька». Ніхто не знає, які саме команди виконано, в якому порядку, з якими версіями. Повторити збирання неможливо;
  • немає історії в Git: зміни не проходять рев'ю, не прив'язані до коміту, їх не відкотити;
  • сміття в образі: кеш менеджерів пакетів, логи, тимчасові файли, історія команд - усе, що було в контейнері, потрапляє в образ;
  • секрети: якщо в контейнері були токени чи ключі - вони тепер в образі.

Принцип незмінної інфраструктури: контейнери не «лагодять», а замінюють. Виправлення - у Dockerfile чи конфігурації, новий образ, новий деплой.

Як правильно розслідувати й виправляти:

  1. зайти в контейнер, щоб з'ясувати проблему (це нормально);
  2. перенести виправлення в Dockerfile, конфіг у репозиторії чи змінні оточення;
  3. зібрати новий образ і задеплоїти.

Що бачити, що змінено в контейнері:

docker diff app
# A /var/www/html/storage/logs/laravel.log   (додано)
# C /usr/local/etc/php/conf.d                 (змінено)
# D /tmp/cache                                (видалено)

Корисно для розслідування: що саме записує застосунок у файлову систему контейнера (і чи не варто це винести в том чи tmpfs).

Коли docker commit доречний:

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

Захист від спокуси в продакшені: файлова система лише для читання (read_only: true) - ручні зміни просто неможливі, а помилки конфігурації виявляються одразу.

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

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

Рівні
Junior 35 Middle 35 Senior 30

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