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 окупається, коли сервісів і серверів багато, а команда готова його підтримувати.
Політика перезапуску визначає, що 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 може тихо змінитися між збираннями - версії варто фіксувати точніше.
Правило: у продакшені має бути можливо відповісти «який саме код зараз працює» і «як повернутися на попередню версію» за одну хвилину.
Реєстр образів (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, хмарний сервіс). Локальні файли - лише короткий буфер з ротацією.