Индекс журналаDockup / заметка с места
Note / env-vars-not-reaching-container

Переменные окружения не попадают в контейнер

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

Вы задали переменную. В dashboard она отображается. Приложение сообщает, что она undefined. Переменные окружения, которые не работают в контейнере, — одна из самых распространённых проблем с конфигурацией при размещении приложений, и почти всегда причина кроется в одном из пяти конкретных факторов.

Ниже они перечислены в порядке, который позволяет быстрее всего найти проблему.

1. Время сборки и время выполнения — это разные среды

Эта причина встречается чаще, чем остальные четыре вместе взятые, и именно её сложнее всего интуитивно понять.

Переменные, заданные для сервиса, существуют, когда контейнер запускается. Всё, что делает Dockerfile, происходит раньше и в отдельной среде. Шаг RUN не может увидеть переменную времени выполнения, потому что в этот момент времени выполнения ещё нет.

# This is empty during build. Always.
RUN echo $DATABASE_URL

# This is available at runtime, because it is the running process reading it
CMD ["node", "server.js"]

Если значение действительно нужно во время сборки, его необходимо передать как build argument — это другой механизм с другими свойствами безопасности:

ARG BUILD_VERSION
RUN echo "Building $BUILD_VERSION"

Никогда не передавайте секрет таким способом. Build arguments сохраняются в истории слоёв образа. Любой, кто может скачать образ, сможет их прочитать.

2. Переменные frontend встраиваются в сборку, а не считываются

Если в production ваш frontend сообщает undefined, почти наверняка причина именно в этом.

У браузера нет окружения. Когда вы пишете import.meta.env.VITE_API_URL или process.env.NEXT_PUBLIC_API_URL, bundler подставляет строковое значение во время сборки. В браузере не выполняется никакой поиск — значение уже было скомпилировано в приложение.

Из этого следуют три последствия, о которых часто забывают:

  • Изменение переменной ничего не даст, пока вы не выполните повторную сборку. Старое значение уже находится в JavaScript-файле.
  • Префикс обязателен. Vite предоставляет только переменные с префиксом VITE_, а Next.js — только с префиксом NEXT_PUBLIC_. Переменная без нужного префикса намеренно не передаётся.
  • Всё, что так раскрывается, становится публичным. Значение находится в файле, который вы отдаёте любому пользователю. Никогда не помещайте секрет за префиксом NEXT_PUBLIC_, несмотря на то что может подсказывать название.

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

3. Кавычки

Значения со специальными символами могут искажаться, что приводит к непонятным ошибкам, а не к очевидным сообщениям о проблеме.

# The shell eats everything after #
dockup env set DB_PASS=p@ss#word my-project/my-api

# Quote it
dockup env set 'DB_PASS=p@ss#word' my-project/my-api

Проблемы вызывают следующие символы: # (комментарий), $ (подстановка), пробелы (разделение аргументов), ! (подстановка истории в интерактивном bash) и переводы строк — они появляются ровно в одном распространённом случае: при работе с приватными ключами.

Многострочные значения создают больше всего проблем. Если вставить PEM-ключ в однострочное поле, переводы строк будут удалены, а приложение выдаст ошибку разбора, в которой ничего не говорится о переводах строк. Закодируйте значение в Base64 и декодируйте его в приложении:

dockup env set "PRIVATE_KEY_B64=$(base64 -i key.pem)" my-project/my-api

4. Вы не перезапустили приложение

Процесс считывает переменные окружения при запуске. Изменение переменных влияет на следующий процесс, а не на тот, который уже работает.

Большинство платформ автоматически выполняют повторный deploy при изменении конфигурации, но делают это не все. Кроме того, частичное изменение — задать три переменные, выполнить deploy, а затем задать четвёртую — может оставить одну переменную без обновления.

dockup env list my-project/my-api --json   # what is configured
dockup restart my-project/my-api            # make the process re-read it

Проверка, которая сразу всё проясняет: прочитайте переменную внутри запущенного контейнера, а не в dashboard.

dockup exec "printenv | sort" my-project/my-api

Если переменная есть в этом выводе, а приложение по-прежнему сообщает undefined, проблема в коде. Если её нет в выводе, проблема в конфигурации. Одна эта команда делит область поиска пополам.

5. Неправильная область действия

Обычно переменные имеют область действия — сервис, окружение или проект. Переменная, заданная в production, не будет видна в preview-окружении. Переменная, заданная для другого сервиса в том же проекте, также не будет доступна.

Обычно именно это является причиной ситуации, когда один и тот же код работает в одном месте, но не работает в другом.

Порядок диагностики

# 1. Is it actually in the container's environment?
dockup exec "printenv | sort" my-project/my-api

# 2. Is it configured on the service you think it is?
dockup env list my-project/my-api --json

# 3. Is the running process older than the change?
dockup status my-project/my-api --json

Всегда начинайте с шага 1. Он превращает неоднозначную проблему в одну из двух однозначных.

Особенности работы с секретами

Независимо от платформы стоит выработать две привычки.

Помечайте секреты как секреты. В Dockup переменная с пометкой secret маскируется в списках и ответах API — dockup env list показывает ********, а не её значение. Это важнее, чем может показаться, потому что чаще всего учётные данные утекают не в результате атаки, а через скриншот, обращение в поддержку или строку в логе.

Не помещайте их в build arguments и bundle frontend. Оба варианта позволяют прочитать значение любому, кто получит соответствующий артефакт. Простое правило: если значение попало в файл, который вы распространяете, это больше не секрет.

Часто задаваемые вопросы

Почему моя переменная окружения имеет значение undefined во время сборки? Потому что сборка и время выполнения — это разные среды. Переменных времени выполнения не существует, пока собирается образ. Используйте build argument, если значение действительно нужно во время сборки, — но никогда не передавайте так секрет.

Почему мой frontend не видит переменную? Bundler подставляет значение во время сборки и предоставляет только переменные с нужными префиксами — VITE_, NEXT_PUBLIC_. Для изменения переменной требуется повторная сборка, а всё, что раскрывается таким способом, доступно для чтения публично.

Нужно ли перезапускать приложение после изменения переменной? Да. Работающий процесс уже считал своё окружение. Большинство платформ автоматически выполняют повторный deploy после изменения; проверяйте результат с помощью printenv внутри контейнера, а не полагайтесь только на dashboard.

Как передать многострочное значение, например приватный ключ? Закодируйте его в Base64, задайте полученную строку и декодируйте её в приложении. Однострочные поля для переменных окружения удаляют переводы строк и приводят к ошибкам разбора, в которых о переводах строк не упоминается.