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

Junior: питання на співбесіді з теми «Продакшен і оркестрація»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

5 питань

docker run чи docker compose up запускає контейнери на одному сервері і на цьому зупиняється. У продакшені з'являються задачі, які ця команда не розв'язує:

  • сервер упав - хто перезапустить контейнери на іншому сервері?
  • навантаження зросло - як запустити ще п'ять копій застосунку і розподілити між ними трафік?
  • новий реліз - як замінити контейнери по одному, щоб сайт не зупинявся, і відкотитися, якщо нова версія не стартує?
  • контейнер «завис», але процес живий - хто це помітить і перезапустить?
  • секрети й конфігурація - як доставити їх на всі сервери безпечно?

Оркестратор - система, якій описують бажаний стан («має працювати 4 копії образу app:a1b2c3, з такими ресурсами й перевірками стану»), а вона сама постійно підтримує його на кластері серверів:

  • розподіляє контейнери по вузлах з урахуванням ресурсів;
  • перезапускає й переносить їх при збоях;
  • робить поступові оновлення й відкати;
  • дає внутрішню мережу, балансування й пошук сервісів за іменем;
  • керує секретами й конфігурацією.

Основні варіанти:

  • Kubernetes - стандарт індустрії, величезна екосистема, але й складність: окремі знання, налаштування, супровід. Часто як керований сервіс (EKS, GKE, AKS);
  • Docker Swarm - вбудований у Docker, значно простіший, підходить для невеликих кластерів;
  • PaaS поверх Docker - Kamal, Coolify, Dokploy: деплой і оновлення без повноцінного оркестратора;
  • керовані контейнерні сервіси - AWS ECS/Fargate, Google Cloud Run, Fly.io.

Чи потрібна оркестрація завжди: ні. Невеликий застосунок на одному-двох серверах чудово живе з Compose, політиками перезапуску й простим скриптом деплою. Kubernetes окупається, коли сервісів і серверів багато, а команда готова його підтримувати.

Докладніше в документації: Kubernetes: огляд

Політика перезапуску визначає, що Docker робить, коли процес контейнера завершився, і чи запускати контейнер після перезапуску самого Docker (наприклад, після перезавантаження сервера).

docker run -d --restart unless-stopped myapp:1.4
# compose.yaml
services:
  app:
    image: myapp:1.4
    restart: unless-stopped

Варіанти:

Політика Поведінка
no (за замовчуванням) не перезапускати ніколи
on-failure[:N] лише якщо процес завершився з ненульовим кодом; :N - максимум спроб
always перезапускати завжди; після перезапуску Docker - запустити знову, навіть якщо контейнер зупинили вручну
unless-stopped як always, але якщо контейнер зупинили вручну (docker stop), після перезапуску Docker він лишиться зупиненим

Різниця always і unless-stopped проявляється саме після перезавантаження сервера: вручну зупинений для обслуговування контейнер з always «воскресне», з unless-stopped - ні. Для більшості сервісів зручніший unless-stopped.

Що варто знати:

  • перезапуск з наростаючою затримкою: якщо контейнер падає одразу після старту, Docker збільшує паузу між спробами (подвоюючи її), щоб не створювати навантаження нескінченним циклом. Затримка скидається, якщо контейнер пропрацював хоча б 10 секунд;
  • on-failure доречний для задач, які мають завершитися (міграції, одноразові скрипти): успішне завершення з кодом 0 - не привід запускати знову;
  • політика перезапуску не перевіряє здоров'я: контейнер, що «завис», але процес живий, не буде перезапущений. Для цього потрібні перевірки стану (healthcheck) і оркестратор, який на них реагує;
  • у Swarm і Kubernetes перезапуском керує сам оркестратор (deploy.restart_policy, restartPolicy у pod), і він може перенести контейнер на інший вузол.

Типова помилка: без політики перезапуску після перезавантаження сервера (оновлення ядра, збій живлення) застосунок просто не підніметься, доки хтось не зайде й не запустить його вручну.

Докладніше в документації: Автоматичний запуск контейнерів

Тег - змінна мітка, що вказує на конкретний образ. latest - просто тег за замовчуванням, коли інший не вказано. Він не означає «найновіша версія»: це той образ, який останнім отримав цей тег.

Чому latest у продакшені - проблема:

  • невідомо, що запущено: myapp:latest вчора й сьогодні - різні образи. Розслідуючи інцидент, неможливо сказати, яка версія коду працювала;
  • неможливий надійний відкат: «повернутися на попередню версію» - на яку? latest уже перезаписано;
  • різні сервери - різні версії: сервер, що завантажив образ раніше, і новий сервер після масштабування отримають різні образи під тим самим тегом;
  • неявні оновлення: перезапуск контейнера чи новий вузол кластера раптом підтягує нову версію без деплою.

Як тегувати:

  • незмінні теги з git SHA: myapp:sha-a1b2c3d - однозначний зв'язок образу з комітом. Такий тег ніколи не перезаписується;
  • семантичні версії для релізів: myapp:1.4.2, плюс за бажанням «плаваючі» myapp:1.4 і myapp:1 для зручності;
  • кілька тегів на один образ - нормально: CI ставить і sha-a1b2c3d, і 1.4.2;
  • latest - хіба що для локальної розробки чи як мітка «останньої збірки main», але не в конфігурації деплою.
docker build -t ghcr.io/acme/app:sha-a1b2c3d -t ghcr.io/acme/app:1.4.2 .
docker push --all-tags ghcr.io/acme/app

Ще надійніше - дайджест: ghcr.io/acme/app@sha256:... - адреса вмісту образу. На відміну від тегу, дайджест змінити неможливо: той самий дайджест завжди означає ті самі байти.

Те саме стосується базових образів у Dockerfile: FROM php:latest чи навіть FROM php:8.5-fpm може тихо змінитися між збираннями - версії варто фіксувати точніше.

Правило: у продакшені має бути можливо відповісти «який саме код зараз працює» і «як повернутися на попередню версію» за одну хвилину.

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

Реєстр образів (registry) - сервер, що зберігає образи й віддає їх за назвою й тегом. docker push завантажує образ у реєстр, docker pull - отримує.

docker push ghcr.io/acme/app:1.4.2
docker pull ghcr.io/acme/app:1.4.2

Повна назва образу містить реєстр: ghcr.io/acme/app. Без нього Docker звертається до Docker Hub (docker.io): php:8.5-fpm - це docker.io/library/php:8.5-fpm.

Популярні реєстри:

  • Docker Hub - найбільший публічний, офіційні образи (php, nginx, postgres);
  • GitHub Container Registry (ghcr.io) - зручно разом з GitHub Actions, права доступу як у репозиторію;
  • хмарні: AWS ECR, Google Artifact Registry, Azure Container Registry - близько до серверів, з IAM-доступом;
  • власні: Harbor, GitLab Registry, простий registry:2.

Ліміти Docker Hub (за сторінкою документації «Usage and limits»):

Користувач Ліміт завантажень
без входу 100 за 6 годин на IPv4-адресу (чи IPv6-підмережу /64)
Personal (з входом) 200 за 6 годин
Pro, Team, Business без ліміту (з урахуванням fair use)

Чому це стосується продакшену й CI:

  • CI без автентифікації ділить ліміт з усіма користувачами тієї самої IP-адреси (спільні раннери) - збирання раптом падають з помилкою ліміту;
  • кластер з багатьма вузлами, що одночасно завантажують базові образи, теж швидко вичерпує ліміт однієї публічної IP.

Що робити:

  • docker login у CI й на серверах - навіть безкоштовний обліковий запис подвоює ліміт;
  • власний реєстр для своїх образів (GHCR, ECR) і дзеркало/кеш для базових образів (pull-through cache);
  • не завантажувати зайве: кеш шарів на раннерах, однакові базові образи.

Безпека: приватні образи містять код застосунку - доступ до реєстру з окремими токенами лише на читання для серверів і на запис лише для CI.

Докладніше в документації: Docker Hub: використання й ліміти

Усе, що контейнер пише в stdout і stderr, Docker за замовчуванням зберігає у файли на диску сервера - драйвер json-file (/var/lib/docker/containers/<id>/<id>-json.log).

Проблема: у json-file за замовчуванням немає обмеження розміру (max-size = необмежено). Застосунок, що активно логує, за кілька тижнів чи місяців заповнює диск - і сервер падає разом з базою, яка не може писати.

Ротація для всього сервера - /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Кожен контейнер тримає щонайбільше 3 файли по 10 МБ; старіші видаляються. Значення в log-opts мають бути рядками ("3", а не 3).

Важливо: нові налаштування daemon.json застосовуються лише до нових контейнерів. Наявні треба перестворити (docker compose up -d --force-recreate), інакше вони логуватимуть по-старому.

Для окремого сервісу - у Compose:

services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "5"

Альтернатива - драйвер local: зберігає логи компактніше (стиснення) і за замовчуванням має ротацію. docker logs з ним працює так само.

Як знайти винуватця:

sudo du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail
docker system df           # скільки займають образи, контейнери, томи, кеш збирання

Інші «пожирачі» диска на Docker-сервері: старі образи після кожного деплою, кеш збирання, зупинені контейнери, «осиротілі» томи. Їх прибирають docker image prune, docker builder prune - обережно й розуміючи, що видаляється (особливо docker volume prune, що видаляє дані).

Довгострокове рішення: застосунок пише в stdout, а логи збирає й відправляє централізована система (Loki, ELK, хмарний сервіс). Локальні файли - лише короткий буфер з ротацією.

Докладніше в документації: Драйвер логів json-file