Environment variablesProduction redeployBuild-time configNext.jsAgentic engineeringReliability

Build-time переменные окружения и продакшн-redeploy в агентной инженерии | Контракт запекания

Почему сохранённая настройка может не доехать до работающего приложения и контракт, который это лечит: в агентной инженерии продакшн-redeploy обязан запекать собственный .env.local слота — языки, ключи Stripe, любую build-time переменную — без протечки из процесса, запускающего сборку.

8 мин чтение
Build-time environment variables and production redeploy: an AI agent saves a value to the app slot’s .env.local and the slot-scoped build bakes it correctly on redeploy — languages, Stripe keys and custom variables surviving every rebuild.
Ты меняешь настройку, панель пишет «сохранено», сборка пишет «готово» — а приложение всё ещё показывает старое значение. Ни одной ошибки. Этот молчаливый разрыв и закрывает документ.

Fractera — это Agentic Engineering Infrastructure: безопасное self-hosted рабочее пространство, где ИИ-модели пишут и запускают твоё приложение на твоём собственном сервере. Этот гайд объясняет одно правило надёжности — как build-time конфигурация обязана переживать продакшн-redeploy — чтобы агент (или ты) мог добавить язык, платёжный ключ или любую кастомную переменную и быть уверенным, что она реально доедет до посетителей. Написано и для технического человека, и для ИИ-агента.

Два вида настроек: читать-сейчас против запечь-в-сборку

Приложение читает конфигурацию двумя очень разными способами, и их смешение — корень всей проблемы:

  • Runtime-значения — читаются заново на каждый запрос работающим сервером. Поменял файл, перезапустил — готово.
  • Build-time значения — «запекаются» в приложение в момент *сборки*. Каждая `NEXT_PUBLIC_*` вшивается в браузерный бандл, а всё, что читается при генерации страниц (набор языков, публичный ключ Stripe, фиче-флаг), фиксируется на `next build`. Правка файла позже не меняет ничего до пересборки.

Класс бага: сохранённое значение, которого приложение не видит

Когда это укусило впервые, добавление языка заставило кнопку переключения языков исчезнуть. Но набор языков никогда не был особым — он лишь первым из build-time переменных вскрыл изъян. Тот же механизм молча уронил бы ключ Stripe (мёртвая оплата), кастомный URL API или фиче-флаг (застрял выключенным). Один корень — много будущих жертв:

Сохраняешь в панели  ->  записано в .env.local приложения        (верно)
        |
        v
Запускается пересборка  ->  но сборка читает НЕ ТОТ файл env    (изъян)
        |
        v
Сборка завершается «успешно»  ->  твоё значение запечено как ПУСТОЕ  (молча)
        |
        v
Приложение едет: кнопка пропала / Stripe.js мёртв / флаг выключен

Почему так вышло: одна сборка протекает в другую

В рабочем пространстве два приложения: Admin-кокпит и твой App-слот. Когда ты меняешь настройку, Admin спавнит сборку слота. Загрузчик окружения, который использует Next, при первом запуске ставит маркер и затем отказывается перечитывать файлы настроек. Admin-процесс этот загрузчик уже выполнил (на своих файлах, где переменных твоего слота нет). Спавня сборку слота и передав ей всё своё окружение, он передал и маркер «уже загружено» — и дочерняя сборка полностью пропустила чтение собственного файла настроек слота, так что все объявленные там значения вернулись пустыми.

Лечение: контракт слот-скоупного запекания

Правило простое и общее: собственный файл настроек App-слота авторитетен для каждого ключа, который он объявляет, при каждом redeploy. Сборке, которая производит слот, выдаётся чистое, слот-скоупное окружение — чтобы ничто из процесса-триггера не могло перекрыть или подавить значения слота.

Окружение сборки слота (контракт):
  1. убрать маркер «уже загружено»     -> сборка свежо читает .env.local слота
  2. убрать каждый ключ, объявленный слотом -> наследованная копия не перекроет значение
  3. сохранить всё прочее (PATH, HOME, переменные, провиженные извне)

Итог: что слот объявил в своём .env.local, то сборка и запекает -- каждый раз.

Поскольку правило про *любой* объявленный ключ, оно общее по построению: языки, ключи Stripe и id товаров, кастомные URL БД или API, идентификаторы аналитики и фиче-флаги ведут себя одинаково. Чини класс один раз — и ни одно будущее приложение об это не споткнётся.

Что делает агент, когда фиче нужна build-time переменная

  1. Записать значение через правильный сеттер, никогда не вручную — для настроек приложения и языков использовать валидируемый сеттер (см. коннектор App Config MCP); для совсем новой переменной — добавить её в `.env.local` слота и задокументировать.
  2. Запустить пересборку — build-time значения вступают в силу только после `next build`. Один перезапуск перечитывает уже собранный бандл и не поможет.
  3. Честно назвать цену — пересборка это пара минут; это плата за build-time конфиг, а не регресс.
  4. Не тянуться к `force-dynamic` — сделать публичную страницу динамической ради «мгновенного отражения» значения ломает контракт static-first. Build-time значения меняются пересборкой; мгновенные правки текста — это on-demand ревалидация, другой механизм.

Как убедиться, что работает

Разрешение окружения наблюдаемо меньше чем за секунду, поэтому корректность доказуема ещё до настоящей сборки: при старом поведении ключи слота резолвятся в пустоту, при контракте — в значения слота. End-to-end: запущенный redeploy показывает ноль предупреждений «using default» в логе сборки и пререндерит все объявленные языки — а отданная дефолтная страница содержит кнопку.

Навык — в каждом агенте рабочего пространства

Этот контракт упакован и как самодостаточный навык агента — persist-env-var-with-rebuild — продублированный в каждый кодящий агент (Claude Code, Codex, Gemini CLI, Qwen Code, Kimi Code) и в оркестратор Hermes. Поэтому какой бы единственный агент ни крутил твой проект — даже один агент без оркестратора и без памяти — он знает: записать build-time переменную через правильный сеттер и запустить пересборку, которая запекает собственный `.env.local` слота. Способность никогда не зависит от наличия Hermes — она едет с каждым агентом.

Разверни рабочее пространство, где твои ИИ-агенты меняют реальную конфигурацию — и каждый redeploy запекает её корректно, на твоём собственном сервере.

Развернуть с ИИ

Понимание экономики продаж определяет выбор идеи. Есть много хороших идей, которые не проходят по деньгам. Сначала нужно научиться считать, а потом выбирать идею, на которой можно заработать.

Roma Armstrong photoRoma ArmstrongFounder at Fractera.ai

Частые вопросы

Почему значение, которое я сохранил, пропало из приложения после redeploy?
Потому что это build-time значение. Такие вещи, как NEXT_PUBLIC_* и набор языков, «запекаются» в приложение при сборке, а не читаются заново на каждый запрос. Если пересборка прочитала не тот файл окружения (или никакой), значение запекается как отсутствующее — а сборка всё равно сообщает «успех», поэтому отказ молчаливый. Лечение заставляет каждую пересборку читать собственный .env.local приложения.
Что считается build-time переменной?
Всё, что инлайнится на сборке: каждая NEXT_PUBLIC_* (она вшивается в браузерный бандл) и всё, что читается при генерации страниц — набор языков, публичный ключ Stripe, фиче-флаги, идентификаторы аналитики. Они вступают в силу только после пересборки; правка файла после неё ничего не меняет до следующего build.
Значит, build-time переменных стоит избегать?
Нет. Они корректны и быстры — значение едет внутри статического HTML, который работает с выключенным JavaScript. Просто менять их нужно пересборкой, и пересборка обязана прочитать правильный файл. Ровно это и гарантирует контракт запекания.
Будет ли эта проблема на свежем развёртывании?
Совершенно новый сервер собирается из чистого окружения и в первый день в порядке. Ловушка проявлялась только на более позднем пути владельца «сменил настройку → пересборка». С контрактом и первая сборка, и любая последующая смена ведут себя одинаково.
Спросите у ИИ