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