Капстоун, шаг 2: сборка образа через werf

Уровень: expert ~60 мин Практика: Firefly III (AGPL-3.0)

Что нужно знать перед уроком

  • M11.01 — Планирование пакета и анатомия — нужен готовый вопросник анализа приложения перед написанием werf.yaml
  • M3.03 — Stapel-builder глубоко — нужны 4 user-стадии Stapel и git.stageDependencies
  • M8.01 — Deep dive в линковку образов — нужна модель artifact/import для многообразных сборок

Что нужно знать перед уроком

M3 урок 3 — 4 user-стадии Stapel (beforeInstall → install → beforeSetup → setup), git.stageDependencies. M8 урок 1import/ImageName/ImageTag для передачи артефактов между образами одного werf.yaml. M3 урок 6 — build secrets. Этот урок применяет всё перечисленное к сборке PHP/Laravel-приложения — рантайму, ни разу не встречавшемуся в курсе.

Теория

Композиция стадий сборки Firefly III

PHP/Laravel-приложение имеет две независимые системы зависимостей — PHP (composer.json/composer.lock) и JavaScript-ассеты (package.json/package-lock.json, собираемые через npm run build в статические файлы public/build/). Прямое применение принципа разделения стадий из M3 урока 3 — каждая система зависимостей получает собственную привязку git.stageDependencies, чтобы изменение PHP-кода не форсировало пересборку JS-ассетов и наоборот:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
image: app
from: php:8.4-fpm-bookworm
git:
  - add: /
    to: /var/www/firefly-iii
    stageDependencies:
      install:
        - composer.json
        - composer.lock
      beforeSetup:
        - package.json
        - package-lock.json
      setup:
        - app/
        - resources/
        - routes/
shell:
  beforeInstall:
    - apt-get update -q
    - apt-get install -y --no-install-recommends libzip-dev libpng-dev unzip git
    - docker-php-ext-install pdo_mysql bcmath gd zip
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
  install:
    - cd /var/www/firefly-iii && composer install --no-dev --no-scripts --prefer-dist
  beforeSetup:
    - curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
    - apt-get install -y nodejs
    - cd /var/www/firefly-iii && npm ci && npm run build
  setup:
    - cd /var/www/firefly-iii && composer dump-autoload --optimize --no-dev
    - cd /var/www/firefly-iii && php artisan config:cache

Распределение по стадиям строго следует конвенции таблицы из M3 урока 3: системные PHP-расширения (beforeInstall, меняются реже всего), composer install (install, зависит от composer.lock), сборка Node-ассетов (beforeSetup, зависит от package-lock.json — намеренно после composer install, чтобы изменение JS-кода не инвалидировало уже установленные PHP-зависимости), и, наконец, setup — кэш конфигурации Laravel, максимально чувствительный к изменению кода приложения.

Почему php artisan config:cache — не на этапе выполнения, а на этапе сборки

Важное архитектурное решение, отличающее production-сборку Laravel от разработческого режима: команда php artisan config:cache объединяет все конфигурационные файлы config/*.php в единый закэшированный файл, что заметно ускоряет каждый последующий запрос (Laravel не парсит десятки PHP-файлов конфигурации на каждый HTTP-запрос). Выполнение этой команды на этапе сборки образа (стадия setup), а не при каждом старте контейнера — прямое применение принципа immutable-инфраструктуры, разобранного применительно к образам ещё в M3 уроке 1: результат сборки должен быть полностью готов к запуску, минимизируя работу, выполняемую при каждом старте Pod’а (что напрямую влияет на скорость готовности startupProbe, спланированного в M11 уроке 1).

Важная оговорка: значения окружения, зависящие от конкретного инстанса (APP_URL, реквизиты БД из managed-mysql), кэшировать на этапе сборки нельзяconfig:cache, выполненный с плейсхолдерами вместо реальных DB_HOST/DB_PASSWORD, зафиксирует несуществующие значения в образе. Правильная практика — либо пересчитывать config:cache при старте контейнера (после инъекции реальных ENV из Secret), либо (предпочтительнее для immutable-подхода) не кэшировать секреты вовсе, полагаясь на чтение env() напрямую для чувствительных к инстансу значений — Laravel поддерживает оба режима, конкретный выбор фиксируется в docs/README.md пакета.

Линковка веб-сервера как отдельного образа

Следуя паттерну multi-image werf.yaml, уже примененному для Vikunja (frontend+backend, M3) и для Umami, капстоун разделяет PHP-FPM (app) и nginx (web), передавая статические ассеты через import — прямое применение модели из M8 урока 1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
image: web
from: nginx:1.30-alpine
import:
  - image: app
    add: /var/www/firefly-iii/public
    to: /var/www/firefly-iii/public
    after: install
git:
  - add: /docker/nginx.conf
    to: /etc/nginx/conf.d/default.conf

import копирует уже собранные статические файлы (public/, включая скомпилированные ассеты из beforeSetup образа app) в образ nginx, избегая дублирования всей PHP-стадии сборки внутри лёгкого образа веб-сервера — тот же принцип экономии слоёв, что и ImageName/ImageTag/ImageRepo шаблонные функции, разобранные в M8 уроке 1 для передачи имени/тега образа между шаблонами Nelm; здесь import работает на уровне самой сборки werf.yaml, до какого-либо взаимодействия с Kubernetes-манифестами.

Composer-токен как build secret

Если бы приватные Composer-пакеты требовали аутентификации (для Firefly III это не требуется — все зависимости публичны, но паттерн важен для любого закрытого корпоративного PHP-проекта), инъекция токена происходила бы строго через build secrets, разобранные в M3 уроке 6, а не через ARG/ENV, которые попадают в историю слоёв образа:

1
2
3
4
5
shell:
  install:
    - --mount=type=secret,id=composer_auth cp /run/secrets/composer_auth auth.json
    - composer install --no-dev
    - rm -f auth.json

Частые ошибки и подводные камни

  • Кэшировать конфигурацию Laravel (config:cache) с реальными production-секретами на этапе сборки образа. Секреты БД в этот момент ещё не существуют (они появятся только на этапе Application-чарта, M11 урок 3) — попытка захардкодить их в образ на этапе сборки архитектурно невозможна и означала бы утечку секретов в слои образа, если бы каким-то образом удалась.
  • Устанавливать npm ci && npm run build раньше composer install в порядке стадий. Это меняет семантику кэша: изменение любого PHP-файла (частое) заставило бы пересобираться и JS-стадию (редко меняющуюся) — обратный от желаемого эффект инвалидации кэша.
  • Дублировать полную PHP-стадию сборки внутри образа nginx, вместо использования import. Это не только раздувает образ веб-сервера ненужными PHP-зависимостями, но и создаёт риск рассинхронизации версий ассетов между app и web, если сборки происходят независимо.

Linux / Сети / Docker

PHP-FPM и nginx общаются по протоколу FastCGI, а не по HTTP — это принципиальное архитектурное отличие от связок вида «nginx как reverse-proxy перед HTTP-бэкендом» (например, Go/Node.js-бэкенды Vikunja/Umami из предыдущих модулей общались с nginx именно по HTTP). Конфигурация nginx для проксирования на PHP-FPM использует директиву fastcgi_pass с адресом Unix-сокета или TCP-порта (fastcgi_pass app:9000; при разделении на два контейнера/Pod’а), а не proxy_pass. Разница существенна для сетевой модели Kubernetes: Service, направленный на app (PHP-FPM), должен объявлять targetPort: 9000 (порт FastCGI, а не HTTP), и NetworkPolicy (M10 урок 9), ограничивающая трафик к этому Service, должна разрешать именно этот порт от web (nginx), а не 80/443.

Практика в кластере

Практики в кластере в этом уроке нет — фокус на локальной сборке образов; деплой начнётся в M11 уроке 5 после завершения Application-чарта в M11 уроке 3.

Практика разработки

🧩 Практика урока: Firefly III · лицензия AGPL-3.0
  1. Создайте werf.yaml с двумя образами (app и web) по образцам теории урока в корне пакета firefly-iii, инициализированного в M11 уроке 1:
1
2
cd firefly-iii-src   # клон исходников из M11 урока 1
werf config render   # проверка синтаксиса werf.yaml до полной сборки
  1. Соберите оба образа локально и замерьте время повторной сборки после точечного изменения PHP-кода (должна пересобраться только стадия setup, но не install/beforeSetup):
1
2
3
werf build --dev
touch app/Console/Kernel.php   # тривиальное "изменение" PHP-кода
werf build --dev               # install и beforeSetup — из кэша, пересобирается только setup
  1. Убедитесь, что образ web содержит собранные ассеты public/build/, но не содержит PHP-интерпретатора и composer:
1
2
werf run web -- ls /var/www/firefly-iii/public/build
werf run web -- which php   # ожидаемо: команда не найдена

Шпаргалка команд урока

1
2
3
werf config render                 # проверка синтаксиса werf.yaml
werf build --dev                   # локальная сборка без публикации в registry
werf run <image> -- <command>      # запуск команды внутри собранного образа для отладки
1
2
3
4
5
import:
  - image: <исходный-образ>
    add: <путь-в-исходном>
    to: <путь-в-целевом>
    after: install # install | setup — на какой стадии целевого образа доступен импорт

Полная шпаргалка Stapel/import — 📋 werf .

Вопросы для самопроверки

Почему npm ci && npm run build размещён в стадии beforeSetup, а не install?

Разделение сохраняет независимость инвалидации кэша PHP- и JS-зависимостей.

Источник: Рекомендуемая литература

Почему php artisan config:cache выполняется на этапе сборки образа, а не при каждом старте контейнера?

Принцип immutable-инфраструктуры, применённый к артефакту сборки.

Источник: Рекомендуемая литература

Можно ли кэшировать реквизиты подключения к managed-mysql (DB_HOST/DB_PASSWORD) через config:cache на этапе сборки?

Значения, зависящие от конкретного инстанса Application, не могут быть встроены в образ, общий для всех инстансов пакета.

Источник: Рекомендуемая литература

По какому протоколу nginx взаимодействует с PHP-FPM, в отличие от HTTP-бэкендов Vikunja/Umami из предыдущих модулей?

Это ключевое отличие сетевой модели PHP-FPM от HTTP-бэкендов, встречавшихся ранее в курсе.

Источник: Рекомендуемая литература

Зачем использовать import вместо дублирования полной PHP-стадии сборки в образе nginx?

import переносит только конечный артефакт (собранные статические файлы), не требуя повторения тяжёлой PHP-стадии в лёгком образе nginx.

Источник: Рекомендуемая литература

Рекомендуемая литература

Официальная документация

Статьи и блоги

Книги

  • Gene Kim, Jez Humble, Patrick Debois, John Willis. The DevOps Handbook. 2nd ed., 2021. ISBN 978-1942788003.

Связанные материалы