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

Зачем разделять dev, staging и prod
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): практичный способ подключать настройки
В 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): конфигурация при запуске и безопасные секреты
Для 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] или выводите длину значения. Так вы упрощаете дебаг и не роняете токены в логи.
Мобильное приложение (Flutter): окружения без хаоса
В мобильном клиенте хаос начинается, когда один и тот же билд пытаются подкрутить под разные окружения вручную. Надежнее сделать разные сборки: dev, staging и prod. Тогда всегда понятно, куда идет трафик, какие фичи включены и почему у тестировщика вдруг не работает оплата.
Разделите сборки через flavors и отдельные entrypoints
Практичная схема: на 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
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 держите секреты в защищенном хранилище переменных и выдавайте только нужным джобам, чтобы случайный вывод окружения не раскрыл ключи.
Ротация ключей должна проходить без простоя и без ломания клиентов. Рабочая схема - поддерживать два ключа одновременно на период перехода:
- добавьте новый ключ параллельно старому
- начните подписывать новые токены новым ключом
- принимайте токены, подписанные обоими ключами
- дождитесь окончания TTL старых токенов
- удалите старый ключ и зафиксируйте дату ротации
Самая частая ошибка - секреты в логах, ошибках и аналитике. Не логируйте заголовки Authorization, параметры запросов с токенами и полные конфиги.
Пример из жизни: одна фича, три окружения
Разработчик добавляет интеграцию, например отправку событий во внешний сервис аналитики. Чтобы быстрее проверить, он временно прописывает тестовый URL прямо в коде. Локально все работает, задача закрыта.
Проблема всплывает на staging. Тестировщик включает фичу и видит странность: события уходят, но не появляются в тестовом кабинете, а часть запросов падает. В логах бэкенда видно, что приложение ходит на test-api.example, хотя на staging должен быть staging-api.
Поиск идет по цепочке: сетевые логи фронта, конфиг бэкенда, затем grep по репозиторию. И часто источник один: хардкод в React, дефолт в Go-конфиге или константа во Flutter.
Когда конфигурация разнесена по окружениям, фикса занимает минуты. Добавляете явный параметр, например ANALYTICS_BASE_URL, задаете значения для каждого окружения и ловите ошибку на старте, если переменной нет.
Рабочий порядок действий:
- добавьте параметр в единый список настроек (с описанием и примером)
- задайте значения для dev, staging и prod (в env-файлах или в секретах CI/CD)
- включите валидацию: если переменная не задана или в prod похожа на тестовую, сборка падает
- прогоните staging-сценарий и проверьте, что логи печатают только безопасные поля
Закрепите это в код-ревью: любые URL, ключи и переключатели берутся только из конфигурации, а не из констант.
Частые ошибки и ловушки
Самый частый источник странных багов между dev, staging и prod - когда путают настройки сборки и настройки запуска. Например, фронт собрали с одним API URL, а потом пытаются перекинуть его на другой через переменные окружения на сервере. В итоге часть значений уже вшита в бандл, а часть читается при старте, и в проде получается каша.
Вторая ловушка - забыть выключить dev-инструменты. Подробные логи, отладочные экраны, тестовые меню и «показать ошибку целиком» полезны в разработке, но в prod они мешают и иногда раскрывают детали системы.
Отдельная боль - мобильные сборки. Если зашить URL или тестовые ключи прямо в код, они уедут к пользователям навсегда. Типовой сценарий: Flutter-приложение случайно указывает на staging, а пользователи видят пустую базу и думают, что сервис сломан.
Частые ошибки:
- один и тот же секрет используется в dev, staging и prod
- в коде остаются захардкоженные URL или токены «временно»
- логирование и дебаг включены в prod по умолчанию
- конфиг меняют вручную в одном сервисе и забывают в другом
- после отката (rollback) не возвращают конфигурацию к версии релиза
Практический пример: вы включили новую оплату только на staging через feature flag, а в prod забыли. Потом делаете rollback, но флаг остается включенным, и старый код падает на неизвестной настройке. Выход простой: версионируйте конфиги вместе с релизом и храните секреты раздельно по окружениям.
Быстрый чеклист перед сборкой и релизом
Перед релизом полезно сделать короткую проверку: у каждого окружения должны быть свои значения, секреты не должны утечь, а ошибки конфига должны ловиться до деплоя.
1) Проверка значений по окружениям
Убедитесь, что для dev, staging и prod заданы разные значения всех обязательных параметров: base URL для API, адреса веба и бэка, аналитика, пуши, хранилища, домены, фича-флаги по умолчанию. Частая проблема: staging случайно указывает на prod базу.
2) Секреты и видимость
Проверьте, что секреты и ключи API не попадают в репозиторий и не оказываются в клиенте (web и mobile). Любая сборка, которую можно скачать, считается публичной.
3) Валидация и логи
Перед стартом приложения должна быть валидация конфига: если не хватает переменной окружения или формат неверный, сервис не запускается. В логах выводите только безопасное: имя окружения, включенные фичи, адреса сервисов, но не секреты.
4) Флаги и контроль изменений
Для фиче-флагов держите минимум дисциплины: владелец, цель, значения для dev/staging/prod, срок жизни, план отката, способ ручной проверки.
Следующие шаги: как это собрать в TakProsto
Чтобы конфигурация не расползалась по чатам, файлам и временным правкам, выберите одно понятное правило: где живут значения и как они попадают в web, backend и mobile при сборке и запуске.
В TakProsto обычно удобно держать окружения как отдельные сборки или отдельные проекты (зависит от доменов, баз и доступов). Смысл один: то же приложение, но разные значения параметров и секретов для dev, staging и prod.
Planning mode помогает не забыть важное: выпишите список параметров и места, где они используются (base URL для API, ключи сторонних сервисов, флаги, имя окружения, настройки логов). Это снижает шанс, что кто-то «на минутку» вставит URL прямо в код.
Практичный порядок действий:
- составьте словарь параметров: имя, тип (строка, bool), где используется (React, Go, Flutter), кто меняет
- создайте dev, staging и prod и задайте значения для каждого окружения отдельно
- секреты храните отдельно от обычных настроек и выдавайте доступ только тем, кому нужно
- делайте релиз вместе с версией конфигурации: перед изменениями создавайте снимок, чтобы можно было быстро откатиться
- если планируете экспорт исходников, проверьте, что параметры остаются параметрами и не превращаются в захардкоженные строки
Пример: вы включаете новую оплату. В dev фича включена и ходит на тестовый провайдер, в staging включена только для команды, в prod включена для всех. Меняются лишь значения конфигурации, а не код. Это и есть цель: один код, три окружения, ноль хардкода. "}
FAQ
Зачем вообще нужны dev, staging и prod, если код почти одинаковый?
Разделяйте конфигурацию по окружениям: код один, а значения разные.
Минимум:
dev— быстрые правки, тестовые данные, подробные логиstaging— максимально похоже наprod, но безопасно для проверок релизаprod— реальные пользователи, минимум лишнего, строгий контроль
Чем staging должен отличаться от prod на практике?
Staging должен быть «репетицией» prod:
- те же версии сервисов и зависимости
- похожая инфраструктура (БД, кеш, очереди), но отдельные ресурсы
- те же миграции и пайплайн релиза
Главное правило: staging никогда не должен писать в prod-базу и использовать боевые ключи.
Как не допустить хардкода URL и ключей в коде?
Железное правило: не хардкодить.
Вместо этого:
- значения (URL, флаги) — через переменные окружения/конфиги
- секреты — только через защищенные переменные CI/CD или секрет-хранилище
- в коде — один модуль
config, который читает всё из окружения
Если параметр обязателен — падайте на старте/сборке, а не «как-нибудь потом».
Какие настройки можно хранить в клиенте (web/mobile), а какие нельзя?
Считайте публичным всё, что попадает в сборку web/mobile.
Можно в клиенте:
API_BASE_URL,APP_ENVFEATURE_*флаги- публичные идентификаторы аналитики (если они не дают доступа)
Нельзя в клиенте:
- пароли, приватные ключи,
DATABASE_URL - ключи платежей, OAuth client secret
- любые токены, которые открывают доступ к данным или деньгам
Что такое конфигурация сборки и конфигурация запуска — и почему это важно?
Build-time — запекается в сборку и не меняется без пересборки клиента.
Run-time — читается при запуске сервера и может меняться без пересборки.
Практика:
- Web/Mobile:
API_BASE_URLчасто build-time - Backend:
DATABASE_URL,JWT_SECRETвсегда run-time
Если хотите менять значение «без релиза клиента», не делайте его частью build-конфига.
Как сделать единую схему конфигурации для web, backend и mobile?
Сделайте единый словарь ключей и используйте одинаковые имена везде.
Пример минимального набора:
APP_ENVAPI_BASE_URLLOG_LEVELSENTRY_DSN(если используете)FEATURE_*
И закрепите смысл: один и тот же ключ означает одно и то же в React, Go и Flutter.
Как правильно подключать переменные окружения в React, чтобы не было сюрпризов?
Соберите конфиг в одном месте (например, config.ts) и не раскидывайте process.env по проекту.
Полезные практики:
- отдельные
.env.development/.env.staging/.env.production - маленькая debug-страница только с безопасными значениями (env, base URL, флаги)
- учитывайте кеш: старый бандл может продолжать ходить на старый URL, пока ассеты не обновятся
Как организовать конфигурацию в Go-бэкенде безопасно и предсказуемо?
Загружайте конфиг один раз при старте и валидируйте обязательные переменные.
Практика:
mustGet()для обязательных параметров- сервис не стартует, если нет
DATABASE_URL/JWT_SECRET - в логах выводите только безопасное:
env,port, а секреты маскируйте ([set])
Это дешевле, чем ловить ошибки уже под нагрузкой.
Как не устроить хаос с окружениями в Flutter-мобильном приложении?
Делайте отдельные сборки (flavors/schemes) для dev/staging/prod.
Рекомендации:
- разные entrypoints
main_dev.dart,main_staging.dart,main_prod.dart - публичные значения передавайте через
--dart-define - секретов в приложении быть не должно: всё критичное — на бэкенде
Для тестировщиков можно добавить переключение окружения только в dev/staging сборках и только из фиксированного списка URL.
Как правильно использовать feature flags, чтобы не ломать релизы и откаты?
Заведите простые правила эксплуатации:
- флаг не должен управлять безопасностью (права и проверки только на бэкенде)
- в
prodхраните значения флагов централизованно (например, отдавайте/configс бэкенда) - у флага должны быть: имя, дефолт, владелец, срок удаления
- продумайте откат: выключение флага должно возвращать приложение в рабочее состояние
И не забывайте про совместимость: откат к старому коду при включенном новом флаге — частая причина падений.