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

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.

Кілька мереж дозволяють ізолювати сервіси: фронтенд-проксі бачить застосунок, але не базу.

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

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.

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

Стани контейнера:

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

Пріоритет (від нижчого до вищого):

  1. ENV в Dockerfile - значення за замовчуванням в образі;
  2. env_file / --env-file;
  3. 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 з підставленими значеннями.

Докладніше в документації: docker run: змінні оточення