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

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

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

5 питань

docker stop надсилає головному процесу контейнера (PID 1) сигнал SIGTERM і чекає (за замовчуванням 10 секунд). Якщо процес не завершився - надсилає SIGKILL, який не можна перехопити: усе обривається на півдорозі.

Чому SIGTERM не доходить до застосунку:

  1. Shell-форма команди. CMD php artisan queue:work запускається як /bin/sh -c "...". PID 1 - оболонка, а sh не передає сигнали дочірнім процесам. Воркер не дізнається, що треба зупинитися.
  2. Скрипт-обгортка без exec. entrypoint.sh, що просто викликає php-fpm в кінці, лишається PID 1, а FPM - його дочірнім процесом.
  3. PID 1 особливий у ядрі: для нього сигнали без явно встановленого обробника ігноруються. Процес, що не обробляє SIGTERM, ставши PID 1, не завершиться від нього.

Рішення:

CMD ["php", "artisan", "queue:work"]   # exec-форма: процес - PID 1
#!/bin/sh
# entrypoint.sh
php artisan config:cache
exec "$@"            # замінити оболонку командою, а не запустити дочірній процес
  • --init (init: true у Compose) додає крихітний init-процес (tini) як PID 1: він пересилає сигнали й прибирає процеси-зомбі.
  • stop_grace_period - більше часу, якщо коректне завершення справді довге.

Навіщо це на практиці: воркер черги, що отримав SIGTERM, доробляє поточне завдання й завершується. Убитий SIGKILL - обриває його посередині: напівзапис у базу, повторне виконання після рестарту. Тому --timeout воркера, stop_grace_period і ліміти оркестратора налаштовують узгоджено. Те саме для PHP-FPM (SIGQUIT - м'яке завершення) і Octane.

Докладніше в документації: Shell- і exec-форма в Dockerfile

Docker збирає все, що процес пише в stdout і stderr, і передає драйверу логування. Звідти логи бачить docker logs, їх забирають збирачі (Loki, Fluent Bit, Vector, CloudWatch) і оркестратори.

Чому не файли всередині контейнера:

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

Laravel: канал stderr замість single/daily:

LOG_CHANNEL=stderr

Для PHP-FPM ще потрібно, щоб помилки воркерів потрапляли в stderr (catch_workers_output = yes, error_log = /proc/self/fd/2).

Структуровані логи. JSON-рядки замість тексту дозволяють збирачу індексувати поля: рівень, ID запиту, користувача, тривалість. Тоді пошук «усі помилки запиту X» - це запит, а не grep.

Ротація на хості. Драйвер за замовчуванням json-file не обмежує розмір, і логи балакучого контейнера можуть заповнити диск сервера. Налаштовують:

{
  "log-driver": "local",
  "log-opts": { "max-size": "20m", "max-file": "5" }
}

(у /etc/docker/daemon.json для всіх контейнерів або logging: у Compose для окремого сервісу).

Що ще варто: не логувати секрети й персональні дані; додавати ID запиту в усі записи, щоб пов'язати лог застосунку, проксі й воркера черги; мати окремий моніторинг помилок (Sentry), бо логи - для розслідування, а не для сповіщень.

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

localhost усередині контейнера - це сам контейнер, а не хост. Тому з контейнера застосунку DB_HOST=localhost не знайде базу, що працює на хості.

Як звернутися до хоста:

Docker Desktop (macOS, Windows): спеціальне ім'я host.docker.internal вказує на хост:

DB_HOST=host.docker.internal

Linux: цього імені за замовчуванням немає, але його можна додати:

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

host-gateway Docker підставляє як адресу хоста в мережі контейнерів. Laravel Sail додає це налаштування саме для Xdebug, якому треба підключатися до IDE на хості.

Сервіс на хості має слухати не лише 127.0.0.1, а інтерфейс, доступний з мережі Docker, - інакше з'єднання буде відхилено.

Мережа host (--network host) - контейнер використовує мережевий стек хоста напряму: localhost - це хост, порти не потрібно публікувати. На Linux це просто й без накладних витрат; у Docker Desktop мережа host працює інакше (вона відноситься до віртуальної машини, а підтримка для хоста macOS/Windows з'явилася в нових версіях і має обмеження).

Чим Docker Desktop відрізняється від Linux:

  • контейнери працюють у віртуальній машині з Linux - мережі Docker живуть у ній, а не на хості;
  • IP-адреси контейнерів недоступні з хоста напряму - лише через опубліковані порти;
  • опубліковані порти прокидаються з віртуальної машини на хост автоматично;
  • VPN і корпоративні проксі можуть впливати на мережу VM - Docker Desktop має окремі налаштування проксі.

На Linux:

  • IP-адреси контейнерів (172.17.x.x) доступні з хоста напряму;
  • опубліковані порти Docker відкриває правилами iptables, що можуть обходити ufw - порт бази, опублікований на 0.0.0.0, стає доступним з інтернету, навіть якщо ufw його «закриває».

Практичні правила:

  • сервіси між собою - через мережу Docker за назвою сервісу (DB_HOST=db), а не через хост;
  • до хоста - через host.docker.internal з host-gateway для Linux;
  • не покладатися на IP-адреси контейнерів - вони змінюються при перестворенні;
  • конфігурацію, що «працює на Mac», перевіряти на Linux (CI, сервер) - мережева поведінка відрізняється.

Докладніше в документації: Docker Desktop: мережа

Рекомендація Docker - один основний процес на контейнер, а різні відповідальності - окремі контейнери, що взаємодіють через мережу й томи:

services:
  web:      # Nginx
  app:      # PHP-FPM
  queue:    # php artisan queue:work
  scheduler: # php artisan schedule:work

Чому окремі контейнери кращі:

  • незалежне масштабування: воркерів черги може бути п'ять, а вебконтейнерів два;
  • окремі ресурси й ліміти: воркер, що обробляє відео, не з'їсть пам'ять вебзапитів;
  • ізольовані перезапуски й деплой: падіння воркера не вбиває веб;
  • логи й стан кожного процесу окремо, зрозумілий docker ps і healthcheck;
  • PID 1 - сам процес, тож сигнали зупинки доходять напряму.

Той самий образ - різні команди. Не потрібно чотири образи: один образ застосунку, а контейнери відрізняються command:

x-app: &app
  image: myapp:${TAG}
  env_file: .env

services:
  app:   { <<: *app }
  queue: { <<: *app, command: php artisan queue:work --max-jobs=1000 }
  scheduler: { <<: *app, command: php artisan schedule:work }

Коли кілька процесів в одному контейнері виправдані:

  • тісно пов'язані процеси, що мають жити й масштабуватися разом: Nginx + PHP-FPM у одному «вебконтейнері» - поширений і прийнятний компроміс;
  • середовища, що дають лише один контейнер (деякі PaaS);
  • сайдкар-процеси, без яких основний не працює.

Як робити це правильно:

  • менеджер процесів у PID 1 - supervisord, s6-overlay, tini + скрипт. Він запускає процеси, перезапускає впалі й передає сигнали зупинки всім;
  • вивід усіх процесів у stdout/stderr контейнера;
  • визначитися, що робити при падінні одного процесу: перезапустити лише його чи завершити весь контейнер (щоб оркестратор побачив проблему). Тихий перезапуск ховає помилки.

Сучасна альтернатива для Laravel: FrankenPHP поєднує веб-сервер і PHP в одному процесі - зв'язка Nginx + PHP-FPM стає непотрібною, і «вебконтейнер» знову має один процес.

Антипатерн: запускати в одному контейнері ще й базу чи Redis «для простоти» - дані, оновлення й масштабування бази стають залежними від деплою застосунку.

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

Мінімальні образи (distroless, scratch, «скелетні» Alpine) - добра практика безпеки: менше пакетів - менше вразливостей і можливостей для зловмисника. Але в них немає sh, curl, ps, netstat - docker exec -it app sh не працює.

docker debug (входить у Docker Desktop і передплати Docker) підключає до запущеного контейнера окрему оболонку з набором інструментів, не змінюючи образ:

docker debug app
  • бачить файлову систему й процеси контейнера;
  • інструменти (vim, curl, htop, nslookup...) беруться з окремого образу інструментів і не потрапляють у контейнер застосунку;
  • працює й для зупинених контейнерів і образів (docker debug myapp:latest).

Способи без docker debug:

1. Контейнер-сусід у тих самих просторах імен:

docker run --rm -it \
  --network container:app \
  --pid container:app \
  nicolaka/netshoot

Інструментальний контейнер бачить мережу й процеси контейнера app: ss -tlnp, curl localhost:8080, tcpdump, ps aux. Сам контейнер застосунку не змінюється.

2. Доступ до файлової системи:

docker cp app:/var/www/html/storage/logs/laravel.log ./
docker export app | tar -t | grep config   # вміст файлової системи контейнера

3. nsenter з хоста (Linux, потрібні права root) - увійти в простори імен процесу контейнера й використовувати інструменти хоста.

4. Окрема налагоджувальна стадія в Dockerfile:

FROM gcr.io/distroless/... AS production
FROM production AS debug
COPY --from=busybox:musl /bin/busybox /busybox/

Для локального розслідування збирається --target debug, у продакшен іде production.

У Kubernetes аналог - тимчасові контейнери (kubectl debug), що підключаються до поди.

Що важливо:

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

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