Environment Variables

Managing Configuration Across Different Environments

What Are Environment Variables?

Environment variables are dynamic values that can affect the way running processes behave on a computer. They are part of the environment in which a process runs and provide a way to configure applications externally without changing the code.

Think of environment variables as switches and dials on the outside of a machine. Just as you can adjust the temperature of an oven without opening it or rebuilding it, environment variables let you control an application's behavior without modifying its code.

flowchart LR subgraph Environment A[Environment Variables] end subgraph Application B[Source Code] C[Configuration] end A --> C C --> B

Why Use Environment Variables?

Real-World Analogy: Building a House

Imagine you're building a house. The blueprint (your code) stays the same, but you need to adapt to different conditions based on location and season:

  • In a hot climate, you adjust your thermostat (environment variable) to cool the house
  • In a cold climate, you use the same thermostat to heat the house
  • At night, you may set different lighting levels
  • When away, you may set a security system

The house design doesn't change, but its behavior adapts to external settings. That's what environment variables do for your application.

Environment Variables in Node.js

In Node.js applications, environment variables are accessible through the global process.env object, which contains key-value pairs representing the environment in which the Node.js process is running.

Accessing Environment Variables


// Accessing environment variables in Node.js
console.log(process.env.NODE_ENV); // 'development', 'production', etc.
console.log(process.env.PORT); // Port number the server should listen on
console.log(process.env.DATABASE_URL); // Database connection string
                

Setting Environment Variables

There are several ways to set environment variables for your Node.js application:

Directly in the Terminal (Temporary)


# Unix/Linux/macOS
PORT=3000 NODE_ENV=development node server.js

# Windows (Command Prompt)
set PORT=3000 && set NODE_ENV=development && node server.js

# Windows (PowerShell)
$env:PORT=3000; $env:NODE_ENV="development"; node server.js
                

In npm scripts (package.json)


// package.json
{
  "scripts": {
    "start": "NODE_ENV=production node server.js",
    "dev": "NODE_ENV=development PORT=3000 node server.js"
  }
}
                

Note: For Windows compatibility with npm scripts, consider using cross-env:


npm install --save-dev cross-env

// package.json
{
  "scripts": {
    "start": "cross-env NODE_ENV=production node server.js",
    "dev": "cross-env NODE_ENV=development PORT=3000 node server.js"
  }
}
                

Using .env Files with dotenv

The most common and flexible approach for managing environment variables in Node.js is using .env files with the dotenv package.


// Install dotenv
npm install dotenv

// In your main application file (e.g., server.js or app.js)
require('dotenv').config();

// Now process.env contains the keys and values defined in your .env file
console.log(process.env.DATABASE_URL);
                

Create a .env file in your project root:


# .env file
NODE_ENV=development
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
API_KEY=your_secret_api_key
DEBUG=true
                

Important: Always add .env to your .gitignore file to prevent sensitive information from being committed to version control!

Best Practices for Using dotenv

  1. Early Loading: Load environment variables as early as possible in your application:
    
    // At the very top of your entry file
    require('dotenv').config();
    
    // Rest of your imports and code
    const express = require('express');
    // ...
                        
  2. Environment-Specific Files: Use different .env files for different environments:
    
    # Development environment
    # .env.development
    NODE_ENV=development
    PORT=3000
    DATABASE_URL=mongodb://localhost:27017/myapp_dev
    DEBUG=true
    
    # Production environment
    # .env.production
    NODE_ENV=production
    PORT=80
    DATABASE_URL=mongodb://production-server:27017/myapp
    DEBUG=false
                        

    Load the appropriate file based on the environment:

    
    // Load environment-specific variables
    require('dotenv').config({ 
      path: `.env.${process.env.NODE_ENV || 'development'}` 
    });
                        
  3. Default Values: Provide fallbacks for missing environment variables:
    
    // config.js
    module.exports = {
      nodeEnv: process.env.NODE_ENV || 'development',
      port: parseInt(process.env.PORT, 10) || 3000,
      databaseUrl: process.env.DATABASE_URL || 'mongodb://localhost:27017/myapp',
      apiKey: process.env.API_KEY, // No default for security-critical values
      debug: process.env.DEBUG === 'true'
    };
                        
  4. Validation: Validate required environment variables at startup:
    
    // validateEnv.js
    function validateEnv() {
      const requiredEnvVars = ['DATABASE_URL', 'API_KEY'];
      
      const missingEnvVars = requiredEnvVars.filter(env => !process.env[env]);
      
      if (missingEnvVars.length > 0) {
        throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`);
      }
    }
    
    // In your main file
    require('dotenv').config();
    require('./validateEnv')();
                        

Using Environment Variables in an Express Application


// config.js - Central configuration file
require('dotenv').config();

// Validate required environment variables
const requiredEnvVars = ['DATABASE_URL'];
const missingEnvVars = requiredEnvVars.filter(env => !process.env[env]);
if (missingEnvVars.length > 0) {
  throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`);
}

module.exports = {
  env: process.env.NODE_ENV || 'development',
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    url: process.env.DATABASE_URL
  },
  auth: {
    jwtSecret: process.env.JWT_SECRET || 'your-default-jwt-secret',
    expiresIn: process.env.JWT_EXPIRES_IN || '1d'
  },
  corsOrigins: process.env.CORS_ORIGINS 
    ? process.env.CORS_ORIGINS.split(',') 
    : ['http://localhost:3000'],
  logging: {
    level: process.env.LOG_LEVEL || 'info'
  }
};

// server.js
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
const config = require('./config');

const app = express();

// Apply environment-specific configuration
app.use(cors({
  origin: config.corsOrigins,
  credentials: true
}));

// Connect to database
mongoose.connect(config.database.url)
  .then(() => console.log('Connected to database'))
  .catch(err => console.error('Database connection error:', err));

// Configure routes
app.get('/', (req, res) => {
  res.send(`Running in ${config.env} mode`);
});

// Start server
app.listen(config.port, () => {
  console.log(`Server running in ${config.env} mode on port ${config.port}`);
});
                

Environment Variables in Frontend Applications

Handling environment variables in frontend applications is different from backend applications because frontend code runs in the browser, not on a server.

flowchart TD subgraph "Build Process" A[Environment Variables] --> B[Build Tool] B --> C[Bundled JavaScript] end subgraph "Runtime" C --> D[Browser] end

Create React App (CRA)

Create React App has built-in support for environment variables with a specific naming convention.

Create a .env file in your project root:


# .env
REACT_APP_API_URL=https://api.example.com
REACT_APP_FEATURE_FLAG_NEW_UI=true
                

Important: In Create React App, only variables that start with REACT_APP_ will be included in the bundle.

Using environment variables in your React components:


// API service
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api';

export const fetchUsers = async () => {
  try {
    const response = await fetch(`${API_URL}/users`);
    return response.json();
  } catch (error) {
    console.error('Error fetching users:', error);
    throw error;
  }
};

// Feature flag usage
const NewUIComponent = () => {
  // Only render new UI if feature flag is enabled
  if (process.env.REACT_APP_FEATURE_FLAG_NEW_UI === 'true') {
    return <div>New UI Feature</div>;
  }
  return <div>Legacy UI</div>;
};
                

Environment-Specific Files in CRA

Create React App supports different .env files for different environments:


# .env.development
REACT_APP_API_URL=http://localhost:3001/api
REACT_APP_FEATURE_FLAG_NEW_UI=true

# .env.production
REACT_APP_API_URL=https://api.example.com
REACT_APP_FEATURE_FLAG_NEW_UI=false
                

Environment variables are embedded during the build time. If you need to change them after building, you'll need to rebuild your application.

Vite

If you're using Vite as your build tool, it handles environment variables differently:


# .env
VITE_API_URL=https://api.example.com
VITE_FEATURE_FLAG_NEW_UI=true
                

Note: In Vite, variables must start with VITE_ to be exposed to your application.

Accessing the variables in your code:


// In Vite, use import.meta.env instead of process.env
const API_URL = import.meta.env.VITE_API_URL;
console.log(API_URL); // https://api.example.com

// Feature flag usage
const isNewUIEnabled = import.meta.env.VITE_FEATURE_FLAG_NEW_UI === 'true';
                

Runtime Environment Variable Injection

Sometimes you need to change environment variables after the application is built, especially in containerized deployments. One approach is to use a runtime configuration file:

First, create a configuration file that will be generated at runtime:


// public/config.js
window.__RUNTIME_CONFIG__ = {
  API_URL: '%%API_URL%%',
  FEATURE_FLAGS: {
    NEW_UI: %%FEATURE_FLAG_NEW_UI%%
  }
};
                

Include this file in your HTML:


<!-- public/index.html -->
<head>
  <script src="%PUBLIC_URL%/config.js"></script>
</head>
                

Create a shell script to replace placeholders with actual environment variables at container startup:


#!/bin/sh
# entrypoint.sh

# Replace environment variables in config.js
sed -i "s|%%API_URL%%|$API_URL|g" /usr/share/nginx/html/config.js
sed -i "s|%%FEATURE_FLAG_NEW_UI%%|$FEATURE_FLAG_NEW_UI|g" /usr/share/nginx/html/config.js

# Start nginx
nginx -g "daemon off;"
                

Access the runtime configuration in your React application:


// Get config from window object
const config = window.__RUNTIME_CONFIG__ || {
  API_URL: 'http://localhost:3001/api', // Fallback
  FEATURE_FLAGS: { NEW_UI: false }
};

// Use the runtime config
function ApiService() {
  const apiUrl = config.API_URL;
  
  return {
    fetchUsers: async () => {
      const response = await fetch(`${apiUrl}/users`);
      return response.json();
    }
  };
}

// Export as a singleton
export default ApiService();
                

Environment Variables in CI/CD Pipelines

Continuous Integration and Continuous Deployment (CI/CD) pipelines often need environment variables for:

flowchart LR A[Source Code] --> B[CI/CD Pipeline] E[Environment Variables] --> B B --> C[Build] B --> D[Test] B --> F[Deploy] subgraph "Pipeline Environment" B E end

GitHub Actions Example


# .github/workflows/deploy.yml
name: Deploy Application

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm ci
      
      - name: Build application
        run: npm run build
        env:
          REACT_APP_API_URL: ${{ secrets.API_URL }}
          REACT_APP_GOOGLE_ANALYTICS_ID: ${{ secrets.GOOGLE_ANALYTICS_ID }}
      
      - name: Run tests
        run: npm test
        env:
          NODE_ENV: test
          TEST_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
      
      - name: Deploy to AWS
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1
      
      - name: Upload to S3
        run: aws s3 sync ./build s3://${{ secrets.S3_BUCKET }}
      
      - name: Invalidate CloudFront cache
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"
                

Managing Secrets in CI/CD

Never hardcode sensitive information in your CI/CD configuration files. Instead, use your CI/CD platform's secrets management:

Environment-Specific Deployments


# .github/workflows/deploy-environments.yml
name: Multi-Environment Deployment

on:
  push:
    branches:
      - develop
      - staging
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set environment based on branch
        id: set-env
        run: |
          if [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then
            echo "ENVIRONMENT=development" >> $GITHUB_ENV
            echo "API_URL=${{ secrets.DEV_API_URL }}" >> $GITHUB_ENV
          elif [[ "${{ github.ref }}" == "refs/heads/staging" ]]; then
            echo "ENVIRONMENT=staging" >> $GITHUB_ENV
            echo "API_URL=${{ secrets.STAGING_API_URL }}" >> $GITHUB_ENV
          elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
            echo "ENVIRONMENT=production" >> $GITHUB_ENV
            echo "API_URL=${{ secrets.PROD_API_URL }}" >> $GITHUB_ENV
          fi
      
      - name: Build for environment
        run: npm run build
        env:
          REACT_APP_API_URL: ${{ env.API_URL }}
          REACT_APP_ENVIRONMENT: ${{ env.ENVIRONMENT }}
      
      - name: Deploy to environment
        run: |
          echo "Deploying to ${{ env.ENVIRONMENT }} environment"
          # Deployment commands specific to each environment
                

Environment Variables in Docker

Docker containers are designed to be portable and run consistently in different environments. Environment variables are a key mechanism for configuring containers without modifying the container image.

Setting Environment Variables in Dockerfile


# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

# Set default environment variables
ENV NODE_ENV=production
ENV PORT=3000

# Build the application
RUN npm run build

# Expose the port
EXPOSE ${PORT}

# Start the application
CMD ["node", "server.js"]
                

Overriding Environment Variables at Runtime


# Run a container with specific environment variables
docker run -e NODE_ENV=development -e PORT=8080 -p 8080:8080 my-app

# Using an environment file
docker run --env-file .env.docker -p 3000:3000 my-app
                

Docker Compose Environment Variables


# docker-compose.yml
version: '3'

services:
  app:
    build: .
    ports:
      - "${PORT:-3000}:3000"
    environment:
      - NODE_ENV=${NODE_ENV:-production}
      - DATABASE_URL=mongodb://mongo:27017/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      - mongo
      - redis
  
  mongo:
    image: mongo:6
    volumes:
      - mongo-data:/data/db
  
  redis:
    image: redis:alpine
    volumes:
      - redis-data:/data

volumes:
  mongo-data:
  redis-data:
                

Using .env files with Docker Compose:


# .env file for docker-compose
NODE_ENV=development
PORT=3000
                

Docker Compose automatically loads variables from a .env file in the same directory as your docker-compose.yml.

Multi-stage Builds with Environment Variables


# Dockerfile for a React + Node.js application
# Build stage
FROM node:18-alpine AS build

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

# Build with environment variables
ARG REACT_APP_API_URL
ENV REACT_APP_API_URL=${REACT_APP_API_URL}

RUN npm run build

# Production stage
FROM nginx:alpine

# Copy built files from build stage
COPY --from=build /app/build /usr/share/nginx/html

# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Expose port
EXPOSE 80

# Start nginx
CMD ["nginx", "-g", "daemon off;"]
                

Building with build arguments:


docker build --build-arg REACT_APP_API_URL=https://api.example.com -t my-react-app .
                

Runtime Configuration for Frontend Apps in Docker

For frontend applications that need environment variables at runtime, not just build time, use an entrypoint script:


# Dockerfile
FROM nginx:alpine

# Copy built app
COPY ./build /usr/share/nginx/html

# Copy config template and entrypoint script
COPY ./config.js.template /usr/share/nginx/html/config.js.template
COPY ./entrypoint.sh /entrypoint.sh

# Make the entrypoint script executable
RUN chmod +x /entrypoint.sh

# Set environment variables with defaults
ENV API_URL=https://api.example.com
ENV FEATURE_FLAG_NEW_UI=false

# Run the entrypoint script
ENTRYPOINT ["/entrypoint.sh"]
                

Create an entrypoint script to substitute environment variables:


#!/bin/sh
# entrypoint.sh

# Replace variables in config.js
envsubst < /usr/share/nginx/html/config.js.template > /usr/share/nginx/html/config.js

# Start nginx
exec nginx -g "daemon off;"
                

Create a config template with placeholders:


// config.js.template
window.__RUNTIME_CONFIG__ = {
  API_URL: '${API_URL}',
  FEATURE_FLAGS: {
    NEW_UI: ${FEATURE_FLAG_NEW_UI}
  }
};
                

Security Considerations

Environment variables are often used for sensitive information, so security is a critical concern.

Common Security Pitfalls

Best Practices for Secure Environment Variables

  1. Use secret management services instead of plain environment variables for sensitive data:
    • AWS Secrets Manager
    • Google Secret Manager
    • Azure Key Vault
    • HashiCorp Vault
  2. Provide template .env files without sensitive values:
    
    # .env.example
    NODE_ENV=development
    PORT=3000
    DATABASE_URL=
    API_KEY=
                        
  3. Validate environment variables at startup:
    
    function validateRequiredEnvVars() {
      const required = [
        'DATABASE_URL',
        'JWT_SECRET',
        'API_KEY'
      ];
      
      const missing = required.filter(env => !process.env[env]);
      
      if (missing.length > 0) {
        throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
      }
    }
                        
  4. Use different variable sets for different environments:
    
    # .env.development - Developers can create their own with local values
    
    # .env.test - Shared for testing, but with non-production credentials
    
    # .env.staging - Managed by DevOps, not accessible to all developers
    
    # .env.production - Highly restricted access
                        
  5. Rotate secrets regularly, especially in production environments.

Real-World Example: The GitHub Token Leak Problem

In recent years, thousands of developers have accidentally committed API keys, access tokens, and other secrets to public GitHub repositories. These leaks are often quickly found by automated bots that scan for credential patterns, leading to compromised accounts and services.

To combat this, GitHub introduced secret scanning, which detects common credential patterns and notifies the affected service providers. Additionally, tools like git-secrets and pre-commit hooks help prevent secrets from being committed in the first place.

Practical Examples

Complete Node.js Application with Environment Configuration

Project Structure:


my-app/
├── .env                     # Default environment variables
├── .env.example             # Template file for developers
├── .env.test                # Environment variables for testing
├── .gitignore               # Ignores all .env files
├── src/
│   ├── config/
│   │   ├── index.js         # Central config file
│   │   ├── database.js      # Database configuration
│   │   └── auth.js          # Authentication configuration
│   ├── middleware/
│   │   └── validateEnv.js   # Environment validation middleware
│   ├── models/              # Database models
│   ├── routes/              # API routes
│   └── server.js            # Main application file
└── package.json             # Project dependencies and scripts
                

Environment Files:


# .env.example (committed to repository as a template)
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=change_this_to_a_secure_random_string
JWT_EXPIRES_IN=1d
CORS_ORIGINS=http://localhost:3000
REDIS_URL=redis://localhost:6379
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your_smtp_username
SMTP_PASS=your_smtp_password
                

Configuration Files:


// src/config/index.js
const dotenv = require('dotenv');
const path = require('path');

// Load environment variables from .env file
dotenv.config({
  path: path.resolve(process.cwd(), `.env${process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ''}`)
});

// Import configuration modules
const database = require('./database');
const auth = require('./auth');

module.exports = {
  env: process.env.NODE_ENV || 'development',
  port: parseInt(process.env.PORT, 10) || 3000,
  logLevel: process.env.LOG_LEVEL || 'info',
  database,
  auth,
  cors: {
    origins: process.env.CORS_ORIGINS 
      ? process.env.CORS_ORIGINS.split(',') 
      : ['http://localhost:3000']
  },
  redis: {
    url: process.env.REDIS_URL || 'redis://localhost:6379'
  },
  email: {
    host: process.env.SMTP_HOST,
    port: parseInt(process.env.SMTP_PORT, 10) || 587,
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASS
    },
    from: process.env.SMTP_FROM || 'noreply@example.com'
  }
};
                

// src/config/database.js
module.exports = {
  url: process.env.DATABASE_URL,
  options: {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    serverSelectionTimeoutMS: 5000,
    maxPoolSize: parseInt(process.env.DB_MAX_POOL_SIZE, 10) || 10,
    socketTimeoutMS: parseInt(process.env.DB_SOCKET_TIMEOUT_MS, 10) || 45000
  }
};
                

// src/config/auth.js
module.exports = {
  jwtSecret: process.env.JWT_SECRET,
  jwtExpiresIn: process.env.JWT_EXPIRES_IN || '1d',
  bcryptSaltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS, 10) || 12,
  passwordResetExpires: parseInt(process.env.PASSWORD_RESET_EXPIRES, 10) || 3600000, // 1 hour
  google: {
    clientId: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackUrl: process.env.GOOGLE_CALLBACK_URL
  }
};
                

Environment Validation:


// src/middleware/validateEnv.js
const validateEnv = () => {
  // Required environment variables
  const requiredEnvVars = [
    'DATABASE_URL',
    'JWT_SECRET'
  ];
  
  const missing = requiredEnvVars.filter(env => !process.env[env]);
  
  if (missing.length > 0) {
    throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
  }
  
  // Validate specific values
  if (process.env.NODE_ENV && !['development', 'test', 'production'].includes(process.env.NODE_ENV)) {
    throw new Error(`Invalid NODE_ENV: ${process.env.NODE_ENV}`);
  }
};

module.exports = validateEnv;
                

Main Application:


// src/server.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const morgan = require('morgan');

// Load environment configuration
const validateEnv = require('./middleware/validateEnv');
validateEnv();

const config = require('./config');

// Initialize Express app
const app = express();

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors({
  origin: config.cors.origins,
  credentials: true
}));

// Logging
if (config.env !== 'test') {
  app.use(morgan(config.env === 'development' ? 'dev' : 'combined'));
}

// Database connection
mongoose.connect(config.database.url, config.database.options)
  .then(() => {
    console.log('Connected to database');
  })
  .catch(err => {
    console.error('Database connection error:', err);
    process.exit(1);
  });

// API routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/users', require('./routes/users'));
app.use('/api/products', require('./routes/products'));

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: {
      message: config.env === 'development' ? err.message : 'Internal Server Error'
    }
  });
});

// Start server
const PORT = config.port;
app.listen(PORT, () => {
  console.log(`Server running in ${config.env} mode on port ${PORT}`);
});

module.exports = app; // For testing
                

Package Scripts:


// package.json
{
  "scripts": {
    "start": "cross-env NODE_ENV=production node src/server.js",
    "dev": "cross-env NODE_ENV=development nodemon src/server.js",
    "test": "cross-env NODE_ENV=test jest --runInBand"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "mongoose": "^6.7.2",
    "morgan": "^1.10.0"
  },
  "devDependencies": {
    "cross-env": "^7.0.3",
    "jest": "^29.3.1",
    "nodemon": "^2.0.20",
    "supertest": "^6.3.1"
  }
}
                

Full-Stack Application with Environment Configuration

Backend (.env):


# backend/.env.example
NODE_ENV=development
PORT=3001
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your_jwt_secret
CORS_ORIGINS=http://localhost:3000
                

Frontend (.env):


# frontend/.env.example
REACT_APP_API_URL=http://localhost:3001/api
REACT_APP_GOOGLE_MAPS_API_KEY=your_google_maps_api_key
REACT_APP_SENTRY_DSN=your_sentry_dsn
                

Docker Compose Setup:


# docker-compose.yml
version: '3.8'

services:
  frontend:
    build:
      context: ./frontend
      args:
        - REACT_APP_API_URL=http://localhost:3001/api
    ports:
      - "3000:80"
    depends_on:
      - backend
    environment:
      - NODE_ENV=production
  
  backend:
    build: ./backend
    ports:
      - "3001:3001"
    depends_on:
      - mongo
    environment:
      - NODE_ENV=production
      - PORT=3001
      - DATABASE_URL=mongodb://mongo:27017/myapp
      - JWT_SECRET=${JWT_SECRET}
      - CORS_ORIGINS=http://localhost:3000
  
  mongo:
    image: mongo:latest
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db

volumes:
  mongo-data:
                

Production Deployment Example (Docker Swarm or Kubernetes):


# docker-stack.yml or kubernetes-deployment.yml
# This would use environment variables from the production environment
# or from a secrets management service
services:
  frontend:
    image: myapp/frontend:latest
    environment:
      - NODE_ENV=production
    # Use external secrets for sensitive data
    secrets:
      - source: google_maps_api_key
        target: /app/secrets/google_maps_api_key
  
  backend:
    image: myapp/backend:latest
    environment:
      - NODE_ENV=production
      - PORT=3001
      - DATABASE_URL=mongodb://mongo-service:27017/myapp
      - CORS_ORIGINS=https://app.example.com
    # Use external secrets for sensitive data
    secrets:
      - source: jwt_secret
        target: /app/secrets/jwt_secret
      - source: db_credentials
        target: /app/secrets/db_credentials

secrets:
  jwt_secret:
    external: true
  db_credentials:
    external: true
  google_maps_api_key:
    external: true
                

Practical Exercises

Exercise 1: Basic Environment Configuration

Create a simple Express.js application that uses environment variables for configuration:

  1. Set up a project with Express and dotenv
  2. Create a .env file with variables for PORT, NODE_ENV, and a simple API_KEY
  3. Configure the server to use these variables
  4. Create a /config endpoint that returns non-sensitive configuration (not the API key)
  5. Add environment validation to ensure required variables are set

Exercise 2: Environment-Specific Configuration

Extend the previous exercise to support different environments:

  1. Create .env.development, .env.test, and .env.production files
  2. Modify your application to load the appropriate environment file
  3. Add npm scripts to run the application in different environments
  4. Implement a config module that centralizes all configuration

Exercise 3: Frontend Environment Variables

Create a React application with environment-specific configuration:

  1. Set up a new React application (using Create React App or Vite)
  2. Create .env files for development and production
  3. Configure the API URL and feature flags in these files
  4. Create a config service that loads these variables
  5. Display the current environment and configuration in the UI

Exercise 4: Dockerized Application with Environment Variables

Create a Dockerized application with runtime environment configuration:

  1. Create a simple Express.js API
  2. Write a Dockerfile that uses environment variables
  3. Create a docker-compose.yml file with environment variables
  4. Implement an entrypoint script that sets up configuration at container startup
  5. Test running the container with different environment settings

Additional Resources

Summary

Environment variables are a crucial part of modern application development, enabling flexible configuration across different environments without changing code. Key takeaways include:

By implementing proper environment variable management, you create applications that are more secure, maintainable, and adaptable to different deployment contexts.