|$ curl https://forge-ai.dev/api/markdown?path=docs/deploy/environments
$cat docs/environment-management.md
updated Recently·28 min read·published

Environment Management

DeploymentConfigurationIntermediate
Introduction

Environment management is the practice of maintaining separate, consistent configurations for development, staging, and production environments. Each environment should be isolated, reproducible, and secure, with clear promotion paths between them.

Well-managed environments reduce deployment failures, prevent configuration drift, and ensure that code can be tested in production-like conditions before reaching users.

Environment Branch Strategy

The Git branching model maps directly to environments. Each branch or tag triggers automated deployments to a specific environment.

BranchEnvironmentDeploy TriggerGate
feature/*Development/PreviewPush to branchNone
developStagingPush to developCI passes
mainProductionPush to mainPR approved + CI + manual approval
v*.*.*Production (release)Tag creationTag signed + CI + approval
deploy-env-aware.yml
YAML
1# GitHub Actions — Environment-aware deployment
2name: Deploy
3
4on:
5 push:
6 branches: [develop, main]
7 tags: ['v*']
8
9jobs:
10 deploy:
11 runs-on: ubuntu-latest
12 environment:
13 name: ${{ github.ref_name == 'main' && 'production' || 'staging' }}
14 url: ${{ github.ref_name == 'main' && 'https://example.com' || 'https://staging.example.com' }}
15
16 steps:
17 - uses: actions/checkout@v4
18 - uses: actions/setup-node@v4
19
20 - run: npm ci
21 - run: npm run build
22 env:
23 NEXT_PUBLIC_API_URL: ${{ vars.API_URL }}
24
25 - name: Deploy
26 run: |
27 if [[ "${{ github.ref_name }}" == "main" ]]; then
28 ./deploy-production.sh
29 else
30 ./deploy-staging.sh
31 fi

info

Use GitHub Environments to configure required reviewers, wait timers, and deployment branch restrictions per environment. This adds a manual approval gate for production deployments while keeping staging fully automated.
Environment Variables Per Environment

Environment variables change per environment. The application code should read them from process.env without hardcoding environment-specific values.

VariableDevelopmentStagingProduction
API_URLhttp://localhost:4000https://api.staging.example.comhttps://api.example.com
DATABASE_URLpostgres://local/dbpostgres://staging/dbpostgres://prod/db
LOG_LEVELdebuginfowarn
NEXT_PUBLIC_SENTRY_DSN(empty)staging DSNproduction DSN
.env.development
Bash
1# .env.development (local)
2DATABASE_URL=postgres://user:pass@localhost:5432/dev
3API_URL=http://localhost:4000
4NEXT_PUBLIC_SENTRY_DSN=
5LOG_LEVEL=debug
6
7# .env.staging (CI/CD)
8DATABASE_URL=postgres://user:pass@staging-db:5432/app
9API_URL=https://api.staging.example.com
10NEXT_PUBLIC_SENTRY_DSN=https://staging-dsn@sentry.io/1
11LOG_LEVEL=info
12
13# .env.production (CI/CD — secrets in vault)
14DATABASE_URL=postgres://user:pass@prod-db:5432/app
15API_URL=https://api.example.com
16NEXT_PUBLIC_SENTRY_DSN=https://prod-dsn@sentry.io/2
17LOG_LEVEL=warn

danger

Never commit .env.production files to version control. Production secrets should be stored in a secrets manager (Vault, AWS Secrets Manager) and injected at deploy time. Use .env.example as a template in your repository instead.
Feature Flags (LaunchDarkly / Flagsmith)

Feature flags decouple deployment from feature release. A feature can be deployed to production but remain hidden behind a flag, enabling safe rollouts, A/B testing, and instant kill-switches.

feature-flags.ts
TypeScript
1// LaunchDarkly — Feature flag evaluation
2import { LDClient } from 'launchdarkly-node-server-sdk';
3
4const ldClient = LDClient.init(process.env.LAUNCHDARKLY_SDK_KEY);
5
6async function getCheckoutVersion(user) {
7 const flagValue = await ldClient.variation(
8 'new-checkout-flow',
9 { key: user.id, email: user.email },
10 false // default value when flag not found
11 );
12
13 return flagValue ? newCheckout() : legacyCheckout();
14}
15
16// Flagsmith — Server-side evaluation
17import flagsmith from 'flagsmith';
18
19async function middleware(req, res, next) {
20 const flags = await flagsmith.getEnvironmentFlags();
21
22 if (flags.isFeatureEnabled('dark_mode')) {
23 res.locals.theme = 'dark';
24 }
25
26 // Get multivariate value
27 const pricingVersion = flags.getValue('pricing_version');
28 res.locals.pricing = pricingVersion || 'v1';
29
30 next();
31}
32
33// React client-side with LaunchDarkly
34import { useFlags } from '@launchdarkly/react-client-sdk';
35
36function CheckoutButton() {
37 const { newCheckout } = useFlags();
38
39 return (
40 <button onClick={newCheckout ? handleNew : handleLegacy}>
41 {newCheckout ? 'Checkout (New)' : 'Checkout'}
42 </button>
43 );
44}
🔥

pro tip

Use feature flags for: gradual rollouts (1% → 10% → 50% → 100%), instant kill-switches for disabling problematic features, environment-specific behavior, and A/B testing. Clean up flags after full rollout to avoid technical debt — stale flags accumulate and become untestable.
Secrets Management

Secrets (API keys, database credentials, tokens) must be stored securely and never committed to version control. Use a dedicated secrets management solution.

SolutionUse CaseIntegration
HashiCorp VaultEnterprise, dynamic secretsCLI, API, Kubernetes sidecar
AWS Secrets ManagerAWS-native applicationsSDK, CLI, Lambda extensions
GitHub SecretsCI/CD pipeline secretsBuilt into Actions and Dependabot
Vercel/Netlify Env VarsPlatform-deployed appsDashboard or CLI
Doppler / 1PasswordTeam sync, developer experienceCLI, SDK, sync to cloud providers
secrets-management.sh
Bash
1# HashiCorp Vault — Store and retrieve secrets
2# Store a secret
3vault kv put secret/my-app DATABASE_URL=postgres://...
4vault kv put secret/my-app API_KEY=abc123
5
6# Retrieve a secret
7vault kv get secret/my-app
8
9# AWS Secrets Manager
10aws secretsmanager create-secret \
11 --name my-app/production/db-url \
12 --secret-string "postgres://..."
13
14aws secretsmanager get-secret-value \
15 --secret-id my-app/production/db-url \
16 --query SecretString
17
18# GitHub Actions — Reference secrets
19steps:
20 - name: Deploy
21 env:
22 DATABASE_URL: ${{ secrets.DATABASE_URL }}
23 API_KEY: ${{ secrets.API_KEY }}
24 run: ./deploy.sh
Promoting Between Environments

Environment promotion is the process of moving a build artifact from one environment to the next, running progressively more rigorous tests at each stage.

promote.yml
YAML
1# Promotion pipeline: Build → Staging → Production
2name: Promote
3
4on:
5 workflow_dispatch:
6 inputs:
7 environment:
8 type: choice
9 options: [staging, production]
10 version:
11 description: "Build version to promote"
12 required: true
13
14jobs:
15 promote:
16 runs-on: ubuntu-latest
17 environment: ${{ inputs.environment }}
18 steps:
19 - name: Download build artifact
20 uses: actions/download-artifact@v4
21 with:
22 name: build-${{ inputs.version }}
23
24 - name: Deploy to ${{ inputs.environment }}
25 run: |
26 if [[ "${{ inputs.environment }}" == "production" ]]; then
27 echo "Running production deployment..."
28 # Additional checks before production:
29 # - Verify staging passed all tests
30 # - Check for recent rollbacks
31 # - Notify team
32 fi
33 ./deploy.sh ${{ inputs.environment }}
34
35 - name: Run smoke tests
36 run: |
37 URL="${{ inputs.environment == 'production' && 'https://example.com' || 'https://staging.example.com' }}"
38 curl -f $URL/api/health
39
40 - name: Notify
41 uses: slackapi/slack-github-action@v1
42 with:
43 payload: |
44 {
45 "text": "Promoted build ${{ inputs.version }} to ${{ inputs.environment }} ✅"
46 }

best practice

Promote the same build artifact across environments — never rebuild for promotion. This guarantees that the artifact running in production is exactly the one that passed tests in staging. Use build artifacts (zip, Docker image) tagged with the commit SHA for traceability.
$Blueprint — Engineering Documentation·Section ID: ENVIRONMENTS-01·Revision: 1.0