Питання на співбесіді: Compose, мережі й томи
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
15 питань
Файлова система контейнера - тимчасова: усе, що записано всередину, зникає разом з контейнером (docker rm). Для даних, що мають жити довше, є два основні способи:
Том (volume) - сховище, яким керує Docker (зазвичай у /var/lib/docker/volumes). Живе незалежно від контейнерів.
services:
db:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Bind mount - каталог хоста, змонтований у контейнер напряму. Зміни видно з обох боків одразу.
services:
app:
volumes:
- ./:/var/www # код з машини розробника
Коли що:
- Томи - для даних сервісів: бази, Redis, завантажені файли. Портабельні, не залежать від структури каталогів хоста, з ними простіше робити бекапи. На macOS і Windows значно швидші за bind mount.
- Bind mount - у розробці: правите код в IDE, контейнер одразу бачить зміни. На проді - лише для конфігурацій, якщо взагалі.
Пастки:
docker compose down -vвидаляє томи разом з даними бази. Без-vтоми лишаються.- Права доступу в bind mount: файли, створені в контейнері від root, на хості теж належать root.
- Анонімні томи (без імені) легко загубити й накопичити -
docker volume pruneприбирає невикористані.
На проді для коду ні том, ні bind mount не потрібні: код запікають в образ, а новий реліз - це новий образ.
Compose створює для проєкту окрему мережу, і всі сервіси в ній доступні один одному за назвою сервісу - вбудований DNS Docker перетворює її на IP контейнера.
services:
app:
build: .
environment:
DB_HOST: db # не localhost і не IP
REDIS_HOST: redis
db:
image: postgres:17
redis:
image: redis:7
Типова помилка новачка - localhost. У контейнері app адреса localhost - це сам контейнер app, а не база. Бази там немає, тож DB_HOST=localhost дає «connection refused». Потрібно DB_HOST=db.
Порти: внутрішні й опубліковані.
- Усередині мережі сервіси звертаються один до одного за внутрішнім портом контейнера:
db:5432. ports: ["5433:5432"]публікує порт на хост - щоб підключитися з машини розробника чи з інтернету. Для зв'язку між контейнерами публікувати не потрібно.
db:
ports:
- "127.0.0.1:5433:5432" # доступно лише з цього комп'ютера
Безпека: ports: ["5432:5432"] на сервері відкриває базу на всі інтерфейси - і Docker при цьому обходить правила ufw, бо сам керує iptables. Базу на проді або не публікують узагалі, або прив'язують до 127.0.0.1.
Кілька мереж дозволяють ізолювати сервіси: фронтенд-проксі бачить застосунок, але не базу.
docker run створює контейнер з образу й запускає його.
docker run -d --name redis --restart unless-stopped -p 127.0.0.1:6379:6379 -v redis-data:/data redis:8-alpine
Основні прапорці:
| Прапорець | Що робить |
|---|---|
-d |
у фоні (detached); без нього - вивід у терміналі |
--name redis |
ім'я замість випадкового (hungry_turing) |
-p 8080:80 |
опублікувати порт контейнера 80 на порту хоста 8080 |
-v назва:/шлях |
іменований том; -v ./src:/app - каталог хоста (bind mount) |
-e KEY=value |
змінна оточення; --env-file .env - з файлу |
--rm |
видалити контейнер після завершення (для разових команд) |
-it |
інтерактивний режим з терміналом (для bash, tinker) |
--restart |
політика перезапуску: no, on-failure, always, unless-stopped |
--network |
приєднати до мережі |
-w /app |
робочий каталог |
--memory 512m, --cpus 1 |
ліміти ресурсів |
--user 1000:1000 |
від імені якого користувача запустити процес |
Команда після образу замінює CMD образу:
docker run --rm -it php:8.5-cli php -v
docker run --rm -v "$PWD":/app -w /app composer:2 composer install
Другий приклад - класичний спосіб запустити інструмент без встановлення на хост: контейнер існує лише на час команди.
Що варто пам'ятати:
- без
--rmзупинені контейнери накопичуються (docker ps -a) разом зі своїми записуваними шарами; - порядок має значення: прапорці Docker - до назви образу, аргументи для процесу - після.
docker run nginx -p 80:80передасть-p 80:80самому nginx; -p 8080:80публікує порт на всіх інтерфейсах хоста - для служб, які не мають бути доступні ззовні, --p 127.0.0.1:8080:80;-vз відносним шляхом без./Docker вважає назвою тому, а не каталогом;--restart alwaysперезапускає навіть контейнер, зупинений вручну, після перезапуску Docker;unless-stopped- поважає ручну зупинку.
Для кількох пов'язаних контейнерів довгі команди docker run швидко стають незручними - їх описують у compose.yaml.
Стани контейнера:
created → running → (paused) → exited → (removed)
↑______restart______|
- created - створений (
docker create), але не запущений; - running - головний процес працює;
- paused - процеси заморожено (
docker pause), пам'ять зберігається; - exited - головний процес завершився; файлова система контейнера й логи лишаються, доки контейнер не видалено;
- removed -
docker rm: контейнер і його записуваний шар знищено (томи - ні).
Головне правило: контейнер живе, поки працює його головний процес (PID 1). Процес завершився - контейнер зупинився. Тому контейнер з CMD ["php", "artisan", "migrate"] зупиняється одразу після міграцій - це нормально.
Команди:
docker stop app # SIGTERM, а через 10 с - SIGKILL
docker kill app # одразу SIGKILL (чи інший сигнал: --signal)
docker restart app
docker ps -a # усі контейнери, включно з exited, і їхні коди завершення
Коди завершення - діагностика:
| Код | Значення |
|---|---|
0 |
процес завершився нормально |
1 |
помилка застосунку (виняток, невдала команда) |
126 / 127 |
команду неможливо виконати / не знайдено (помилка в CMD/ENTRYPOINT) |
137 |
128 + 9 (SIGKILL) - процес убито примусово |
143 |
128 + 15 (SIGTERM) - процес завершився на запит зупинки |
Код 137 - найчастіше:
- OOM-killer: контейнер перевищив ліміт пам'яті. Перевірка:
docker inspect app --format '{{.State.OOMKilled}}'-true; docker stopне дочекався: процес не обробив SIGTERM за 10 секунд і отримав SIGKILL (проблема з PID 1 чи довге завершення).
Код 143 при docker stop - очікуваний: процес коректно відреагував на SIGTERM.
Політики перезапуску (--restart) визначають, що відбувається після завершення: no, on-failure[:N] (лише при ненульовому коді), always, unless-stopped. Контейнер, що падає одразу після старту, з always потрапляє в цикл перезапусків з наростаючою затримкою - видно в docker ps як Restarting (1) ....
Перша дія при падінні: docker logs app (логи зупиненого контейнера доступні, доки його не видалено) і docker inspect для коду й причини.
Докладніше в документації: Docker: автоматичний запуск контейнерів
Принцип «конфігурація в оточенні» (з методології Twelve-Factor App): той самий образ запускається в різних середовищах, а відмінності (адреси баз, ключі, режими) передаються змінними оточення.
Способи передачі:
docker run -e APP_ENV=production -e DB_HOST=db myapp # окремі змінні
docker run -e DB_PASSWORD myapp # значення з оточення хоста
docker run --env-file ./production.env myapp # з файлу KEY=value
# compose.yaml
services:
app:
image: myapp
environment:
APP_ENV: production
DB_HOST: db
env_file:
- .env.production
Пріоритет (від нижчого до вищого):
ENVв Dockerfile - значення за замовчуванням в образі;env_file/--env-file;environment/-e- перекривають попередні.
ENV в Dockerfile доречний для незмінних налаштувань образу (PHP_INI_DIR, шляхи, COMPOSER_ALLOW_SUPERUSER), але не для секретів чи значень, що відрізняються між середовищами: усе з ENV видно в docker image inspect і в кожному контейнері з цього образу.
Для Laravel:
- Laravel читає змінні оточення процесу через
env()- файл.envу контейнері не обов'язковий, якщо змінні передано Docker; php artisan config:cache«заморожує» значення на момент виконання команди. Якщо кеш зроблено під час збирання образу, змінні оточення, передані при запуску, ігноруватимуться. Кешувати конфігурацію треба при старті контейнера, коли змінні вже доступні;- поза файлами конфігурації
env()не працює післяconfig:cache- у коді лишеconfig().
Безпека:
- змінні оточення видно в
docker inspect, у/proc/<pid>/environ, вони потрапляють у звіти про помилки й дочірні процеси; - для секретів кращі файли секретів (Docker/Compose secrets монтуються в
/run/secrets/...) або менеджер секретів; .envз секретами - не в образ (.dockerignore) і не в Git.
Перевірка того, що бачить контейнер: docker exec app env чи docker compose config - підсумкова конфігурація Compose з підставленими значеннями.
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) - ручні зміни просто неможливі, а помилки конфігурації виявляються одразу.
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- часто достатньо без входу в контейнер.