Junior: питання на співбесіді з теми «Compose, мережі й томи»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
Файлова система контейнера - тимчасова: усе, що записано всередину, зникає разом з контейнером (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 з підставленими значеннями.