> ## Documentation Index
> Fetch the complete documentation index at: https://docs.loremstock.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Managing Environment Variables Across Deployment Environments

> Manage config across development, staging, and production using dotenv-flow, cross-env, config modules, Docker env vars, GitHub Actions secrets, and secret rotation.

Modern applications run in multiple environments. The same codebase needs different database connections, logging levels, and API keys in development, staging, and production. This page covers patterns for loading the right config in each environment, validating it, passing it through Docker and CI/CD, and safely rotating secrets.

## Environment Lifecycle

<Steps>
  <Step title="Development">
    Your local machine. Local database, verbose logging, hot reload. Config: .env.development
  </Step>

  <Step title="Staging">
    Pre-production server. Production-like setup, staging database, sandbox API keys. Config: .env.staging
  </Step>

  <Step title="Production">
    Live server with real users. Production database, real API keys, minimal logging. Config: .env.production
  </Step>
</Steps>

**Environment parity**: keep environments as similar as possible. Differences should be config values only, not architecture or code behavior.

## Pattern 1: Per-Environment .env Files

```bash theme={null}
# Project structure
.env.development   # NOT committed
.env.staging       # NOT committed
.env.production    # NOT committed
.env.example       # Committed (template)
.env.test          # NOT committed (test DB)
```

Load by NODE\_ENV:

```javascript theme={null}
require('dotenv').config({
  path: `.env.${process.env.NODE_ENV || 'development'}`
});
```

## Pattern 2: dotenv-flow (Recommended)

Loads files in cascading order, with later files overriding earlier ones:

```bash theme={null}
npm install dotenv-flow
```

Load order:

1. `.env` (shared defaults)
2. `.env.local` (local overrides, not committed)
3. `.env.development` (or staging/production/test)
4. `.env.development.local`

```javascript theme={null}
// app.js
require('dotenv-flow').config();
// Automatically loads the right files based on NODE_ENV
```

## cross-env: Cross-Platform NODE\_ENV

Setting `NODE_ENV` in npm scripts works differently on Windows vs Unix:

| Platform           | Syntax                                    |
| ------------------ | ----------------------------------------- |
| Unix/Mac           | `NODE_ENV=production node app.js`         |
| Windows CMD        | `set NODE_ENV=production && node app.js`  |
| Windows PowerShell | `$env:NODE_ENV="production"; node app.js` |

`cross-env` normalizes this:

```bash theme={null}
npm install --save-dev cross-env
```

```json theme={null}
{
  "scripts": {
    "start": "node app.js",
    "dev": "cross-env NODE_ENV=development nodemon app.js",
    "staging": "cross-env NODE_ENV=staging node app.js",
    "start:prod": "cross-env NODE_ENV=production node app.js",
    "test": "cross-env NODE_ENV=test jest"
  }
}
```

## Centralized Config Module

Never scatter `process.env` throughout your codebase. Create one file that reads, validates, and exports all variables:

```javascript theme={null}
// config/index.js
require('dotenv-flow').config();

const required = ['PORT', 'DB_URI', 'JWT_SECRET', 'NODE_ENV'];
const missing = required.filter(k => !process.env[k]);
if (missing.length > 0) {
  throw new Error(`Missing required env vars: ${missing.join(', ')}`);
}

if (process.env.JWT_SECRET.length < 32) {
  throw new Error('JWT_SECRET must be at least 32 characters');
}

module.exports = {
  port: parseInt(process.env.PORT, 10),
  nodeEnv: process.env.NODE_ENV,
  dbUri: process.env.DB_URI,
  jwtSecret: process.env.JWT_SECRET,
  jwtExpiresIn: process.env.JWT_EXPIRES_IN || '1d',
  isDev: process.env.NODE_ENV === 'development',
  isProd: process.env.NODE_ENV === 'production',
  isTest: process.env.NODE_ENV === 'test',
};
```

```javascript theme={null}
// Anywhere in app
const config = require('./config');
mongoose.connect(config.dbUri);
jwt.sign(payload, config.jwtSecret, { expiresIn: config.jwtExpiresIn });
```

<Warning>
  Never write `process.env.JWT_SECRET || 'fallback-secret'`. If the env var is unset in production, the app silently falls back to a known/weak default. Validate and fail loudly instead.
</Warning>

## Environment Variables in Docker

### Dockerfile (default values)

```dockerfile theme={null}
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "app.js"]
```

### docker run with --env-file

```bash theme={null}
docker run --env-file .env.production -p 3000:3000 myapp
```

### docker-compose.yml

```yaml theme={null}
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    env_file:
      - .env.production
    environment:
      - NODE_ENV=production    # Override specific vars inline
    depends_on:
      - mongodb

  mongodb:
    image: mongo:6
    volumes:
      - mongo_data:/data/db
    env_file:
      - .env.production

volumes:
  mongo_data:
```

| Docker Option | Effect                                              |
| ------------- | --------------------------------------------------- |
| `env_file`    | Load all variables from a file                      |
| `environment` | Set specific variables inline (overrides env\_file) |

## GitHub Actions Secrets

Store sensitive values in GitHub: Settings > Secrets and variables > Actions > New repository secret.

```yaml theme={null}
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production   # Use environment-specific secrets

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Install
        run: npm ci

      - name: Test
        run: npm test
        env:
          NODE_ENV: test
          DB_URI: ${{ secrets.TEST_DB_URI }}
          JWT_SECRET: ${{ secrets.TEST_JWT_SECRET }}

      - name: Deploy
        env:
          DB_URI: ${{ secrets.PROD_DB_URI }}
          JWT_SECRET: ${{ secrets.PROD_JWT_SECRET }}
        run: npm run deploy
```

## Secret Rotation: Zero-Downtime Process

<Steps>
  <Step title="Generate new secret">
    ```bash theme={null}
    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
    ```
  </Step>

  <Step title="Update in secret store">
    Add the new secret to GitHub Actions secrets, Heroku Config Vars, or your secret manager.
  </Step>

  <Step title="Redeploy the application">
    Restart the app so it picks up the new value.
  </Step>

  <Step title="Verify new secret works">
    Run health checks. Test authenticated endpoints.
  </Step>

  <Step title="Revoke the old secret">
    Only disable the old credential AFTER confirming the new one is active.
  </Step>
</Steps>

## Monitoring for Leaked Secrets

### git-secrets (Pre-commit hook)

```bash theme={null}
# macOS
brew install git-secrets

# Setup in repo
cd my-project
git-secrets --install
git-secrets --register-aws

# Scan entire history
git-secrets --scan-history
```

### GitHub Secret Scanning

Enable in repository settings. GitHub automatically scans every commit and pull request for known secret patterns (AWS keys, GitHub tokens, etc.).

### Emergency: Secret Found in Git History

If you discover a secret was committed:

1. **Rotate the secret immediately** (generate new, update everywhere)
2. Use `git filter-repo` or BFG Repo Cleaner to remove from history
3. Force-push the cleaned history
4. Notify all team members to re-clone the repo

<Warning>
  Simply deleting the file and committing "remove secrets" does NOT remove the secret from git history. Anyone can still run `git log -p` to see it. Always rotate first, then clean history.
</Warning>

## Key Terms

| Term                   | Definition                                                                         |
| ---------------------- | ---------------------------------------------------------------------------------- |
| **Environment parity** | Development and production are as similar as possible, differing only in config    |
| **Config module**      | Single file that reads, validates, and exports all env vars                        |
| **Secret rotation**    | Replacing old credentials with new ones to limit exposure window                   |
| **cross-env**          | Package that sets env vars in npm scripts for Windows + Unix compatibility         |
| **CI/CD secrets**      | Encrypted variables injected into build pipelines by platforms like GitHub Actions |
| **dotenv-flow**        | dotenv extension that loads multiple .env files in a cascading order               |

## Common Mistakes

<Accordion title="Scattering process.env throughout the codebase">
  When process.env.X appears in 50 files, refactoring and testing become painful. Use a config module: one place to change, mock, and validate.
</Accordion>

<Accordion title="Committing .env.production to git">
  Every .env.\* file with real secrets must be in .gitignore. Only .env.example is safe to commit.
</Accordion>

<Accordion title="Using the same secrets across all environments">
  Development and production must have separate credentials. If a developer's laptop is compromised, you don't want production credentials exposed.
</Accordion>

<Accordion title="No validation at startup">
  If DB\_URI is undefined, your app might start successfully and crash only when the first database query runs. Validate all required vars before the server starts.
</Accordion>
