Питання на співбесіді: Деплой і DevOps
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
8 питань
Laravel має вбудований маршрут перевірки стану: /up повертає 200, якщо застосунок завантажився без винятків, і 500 - якщо ні.
// bootstrap/app.php
->withRouting(
web: __DIR__.'/../routes/web.php',
health: '/up',
)
Хто його опитує:
- балансувальник - не слати трафік на екземпляр, що не відповідає;
- оркестратор (Kubernetes, Docker Swarm) - перезапустити контейнер;
- моніторинг доступності - сповістити команду.
Чого він не перевіряє: базу, Redis, черги. Застосунок може завантажитися й віддати 200, а база при цьому лежить. Для глибшої перевірки слухають подію DiagnosingHealth, яку маршрут запускає, і кидають виняток, якщо залежність недоступна - тоді відповідь буде 500.
Обережно з глибиною: якщо балансувальник знімає всі екземпляри через те, що впала база, сайт не віддасть навіть сторінку «технічні роботи». Тому часто розділяють «живий» (процес працює) і «готовий» (залежності доступні).
Маршрут варто виключити з журналів доступу й аналітики - його опитують щохвилини.
Порядок кроків важливіший за їхній перелік: та сама команда, виконана не там, ламає деплой.
Типова послідовність:
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
php artisan queue:restart
Чому саме так:
- Кешування - після викладення коду. Інакше в кеш потрапить попередня версія конфігурації й маршрутів.
--forceдля міграцій потрібен, бо в проді Artisan питає підтвердження, а деплой неінтерактивний. Це стосується лишеmigrate;migrate:freshу проді не запускають ніколи.queue:restartобовʼязковий. Воркери - довгоживучі процеси: вони тримають у памʼяті стару версію коду й працюватимуть з нею, поки їх не перезапустити. Це найчастіша причина «код виклали, а черга робить по-старому».
Про що забувають:
- Порядок міграції та коду. Видалення колонки у тому ж випуску, що й код, який її ще читає, ламає застосунок у проміжку між кроками. Такі зміни розводять на два випуски.
storage:linkпісля першого розгортання, інакше публічні файли не віддаються.- Права на
storageіbootstrap/cache- веб-сервер має писати в обидві. php artisan downдає режим обслуговування, але з ним застосунок недоступний; безпростійний деплой роблять перемиканням симлінка на новий реліз.
Готові рішення - Envoyer, Deployer, Laravel Cloud - роблять саме це: викладають реліз поруч і перемикають симлінк, коли він готовий.
Воркер черги, Horizon, Octane, Reverb, schedule:work завантажують код один раз і тримають його в пам'яті. Після деплою вони продовжують виконувати старий код, доки їх не перезапустити.
Команда в Laravel 13:
php artisan reload
Вона завершує ці процеси, а підняти їх знову має монітор процесів: Supervisor, systemd чи оркестратор. Без монітора процеси просто зупиняться.
Окремі команди для конкретних сервісів:
php artisan queue:restart- воркери завершать поточне завдання й вийдуть;php artisan horizon:terminate- те саме для Horizon;php artisan octane:reload.
Приклад Supervisor:
[program:laravel-worker]
command=php /var/www/app/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=2
stopwaitsecs=3600
stopwaitsecs має бути більшим за найдовше завдання, інакше Supervisor уб'є воркер посеред роботи.
Порядок деплою: новий код і optimize → міграції → reload. Воркер, що стартує зі старою схемою й новим кодом (чи навпаки), - типове джерело помилок одразу після релізу.
Один образ - кілька процесів. Той самий образ запускається як вебсервер, воркер черги й планувальник - різними командами. Код у всіх однаковий, масштабуються вони окремо.
services:
app: { image: app:1.4.2, command: frankenphp run }
worker: { image: app:1.4.2, command: php artisan queue:work --max-time=3600 }
scheduler: { image: app:1.4.2, command: php artisan schedule:work }
Що робити під час збирання образу: composer install --no-dev --optimize-autoloader, збирання ассетів, view:cache, event:cache, route:cache.
Що НЕ робити під час збирання: config:cache. У кеш потрапить оточення збирання, а не продакшену - конфігурацію кешують на старті контейнера, коли змінні оточення вже є.
Стан - назовні:
- сесії, кеш, черги - Redis чи база, а не файли контейнера;
- завантаження - S3-сумісне сховище чи том;
- журнали - у stdout/stderr (канал
stderr), їх збирає платформа.
Інше:
- права на
storageіbootstrap/cacheдля користувача PHP-процесу; - міграції - окремим кроком деплою, не в старті кожного контейнера;
- пінити версію базового образу: плаваючий тег може принести нову версію PHP чи бібліотек без вашого відома;
/up- для перевірки стану контейнера.
Деплой без простою: користувачі весь час бачать робочу версію.
Atomic (symlink) deploy - кожен реліз клонується в нову папку, там встановлюються залежності й збираються ассети, після чого current атомарно перемикається через symlink:
releases/2026_06_05_120000/ ← новий
current → releases/... ← атомарне перемикання
Кроки на деплої: composer install --no-dev, npm run build, migrate --force, кеш конфіг/маршрутів, перезапуск воркерів (queue:restart) і OPcache.
Інструменти: Envoyer, Deployer, CI/CD-пайплайни, Kubernetes (rolling update). Окрема увага - сумісність міграцій із попередньою версією коду під час перемикання.
CI автоматично перевіряє кожен пуш, CD - автоматично доставляє код.
Типовий пайплайн (GitHub Actions / GitLab CI):
- composer install
- vendor/bin/pint --test # стиль
- vendor/bin/phpstan analyse # статичний аналіз
- php artisan test --parallel # тести
- npm ci && npm run build # ассети
# → деплой при успіху
Деплой: SSH-скрипт, Docker-образ у реєстрі + rolling update у Kubernetes, або сервіси на кшталт Forge/Envoyer. На етапі деплою - migrate --force, кешування конфіга/маршрутів, queue:restart. CI/CD дає швидкий зворотний зв'язок і знижує ризик людської помилки при релізі.
За замовчуванням php artisan down створює файл у storage/framework - лише на тому сервері, де виконали команду. На п'яти серверах решта чотири продовжать приймати запити.
Драйвер на основі кешу:
APP_MAINTENANCE_DRIVER=cache
APP_MAINTENANCE_STORE=redis
Сховище має бути спільним для всіх серверів - тоді досить одного down.
Корисні опції:
php artisan down --secret="team-preview" --render="errors::503" --retry=60
--secret- команда відкриваєhttps://example.com/team-previewі бачить сайт через cookie обходу;--render- сторінку віддають до завантаження фреймворку, тож вона не впаде під часcomposer install;--retry- заголовокRetry-Afterдля клієнтів і пошукових ботів;--redirect=/- перенаправляти всі запити.
Черги: поки застосунок у режимі обслуговування, воркери не обробляють завдання - вони накопичуються й виконаються після up. Довге обслуговування - велика черга на виході; варто оцінити, чи витримають її воркери, і чи не застаріють завдання (листи «ваш код дійсний 10 хвилин»).
Планувальник теж не запускає завдання, окрім evenInMaintenanceMode().
Для деплою без простою режим обслуговування взагалі не потрібен - його залишають для робіт, що справді вимагають зупинки.
Докладніше в документації: Режим обслуговування на кількох серверах
Журнал корисний, лише якщо за ним можна відновити, що сталося з конкретним запитом чи завданням.
1. Контекст у кожному записі:
// middleware
Context::add('request_id', (string) Str::uuid());
Context::add('user_id', $request->user()?->id);
Log::info('Замовлення оформлено', ['order_id' => $order->id]);
Дані з Context автоматично додаються до кожного запису журналу й передаються в завдання черги, які цей запит поставив. Тоді один request_id зв'язує запит, завдання й виклики API.
2. Структурований формат - JSON (JsonFormatter) замість рядків, щоб журнал фільтрувався в Loki, ELK чи Datadog за полями.
3. Рівні з розумом: error - те, що вимагає уваги; warning - підозріле, але оброблене; info - бізнес-події; debug - вимкнений на продакшені. Якщо все error, сигнал губиться.
4. Канали: stack з кількох каналів - stderr для платформи, окремий канал для платежів, Slack лише для critical.
5. Винятки - у сервіс відстеження помилок (Sentry, Flare, Nightwatch) з групуванням і стектрейсом; throttle() у withExceptions(), щоб шторм однакових помилок не з'їв квоту.
Чого не писати: паролі, токени, повні номери карток, персональні дані без потреби. Витік журналу - теж витік.
Локально журнал зручно читати через php artisan pail з фільтрами за рівнем і користувачем.