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) потрібен і проксі, і оркестратору - щоб не відправляти трафік у контейнер, що ще не готовий.
За замовчуванням контейнер може використати всю пам'ять і 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. Тоді коду застосунку взагалі не потрібен запис на диск.
Контейнер у циклі перезапусків (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 exec -it app bash), встановити пакет, виправити конфіг - «і все запрацювало». А потім зберегти результат як образ:
docker commit app myapp:fixed
Чому це погана практика:
- зміни зникнуть: записуваний шар контейнера знищується разом з контейнером. Наступний деплой, перезапуск оркестратором, масштабування - і ручні виправлення втрачено;
- невідтворюваність: образ з
docker commit- «чорна скринька». Ніхто не знає, які саме команди виконано, в якому порядку, з якими версіями. Повторити збирання неможливо; - немає історії в Git: зміни не проходять рев'ю, не прив'язані до коміту, їх не відкотити;
- сміття в образі: кеш менеджерів пакетів, логи, тимчасові файли, історія команд - усе, що було в контейнері, потрапляє в образ;
- секрети: якщо в контейнері були токени чи ключі - вони тепер в образі.
Принцип незмінної інфраструктури: контейнери не «лагодять», а замінюють. Виправлення - у Dockerfile чи конфігурації, новий образ, новий деплой.
Як правильно розслідувати й виправляти:
- зайти в контейнер, щоб з'ясувати проблему (це нормально);
- перенести виправлення в Dockerfile, конфіг у репозиторії чи змінні оточення;
- зібрати новий образ і задеплоїти.
Що бачити, що змінено в контейнері:
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) - ручні зміни просто неможливі, а помилки конфігурації виявляються одразу.