Environment Management
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.
The Git branching model maps directly to environments. Each branch or tag triggers automated deployments to a specific environment.
| Branch | Environment | Deploy Trigger | Gate |
|---|---|---|---|
| feature/* | Development/Preview | Push to branch | None |
| develop | Staging | Push to develop | CI passes |
| main | Production | Push to main | PR approved + CI + manual approval |
| v*.*.* | Production (release) | Tag creation | Tag signed + CI + approval |
| 1 | # GitHub Actions — Environment-aware deployment |
| 2 | name: Deploy |
| 3 | |
| 4 | on: |
| 5 | push: |
| 6 | branches: [develop, main] |
| 7 | tags: ['v*'] |
| 8 | |
| 9 | jobs: |
| 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
Environment variables change per environment. The application code should read them from process.env without hardcoding environment-specific values.
| Variable | Development | Staging | Production |
|---|---|---|---|
| API_URL | http://localhost:4000 | https://api.staging.example.com | https://api.example.com |
| DATABASE_URL | postgres://local/db | postgres://staging/db | postgres://prod/db |
| LOG_LEVEL | debug | info | warn |
| NEXT_PUBLIC_SENTRY_DSN | (empty) | staging DSN | production DSN |
| 1 | # .env.development (local) |
| 2 | DATABASE_URL=postgres://user:pass@localhost:5432/dev |
| 3 | API_URL=http://localhost:4000 |
| 4 | NEXT_PUBLIC_SENTRY_DSN= |
| 5 | LOG_LEVEL=debug |
| 6 | |
| 7 | # .env.staging (CI/CD) |
| 8 | DATABASE_URL=postgres://user:pass@staging-db:5432/app |
| 9 | API_URL=https://api.staging.example.com |
| 10 | NEXT_PUBLIC_SENTRY_DSN=https://staging-dsn@sentry.io/1 |
| 11 | LOG_LEVEL=info |
| 12 | |
| 13 | # .env.production (CI/CD — secrets in vault) |
| 14 | DATABASE_URL=postgres://user:pass@prod-db:5432/app |
| 15 | API_URL=https://api.example.com |
| 16 | NEXT_PUBLIC_SENTRY_DSN=https://prod-dsn@sentry.io/2 |
| 17 | LOG_LEVEL=warn |
danger
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.
| 1 | // LaunchDarkly — Feature flag evaluation |
| 2 | import { LDClient } from 'launchdarkly-node-server-sdk'; |
| 3 | |
| 4 | const ldClient = LDClient.init(process.env.LAUNCHDARKLY_SDK_KEY); |
| 5 | |
| 6 | async 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 |
| 17 | import flagsmith from 'flagsmith'; |
| 18 | |
| 19 | async 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 |
| 34 | import { useFlags } from '@launchdarkly/react-client-sdk'; |
| 35 | |
| 36 | function 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
Secrets (API keys, database credentials, tokens) must be stored securely and never committed to version control. Use a dedicated secrets management solution.
| Solution | Use Case | Integration |
|---|---|---|
| HashiCorp Vault | Enterprise, dynamic secrets | CLI, API, Kubernetes sidecar |
| AWS Secrets Manager | AWS-native applications | SDK, CLI, Lambda extensions |
| GitHub Secrets | CI/CD pipeline secrets | Built into Actions and Dependabot |
| Vercel/Netlify Env Vars | Platform-deployed apps | Dashboard or CLI |
| Doppler / 1Password | Team sync, developer experience | CLI, SDK, sync to cloud providers |
| 1 | # HashiCorp Vault — Store and retrieve secrets |
| 2 | # Store a secret |
| 3 | vault kv put secret/my-app DATABASE_URL=postgres://... |
| 4 | vault kv put secret/my-app API_KEY=abc123 |
| 5 | |
| 6 | # Retrieve a secret |
| 7 | vault kv get secret/my-app |
| 8 | |
| 9 | # AWS Secrets Manager |
| 10 | aws secretsmanager create-secret \ |
| 11 | --name my-app/production/db-url \ |
| 12 | --secret-string "postgres://..." |
| 13 | |
| 14 | aws secretsmanager get-secret-value \ |
| 15 | --secret-id my-app/production/db-url \ |
| 16 | --query SecretString |
| 17 | |
| 18 | # GitHub Actions — Reference secrets |
| 19 | steps: |
| 20 | - name: Deploy |
| 21 | env: |
| 22 | DATABASE_URL: ${{ secrets.DATABASE_URL }} |
| 23 | API_KEY: ${{ secrets.API_KEY }} |
| 24 | run: ./deploy.sh |
Environment promotion is the process of moving a build artifact from one environment to the next, running progressively more rigorous tests at each stage.
| 1 | # Promotion pipeline: Build → Staging → Production |
| 2 | name: Promote |
| 3 | |
| 4 | on: |
| 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 | |
| 14 | jobs: |
| 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