Обидві команди виконують команду «в сервісі», але по-різному.
docker compose exec - виконує команду в уже запущеному контейнері сервісу:
docker compose exec app php artisan migrate
docker compose exec app sh # оболонка в працюючому контейнері
docker compose exec -u root app apk add htop # від іншого користувача
- контейнер має бути запущений;
- команда бачить той самий стан: файли, процеси, змінні оточення, з'єднання;
- після завершення контейнер продовжує працювати.
docker compose run - створює новий тимчасовий контейнер з конфігурації сервісу й виконує в ньому команду:
docker compose run --rm app composer install
docker compose run --rm app php artisan test
docker compose run --rm --no-deps node npm run build
- працює, навіть якщо сервіс не запущено;
- за замовчуванням запускає залежності (
depends_on) ---no-depsвимикає це; - не публікує порти сервісу (щоб не конфліктувати із запущеним), якщо не вказати
--service-ports; --rm- видалити контейнер після завершення, інакше накопичуються зупинені контейнери.
Коли що:
| Задача | Команда |
|---|---|
| міграції, tinker, черга в робочому оточенні | exec |
| подивитися, що відбувається в працюючому контейнері | exec |
| одноразова задача без запущеного сервісу (встановлення залежностей, тести в CI) | run --rm |
| команда в чистому оточенні, щоб не зачіпати запущений контейнер | run --rm |
Laravel Sail обгортає саме ці команди: sail artisan migrate - це docker compose exec laravel.test php artisan migrate, а sail shell - оболонка в працюючому контейнері.
Типова помилка: docker compose run app php artisan queue:work без --rm щодня - десятки забутих контейнерів. Подивитися їх: docker compose ps -a.