Практичная схема конфигурации dev staging prod для веб, бэкенда и мобильного приложения: где хранить URL, ключи и фичи, чтобы не хардкодить.

development (dev), staging и production (prod) - это одно и то же приложение в разных условиях. Код часто совпадает почти полностью, но настройки обязаны отличаться. Иначе вы проверяете одно, а пользователям уезжает другое.
Dev нужен для быстрой разработки: частые перезапуски, тестовые данные, подробные логи. Staging - репетиция продакшена: максимально похоже на prod, но безопасно, чтобы прогонять релизы, миграции и интеграции. Prod - среда для реальных пользователей, поэтому там минимум лишнего и максимум контроля.
Обычно различаются: адреса внешних сервисов и API, ключи и токены (платежи, push, SMTP), feature flags, аналитика, уровень логирования и отладочные экраны.
Хардкод ломает все сразу. Один случайный коммит с боевым ключом в мобильное приложение - и его уже не отозвать у пользователей. Один забытый URL в вебе - и staging начинает писать данные в продовую базу. Еще хуже, когда фича включена прямо в коде: тесты проходят, а релиз внезапно ведет себя иначе.
Хорошая схема конфигурации делает поведение предсказуемым: вы меняете окружение, и меняются только настройки, а не логика. Она должна быть повторяемой (сборки одинаковые), понятной команде и безопасной по умолчанию.
Практический ориентир простой: разработчик запускает dev локально, QA проверяет staging, а релизный пайплайн собирает prod без ручных правок. Если вы собираете проект в TakProsto (takprosto.ai), удобно держать значения по окружениям через переменные окружения и заранее планировать, какие параметры где задаются, чтобы не хранить важное в исходниках.
Цель одна: ни один URL, ключ или переключатель не должен быть захардкожен в коде. Тогда смена окружения - это подстановка настроек, а не правка файлов и нервные релизы.
Первое правило - отделяйте публичные настройки от секретов. Все, что попадает в клиент (браузер или мобильное приложение), считайте публичным: это можно извлечь из сборки и прочитать.
На клиенте допустимы: API_BASE_URL, имя окружения (APP_ENV), включенность фичи (FEATURE_NEW_UI), публичный ключ аналитики.
На клиенте недопустимы: токены доступа, приватные ключи, пароли к БД, ключи платежей и любые сервисные секреты.
Второе правило - выбирайте источник конфигурации по типу данных. Простые параметры удобно задавать через переменные окружения или локальные конфиг-файлы. Секреты должны приходить из секрет-хранилища или защищенных переменных CI/CD. Переключатели, которые вы хотите менять без релиза, живут в удаленной конфигурации, но она все равно не должна раздавать секреты.
Третье правило - одинаковые имена везде. Если у вас есть API_BASE_URL и SENTRY_DSN, держите эти названия одинаковыми в web, backend и mobile. Это резко снижает путаницу.
Четвертое правило - различайте настройки сборки и запуска. Сборка - то, что запекается в клиент (например, базовый URL для web/mobile). Запуск - то, что читает сервер при старте (например, DATABASE_URL, JWT_SECRET). Если значение должно меняться без пересборки клиента, оно не должно быть частью build-конфига.
У web, backend и mobile должен быть один и тот же набор настроек и одни и те же названия. Тогда схема не превращается в набор исключений.
Начните со словаря ключей и держите его единым для всех платформ. Не так важно, где лежат значения (env, файлы, секрет-хранилище). Важны названия и смысл. Хорошее правило: дефолты допустимы для разработки, но в продакшене все критичное должно быть задано явно, а сборка или запуск должны падать, если чего-то не хватает.
Чтобы не смешивать все в одну кучу, группируйте параметры по доменам: network (URL, таймауты), auth (issuer, redirect), features (флаги), logging (уровни, сбор ошибок), payments (провайдер, тестовый режим).
Документировать конфиг удобно одной таблицей и короткими примерами:
| Ключ | Домен | Пример (dev) | Пример (prod) | Обязателен в prod |
|---|---|---|---|---|
| APP_ENV | core | dev | prod | да |
| API_BASE_URL | network | http://localhost:8080 | https://api.example.ru | да |
| FEATURE_NEW_CHECKOUT | features | true | false | нет |
| LOG_LEVEL | logging | debug | warn | да |
Практика, которая спасает нервы: один и тот же ключ должен значить одно и то же везде. Если в Go это API_BASE_URL, то и в React, и во Flutter он называется так же, а не BASE_URL и server.
В React важно помнить: большинство переменных окружения попадает в бандл на этапе сборки. Если вы хотите поменять API_BASE_URL после сборки, обычно ничего не выйдет, пока вы не пересоберете фронтенд.
Рабочая схема: читайте настройки из одного места, например из config.ts. Так по коду не расползаются магические строки, и URL не приходится править руками.
// src/config.ts
const env = (import.meta as any).env || (process as any).env;
export const config = {
envName: env.VITE_ENV_NAME || env.REACT_APP_ENV_NAME || "dev",
apiBaseUrl: env.VITE_API_BASE_URL || env.REACT_APP_API_BASE_URL || "http://localhost:8080",
enableNewCheckout: (env.VITE_FEATURE_NEW_CHECKOUT || env.REACT_APP_FEATURE_NEW_CHECKOUT) === "true",
};
Для окружений заведите отдельные файлы, которые подхватывает сборка или CI. Типовой набор: .env.development, .env.staging, .env.production, плюс переменные в CI (чтобы не хранить значения в репозитории).
Чтобы быстрее ловить ошибки, сделайте небольшую debug-страницу, которая показывает только безопасные настройки: имя окружения, базовый URL API, включенные фичи. Никогда не выводите токены, секреты и ключи.
Отдельная ловушка - кэш и пересборка. При service worker или агрессивном кешировании браузер может продолжать отдавать старый бандл со старыми переменными. Симптом простой: API_BASE_URL поменяли, а запросы все равно идут на прежний адрес. Обычно помогает пересборка, новая версия ассетов и корректная инвалидизация кэша.
Для Go-сервиса надежный подход простой: вся конфигурация читается один раз при старте, затем сервис работает с уже собранным Config. Так вы не держите URL, ключи и пароли в коде, а поведение в каждом окружении предсказуемо.
Для локальной разработки удобно иметь файл (например, .env.local или config.local.yaml), который подхватывается только на вашей машине. В staging и prod лучше опираться на переменные окружения, которые задаются в системе деплоя. В репозиторий файл не коммитьте, максимум шаблон.
Секреты (пароль БД, токены, ключи шифрования) должны приходить только из секрет-хранилища или ENV. Даже если несекретные настройки лежат в файле, секреты держите отдельно.
Нужна жесткая валидация: если нет обязательных значений, сервис не должен стартовать. Это дешевле, чем ловить ошибки в рантайме.
package config
import (
"log"
"os"
)
type Config struct {
Env string
HTTPPort string
DatabaseURL string
JWTSecret string
FeatureX bool
}
func mustGet(key string) string {
v := os.Getenv(key)
if v == "" {
log.Fatalf("missing required env: %s", key)
}
return v
}
func Load() Config {
cfg := Config{
Env: mustGet("APP_ENV"),
HTTPPort: mustGet("HTTP_PORT"),
DatabaseURL: mustGet("DATABASE_URL"),
JWTSecret: mustGet("JWT_SECRET"),
}
log.Printf("config: env=%s port=%s db=%s", cfg.Env, cfg.HTTPPort, "[set]")
return cfg
}
В логах печатайте только безопасные поля. Для секретов используйте маску вроде [set] или выводите длину значения. Так вы упрощаете дебаг и не роняете токены в логи.
В мобильном клиенте хаос начинается, когда один и тот же билд пытаются подкрутить под разные окружения вручную. Надежнее сделать разные сборки: dev, staging и prod. Тогда всегда понятно, куда идет трафик, какие фичи включены и почему у тестировщика вдруг не работает оплата.
Практичная схема: на Android завести productFlavors (dev/staging/prod), на iOS - Schemes (Dev/Staging/Prod), а в Flutter - три входных файла main_dev.dart, main_staging.dart, main_prod.dart.
В каждом main_*.dart передавайте только публичные настройки и идентификатор окружения, например через --dart-define (удобно для CI) или через фабрику конфигурации.
Обычно в конфиг клиента кладут: envName, apiBaseUrl, публичные идентификаторы аналитики (если они не секрет), дефолтные feature flags.
Если ключ дает доступ к данным или деньгам, его не должно быть в приложении вообще. Мобильное приложение легко декомпилировать, и скрытый ключ быстро станет публичным. Правильный путь: хранить секреты на бэкенде, а клиенту отдавать только короткоживущие токены или уже готовый результат (например, подпись, платежную сессию, прокси-запрос).
Тестировщикам часто нужно быстро переключиться между dev и staging. Делайте это только в dev/staging сборках: скрытый экран по долгому нажатию на версию в настройках. На экране - выбор из заранее зашитого списка apiBaseUrl (без ввода произвольного URL) и кнопка «Перезапустить с новым окружением». Выбор храните в SharedPreferences.
Feature flags (фиче-флаги) - переключатели, которые меняют поведение приложения без нового релиза. Они помогают выкатывать изменения по шагам, включать фичу части пользователей и быстро выключать проблемную часть, если что-то пошло не так.
Флаги хорошо ложатся на разделение окружений, если заранее договориться, где они живут и кто ими управляет.
На практике обычно комбинируют несколько источников: локально в dev (env или файл) для экспериментов, а на staging и prod - через бэкенд. Например, бэкенд отдает /config, а сами флаги лежат в БД и кешируются.
Флаг не должен решать вопросы безопасности. Он может скрыть кнопку или поменять сценарий, но не должен открывать доступ к данным. Права и проверки всегда на бэкенде.
Чтобы флаги не превратились в мусор, договоритесь о дисциплине: понятное имя (например, new_checkout_ui), значение по умолчанию, владелец, срок удаления, и минимальное логирование, чтобы было видно, когда флаг включен.
Пример: добавили новый экран оплаты. В dev флаг включен всегда. На staging включаете для тестировщиков. На prod включаете для 5% пользователей, смотрите ошибки и метрики, затем расширяете. Если пошли сбои, выключаете флаг, и продукт продолжает работать без срочного релиза.
Секреты - это не просто строчки в .env. Это данные, утечка которых дает доступ к деньгам, аккаунтам или базе. В нормальной схеме секреты всегда отделены от обычных настроек и никогда не попадают в репозиторий.
Типовые секреты: JWT_SECRET или ключи подписи, доступ к базе (пароль, DSN), ключи платежных систем, OAuth client secret, API keys для почты, SMS, пушей, аналитики.
Дальше важнее не «где хранить», а кто имеет доступ. Разделяйте права: разработчику обычно достаточно dev, тестировщикам - staging, а prod должен быть доступен только тем, кто релизит и поддерживает. В CI держите секреты в защищенном хранилище переменных и выдавайте только нужным джобам, чтобы случайный вывод окружения не раскрыл ключи.
Ротация ключей должна проходить без простоя и без ломания клиентов. Рабочая схема - поддерживать два ключа одновременно на период перехода:
Самая частая ошибка - секреты в логах, ошибках и аналитике. Не логируйте заголовки Authorization, параметры запросов с токенами и полные конфиги.
Разработчик добавляет интеграцию, например отправку событий во внешний сервис аналитики. Чтобы быстрее проверить, он временно прописывает тестовый URL прямо в коде. Локально все работает, задача закрыта.
Проблема всплывает на staging. Тестировщик включает фичу и видит странность: события уходят, но не появляются в тестовом кабинете, а часть запросов падает. В логах бэкенда видно, что приложение ходит на test-api.example, хотя на staging должен быть staging-api.
Поиск идет по цепочке: сетевые логи фронта, конфиг бэкенда, затем grep по репозиторию. И часто источник один: хардкод в React, дефолт в Go-конфиге или константа во Flutter.
Когда конфигурация разнесена по окружениям, фикса занимает минуты. Добавляете явный параметр, например ANALYTICS_BASE_URL, задаете значения для каждого окружения и ловите ошибку на старте, если переменной нет.
Рабочий порядок действий:
Закрепите это в код-ревью: любые URL, ключи и переключатели берутся только из конфигурации, а не из констант.
Самый частый источник странных багов между dev, staging и prod - когда путают настройки сборки и настройки запуска. Например, фронт собрали с одним API URL, а потом пытаются перекинуть его на другой через переменные окружения на сервере. В итоге часть значений уже вшита в бандл, а часть читается при старте, и в проде получается каша.
Вторая ловушка - забыть выключить dev-инструменты. Подробные логи, отладочные экраны, тестовые меню и «показать ошибку целиком» полезны в разработке, но в prod они мешают и иногда раскрывают детали системы.
Отдельная боль - мобильные сборки. Если зашить URL или тестовые ключи прямо в код, они уедут к пользователям навсегда. Типовой сценарий: Flutter-приложение случайно указывает на staging, а пользователи видят пустую базу и думают, что сервис сломан.
Частые ошибки:
Практический пример: вы включили новую оплату только на staging через feature flag, а в prod забыли. Потом делаете rollback, но флаг остается включенным, и старый код падает на неизвестной настройке. Выход простой: версионируйте конфиги вместе с релизом и храните секреты раздельно по окружениям.
Перед релизом полезно сделать короткую проверку: у каждого окружения должны быть свои значения, секреты не должны утечь, а ошибки конфига должны ловиться до деплоя.
Убедитесь, что для dev, staging и prod заданы разные значения всех обязательных параметров: base URL для API, адреса веба и бэка, аналитика, пуши, хранилища, домены, фича-флаги по умолчанию. Частая проблема: staging случайно указывает на prod базу.
Проверьте, что секреты и ключи API не попадают в репозиторий и не оказываются в клиенте (web и mobile). Любая сборка, которую можно скачать, считается публичной.
Перед стартом приложения должна быть валидация конфига: если не хватает переменной окружения или формат неверный, сервис не запускается. В логах выводите только безопасное: имя окружения, включенные фичи, адреса сервисов, но не секреты.
Для фиче-флагов держите минимум дисциплины: владелец, цель, значения для dev/staging/prod, срок жизни, план отката, способ ручной проверки.
Чтобы конфигурация не расползалась по чатам, файлам и временным правкам, выберите одно понятное правило: где живут значения и как они попадают в web, backend и mobile при сборке и запуске.
В TakProsto обычно удобно держать окружения как отдельные сборки или отдельные проекты (зависит от доменов, баз и доступов). Смысл один: то же приложение, но разные значения параметров и секретов для dev, staging и prod.
Planning mode помогает не забыть важное: выпишите список параметров и места, где они используются (base URL для API, ключи сторонних сервисов, флаги, имя окружения, настройки логов). Это снижает шанс, что кто-то «на минутку» вставит URL прямо в код.
Практичный порядок действий:
Пример: вы включаете новую оплату. В dev фича включена и ходит на тестовый провайдер, в staging включена только для команды, в prod включена для всех. Меняются лишь значения конфигурации, а не код. Это и есть цель: один код, три окружения, ноль хардкода. "}
Разделяйте конфигурацию по окружениям: код один, а значения разные.
Минимум:
dev — быстрые правки, тестовые данные, подробные логиstaging — максимально похоже на prod, но безопасно для проверок релизаprod — реальные пользователи, минимум лишнего, строгий контрольStaging должен быть «репетицией» prod:
Главное правило: staging никогда не должен писать в prod-базу и использовать боевые ключи.
Железное правило: не хардкодить.
Вместо этого:
config, который читает всё из окруженияЕсли параметр обязателен — падайте на старте/сборке, а не «как-нибудь потом».
Считайте публичным всё, что попадает в сборку web/mobile.
Можно в клиенте:
API_BASE_URL, APP_ENVFEATURE_* флагиBuild-time — запекается в сборку и не меняется без пересборки клиента.
Run-time — читается при запуске сервера и может меняться без пересборки.
Практика:
Сделайте единый словарь ключей и используйте одинаковые имена везде.
Пример минимального набора:
APP_ENVAPI_BASE_URLСоберите конфиг в одном месте (например, config.ts) и не раскидывайте process.env по проекту.
Полезные практики:
.env.development/.env.staging/.env.productionЗагружайте конфиг один раз при старте и валидируйте обязательные переменные.
Практика:
Делайте отдельные сборки (flavors/schemes) для dev/staging/prod.
Рекомендации:
main_dev.dart, main_staging.dart, main_prod.dartЗаведите простые правила эксплуатации:
prod храните значения флагов централизованно (например, отдавайте /config с бэкенда)И не забывайте про совместимость: откат к старому коду при включенном новом флаге — частая причина падений.
DATABASE_URLAPI_BASE_URLDATABASE_URL, JWT_SECRET всегда run-timeЕсли хотите менять значение «без релиза клиента», не делайте его частью build-конфига.
LOG_LEVELSENTRY_DSN (если используете)FEATURE_*И закрепите смысл: один и тот же ключ означает одно и то же в React, Go и Flutter.
mustGet() для обязательных параметровDATABASE_URL/JWT_SECRETenv, port, а секреты маскируйте ([set])Это дешевле, чем ловить ошибки уже под нагрузкой.
--dart-defineДля тестировщиков можно добавить переключение окружения только в dev/staging сборках и только из фиксированного списка URL.