Переменные окружения не попадают в контейнер
Переменные окружения, которые не работают в контейнере, обычно не передаются по одной из пяти причин: время сборки вместо времени выполнения, сборка 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, задайте полученную строку и декодируйте её в приложении. Однострочные поля для переменных окружения удаляют переводы строк и приводят к ошибкам разбора, в которых о переводах строк не упоминается.
