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.
Why Use Environment Variables?
- Separation of Configuration from Code: Following the Twelve-Factor App methodology, keeping configuration separate from code helps maintain a clean codebase
- Security: Sensitive information like API keys and database credentials should never be hardcoded in source code
- Environment-Specific Configuration: Applications typically run in multiple environments (development, staging, production) with different configuration needs
- Deployment Flexibility: Allows changing configuration without rebuilding or redeploying applications
- Containerization Compatibility: Docker, Kubernetes, and other container technologies rely heavily on 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
-
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'); // ... -
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=falseLoad the appropriate file based on the environment:
// Load environment-specific variables require('dotenv').config({ path: `.env.${process.env.NODE_ENV || 'development'}` }); -
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' }; -
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.
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: Default for all environments.env.development: Used when runningnpm start.env.test: Used when runningnpm test.env.production: Used when runningnpm run build
# .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:
- Building applications with environment-specific configurations
- Running tests against different environments
- Deploying to different environments (staging, production)
- Managing secrets and credentials securely
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:
- GitHub: Repository Secrets or Organization Secrets
- GitLab: CI/CD Variables
- Jenkins: Credentials Plugin
- CircleCI: Environment Variables or Contexts
- AWS CodeBuild: Parameter Store or Secrets Manager
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
-
Committing .env files to version control
# Always add to .gitignore .env .env.local .env.* -
Exposing sensitive variables in client-side code
Remember that all frontend environment variables are visible to users in the bundled JavaScript.
-
Logging environment variables
// Don't do this console.log('Environment variables:', process.env); // Instead, if needed for debugging, log only non-sensitive variables console.log('Server running in mode:', process.env.NODE_ENV); -
Insufficient access controls in CI/CD systems
Restrict who can view and modify environment secrets in your CI/CD system.
Best Practices for Secure Environment Variables
-
Use secret management services instead of plain environment variables for sensitive data:
- AWS Secrets Manager
- Google Secret Manager
- Azure Key Vault
- HashiCorp Vault
-
Provide template .env files without sensitive values:
# .env.example NODE_ENV=development PORT=3000 DATABASE_URL= API_KEY= -
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(', ')}`); } } -
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 - 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:
- Set up a project with Express and dotenv
- Create a .env file with variables for PORT, NODE_ENV, and a simple API_KEY
- Configure the server to use these variables
- Create a /config endpoint that returns non-sensitive configuration (not the API key)
- Add environment validation to ensure required variables are set
Exercise 2: Environment-Specific Configuration
Extend the previous exercise to support different environments:
- Create .env.development, .env.test, and .env.production files
- Modify your application to load the appropriate environment file
- Add npm scripts to run the application in different environments
- Implement a config module that centralizes all configuration
Exercise 3: Frontend Environment Variables
Create a React application with environment-specific configuration:
- Set up a new React application (using Create React App or Vite)
- Create .env files for development and production
- Configure the API URL and feature flags in these files
- Create a config service that loads these variables
- Display the current environment and configuration in the UI
Exercise 4: Dockerized Application with Environment Variables
Create a Dockerized application with runtime environment configuration:
- Create a simple Express.js API
- Write a Dockerfile that uses environment variables
- Create a docker-compose.yml file with environment variables
- Implement an entrypoint script that sets up configuration at container startup
- 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:
- Environment variables provide a way to separate configuration from code
- They are essential for security, allowing sensitive information to be kept out of source code
- Different environments (development, testing, production) require different configurations
- Backend and frontend applications handle environment variables differently
- Tools like dotenv simplify environment variable management in Node.js
- Docker and CI/CD pipelines have specific patterns for environment variable usage
- Security considerations are paramount when dealing with sensitive configuration
By implementing proper environment variable management, you create applications that are more secure, maintainable, and adaptable to different deployment contexts.