Senior: питання на співбесіді з теми «Compose, мережі й томи»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
docker stop надсилає головному процесу контейнера (PID 1) сигнал SIGTERM і чекає (за замовчуванням 10 секунд). Якщо процес не завершився - надсилає SIGKILL, який не можна перехопити: усе обривається на півдорозі.
Чому SIGTERM не доходить до застосунку:
- Shell-форма команди.
CMD php artisan queue:workзапускається як/bin/sh -c "...". PID 1 - оболонка, аshне передає сигнали дочірнім процесам. Воркер не дізнається, що треба зупинитися. - Скрипт-обгортка без
exec.entrypoint.sh, що просто викликаєphp-fpmв кінці, лишається PID 1, а FPM - його дочірнім процесом. - 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.
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), бо логи - для розслідування, а не для сповіщень.
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 - один основний процес на контейнер, а різні відповідальності - окремі контейнери, що взаємодіють через мережу й томи:
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- часто достатньо без входу в контейнер.