Питання на співбесіді з Docker
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
100 питань
- Образ (image) - незмінний шаблон: файлова система з усім потрібним (ОС-шар, PHP, розширення, код, залежності) і метадані - яку команду запускати, які порти слухати. Збирається з
Dockerfile, зберігається в реєстрі (Docker Hub, GHCR). - Контейнер - запущений екземпляр образу: ізольований процес з власною файловою системою, мережею й обмеженнями ресурсів.
Аналогія: образ - як клас, контейнер - як об'єкт. З одного образу можна запустити скільки завгодно контейнерів.
docker build -t myapp:1.4 . # зібрати образ
docker run -d --name web myapp:1.4 # запустити контейнер
docker ps # запущені контейнери
docker images # локальні образи
Шари. Образ складається з шарів тільки для читання - по одному на інструкцію Dockerfile, що змінює файли. Контейнер додає зверху тонкий записуваний шар. Тому:
- десять контейнерів з одного образу ділять його шари й не займають десятикратно більше місця;
- зміни, зроблені всередині контейнера, зникають разом із ним. Дані, що мають жити довше (база, завантажені файли), зберігають у томах (volumes).
Практичне правило: контейнер має бути «одноразовим» - його можна знищити й створити заново з того ж образу без втрати даних. Змінити застосунок - це зібрати новий образ, а не зайти в контейнер і поправити файл.
Кожна інструкція Dockerfile (RUN, COPY, ADD...) створює шар. Під час повторного збирання Docker перевіряє, чи змінилася інструкція та файли, що в неї входять. Якщо ні - бере шар з кешу. Щойно один шар змінився, усі наступні збираються заново.
Погано:
COPY . /var/www
RUN composer install --no-dev
Будь-яка зміна в коді (навіть README) інвалідує COPY ., і composer install виконується щоразу - хвилини на кожне збирання.
Добре: спершу те, що змінюється рідко, потім - часто:
COPY composer.json composer.lock /var/www/
RUN composer install --no-dev --no-scripts --no-autoloader
COPY . /var/www
RUN composer dump-autoload --optimize
Тепер залежності перевстановлюються лише при зміні composer.json/composer.lock, а зміна коду перебудовує тільки останні шари.
Ще правила:
.dockerignore- щоб у контекст збирання не потраплялиvendor/,node_modules/,.git/,.env, логи. ІнакшеCOPY .інвалідується через файли, що до застосунку не мають стосунку, а секрети можуть потрапити в образ.- Встановлення пакетів ОС - на початку, одним
RUN, разом з очищенням кешу менеджера пакетів. - Кеш-монтування BuildKit зберігає кеш Composer чи npm між збираннями навіть після інвалідації шару:
RUN --mount=type=cache,target=/root/.composer/cache composer install
- У CI кеш зникає з кожною новою машиною - його експортують у реєстр (
--cache-to/--cache-from).
Обидві інструкції додають файли в образ, але ADD робить більше, і саме це робить його менш передбачуваним.
COPY - просто копіює файли й каталоги з контексту збирання (або з іншої стадії --from=):
COPY composer.json composer.lock ./
COPY --chown=www-data:www-data . /var/www/html
ADD уміє ще й:
- завантажувати файли за URL:
ADD https://example.com/app.tar.gz /tmp/; - автоматично розпаковувати локальні архіви (
.tar,.tar.gz,.tar.xz) у каталог призначення; - клонувати Git-репозиторії:
ADD https://github.com/org/repo.git#v1.2 /src.
Чому за замовчуванням - COPY:
- передбачуваність:
COPY archive.tar.gz /app/копіює архів як файл.ADDз тим самим рядком розпакує його - поведінка залежить від типу файлу; - кеш: для URL Docker мусить завантажити файл, щоб перевірити, чи він змінився;
- безпека: завантаження з інтернету без перевірки вмісту - ризик ланцюжка постачання.
Коли ADD доречний:
- розпакувати локальний архів одним кроком (наприклад, бінарник з архіву поруч із Dockerfile);
- завантажити файл із перевіркою контрольної суми - сучасний
ADDпідтримує--checksum=sha256:..., тож вміст гарантовано той, що очікувався; - Git-репозиторій як джерело без окремого
git cloneв образі.
Корисні параметри обох інструкцій:
--chown=user:group- власник файлів одразу при копіюванні. ОкремийRUN chown -RпісляCOPYстворює ще один шар з копією всіх файлів - образ подвоюється в розмірі;--chmod=755- права доступу;--link- копіювання незалежним шаром, який не інвалідується при зміні попередніх шарів (корисно для фінальних стадій).
Що потрапляє в контекст визначає .dockerignore - без нього COPY . . скопіює .git, node_modules, .env і локальні логи.
Правило Docker Best Practices: COPY для локальних файлів; ADD - лише коли потрібні його додаткові можливості, і то свідомо.
Образ - це набір шарів, і кожна інструкція Dockerfile, що змінює файлову систему (RUN, COPY, ADD), створює новий шар. Розмір образу - сума шарів.
Історія шарів:
docker image history myapp:latest
IMAGE CREATED BY SIZE
a1b2c3d4e5f6 COPY . /var/www/html 48MB
<missing> RUN composer install --no-dev 62MB
<missing> RUN apt-get update && apt-get install ... 310MB
<missing> FROM php:8.5-fpm ...
Видно, яка інструкція скільки додала. --no-trunc показує повні команди.
Метадані образу:
docker image inspect myapp:latest
Змінні оточення, ENTRYPOINT/CMD, відкриті порти, мітки, архітектура (amd64/arm64), кількість шарів.
Що всередині файлової системи:
docker run --rm -it myapp:latest sh # зайти й подивитися
docker run --rm myapp:latest du -sh /var/www/html/* | sort -h
Інструмент dive показує вміст кожного шару і файли, які були додані в одному шарі й видалені в наступному (марнування місця).
Типові причини великих образів:
- кеш менеджерів пакетів:
apt-get installбезrm -rf /var/lib/apt/lists/*в тому самомуRUN; - інструменти збирання у фінальному образі: компілятори,
git,node_modulesдля збирання фронтенду - їх прибирає multi-stage build; - зайве в контексті:
.git, тести, локальні файли - потрібен.dockerignore; chown -Rокремою інструкцією - копія всіх файлів у новому шарі;- важкий базовий образ - повний Debian замість
slim.
Чому docker image ls і реальний розмір відрізняються: спільні шари базового образу зберігаються на диску один раз для всіх образів, що на ньому побудовані. docker system df -v показує, скільки займають шари насправді.
Розмір має значення: час завантаження при деплої й масштабуванні, місце в реєстрі, поверхня атаки (кожен зайвий пакет - потенційна вразливість).
Офіційний образ php має кілька варіантів - вони відрізняються тим, як запускається PHP:
| Тег | Що всередині | Для чого |
|---|---|---|
php:8.5-cli |
лише PHP CLI | команди, воркери черг, скрипти |
php:8.5-fpm |
PHP-FPM (FastCGI) | за Nginx/Caddy, класичний веб-стек |
php:8.5-apache |
Apache з mod_php |
простий «все в одному» веб-сервер |
php:8.5-zts |
збірка з потокобезпекою | розширення на кшталт parallel, вбудовування |
Кожен варіант має основу Debian (за замовчуванням) або Alpine (php:8.5-fpm-alpine) - менший розмір, але інша бібліотека C (musl), що інколи дає несумісності.
Альтернатива для Laravel - образи FrankenPHP (dunglas/frankenphp): сучасний сервер на основі Caddy з вбудованим PHP, HTTPS і режимом воркера для Octane - один процес замість зв'язки Nginx + PHP-FPM.
Встановлення розширень - допоміжні скрипти офіційного образу:
FROM php:8.5-fpm
RUN apt-get update \
&& apt-get install -y --no-install-recommends libpq-dev libzip-dev libicu-dev \
&& docker-php-ext-install pdo_pgsql zip intl opcache \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& rm -rf /var/lib/apt/lists/*
docker-php-ext-install- розширення з вихідних кодів PHP (pdo_mysql,intl,zip,gd,opcache...);docker-php-ext-configure- параметри перед збиранням (наприклад,gd --with-jpeg --with-freetype);pecl install+docker-php-ext-enable- розширення з PECL (redis,xdebug,imagick);- системні бібліотеки (
libpq-dev,libzip-dev) треба встановити самостійно - їх назви різняться в Debian і Alpine.
Простіше - install-php-extensions (проєкт mlocati): сам встановлює потрібні системні залежності для обох дистрибутивів:
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions pdo_pgsql intl zip redis opcache
Конфігурація PHP: готові шаблони php.ini-production / php.ini-development лежать у $PHP_INI_DIR; власні налаштування - окремим файлом у $PHP_INI_DIR/conf.d/.
Пастка: інструменти збирання (*-dev пакети, компілятори) лишаються в образі - їх прибирають у тому самому RUN чи через multi-stage build.
Файлова система контейнера - тимчасова: усе, що записано всередину, зникає разом з контейнером (docker rm). Для даних, що мають жити довше, є два основні способи:
Том (volume) - сховище, яким керує Docker (зазвичай у /var/lib/docker/volumes). Живе незалежно від контейнерів.
services:
db:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Bind mount - каталог хоста, змонтований у контейнер напряму. Зміни видно з обох боків одразу.
services:
app:
volumes:
- ./:/var/www # код з машини розробника
Коли що:
- Томи - для даних сервісів: бази, Redis, завантажені файли. Портабельні, не залежать від структури каталогів хоста, з ними простіше робити бекапи. На macOS і Windows значно швидші за bind mount.
- Bind mount - у розробці: правите код в IDE, контейнер одразу бачить зміни. На проді - лише для конфігурацій, якщо взагалі.
Пастки:
docker compose down -vвидаляє томи разом з даними бази. Без-vтоми лишаються.- Права доступу в bind mount: файли, створені в контейнері від root, на хості теж належать root.
- Анонімні томи (без імені) легко загубити й накопичити -
docker volume pruneприбирає невикористані.
На проді для коду ні том, ні bind mount не потрібні: код запікають в образ, а новий реліз - це новий образ.
Compose створює для проєкту окрему мережу, і всі сервіси в ній доступні один одному за назвою сервісу - вбудований DNS Docker перетворює її на IP контейнера.
services:
app:
build: .
environment:
DB_HOST: db # не localhost і не IP
REDIS_HOST: redis
db:
image: postgres:17
redis:
image: redis:7
Типова помилка новачка - localhost. У контейнері app адреса localhost - це сам контейнер app, а не база. Бази там немає, тож DB_HOST=localhost дає «connection refused». Потрібно DB_HOST=db.
Порти: внутрішні й опубліковані.
- Усередині мережі сервіси звертаються один до одного за внутрішнім портом контейнера:
db:5432. ports: ["5433:5432"]публікує порт на хост - щоб підключитися з машини розробника чи з інтернету. Для зв'язку між контейнерами публікувати не потрібно.
db:
ports:
- "127.0.0.1:5433:5432" # доступно лише з цього комп'ютера
Безпека: ports: ["5432:5432"] на сервері відкриває базу на всі інтерфейси - і Docker при цьому обходить правила ufw, бо сам керує iptables. Базу на проді або не публікують узагалі, або прив'язують до 127.0.0.1.
Кілька мереж дозволяють ізолювати сервіси: фронтенд-проксі бачить застосунок, але не базу.
docker run створює контейнер з образу й запускає його.
docker run -d --name redis --restart unless-stopped -p 127.0.0.1:6379:6379 -v redis-data:/data redis:8-alpine
Основні прапорці:
| Прапорець | Що робить |
|---|---|
-d |
у фоні (detached); без нього - вивід у терміналі |
--name redis |
ім'я замість випадкового (hungry_turing) |
-p 8080:80 |
опублікувати порт контейнера 80 на порту хоста 8080 |
-v назва:/шлях |
іменований том; -v ./src:/app - каталог хоста (bind mount) |
-e KEY=value |
змінна оточення; --env-file .env - з файлу |
--rm |
видалити контейнер після завершення (для разових команд) |
-it |
інтерактивний режим з терміналом (для bash, tinker) |
--restart |
політика перезапуску: no, on-failure, always, unless-stopped |
--network |
приєднати до мережі |
-w /app |
робочий каталог |
--memory 512m, --cpus 1 |
ліміти ресурсів |
--user 1000:1000 |
від імені якого користувача запустити процес |
Команда після образу замінює CMD образу:
docker run --rm -it php:8.5-cli php -v
docker run --rm -v "$PWD":/app -w /app composer:2 composer install
Другий приклад - класичний спосіб запустити інструмент без встановлення на хост: контейнер існує лише на час команди.
Що варто пам'ятати:
- без
--rmзупинені контейнери накопичуються (docker ps -a) разом зі своїми записуваними шарами; - порядок має значення: прапорці Docker - до назви образу, аргументи для процесу - після.
docker run nginx -p 80:80передасть-p 80:80самому nginx; -p 8080:80публікує порт на всіх інтерфейсах хоста - для служб, які не мають бути доступні ззовні, --p 127.0.0.1:8080:80;-vз відносним шляхом без./Docker вважає назвою тому, а не каталогом;--restart alwaysперезапускає навіть контейнер, зупинений вручну, після перезапуску Docker;unless-stopped- поважає ручну зупинку.
Для кількох пов'язаних контейнерів довгі команди docker run швидко стають незручними - їх описують у compose.yaml.
Стани контейнера:
created → running → (paused) → exited → (removed)
↑______restart______|
- created - створений (
docker create), але не запущений; - running - головний процес працює;
- paused - процеси заморожено (
docker pause), пам'ять зберігається; - exited - головний процес завершився; файлова система контейнера й логи лишаються, доки контейнер не видалено;
- removed -
docker rm: контейнер і його записуваний шар знищено (томи - ні).
Головне правило: контейнер живе, поки працює його головний процес (PID 1). Процес завершився - контейнер зупинився. Тому контейнер з CMD ["php", "artisan", "migrate"] зупиняється одразу після міграцій - це нормально.
Команди:
docker stop app # SIGTERM, а через 10 с - SIGKILL
docker kill app # одразу SIGKILL (чи інший сигнал: --signal)
docker restart app
docker ps -a # усі контейнери, включно з exited, і їхні коди завершення
Коди завершення - діагностика:
| Код | Значення |
|---|---|
0 |
процес завершився нормально |
1 |
помилка застосунку (виняток, невдала команда) |
126 / 127 |
команду неможливо виконати / не знайдено (помилка в CMD/ENTRYPOINT) |
137 |
128 + 9 (SIGKILL) - процес убито примусово |
143 |
128 + 15 (SIGTERM) - процес завершився на запит зупинки |
Код 137 - найчастіше:
- OOM-killer: контейнер перевищив ліміт пам'яті. Перевірка:
docker inspect app --format '{{.State.OOMKilled}}'-true; docker stopне дочекався: процес не обробив SIGTERM за 10 секунд і отримав SIGKILL (проблема з PID 1 чи довге завершення).
Код 143 при docker stop - очікуваний: процес коректно відреагував на SIGTERM.
Політики перезапуску (--restart) визначають, що відбувається після завершення: no, on-failure[:N] (лише при ненульовому коді), always, unless-stopped. Контейнер, що падає одразу після старту, з always потрапляє в цикл перезапусків з наростаючою затримкою - видно в docker ps як Restarting (1) ....
Перша дія при падінні: docker logs app (логи зупиненого контейнера доступні, доки його не видалено) і docker inspect для коду й причини.
Докладніше в документації: Docker: автоматичний запуск контейнерів
Принцип «конфігурація в оточенні» (з методології Twelve-Factor App): той самий образ запускається в різних середовищах, а відмінності (адреси баз, ключі, режими) передаються змінними оточення.
Способи передачі:
docker run -e APP_ENV=production -e DB_HOST=db myapp # окремі змінні
docker run -e DB_PASSWORD myapp # значення з оточення хоста
docker run --env-file ./production.env myapp # з файлу KEY=value
# compose.yaml
services:
app:
image: myapp
environment:
APP_ENV: production
DB_HOST: db
env_file:
- .env.production
Пріоритет (від нижчого до вищого):
ENVв Dockerfile - значення за замовчуванням в образі;env_file/--env-file;environment/-e- перекривають попередні.
ENV в Dockerfile доречний для незмінних налаштувань образу (PHP_INI_DIR, шляхи, COMPOSER_ALLOW_SUPERUSER), але не для секретів чи значень, що відрізняються між середовищами: усе з ENV видно в docker image inspect і в кожному контейнері з цього образу.
Для Laravel:
- Laravel читає змінні оточення процесу через
env()- файл.envу контейнері не обов'язковий, якщо змінні передано Docker; php artisan config:cache«заморожує» значення на момент виконання команди. Якщо кеш зроблено під час збирання образу, змінні оточення, передані при запуску, ігноруватимуться. Кешувати конфігурацію треба при старті контейнера, коли змінні вже доступні;- поза файлами конфігурації
env()не працює післяconfig:cache- у коді лишеconfig().
Безпека:
- змінні оточення видно в
docker inspect, у/proc/<pid>/environ, вони потрапляють у звіти про помилки й дочірні процеси; - для секретів кращі файли секретів (Docker/Compose secrets монтуються в
/run/secrets/...) або менеджер секретів; .envз секретами - не в образ (.dockerignore) і не в Git.
Перевірка того, що бачить контейнер: docker exec app env чи docker compose config - підсумкова конфігурація Compose з підставленими значеннями.
Laravel-застосунок - це не лише вебсервер. У продакшені працює кілька процесів, і в Docker кожен зазвичай отримує свій контейнер:
services:
app: # вебзапити: PHP-FPM (+ Nginx) або FrankenPHP
image: myapp:1.4.0
queue: # воркер черги
image: myapp:1.4.0
command: php artisan queue:work --tries=3 --max-jobs=1000
scheduler: # планувальник задач
image: myapp:1.4.0
command: php artisan schedule:work
db:
image: postgres:18
redis:
image: redis:8-alpine
Ролі контейнерів:
| Контейнер | Що робить |
|---|---|
| вебсервер + PHP | обробляє HTTP-запити. Класична пара - Nginx і PHP-FPM (два контейнери чи один), сучасна - FrankenPHP чи Octane |
| воркер черги | виконує задачі з черги: листи, обробку файлів, сповіщення |
| планувальник | щохвилини запускає schedule:run (чи працює постійно через schedule:work) |
| база даних | PostgreSQL чи MySQL з томом для даних |
| Redis | кеш, сесії, черги, блокування |
| опційно | Horizon замість простого воркера, Reverb для вебсокетів, Meilisearch для пошуку |
Ключова ідея - один образ, різні команди. Веб, воркер і планувальник використовують той самий образ застосунку з тим самим кодом і залежностями - відрізняється лише команда запуску. Це гарантує, що воркер виконує задачі тим самим кодом, що й веб, який їх поставив.
Що зазвичай не в контейнерах у продакшені:
- база даних часто винесена в керований сервіс (RDS, Cloud SQL) - бекапи, реплікація й оновлення стають чужою турботою;
- файли користувачів - в об'єктному сховищі (S3, R2), а не на диску контейнера.
Локально все це збирає Laravel Sail чи власний compose.yaml, а в продакшені - Compose на одному сервері, Docker Swarm, Kubernetes чи платформи на кшталт Laravel Cloud, Dokploy, Coolify.
Laravel пише у два каталоги:
storage/- логи, скомпільовані шаблони Blade, файловий кеш, сесії, завантаження на дискlocal;bootstrap/cache/- кеш конфігурації, маршрутів, подій,packages.php,services.php.
Типова помилка:
The stream or file "/var/www/html/storage/logs/laravel.log" could not be opened in append mode: Failed to open stream: Permission denied
Причина: процес PHP працює від одного користувача (www-data, UID 33 в офіційних образах Debian чи 82 в Alpine), а файли належать іншому - найчастіше root, бо COPY у Dockerfile за замовчуванням створює файли власника root.
Виправлення в Dockerfile:
COPY --chown=www-data:www-data . /var/www/html
# або для вже скопійованих файлів - лише каталоги для запису
RUN chown -R www-data:www-data storage bootstrap/cache \
&& chmod -R ug+rwX storage bootstrap/cache
USER www-data
--chown при COPY кращий за окремий RUN chown -R: окремий chown створює ще один шар з копією всіх файлів і збільшує образ.
Чого не робити:
chmod -R 777 storage- «працює», але дає запис усім і маскує справжню проблему;- запускати PHP від
root, щоб «не було проблем з правами» - якщо зловмисник виконає код, він отримає root у контейнері.
Типові пастки:
- bind mount у розробці (
./:/var/www/html) - файли мають власника з хоста (UID 501 на macOS, 1000 на Linux). На Linux процес у контейнері з іншим UID не може писати. Рішення - запускати PHP з UID розробника (Sail робить це черезWWWUSER); - том для storage створюється при першому запуску з правами
root- потрібно задати власника в образі до оголошення тому чи при старті; - файли, створені
php artisanвід root черезdocker exec(наприклад, кеш конфігурації чи лог) потім недоступні дляwww-data. Виконувати команди від того самого користувача:docker exec -u www-data app php artisan ....
Laravel читає значення через env() у файлах config/*.php. Джерела:
- змінні оточення процесу - те, що передав Docker (
-e,environment,env_fileу Compose, секрети оркестратора); - файл
.env- його завантажує бібліотека phpdotenv при старті.
Пріоритет: справжні змінні оточення важливіші за .env - phpdotenv не перезаписує вже встановлені змінні. Тому в контейнері можна взагалі не мати .env: усі значення приходять від Docker.
Чому .env не варто класти в образ:
- образ стає прив'язаним до одного середовища - для staging і продакшену потрібні різні образи;
- секрети (
APP_KEY, паролі бази, ключі API) потрапляють у шари образу й у реєстр - їх видно будь-кому з доступом до образу; .envмає бути в.dockerignore.
Пастка config:cache:
php artisan config:cache зберігає всю конфігурацію з уже підставленими значеннями в bootstrap/cache/config.php. Після цього .env і змінні оточення більше не читаються.
- якщо зробити
config:cacheпід час збирання образу, туди потраплять значення з середовища збирання (чи порожні) - а змінні, передані при запуску, буде проігноровано; - тому кешувати конфігурацію треба при старті контейнера, коли оточення вже відоме:
#!/bin/sh
# entrypoint.sh
php artisan config:cache
php artisan route:cache
php artisan view:cache
exec "$@"
Ще один наслідок: після config:cache функція env() поза файлами config/ повертає null. У коді застосунку - лише config('services.stripe.key'), а не env('STRIPE_KEY').
Перевірка в контейнері:
docker exec app php artisan about # середовище, драйвери, чи закешовано конфігурацію
docker exec app php artisan config:show database.default
У розробці npm run dev запускає dev-сервер Vite (порт 5173) і створює файл public/hot з його адресою. Директива @vite бачить цей файл і підключає скрипти не зі зібраних файлів, а з dev-сервера, - звідси гаряче перезавантаження (HMR).
Чому в Docker це часто не працює з коробки:
- Vite за замовчуванням слухає лише
localhostусередині контейнера - з браузера на хості до нього не достукатися; - порт 5173 не опубліковано;
- у
public/hotзаписується адреса, яку бачить контейнер, а браузер має звертатися до хоста.
Налаштування для контейнера:
// vite.config.js
export default defineConfig({
plugins: [laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true })],
server: {
host: '0.0.0.0', // слухати всі інтерфейси контейнера
port: 5173,
strictPort: true,
hmr: { host: 'localhost' }, // адреса, яку бачить браузер
},
});
# compose.yaml
services:
app:
ports:
- "127.0.0.1:5173:5173"
Laravel Sail робить саме це: публікує VITE_PORT і запускає sail npm run dev всередині контейнера.
Типові симптоми:
- сторінка без стилів і помилки
ERR_CONNECTION_REFUSEDдо:5173- порт не опубліковано чи Vite слухає лишеlocalhost; - стилі є, але зміни не підхоплюються - файлова система через bind mount не надсилає подій про зміни (буває на Windows і macOS), допомагає
server.watch.usePolling: true; - у продакшені запити до
:5173- у образ потрапив залишений файлpublic/hot. Його треба додати в.dockerignore.
Продакшен - зібрані файли, без Node в образі:
FROM node:24-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM php:8.5-fpm-alpine
COPY --from=assets /app/public/build /var/www/html/public/build
npm run build створює public/build з маніфестом і хешованими іменами файлів, а @vite читає маніфест. Окрема стадія збирання означає, що Node, node_modules і вихідні JS-файли не потрапляють у фінальний образ.
Помилка «Unable to locate file in Vite manifest» у контейнері - файли фронтенду не зібрано або не скопійовано в образ (чи public/build перекрито томом).
Воркер - окремий контейнер з тим самим образом, але іншою командою:
services:
queue:
image: myapp:1.4.0
command: php artisan queue:work redis --queue=high,default --tries=3 --max-jobs=1000 --max-time=3600
restart: unless-stopped
stop_grace_period: 60s
Чому окремий контейнер:
- масштабується незалежно:
docker compose up -d --scale queue=4; - важка задача не забирає ресурси у вебзапитів;
- падіння воркера не впливає на сайт, а Docker перезапускає його сам (
restart), тож Supervisor усередині не потрібен.
Чому queue:work треба перезапускати після деплою:
queue:work - довгоживучий процес: він завантажує застосунок один раз і тримає код у пам'яті. Після оновлення файлів старий воркер продовжує виконувати старий код.
- на звичайному сервері
php artisan queue:restartставить мітку в кеш, і воркери завершуються після поточної задачі (а Supervisor запускає їх заново); - у Docker код у контейнері не змінюється - деплой нової версії означає новий образ і нові контейнери. Старі контейнери зупиняються, нові стартують з новим кодом.
queue:restartу такому разі не обов'язковий, але корисний, якщо воркери живуть довше за деплой.
Коректна зупинка:
docker stopнадсилає SIGTERM - воркер Laravel завершує поточну задачу і виходить;- через
stop_grace_period(за замовчуванням 10 секунд) Docker надсилає SIGKILL - задача обривається посередині. Цей час має бути більшим за найдовшу задачу; - щоб сигнал дійшов, PHP має бути PID 1 (
execу скрипті запуску чи exec-формаcommand).
Від витоків пам'яті: --max-jobs і --max-time змушують воркер періодично завершуватися, а Docker його перезапускає з чистою пам'яттю. Для --memory враховуйте ліміт пам'яті контейнера.
Horizon - замість кількох queue:work один контейнер з php artisan horizon, який сам керує процесами за конфігурацією й балансує їх між чергами. Після деплою - horizon:terminate чи заміна контейнера.
Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії