Overview
Environment management addresses the challenge of running the same application codebase across multiple contexts—development machines, testing servers, staging environments, and production systems—while maintaining different configurations for each. Applications require different database connections, API keys, feature flags, and service endpoints depending on where they execute. Environment management provides mechanisms to externalize these configuration values from application code.
The Twelve-Factor App methodology established environment variables as the standard approach for configuration management. Environment variables store configuration data outside the application codebase in the operating system's process environment. This separation prevents sensitive credentials from appearing in version control and enables the same code artifact to deploy across multiple environments without modification.
Environment management spans several concerns: isolating configuration from code, securing sensitive credentials, managing environment-specific behavior, supporting local development workflows, and coordinating configuration across distributed systems. Each environment—development, test, staging, production—maintains its own configuration namespace while the application code references standardized variable names.
# Application references environment variables
database_url = ENV['DATABASE_URL']
api_key = ENV['THIRD_PARTY_API_KEY']
log_level = ENV['LOG_LEVEL'] || 'info'
# Same code runs in all environments
# Configuration differs based on ENV values
The operating system provides environment variables as key-value string pairs to running processes. Child processes inherit their parent's environment. Most operating systems expose environment variables through shell commands and programming language APIs. Ruby accesses environment variables through the ENV constant, which behaves like a hash.
Modern applications interact with numerous external services: databases, message queues, caching systems, third-party APIs, monitoring tools, and logging services. Each service requires connection credentials and configuration parameters. Hard-coding these values couples the application to specific infrastructure. Environment variables decouple application logic from deployment infrastructure.
Key Principles
Configuration Externalization: Application code contains no environment-specific values. All configuration that varies between deployments lives outside the codebase in environment variables or configuration files. This principle enables deploying the same compiled or packaged application to any environment by providing different configuration at runtime.
Environment Isolation: Each deployment environment maintains separate configuration namespaces. Development configurations point to local services, staging configurations use staging infrastructure, and production configurations reference production systems. Isolation prevents development database connections from appearing in production deployments or test data from entering production databases.
Least Privilege Access: Applications receive only the credentials and permissions they need for their specific environment and role. Development environments use read-only database replicas when possible. Staging environments access staging-specific resources. Production credentials grant the minimum necessary permissions. This principle limits the impact of credential compromise.
Default Values and Fallbacks: Applications provide sensible defaults for non-sensitive configuration while requiring explicit values for critical settings like database credentials. Default values reduce configuration burden for developers while mandatory variables enforce explicit configuration for important decisions.
# Require critical configuration
DATABASE_URL = ENV.fetch('DATABASE_URL')
# Provide defaults for optional settings
LOG_LEVEL = ENV.fetch('LOG_LEVEL', 'info')
CACHE_ENABLED = ENV.fetch('CACHE_ENABLED', 'true') == 'true'
WORKER_COUNT = ENV.fetch('WORKER_COUNT', '5').to_i
Immutable Configuration: Configuration loads once at application startup rather than reading environment variables throughout execution. This approach makes application behavior predictable and eliminates race conditions from configuration changes during runtime. Configuration changes require application restart to take effect.
Structured Naming Conventions: Environment variable names follow consistent patterns that group related configuration and indicate purpose. Prefixes namespace variables by application or component. Common patterns include DATABASE_URL, REDIS_URL, LOG_LEVEL, FEATURE_ANALYTICS_ENABLED. Consistent naming helps operators understand configuration structure.
Type Coercion and Validation: Environment variables arrive as strings. Applications convert string values to appropriate types (integers, booleans, arrays) and validate configuration at startup. Invalid configuration causes the application to fail during initialization rather than producing runtime errors.
Secret Rotation Support: Configuration systems support updating sensitive credentials without application downtime. Applications periodically reload credentials from external secret management systems or support graceful restart mechanisms. Secret rotation limits the window of credential compromise.
Ruby Implementation
Ruby provides environment variable access through the ENV constant, a hash-like object that retrieves values from the process environment. The ENV object supports most hash operations but converts all keys to strings and returns nil for undefined variables.
# Reading environment variables
database_url = ENV['DATABASE_URL']
port = ENV['PORT']
# Hash-like operations
ENV.key?('DATABASE_URL') # => true
ENV.fetch('DATABASE_URL') # Raises KeyError if undefined
ENV.fetch('LOG_LEVEL', 'info') # Returns default if undefined
# Iteration
ENV.each do |key, value|
puts "#{key}=#{value}"
end
The ENV.fetch method enforces required configuration by raising KeyError when variables are undefined. This pattern causes applications to fail fast during initialization if required configuration is missing, preventing deployment with incomplete configuration.
class Configuration
def initialize
@database_url = ENV.fetch('DATABASE_URL')
@redis_url = ENV.fetch('REDIS_URL')
@api_key = ENV.fetch('API_KEY')
@log_level = ENV.fetch('LOG_LEVEL', 'info')
@max_connections = ENV.fetch('MAX_CONNECTIONS', '10').to_i
end
attr_reader :database_url, :redis_url, :api_key, :log_level, :max_connections
end
# Fails immediately if DATABASE_URL, REDIS_URL, or API_KEY undefined
config = Configuration.new
Type conversion requires explicit coercion since ENV returns strings. Boolean values need string comparison since Ruby considers non-empty strings truthy. Applications establish conventions for boolean values like "true"/"false" or "1"/"0".
class ApplicationConfig
def self.from_env
new(
cache_enabled: parse_bool(ENV.fetch('CACHE_ENABLED', 'true')),
worker_count: ENV.fetch('WORKER_COUNT', '5').to_i,
timeout_seconds: ENV.fetch('TIMEOUT_SECONDS', '30').to_f,
allowed_hosts: parse_array(ENV.fetch('ALLOWED_HOSTS', '')),
environment: ENV.fetch('RACK_ENV', 'development').to_sym
)
end
private
def self.parse_bool(value)
['true', '1', 'yes', 'on'].include?(value.downcase)
end
def self.parse_array(value)
value.split(',').map(&:strip).reject(&:empty?)
end
end
Rails applications conventionally use Rails.env to determine the current environment. Rails loads environment-specific configuration from config/environments/ files and supports the DATABASE_URL environment variable for database configuration.
# Rails environment detection
Rails.env.production? # => true in production
Rails.env.development? # => true in development
Rails.env.test? # => true in test
# Environment-specific configuration
if Rails.env.production?
config.log_level = :info
config.cache_classes = true
else
config.log_level = :debug
config.cache_classes = false
end
The dotenv gem loads environment variables from .env files during development. This approach prevents developers from manually setting environment variables in their shells while keeping credentials out of version control. The .env file contains key-value pairs in shell format.
# Gemfile
gem 'dotenv-rails', groups: [:development, :test]
# .env file
DATABASE_URL=postgresql://localhost/myapp_development
REDIS_URL=redis://localhost:6379/0
API_KEY=dev_api_key_12345
LOG_LEVEL=debug
# Application automatically loads .env values
# ENV['DATABASE_URL'] => "postgresql://localhost/myapp_development"
The figaro gem provides an alternative approach that stores configuration in config/application.yml and loads values into ENV. Figaro supports environment-specific sections within the YAML file and generates example configuration files.
# Gemfile
gem 'figaro'
# config/application.yml
development:
DATABASE_URL: postgresql://localhost/myapp_development
API_KEY: dev_api_key
production:
DATABASE_URL: <%= ENV["PROD_DATABASE_URL"] %>
API_KEY: <%= ENV["PROD_API_KEY"] %>
# Access via ENV as normal
ENV['DATABASE_URL']
Implementation Approaches
Direct Environment Variable Access: Applications read environment variables directly through the operating system's environment. System administrators set variables in shell profiles, systemd service files, Docker configurations, or orchestration platforms. This approach requires no additional libraries but offers no validation or type conversion.
# Shell export
export DATABASE_URL=postgresql://localhost/myapp
export LOG_LEVEL=info
# Systemd service file
[Service]
Environment="DATABASE_URL=postgresql://localhost/myapp"
Environment="LOG_LEVEL=info"
# Docker
docker run -e DATABASE_URL=postgresql://localhost/myapp \
-e LOG_LEVEL=info \
myapp
Environment File Loading: Development environments load variables from .env files containing key-value pairs. The dotenv library reads these files during application initialization and populates ENV. Files stay out of version control through .gitignore entries. Teams maintain example files like .env.example showing required variables without exposing values.
# .env
DATABASE_URL=postgresql://localhost/myapp_dev
REDIS_URL=redis://localhost:6379
API_KEY=dev_key_123
CACHE_TTL=300
# .env.example (committed to git)
DATABASE_URL=
REDIS_URL=
API_KEY=
CACHE_TTL=300
# Application code
require 'dotenv/load'
# ENV now contains .env values
Configuration Objects: Applications wrap environment access in configuration objects that handle validation, type conversion, and default values. Configuration classes centralize all environment variable access, making it easier to document required variables and their purposes.
class DatabaseConfig
def self.from_env
new(
url: ENV.fetch('DATABASE_URL'),
pool_size: ENV.fetch('DB_POOL_SIZE', '5').to_i,
timeout: ENV.fetch('DB_TIMEOUT', '5').to_i,
ssl_mode: ENV.fetch('DB_SSL_MODE', 'prefer')
)
end
def initialize(url:, pool_size:, timeout:, ssl_mode:)
@url = url
@pool_size = pool_size
@timeout = timeout
@ssl_mode = ssl_mode
validate!
end
private
def validate!
raise ArgumentError, "Pool size must be positive" unless @pool_size > 0
raise ArgumentError, "Invalid SSL mode" unless %w[disable prefer require].include?(@ssl_mode)
end
end
Hierarchical Configuration: Applications combine multiple configuration sources with precedence rules. Environment variables override configuration files, which override application defaults. This layering enables sensible defaults while allowing operators to override specific values without replacing entire configuration files.
class Config
DEFAULTS = {
log_level: 'info',
worker_count: 5,
cache_ttl: 300
}.freeze
def initialize
@settings = DEFAULTS.dup
load_config_file if config_file_exists?
apply_env_overrides
end
private
def load_config_file
yaml_config = YAML.load_file('config/settings.yml')
@settings.merge!(yaml_config)
end
def apply_env_overrides
@settings[:log_level] = ENV['LOG_LEVEL'] if ENV['LOG_LEVEL']
@settings[:worker_count] = ENV['WORKER_COUNT'].to_i if ENV['WORKER_COUNT']
@settings[:cache_ttl] = ENV['CACHE_TTL'].to_i if ENV['CACHE_TTL']
end
end
Secret Management Integration: Production systems retrieve sensitive credentials from dedicated secret management services rather than plain environment variables. Applications authenticate to secret managers using instance roles or service credentials, then fetch secrets at startup. This approach supports secret rotation and audit logging.
require 'aws-sdk-secretsmanager'
class SecretManager
def initialize(region: ENV['AWS_REGION'])
@client = Aws::SecretsManager::Client.new(region: region)
end
def fetch_database_credentials
response = @client.get_secret_value(secret_id: 'prod/database/credentials')
JSON.parse(response.secret_string)
end
def fetch_api_key(service_name)
response = @client.get_secret_value(secret_id: "prod/api-keys/#{service_name}")
response.secret_string
end
end
# Application startup
secrets = SecretManager.new
db_creds = secrets.fetch_database_credentials
ENV['DATABASE_URL'] = db_creds['url']
ENV['DB_PASSWORD'] = db_creds['password']
Environment Detection: Applications determine their runtime environment through standard variables like RACK_ENV, RAILS_ENV, or custom variables like APP_ENV. Environment detection controls behavior like debug logging, error reporting, and feature availability.
class Environment
VALID_ENVIRONMENTS = %w[development test staging production].freeze
def self.current
@current ||= determine_environment
end
def self.production?
current == 'production'
end
def self.development?
current == 'development'
end
def self.test?
current == 'test'
end
private
def self.determine_environment
env = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
unless VALID_ENVIRONMENTS.include?(env)
raise "Invalid environment: #{env}"
end
env
end
end
Security Implications
Credential Exposure in Version Control: Environment variables prevent credentials from appearing in source code, but developers sometimes commit .env files containing real credentials. Version control systems preserve file history forever. Once committed, credentials remain accessible in repository history even after removal. Teams must treat any committed credential as compromised and rotate it immediately.
Configuration files belong in .gitignore before any commits. Teams maintain example files like .env.example with variable names but dummy values. Code review processes check for credential commits.
# .gitignore
.env
.env.local
.env.production
config/secrets.yml
config/credentials/*.key
Process Environment Visibility: Environment variables are visible to any process with access to inspect the process table. On Unix systems, /proc/[pid]/environ exposes environment variables to other users depending on permissions. Process listing tools may display environment variables in command output.
Critical secrets like database passwords or API keys should come from secure secret stores rather than plain environment variables in production. Secret management services provide encrypted storage, access controls, and audit logs.
Logging and Error Reporting: Applications must sanitize environment variables before logging or sending to error tracking services. Stack traces and debug logs can expose credentials if the application includes environment variable dumps. Error monitoring tools receive full application context including environment variables unless explicitly filtered.
class SecureLogger
SENSITIVE_KEYS = %w[
PASSWORD
SECRET
TOKEN
API_KEY
PRIVATE_KEY
DATABASE_URL
].freeze
def self.log_environment
sanitized = ENV.to_h.transform_values do |key, value|
sensitive?(key) ? '[FILTERED]' : value
end
logger.info("Environment: #{sanitized}")
end
def self.sensitive?(key)
SENSITIVE_KEYS.any? { |pattern| key.include?(pattern) }
end
end
URL-Encoded Credentials: Database URLs and service connection strings often embed credentials. These URLs appear in logs, error messages, and debugging output. Applications must parse and sanitize URLs before logging.
require 'uri'
class ConnectionLogger
def self.log_connection(url)
uri = URI.parse(url)
safe_url = "#{uri.scheme}://[USER]:[PASSWORD]@#{uri.host}:#{uri.port}#{uri.path}"
logger.info("Connected to: #{safe_url}")
rescue URI::InvalidURIError
logger.info("Connected to: [URL PARSE ERROR]")
end
end
Environment Variable Injection: Applications that construct environment variable names dynamically or pass user input to configuration lookups risk environment variable injection attacks. Attackers manipulate variable names to access unintended configuration or override security settings.
# Vulnerable: User controls variable name
def get_config(name)
ENV["CONFIG_#{name}"]
end
# Attacker provides name="../../SECRET_KEY"
# May access unintended variables
# Secure: Whitelist allowed variables
def get_config(name)
allowed = %w[DATABASE_URL REDIS_URL LOG_LEVEL]
raise ArgumentError unless allowed.include?(name)
ENV[name]
end
Secrets in Container Images: Docker images capture environment variables set during build. Variables defined with ENV instructions in Dockerfiles become part of the image and remain visible to anyone with image access. Secrets must come from runtime environment variables or mounted secrets, never build-time variables.
# Insecure: Secret in build-time variable
ENV SECRET_KEY=production_secret_value
# Secure: Expect runtime variable
# Secret provided via docker run -e SECRET_KEY=...
Secret Rotation Challenges: Applications that cache configuration at startup cannot respond to secret rotation without restart. Database credentials, API keys, and tokens eventually require rotation. Applications need mechanisms to reload credentials or support graceful restart procedures. Long-running workers and background jobs complicate rotation.
Tools & Ecosystem
dotenv Family: The dotenv gem loads environment variables from .env files during application startup. The dotenv-rails variant integrates with Rails, automatically loading .env files before application initialization. Dotenv supports multiple files for environment-specific overrides: .env.local, .env.development, .env.test.
# Gemfile
gem 'dotenv-rails', groups: [:development, :test]
# Automatically loads in order, later files override earlier:
# .env
# .env.local
# .env.development (or .env.test, .env.production)
# .env.development.local
Figaro: The figaro gem manages Rails application configuration through config/application.yml. Figaro loads YAML configuration into environment variables and supports environment-specific sections. The gem provides Heroku deployment integration and encrypted credential management.
# Install figaro
bundle exec figaro install
# Creates config/application.yml and adds to .gitignore
# Generates config/application.yml.example
Chamber: Chamber provides hierarchical configuration management with encryption support. It loads configuration from YAML files, supports environment-specific overrides, and encrypts sensitive values using GPG or key management services. Chamber handles complex nested configuration structures.
require 'chamber'
Chamber.load basepath: Rails.root.join('config'),
namespaces: { environment: Rails.env },
encryption_keys: { symmetric_key: ENV['CHAMBER_KEY'] }
# Access via Chamber
Chamber.env.database.url
Chamber.env.api.key
AWS Systems Manager Parameter Store: AWS Parameter Store provides hierarchical configuration storage with access control and encryption. Applications retrieve parameters at startup using instance IAM roles. Parameter Store supports versioning, change notifications, and audit logging.
require 'aws-sdk-ssm'
class ParameterStore
def initialize(region: ENV['AWS_REGION'])
@client = Aws::SSM::Client.new(region: region)
end
def get_parameters(path)
response = @client.get_parameters_by_path(
path: path,
recursive: true,
with_decryption: true
)
response.parameters.to_h do |param|
key = param.name.split('/').last.upcase
[key, param.value]
end
end
end
# Load parameters into ENV
store = ParameterStore.new
params = store.get_parameters('/myapp/production')
params.each { |key, value| ENV[key] = value }
HashiCorp Vault: Vault provides dynamic secret generation, lease management, and encryption services. Applications authenticate to Vault using tokens, AWS IAM, or Kubernetes service accounts, then retrieve secrets with automatic renewal. Vault generates database credentials on demand with time-limited leases.
require 'vault'
Vault.address = ENV['VAULT_ADDR']
Vault.token = ENV['VAULT_TOKEN']
# Read static secrets
db_secret = Vault.logical.read('secret/data/database')
db_url = db_secret.data[:data][:url]
# Request dynamic database credentials
dynamic_secret = Vault.logical.read('database/creds/readonly')
username = dynamic_secret.data[:username]
password = dynamic_secret.data[:password]
lease_id = dynamic_secret.lease_id
# Renew lease before expiration
Vault.sys.renew(lease_id)
Envyable: Envyable loads YAML configuration files into environment variables with environment-specific sections. It provides a simpler alternative to figaro with less Rails-specific integration. Envyable supports ERB in YAML files for dynamic configuration.
# config/env.yml
development:
DATABASE_URL: postgresql://localhost/myapp_dev
API_KEY: dev_key_123
production:
DATABASE_URL: <%= ENV['PROD_DATABASE_URL'] %>
API_KEY: <%= ENV['PROD_API_KEY'] %>
Config Gem: The config gem provides deeply nested configuration management with environment-specific overrides, YAML files, and environment variable integration. It supports Rails engines, multiple configuration sources, and runtime configuration reloading.
# Gemfile
gem 'config'
# config/settings.yml
defaults: &defaults
database:
pool: 5
timeout: 5000
cache:
ttl: 300
development:
<<: *defaults
database:
pool: 2
# Access nested configuration
Settings.database.pool # => 2 in development
Settings.cache.ttl # => 300
Common Pitfalls
String Type Confusion: Environment variables always arrive as strings. Boolean comparisons fail when testing string values as truthy/falsy since non-empty strings evaluate as true in Ruby. Applications must explicitly compare string values or convert to booleans.
# Incorrect: "false" is truthy
enabled = ENV['FEATURE_ENABLED']
if enabled # Always true for any non-nil value
enable_feature
end
# Correct: String comparison
enabled = ENV.fetch('FEATURE_ENABLED', 'false')
if enabled == 'true'
enable_feature
end
# Better: Boolean conversion
enabled = ENV.fetch('FEATURE_ENABLED', 'false') == 'true'
if enabled
enable_feature
end
Missing Required Variables: Applications that read undefined environment variables receive nil values, leading to failures deep in application code rather than startup time. Using ENV.fetch without defaults for required configuration causes immediate failure with clear error messages.
# Silent failure - error surfaces later
database_url = ENV['DATABASE_URL']
Database.connect(database_url) # Fails with cryptic error if nil
# Fail fast with clear error
database_url = ENV.fetch('DATABASE_URL')
Database.connect(database_url) # Raises KeyError immediately if undefined
Local Environment Pollution: Developers who set environment variables in shell profiles pollute their local environment with configuration that persists across projects. Variables set in .bashrc or .zshrc apply to all terminal sessions. Applications receive unexpected configuration from unrelated variable definitions.
Using .env files or project-specific shell scripts prevents environment pollution. Each project maintains isolated configuration that loads only when working in that project directory.
Committing Secrets: Developers occasionally commit .env files or other configuration containing real credentials. Git history preserves these secrets forever. Public repositories expose credentials immediately. Private repositories risk exposure through future access grants or security breaches.
Configure .gitignore before making initial commits. Use pre-commit hooks to scan for credential patterns. Provide .env.example files showing required variables with placeholder values.
# Pre-commit hook to detect secrets
#!/bin/bash
if git diff --cached --name-only | grep -q "\.env$"; then
echo "ERROR: Attempting to commit .env file"
exit 1
fi
if git diff --cached -U0 | grep -qE '(password|secret|key|token)\s*=\s*[^\s]+'; then
echo "ERROR: Potential secret detected in commit"
exit 1
fi
Embedding Configuration in URLs: Database URLs and service connection strings embed usernames and passwords. These URLs appear in logs, error reports, and debugging output. Applications must sanitize URLs before logging or transmitting to external services.
# URL contains password
DATABASE_URL="postgresql://user:secretpass@localhost/myapp"
# Sanitize before logging
def sanitize_url(url)
uri = URI.parse(url)
uri.password = '[FILTERED]' if uri.password
uri.to_s
end
logger.info("Connecting to: #{sanitize_url(DATABASE_URL)}")
# => "Connecting to: postgresql://user:[FILTERED]@localhost/myapp"
Integer and Float Conversion Errors: Environment variables containing numeric values require explicit type conversion. Missing conversion causes string/integer method errors. Invalid numeric formats raise exceptions during conversion.
# Incorrect: PORT is string "3000"
port = ENV['PORT']
server.listen(port) # May expect integer
# Correct: Convert to integer
port = ENV.fetch('PORT', '3000').to_i
server.listen(port)
# Better: Validate conversion
port_string = ENV.fetch('PORT', '3000')
port = Integer(port_string) # Raises ArgumentError for invalid numbers
Case Sensitivity Variations: Environment variable names are case-sensitive on most systems but convention varies. Some teams use DATABASE_URL, others database_url, creating confusion. Consistent naming conventions prevent case-related access failures.
# Different case variations may exist
ENV['database_url'] # nil
ENV['DATABASE_URL'] # "postgresql://..."
# Normalize access
def get_env(key)
ENV[key] || ENV[key.upcase] || ENV[key.downcase]
end
Overlooking Environment-Specific Behavior: Applications that reference ENV directly throughout codebase scatter environment-specific behavior. Centralizing environment detection and configuration loading in initialization code makes environment-specific logic explicit and testable.
# Scattered environment checks
if ENV['RAILS_ENV'] == 'production'
enable_caching
end
# In another file
logger.level = ENV['RAILS_ENV'] == 'production' ? :info : :debug
# Better: Centralized environment config
class AppConfig
def initialize
@environment = ENV.fetch('RAILS_ENV', 'development')
@cache_enabled = @environment == 'production'
@log_level = @environment == 'production' ? :info : :debug
end
end
Array and Hash Serialization: Environment variables store strings only. Arrays and hashes require serialization to strings and deserialization when reading. JSON or comma-separated formats enable complex data structures but require explicit parsing.
# Store array as comma-separated values
ENV['ALLOWED_HOSTS'] = 'example.com,api.example.com,www.example.com'
# Parse into array
allowed_hosts = ENV.fetch('ALLOWED_HOSTS', '').split(',').map(&:strip)
# Store hash as JSON
ENV['FEATURE_FLAGS'] = '{"analytics":true,"beta":false}'
# Parse JSON
require 'json'
feature_flags = JSON.parse(ENV.fetch('FEATURE_FLAGS', '{}'))
Reference
Standard Environment Variables
| Variable | Purpose | Example Value |
|---|---|---|
| RACK_ENV | Rack application environment | development, production |
| RAILS_ENV | Rails application environment | development, test, production |
| DATABASE_URL | Database connection string | postgresql://localhost/myapp |
| REDIS_URL | Redis connection string | redis://localhost:6379/0 |
| PORT | Application server port | 3000 |
| LOG_LEVEL | Logging verbosity | debug, info, warn, error |
| SECRET_KEY_BASE | Rails secret key base | 64-character hex string |
| RACK_TIMEOUT_SERVICE_TIMEOUT | Request timeout in seconds | 15 |
Common Variable Patterns
| Pattern | Purpose | Examples |
|---|---|---|
| *_URL | Service connection URLs | DATABASE_URL, REDIS_URL, ELASTICSEARCH_URL |
| *_KEY or *_SECRET | API keys and secrets | API_KEY, SECRET_KEY, AWS_SECRET_KEY |
| *_TOKEN | Authentication tokens | GITHUB_TOKEN, SLACK_TOKEN |
| *_ENABLED | Feature flags | CACHE_ENABLED, ANALYTICS_ENABLED |
| *_TIMEOUT | Timeout durations | DB_TIMEOUT, REQUEST_TIMEOUT |
| *_COUNT or *_SIZE | Numeric limits | WORKER_COUNT, POOL_SIZE, BATCH_SIZE |
| *_HOST, *_PORT | Service endpoints | DB_HOST, DB_PORT, REDIS_HOST |
ENV Object Methods
| Method | Description | Returns |
|---|---|---|
| ENV[key] | Retrieves value for key | String or nil |
| ENV.fetch(key) | Retrieves value, raises if undefined | String |
| ENV.fetch(key, default) | Retrieves value or returns default | String |
| ENV.key?(key) | Checks if key exists | Boolean |
| ENV.keys | Returns all variable names | Array |
| ENV.values | Returns all values | Array |
| ENV.to_h | Converts to hash | Hash |
| ENV.each | Iterates key-value pairs | Enumerator |
Type Conversion Patterns
| Type | Pattern | Example |
|---|---|---|
| Integer | to_i method | ENV['PORT'].to_i |
| Float | to_f method | ENV['TIMEOUT'].to_f |
| Boolean | String comparison | ENV['ENABLED'] == 'true' |
| Symbol | to_sym method | ENV['LEVEL'].to_sym |
| Array | split and map | ENV['HOSTS'].split(',').map(&:strip) |
| Hash | JSON.parse | JSON.parse(ENV['CONFIG']) |
Security Checklist
| Practice | Implementation |
|---|---|
| Never commit credentials | Add .env to .gitignore before initial commit |
| Use secret management | Integrate Vault, Parameter Store, or Secrets Manager |
| Sanitize logs | Filter sensitive variables before logging |
| Rotate secrets regularly | Implement credential rotation procedures |
| Use least privilege | Grant minimum necessary permissions per environment |
| Validate at startup | Use ENV.fetch for required variables |
| Encrypt at rest | Use encrypted secret storage systems |
| Audit access | Enable audit logging for secret access |
dotenv File Precedence
| File | Purpose | Committed to Git |
|---|---|---|
| .env | Base configuration | No |
| .env.local | Local overrides | No |
| .env.development | Development defaults | Maybe |
| .env.test | Test environment | Maybe |
| .env.production | Production defaults | Maybe |
| .env.example | Documentation template | Yes |
Configuration Loading Order
- Application defaults defined in code
- Configuration file defaults (if used)
- Environment-specific configuration files
- Local configuration files (.env.local)
- Environment variables from process environment
- Secret management system values (highest precedence)
Validation Patterns
# Required variable validation
def require_env(*keys)
missing = keys.reject { |key| ENV.key?(key) }
raise "Missing required environment variables: #{missing.join(', ')}" if missing.any?
end
require_env('DATABASE_URL', 'SECRET_KEY_BASE', 'API_KEY')
# Type validation
def validate_integer(key, min: nil, max: nil)
value = Integer(ENV.fetch(key))
raise "#{key} must be >= #{min}" if min && value < min
raise "#{key} must be <= #{max}" if max && value > max
value
end
worker_count = validate_integer('WORKER_COUNT', min: 1, max: 32)
# Enum validation
def validate_enum(key, allowed_values)
value = ENV.fetch(key)
raise "#{key} must be one of: #{allowed_values.join(', ')}" unless allowed_values.include?(value)
value
end
log_level = validate_enum('LOG_LEVEL', %w[debug info warn error fatal])