Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Docker: Загальний

20 питань · ~18 хв · Версія v3.0

Увійдіть, щоб продовжити

Образи й шари, Dockerfile, кеш збірки, multi-stage, томи й мережі, Compose, ресурси й сигнали, PHP-застосунок у контейнері.

За спробу
20
У пулі
100
Проходжень
0
Середній бал
-
Пройшли на 70%+
-

Питання для підготовки

100 питань

Кожна інструкція 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: ADD

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: розгортання

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: права на каталоги

У розробці 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 перекрито томом).

Докладніше в документації: Laravel: запуск Vite

Прочитати - ще не значить знати

20 питань, по одному на екран, ~18 хв. Після завершення - розбір кожної помилки з посиланням на питання.