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

Middle: питання на співбесіді з теми «Compose, мережі й томи»

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

5 питань

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