Чому я взагалі вирішила змінити свій Laradock workflow
Довгий час мій локальний Laradock був максимально простим у щоденній роботі. Я запускала потрібні сервіси один раз, а далі працювала з проєктами без постійної думки про Docker.
Ситуація змінилася, коли одночасно знадобилися кілька версій PHP. Частина проєктів залишалася на PHP 8.3, інші вже працювали на PHP 8.4, а нові - на PHP 8.5.
Це multi-PHP середовище я підготувала під час оновлення локального Laravel-середовища та пакета до PHP 8.5. Наступною проблемою стало зробити цю конфігурацію достатньо зручною для щоденної роботи з CLI.
Для HTTP це вирішується досить природно: кожен Nginx virtual host може використовувати свій PHP-FPM service. Але з CLI усе стало менш зручним. Для Artisan, Composer або звичайного php -v потрібно було пам'ятати, який workspace належить конкретному проєкту, і писати довгі Docker Compose команди.
Я хотіла повернутися до простого workflow, але при цьому зберегти повноцінне multi-PHP середовище.
docker compose run --rm --no-deps workspace-85 bash -lc \ 'cd /var/www/icanup && php artisan test'dartisan testMulti-PHP архітектура: окремий CLI і PHP-FPM для кожної версії
Перше важливе рішення - не змішувати CLI runtime та PHP-FPM.
PHP-FPM обслуговує HTTP-запити через Nginx, а workspace використовується для CLI: Artisan, Composer, PHPUnit, PHPStan та інших development-команд.
У моєму Laradock фінальна схема виглядає так:
PHP | CLI service | FPM service |
|---|---|---|
PHP 8.3 | workspace | php-fpm |
PHP 8.4 | workspace-84 | php-fpm-84 |
PHP 8.5 | workspace-85 | php-fpm-85 |
PHP 8.3 залишається default runtime, а 8.4 і 8.5 працюють як додаткові паралельні середовища.
Якщо сайт працює через php-fpm-85, це ще не означає, що php artisan або Composer теж запускаються на PHP 8.5. CLI і FPM - незалежні runtime.
Саме тому для multi-PHP середовища я використовую відповідні workspace, workspace-84 і workspace-85.
Як я налаштувала persistent workspaces
Я не хотіла створювати тимчасовий контейнер для кожної CLI-команди. Тому всі три workspace працюють як звичайні persistent Docker Compose services.
Основний workspace зберігає свої host ports, а альтернативні workspace-84 і workspace-85 їх не публікують. Завдяки цьому всі три контейнери можуть бути запущені одночасно без конфліктів портів.
PHP-FPM services також пов'язані зі своїми відповідними workspace.
php-fpm-84: depends_on: !override - workspace-84 workspace-84: ports: !reset [] php-fpm-85: depends_on: !override - workspace-85 workspace-85: ports: !reset []nginx: depends_on: - php-fpm - php-fpm-84 - php-fpm-85Однаковий UID і GID для host та workspace
Ще одна проблема проявилася вже після запуску wrappers.
Код монтується в контейнер із host-машини та належить моєму локальному користувачу з UID/GID 1000:1000. Спочатку користувач laradock у контейнерах мав інший UID, через що Git визначав repository як такий, що має підозріле ownership.
Замість додавання глобального safe.directory=* я вирівняла UID/GID користувача Laradock із користувачем host-машини.
WORKSPACE_PUID=1000WORKSPACE_PGID=1000Коли bind-mounted файли належать UID 1000, development-команди в контейнері також краще виконувати користувачем із UID 1000. Це вирішує не тільки Git warning, але й запобігає появі файлів root:root у робочих проєктах.
Локальний project-to-workspace mapping
Я не можу додавати службові Docker-файли до кожного робочого repository, тому mapping версій PHP знаходиться повністю поза application projects.
Для цього використовую локальний файл:
~/.config/laradock/projects.conf
PHP 8.3 є default, тому проєкти на 8.3 взагалі не потрібно перераховувати. У mapping потрапляють лише проєкти, яким потрібен альтернативний workspace.
default=workspace # PHP 8.4/home/alyona/Projects/some-project/project-core=workspace-84 # PHP 8.5/home/alyona/Projects/icanup=workspace-85Один dispatcher замість десятків PHP-команд
Я не хотіла створювати окремі php83, php84, php85, composer84, composer85 та подібні aliases.
Проєкт уже має локальний mapping, отже CLI сам може визначити потрібний workspace.
Для цього я використовую один dispatcher - laradock-exec.
Він:
визначає Git root поточного проєкту;
знаходить відповідний workspace у
projects.conf;використовує
workspaceяк default;перетворює host path
/home/alyona/Projects/...на container path/var/www/...;перевіряє, чи існує і чи запущений потрібний service;
запускає команду через
docker compose exec;виконує її від користувача
laradock.
exec docker compose exec \ --user laradock \ -w "$container_dir" \ "$service" \ "$@"dphp, dartisan і dcomposer
Поверх dispatcher я залишила лише три маленькі wrapper-команди.
dphpзапускає PHP у правильному workspace.dartisanє shortcut дляphp artisan.dcomposerзапускає Composer на тій самій версії PHP, що й CLI runtime проєкту.
#!/usr/bin/env bashexec "${HOME}/.local/bin/laradock-exec" php "$@"#!/usr/bin/env bashexec "${HOME}/.local/bin/laradock-exec" php artisan "$@"#!/usr/bin/env bashexec "${HOME}/.local/bin/laradock-exec" composer "$@"Як це виглядає у щоденній роботі
Тепер я працюю з Docker-based PHP runtime майже так само, як зі звичайним локальним PHP.
cd ~/Projects/icanup dphp -vdartisan testdartisan migratedcomposer installДля IcanUp dispatcher автоматично вибирає workspace-85, тому всі ці команди працюють на PHP 8.5.
В іншому проєкті Core ті самі команди автоматично використовують workspace-84.
А будь-який проєкт, якого немає у mapping, переходить на default workspace із PHP 8.3.
Це також прибирає одну просту причину помилок — випадковий запуск проєкту або його тестів на неправильній версії PHP. Я вже бачила, наскільки помітним це може бути, коли той самий тест із round() поводився по-різному на PHP 8.3 і PHP 8.4.
Мені більше не потрібно пам'ятати назву контейнера під час кожного запуску Artisan або Composer.
cd ~/Projects/laradock docker compose exec \ --user laradock \ -w /var/www/icanup \ workspace-85 \ php artisan testcd ~/Projects/icanup dartisan testЯк додати ще один PHP 8.4 або PHP 8.5 проєкт
Новий проєкт не потребує жодних Docker-файлів усередині repository. Спочатку я визначаю його Git root:
git rev-parse --show-toplevelПісля цього достатньо додати один рядок до ~/.config/laradock/projects.conf.
# PHP 8.4/home/alyona/Projects/project=workspace-84 # PHP 8.5/home/alyona/Projects/project=workspace-85Команди працюють навіть із вкладених директорій
Dispatcher використовує git rev-parse --show-toplevel, тому мені не потрібно перебувати в root directory проєкту.
Наприклад, навіть із:
~/Projects/icanup/app/Servicesкоманда dphp -v визначає root ~/Projects/icanup, вибирає workspace-85 і запускається з /var/www/icanup усередині контейнера.
Це маленька деталь, але саме вона робить wrapper зручним у реальній щоденній роботі.
Що я отримала в результаті
PHP 8.3, 8.4 та 8.5 працюють одночасно.
Для кожної версії є окремий CLI workspace і PHP-FPM.
Не потрібно входити в Docker containers.
Не потрібно створювати temporary workspace containers.
Не потрібно додавати Docker runtime files до робочих repositories.
Artisan та Composer завжди використовують правильну версію PHP.
Команди виконуються з правильним UID/GID.
PHP 8.3 залишається default для старіших проєктів.
Для PHP 8.4/8.5 достатньо одного рядка локального mapping.
Docker має спрощувати development environment, а не змушувати мене щоразу думати, в якому контейнері виконати php artisan.
Multi-PHP конфігурація сама по собі не повинна робити локальну розробку складнішою.
Для мене найкращим рішенням виявилось залишити чітке розділення між PHP-FPM та CLI runtime, використовувати persistent workspaces і додати дуже тонкий локальний CLI layer поверх Docker Compose.
У результаті інфраструктура залишається повністю контейнеризованою, але під час щоденної роботи я користуюся короткими командами dphp, dartisan і dcomposer.



