Навигация

Переменные окружения и конфигурация

Приложение должно деплоиться на разные окружения без правки кода. Для этого конфигурация выносится в переменные окружения: адреса API, флаги функций, ключи. Разберём, как устроены build-time и runtime-переменные, .env файлы, хранение секретов и валидация конфигурации.

Содержание

  1. 12-factor: конфигурация вне кода
  2. Build-time vs runtime
  3. .env файлы
  4. Порядок загрузки
  5. Что можно в бандл, а что нельзя
  6. Секреты и где их хранить
  7. .env.example и документация
  8. Валидация конфигурации через zod
  9. Runtime-конфигурация в SPA
  10. Переменные в CI/CD и платформах деплоя
  11. Лучшие практики
  12. Антипаттерны

12-factor: конфигурация вне кода

Двенадцатый фактор — конфигурация — гласит: всё, что меняется между окружениями (dev, staging, production), должно выноситься из кода в переменные окружения.

Код не содержит адресов, ключей и флагов:

// ❌ Плохо: конфигурация зашита в код
const API_URL = 'https://api.production.example.com';

// ✅ Хорошо: значение приходит из окружения
const API_URL = import.meta.env.VITE_API_URL;

Преимущества:

  • Один и тот же билд деплоится везде.
  • Секреты не попадают в репозиторий.
  • Окружение переключается без переписывания кода.

Build-time vs runtime

Build-time переменные

Подставляются в бандл во время сборки:

VITE_API_URL=https://api.example.com npm run build
const API_URL = import.meta.env.VITE_API_URL; // уже вшито в JS

Встраиваются навсегда — изменить можно только пересборкой. Это касается VITE_* (Vite), NEXT_PUBLIC_* (Next.js), NUXT_PUBLIC_* (Nuxt), REACT_APP_* (CRA).

Runtime переменные

Задаются при запуске приложения/контейнера:

docker run -e API_URL=https://api.example.com my-app
const API_URL = process.env.API_URL; // читается в runtime

Применимы к SSR-серверу, API-роутам, бэкенду. Для чистого SPA runtime-переменные требуют подстановки на сервере (см. раздел про runtime-конфигурацию).

Правило: build-time — для несекретного и известного на сборке; runtime — для того, что меняется между окружениями без пересборки.


.env файлы

Vite, Next.js и Nuxt автоматически загружают .env файлы из корня проекта.

ФайлНазначение
.envОбщие значения для всех окружений.
.env.localЛокальные переопределения, не коммитится.
.env.developmentЗначения для dev-режима.
.env.productionЗначения для production-сборки.
.env.[mode].localЛокальные для конкретного режима, не коммитится.

Пример:

# .env
VITE_APP_NAME=My App

# .env.development
VITE_API_URL=http://localhost:3000

# .env.production
VITE_API_URL=https://api.example.com

mode для Vite: development / production; для Next.js: development / production / test.


Порядок загрузки

Приоритет (большее переопределяет меньшее):

.env.local
.env.[mode].local
.env.[mode]
.env

Важные нюансы:

  • .env.local не коммитится и должен быть в .gitignore.
  • Для production .env.local игнорируется вовсе (Vite) — защита от случайных локальных значений в проде.
  • Конкретные значения (.env.production) переопределяют общие (.env).
  • Итоговое значение зависит от mode, в котором запущена сборка.

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


Что можно в бандл, а что нельзя

В клиентский бандл попадает только то, что явно помечено публичным префиксом:

import.meta.env.VITE_API_URL;   // попадёт в бандл
import.meta.env.VITE_APP_NAME;  // попадёт
import.meta.env.SECRET_KEY;     // НЕ попадёт — нет префикса VITE_

Можно: адреса API, идентификаторы аналитики, feature-флаги, название приложения — всё несекретное.

Нельзя: ключи API, пароли, токены, секреты БД. Любой пользователь откроет DevTools и увидит их в собранном JS.

Секреты в клиентский бандл не кладут никогда. Для работы с платёжными API на клиенте используются прокси-эндпоинты и publishable-ключи, а не серверные секреты.


Секреты и где их хранить

Секреты — переменные, которые дают доступ к ресурсам: API-ключи, токены, пароли.

Хранилища:

  • Переменные окружения платформы — Vercel, Netlify, Railway: зашифрованы, доступны при деплое.
  • Secrets CI/CD — GitHub Actions secrets, GitLab CI/CD variables — доступны в пайплайне.
  • Vault (HashiCorp) — централизованное хранилище секретов для инфраструктуры.
  • Docker secrets — для контейнеров.

В репозитории секретов нет. Только .env.example с заглушками.

# .gitignore — обязательные строки
.env
.env.local
.env.*.local
.env.production

Если секрет уже закоммичен — он в истории git, удаление файла не помогает. Секрет нужно отозвать и перевыпустить.


.env.example и документация

.env.example — коммитимый шаблон, документирующий все переменные проекта:

# Адрес API. Для dev — http://localhost:3000
VITE_API_URL=

# Включить/выключить фичу поиска
VITE_FEATURE_SEARCH=true

# Google Analytics Measurement ID (не секрет)
VITE_GA_ID=G-XXXXXXXXXX

Почему важен:

  • Новый разработчик копирует .env.example → .env.local и заполняет значения.
  • Документирует, какие переменные существуют и что означают.
  • Становится основой для валидации конфигурации.

Валидация конфигурации через zod

Ошибка конфигурации — «пустой бандл с белым экраном» из-за одной отсутствующей переменной. Валидация на старте решает это: приложение падает с понятным сообщением, а не молча ломается в runtime.

Для Next.js — next/env:

import { z } from 'zod';

const envSchema = z.object({
  API_URL: z.string().url(),
  GA_ID: z.string().optional(),
  FEATURE_SEARCH: z.string().transform((v) => v === 'true'),
});

// next/env
export const env = createEnv({
  server: {
    DATABASE_URL: z.string().min(1),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  },
});

Универсальный инструмент — @t3-oss/env-core:

import { createEnv } from '@t3-oss/env-core';
import { z } from 'zod';

export const env = createEnv({
  server: { DATABASE_URL: z.string().min(1) },
  clientPrefix: 'VITE_',
  client: { VITE_API_URL: z.string().url() },
  runtimeEnv: import.meta.env,
});

Плюсы: типизация env.* в IDE, отказ при невалидной конфигурации, явное разделение серверных и клиентских переменных.


Runtime-конфигурация в SPA

Статический SPA нельзя «перенастроить» после сборки — но можно подставить конфигурацию при отдаче HTML.

Вариант 1: подстановка на сервере

Сервер (nginx/Node) подставляет значения в index.html при каждом ответе:

<script>
  window.env = {
    API_URL: "https://api.example.com",
    FEATURE_SEARCH: "true",
  };
</script>
// приложение читает конфигурацию при старте
const apiUrl = window.env.API_URL;

Бандл собирается один раз, а значения задаются окружением сервера — тот же паттерн, что runtime-переменные в Docker.

Вариант 2: запрос к /api/config

Для SSR/гибридных приложений:

// сервер отдаёт конфигурацию отдельным эндпоинтом
GET /api/config → { API_URL, FEATURE_SEARCH }

// клиент
const config = await fetch('/api/config').then((r) => r.json());

Подходит, когда конфигурация объёмная или секретная (читается на сервере, а клиент получает только публичную часть).


Переменные в CI/CD и платформах деплоя

GitHub Actions

Секреты задаются в настройках репозитория и доступны в пайплайне:

- name: Build
  run: npm run build
  env:
    NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}
    SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

Vercel / Netlify

Переменные задаются на уровне проекта и окружения (Production/Preview/Development), инжектятся при сборке. Preview и production получают свои значения автоматически.

# CLI
vercel env add NEXT_PUBLIC_API_URL production
vercel env add NEXT_PUBLIC_API_URL preview

Docker

Runtime-переменные через -e или compose environment (см. статью про Docker).


Лучшие практики

1. Всё конфигурируемое — в переменных окружения

Никаких захардкоженных URL и ключей в коде.

2. Разделяй build-time и runtime

Build-time — для того, что известно на сборке; runtime — для различий между окружениями.

3. Секреты — только в managed-хранилищах

Vercel/Netlify env, CI secrets, Vault. Не в бандл, не в репозиторий.

4. Коммить .env.example

Документируй переменные и их назначение.

5. Валидируй конфигурацию на старте

zod + next/env/@t3-oss/env — падай с понятной ошибкой, а не с белым экраном.

6. .env* в .gitignore

.env.local, .env.production, .env.*.local — никогда в git.

7. Отзывай утёкшие секреты

Утечка = перевыпуск, удаление файла из git не спасает.


Антипаттерны

1. Секрет в клиентском бандле

// ❌ Плохо: ключ увидят в DevTools
const stripeKey = import.meta.env.VITE_STRIPE_SECRET_KEY;

2. Захардкоженный URL

// ❌ Плохо: staging и prod — разный код
const API = 'https://api.production.example.com';

3. Отсутствие .gitignore для .env

# ❌ Плохо: .env с секретами уходит в репозиторий

4. Нет .env.example

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

5. Нет валидации конфигурации

// ❌ Плохо: undefined в runtime вместо ошибки на старте
const api = env.API_URL; // undefined, если переменной нет

6. Секрет в логи

// ❌ Плохо: ключ утекает в логи CI/CD
console.log('token:', process.env.API_TOKEN);

Ключевые тезисы для интервью

  • 12-factor: конфигурация хранится в переменных окружения, а не в коде.
  • Build-time переменные (VITE_*, NEXT_PUBLIC_*) вшиваются в бандл на сборке.
  • Runtime-переменные задаются при запуске и не требуют пересборки.
  • В бандл попадают только публичные префиксы; секреты — никогда.
  • Порядок приоритета: .env.local > .env.[mode].local > .env.[mode] > .env.
  • .env.local не коммитится; .env.example — коммитится как документация.
  • Секреты хранятся в платформах, CI-secrets, Vault — не в репозитории.
  • Валидация конфигурации через zod (next/env, @t3-oss/env) падает на старте с понятной ошибкой.
  • Runtime-конфигурация SPA — через window.env или /api/config.
  • Утёкший секрет нельзя «удалить из git» — его перевыпускают.

Заключение

Конфигурация через переменные окружения — фундамент надёжных деплоев: один код, много окружений, никаких секретов в репозитории. Ключевые практики: разделение build-time/runtime, .env.example, валидация через zod и управляемые хранилища секретов.

Ключевое для Middle+ разработчика:

  • Понимать, что и когда попадает в клиентский бандл.
  • Уметь настраивать переменные для dev/staging/production.
  • Правильно работать с секретами в CI/CD и на платформах деплоя.
  • Валидировать конфигурацию на старте и документировать через .env.example.

Полезные ссылки