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

Питання на співбесіді: Продакшен і оркестрація

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

14 питань

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

Обидва - оркестратори: керують контейнерами на кластері серверів за описом бажаного стану. Різниця - у складності й можливостях.

Docker Swarm - вбудований у Docker Engine:

docker swarm init
docker stack deploy -c compose.yaml myapp
docker service scale myapp_app=4
  • сервіс - опис того, що має працювати (образ, кількість реплік, порти, ресурси);
  • задача (task) - конкретний контейнер сервісу на конкретному вузлі;
  • стек - набір сервісів з файлу у форматі Compose (з розділом deploy);
  • вбудовані: overlay-мережі між вузлами, балансування (routing mesh), секрети, поступові оновлення з відкатом.

Kubernetes - окрема платформа зі своїми поняттями:

  • Pod - найменша одиниця: один чи кілька контейнерів зі спільною мережею;
  • Deployment - бажана кількість однакових pod-ів і стратегія оновлення;
  • Service - стабільна адреса й балансування до pod-ів;
  • Ingress / Gateway - вхід HTTP-трафіку ззовні;
  • ConfigMap, Secret, томи (PersistentVolume), горизонтальне автомасштабування, оператори для баз даних і черг.

Порівняння:

Swarm Kubernetes
поріг входу низький: знайомий формат Compose високий: багато понять і YAML
встановлення одна команда керований сервіс чи складне налаштування
екосистема обмежена величезна (Helm, оператори, моніторинг)
автомасштабування немає вбудованого є (HPA, кластерні автоскейлери)
популярність, вакансії невелика стандарт індустрії

Коли достатньо Swarm: кілька серверів, кілька сервісів, невелика команда без окремого DevOps, потреба в простих поступових оновленнях і відмовостійкості. Багато PaaS для самостійного хостингу (наприклад, Dokploy) використовують Swarm під капотом.

Коли Kubernetes: десятки сервісів, кілька команд, автомасштабування, складні мережеві й безпекові політики, керований сервіс у хмарі - і готовність інвестувати в його знання.

Помилка вибору: Kubernetes для одного Laravel-застосунку з базою - складність, яка забирає більше часу, ніж дає користі.

Докладніше в документації: Docker Swarm

Найпростіший деплой - docker compose pull && docker compose up -d - зупиняє старий контейнер і лише потім запускає новий. Між цими моментами сайт недоступний, а якщо новий контейнер не стартує - недоступний довго.

Принципи деплою без простою:

1. Спершу запустити нове, потім зупинити старе (start-first). Обидві версії якийсь час працюють паралельно.

2. Трафік на новий контейнер - лише коли він готовий. Перевірка стану (healthcheck) підтверджує, що застосунок справді відповідає, а не просто процес запущено:

healthcheck:
  test: ["CMD", "curl", "-fsS", "http://localhost/up"]
  interval: 10s
  timeout: 3s
  retries: 3
  start_period: 20s

У Laravel 11+ маршрут /up налаштовано за замовчуванням.

3. Старий контейнер завершує поточні запити перед зупинкою (graceful shutdown): отримує SIGTERM, перестає приймати нові запити, дообробляє поточні, і лише потім зупиняється. Балансувальник має встигнути прибрати його з пулу.

4. Автоматичний відкат, якщо нова версія не пройшла перевірку стану.

Як це реалізують:

  • Docker Swarm:
deploy:
  replicas: 2
  update_config:
    parallelism: 1
    order: start-first
    failure_action: rollback

Розділ update_config діє саме в режимі Swarm (docker stack deploy); звичайний docker compose up його не використовує і просто перестворює контейнери.

  • Kubernetes - стратегія RollingUpdate у Deployment з readiness-перевірками;
  • Kamal - запускає новий контейнер, перевіряє його й перемикає трафік через свій проксі (kamal-proxy);
  • власний скрипт з Compose - запустити новий контейнер під іншою назвою, дочекатися здорового стану, перемкнути зворотний проксі (Caddy, Traefik, Nginx), зупинити старий.

Пастки, через які «без простою» не виходить:

  • міграції бази, несумісні зі старою версією коду: під час переходу працюють обидві версії. Видалення чи перейменування колонки ламає стару. Потрібні сумісні в обидва боки міграції (спершу додати, потім - окремим релізом - прибрати);
  • черги: воркери зі старим кодом обробляють задачі нового формату - queue:restart і сумісний формат задач;
  • сесії й кеш у пам'яті контейнера - губляться при заміні.

Докладніше в документації: Compose: deploy.update_config

Перевірки стану відповідають на різні питання, і оркестратор реагує на них по-різному.

Liveness - «процес живий чи завис?»

Якщо перевірка не проходить кілька разів поспіль, контейнер перезапускається. Призначення - виходити з безвихідних станів: взаємоблокування, нескінченний цикл, вичерпані воркери.

Readiness - «чи готовий приймати трафік прямо зараз?»

Якщо не проходить - контейнер прибирається з балансування, але не перезапускається. Призначення - не слати запити туди, де їх не оброблять: застосунок ще прогрівається, тимчасово перевантажений, втратив з'єднання з залежністю.

Startup - «чи завершився запуск?»

Поки вона не пройшла, інші перевірки не виконуються. Для застосунків з довгим стартом: без неї liveness-перевірка може «вбивати» контейнер, що просто повільно запускається.

Що буде, якщо переплутати:

  • liveness перевіряє базу даних. База на хвилину недоступна - усі контейнери застосунку провалюють liveness і одночасно перезапускаються. Після відновлення бази - шторм перезапусків і холодний старт усього сервісу. Залежності - у readiness, а liveness має перевіряти лише сам процес;
  • немає readiness - трафік іде на контейнер, який ще прогріває кеш чи виконує міграції: користувачі отримують помилки після кожного деплою;
  • занадто суворі пороги (тайм-аут 1 секунда під навантаженням) - здорові контейнери перезапускаються саме тоді, коли найбільше потрібні;
  • важка перевірка (повний запит до бази, рендер сторінки) на кожні кілька секунд - зайве навантаження.

Як це виглядає в Docker: HEALTHCHECK у Dockerfile чи healthcheck у Compose дає один статус (healthy/unhealthy). Сам Docker не перезапускає нездоровий контейнер - на статус реагують Swarm, Compose з depends_on: condition: service_healthy чи зовнішні інструменти.

Для Laravel:

  • liveness - легкий ендпойнт без звернень до залежностей (маршрут /up підходить, якщо не перевантажений подіями);
  • readiness - окремий ендпойнт, що перевіряє базу, Redis, наявність закешованої конфігурації;
  • черги й планувальник - окремі перевірки: воркер без HTTP перевіряють командою.

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

Поширена помилка - міграції в команді старту контейнера:

CMD php artisan migrate --force && php-fpm

Проблеми:

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

Правильні варіанти:

1. Окремий крок деплою перед оновленням застосунку - одноразовий контейнер з тим самим образом:

docker run --rm --env-file .env ghcr.io/acme/app:sha-a1b2c3d php artisan migrate --force

У Kubernetes - Job перед оновленням Deployment; у Kamal - хук перед деплоєм; у CI - окремий крок пайплайну.

2. Якщо міграції все ж запускаються зі старту контейнера - з блокуванням:

php artisan migrate --force --isolated

--isolated бере атомарне блокування через кеш - міграції виконає лише один процес, інші пропустять. Потрібен спільний драйвер кешу з підтримкою блокувань (Redis, база даних).

Сумісність версій - найважливіше. Під час поступового деплою старий і новий код працюють одночасно з уже оновленою базою. Тому міграції мають бути сумісними з попередньою версією:

  • додати колонку - безпечно (nullable чи зі значенням за замовчуванням);
  • перейменувати чи видалити колонку - у кілька релізів: додати нову, писати в обидві, перенести дані, перевести читання, і лише потім видалити стару;
  • важкі зміни великих таблиць (індекси, зміна типу) - без довгих блокувань (онлайн-DDL, CREATE INDEX CONCURRENTLY у PostgreSQL), інакше застосунок «зависне» на час міграції.

Відкат: відкат коду не відкочує базу. План має враховувати, що попередня версія коду працюватиме з новою схемою - ще одна причина для сумісних міграцій.

Резервна копія перед міграцією на продакшені - обов'язковий крок для ризикованих змін.

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

Горизонтальне масштабування - запустити кілька однакових контейнерів і розподіляти між ними запити. Працює, лише якщо контейнер без стану (stateless): будь-який запит може обробити будь-яка копія.

Що заважає масштабуванню і куди це винести:

Стан у контейнері Куди винести
сесії у файлах Redis чи база даних (SESSION_DRIVER=redis)
кеш у файлах Redis, Memcached
завантажені файли в storage/app об'єктне сховище (S3, R2) чи спільний том
черги в синхронному режимі чи database на SQLite Redis чи база даних, окремі воркери
планувальник у кожному контейнері окремий контейнер чи onOneServer()
блокування у файлах блокування через Redis/базу

Типові наслідки, якщо цього не зробити:

  • користувача «розлогінює» при кожному запиті - сесія на іншій копії;
  • завантажений аватар то є, то немає;
  • запланована задача виконується стільки разів, скільки контейнерів (листи - вчетверо);
  • кеш скидається лише на одній копії.

Масштабування в різних середовищах:

docker compose up -d --scale app=4          # кілька копій на одному сервері
docker service scale myapp_app=4             # Swarm
kubectl scale deployment app --replicas=4    # Kubernetes

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

Що ще враховувати:

  • з'єднання з базою: кожна копія тримає свій пул - 10 копій по 20 процесів PHP-FPM = 200 з'єднань. База може не витримати раніше, ніж закінчаться ресурси застосунку;
  • база - не масштабується так само: вузьке місце переміщується туди - репліки для читання, кеш, оптимізація запитів;
  • довірені проксі - за балансувальником застосунок має бачити справжні IP і протокол;
  • одноразові задачі при старті (міграції, прогрів кешу) не повинні виконуватися кожною копією.

Для Laravel контейнер з php-fpm чи FrankenPHP, сесіями й кешем у Redis, файлами в S3 і окремими контейнерами для воркерів і планувальника - класична конфігурація, що масштабується.

Докладніше в документації: Compose: deploy.replicas

Тег - змінне посилання: власник реєстру (чи будь-хто з правом запису) може перемістити app:1.4.2 на інший образ. Дайджест - SHA-256 від маніфесту образу:

docker pull ghcr.io/acme/app@sha256:4f8e0a...b21c
docker image ls --digests
docker buildx imagetools inspect ghcr.io/acme/app:1.4.2   # дайджест і платформи

Той самий дайджест завжди означає ті самі байти. Змінити вміст, не змінивши дайджесту, неможливо.

Що дає деплой за дайджестом:

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

Мультиплатформні образи: для образу з кількома архітектурами (amd64, arm64) тег вказує на індекс (manifest list), а кожна платформа має свій дайджест. Зазвичай фіксують дайджест індексу - і кожен сервер сам обере потрібну архітектуру.

Практичний підхід - тег для людей, дайджест для машин:

  • CI збирає образ з тегами sha-a1b2c3d і 1.4.2, отримує дайджест і записує його в маніфест деплою (Kubernetes, Helm values, Compose-файл);
  • інструменти на кшталт Renovate оновлюють зафіксовані дайджести автоматично pull-request-ами.

Базові образи в Dockerfile теж можна фіксувати дайджестом:

FROM php:8.5-fpm-alpine@sha256:...

Тег лишається для читабельності, але використовується дайджест. Без цього той самий Dockerfile сьогодні й через місяць може зібрати різні образи - з іншою версією PHP чи системних бібліотек.

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

Докладніше в документації: docker image pull: завантаження за дайджестом

Образ, що потрапляє на сервер, - результат довгого ланцюжка: базовий образ, системні пакети, залежності Composer і npm, CI, реєстр. Атака на будь-яку ланку (скомпрометований CI, підмінений образ у реєстрі, шкідливий пакет) дає зловмиснику код у продакшені. Захист - перевірювані відомості про образ.

Атестації BuildKit - метадані, прикріплені до образу під час збирання:

docker buildx build --sbom=true --provenance=mode=max -t ghcr.io/acme/app:1.4.2 --push .
  • provenance - як і з чого зібрано образ: репозиторій і коміт, параметри збирання, базові образи, середовище CI. За замовчуванням buildx додає мінімальну provenance-атестацію; mode=max - детальну;
  • SBOM (Software Bill of Materials) - перелік усіх пакетів і бібліотек в образі з версіями.
docker buildx imagetools inspect ghcr.io/acme/app:1.4.2 --format '{{ json .SBOM }}'

Навіщо SBOM: коли виходить критична вразливість у бібліотеці, за SBOM можна за хвилини знайти всі образи, що її містять, замість сканування кожного вручну. Сканери (Docker Scout, Trivy, Grype) використовують SBOM для пошуку CVE.

Підпис образів - криптографічне підтвердження, що образ зібрав саме ваш CI і він не змінювався:

cosign sign ghcr.io/acme/app@sha256:...
cosign verify ghcr.io/acme/app@sha256:... --certificate-identity=... --certificate-oidc-issuer=https://token.actions.githubusercontent.com

Cosign (проєкт Sigstore) підтримує «безключовий» підпис: CI отримує короткоживучий сертифікат через OIDC (наприклад, GitHub Actions), тож не треба зберігати довгоживучий приватний ключ.

Де це перевіряється:

  • політики допуску в Kubernetes (Kyverno, Sigstore policy-controller) - кластер не запустить непідписаний образ чи образ не з вашого репозиторію;
  • CI перед деплоєм - перевірка підпису й відсутності критичних вразливостей у SBOM.

Підписують дайджест, а не тег - тег можна перемістити, а підпис має стосуватися конкретного вмісту.

Рамки: SLSA описує рівні гарантій ланцюжка збирання; provenance BuildKit - крок до вищих рівнів.

Мінімум для невеликого проєкту: збирання лише в CI (не з ноутбуків), SBOM і сканування вразливостей, деплой за дайджестом. Підписи й політики допуску - наступний крок.

Докладніше в документації: Docker Build: атестації

Між «запускаю docker compose up по SSH» і «маємо кластер Kubernetes» є проміжний клас інструментів - PaaS для самостійного хостингу поверх Docker.

Kamal (від 37signals, творців Basecamp):

  • конфігурація в одному YAML, деплой командою kamal deploy по SSH на звичайні сервери;
  • збирає образ, завантажує в реєстр, запускає на серверах;
  • деплой без простою через власний проксі kamal-proxy: новий контейнер запускається, проходить перевірку стану, і лише тоді на нього перемикається трафік;
  • допоміжні сервіси (accessories) - база, Redis - як окремі контейнери;
  • без вебінтерфейсу й без «панелі» - інструмент командного рядка.

Coolify і Dokploy - самостійно розміщувані аналоги Heroku/Vercel з вебінтерфейсом:

  • деплой з Git-репозиторію чи образу, автоматичні збирання з Dockerfile чи Nixpacks/Buildpacks;
  • TLS-сертифікати, домени, змінні оточення, бази даних «в один клік», резервні копії;
  • кілька серверів (Dokploy використовує Docker Swarm для розподілу між вузлами).

Коли такого інструменту достатньо:

  • один чи кілька серверів, кілька застосунків;
  • команда без окремого DevOps-інженера;
  • потрібні деплой без простою, TLS, перезапуски, базові резервні копії - але не автомасштабування й складні мережеві політики;
  • важлива ціна: свої сервери (Hetzner, DigitalOcean) значно дешевші за керовані платформи.

Обмеження й ризики:

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

Порівняно з Kubernetes: значно менше понять і супроводу, але й менше гнучкості. Для більшості невеликих і середніх Laravel-застосунків такого рівня достатньо надовго.

Порівняно з керованими PaaS (Laravel Cloud, Render, Fly.io): дешевше й більше контролю, але відповідальність за сервери, оновлення ОС і безпеку - на вас.

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

Контейнери одноразові, а томи - ні: там бази даних, завантажені файли, дані Redis. Резервна копія контейнерної інфраструктури - це насамперед копія томів і конфігурації.

Копія тому через тимчасовий контейнер (з документації Docker):

docker run --rm \
  -v app_storage:/data:ro \
  -v "$PWD":/backup \
  alpine tar czf /backup/storage-$(date +%F).tar.gz -C /data .

Відновлення - зворотна операція в новий том.

Бази даних - не копіювати файли працюючої бази. Копія каталогу даних PostgreSQL чи MySQL під час роботи може бути неузгодженою й непридатною до відновлення. Правильно:

  • логічний дамп засобами бази: docker exec db pg_dump -Fc app > app.dump, mysqldump --single-transaction;
  • фізичні бекапи з WAL/binlog (pgBackRest, WAL-G, Percona XtraBackup) - для великих баз і відновлення на момент часу;
  • або зупинка бази на час копіювання файлів - якщо простій допустимий.

Що ще входить у «відновити все»:

  • конфігурація: Compose-файли, змінні оточення, секрети (у сховищі секретів, а не лише на сервері);
  • образи: у реєстрі за незмінними тегами - щоб розгорнути ту саму версію;
  • файли користувачів: краще одразу в об'єктному сховищі (S3, R2) з версіонуванням, а не в томі на одному сервері;
  • інфраструктура як код: як заново створити сервер, мережу, DNS.

Правило 3-2-1: щонайменше 3 копії, на 2 різних типах носіїв, 1 - поза основною площадкою (інший провайдер чи регіон). Плюс незмінні копії (object lock), які не може видалити зловмисник чи програма-вимагач із доступом до сервера.

Плануючи відновлення, визначають:

  • RPO - скільки даних допустимо втратити (година? хвилина?) - визначає частоту копій;
  • RTO - за скільки часу треба відновитися - визначає підхід (запасний сервер, автоматизація).

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

Моніторинг бекапів: сповіщення, якщо копія не створилася чи її розмір різко змінився.

Докладніше в документації: Томи: резервне копіювання й відновлення