CrackedRuby logo

CrackedRuby

Environment Variables

Overview

Environment variables provide runtime configuration through key-value pairs set in the operating system shell. Ruby exposes environment variables through the ENV constant, which behaves like a Hash containing string keys and values. The ENV object connects Ruby programs to their execution environment, enabling configuration without hardcoding values.

Ruby treats ENV as a special Hash-like object that directly interfaces with the underlying system's environment. All environment variable names and values are strings in Ruby, regardless of how they appear in the shell. The ENV constant provides immediate access to variables set before program execution and supports runtime modification that affects child processes.

# Reading environment variables
database_url = ENV['DATABASE_URL']
port = ENV['PORT'] || '3000'

# Setting environment variables
ENV['APP_ENV'] = 'development'
ENV['DEBUG'] = 'true'

Environment variables serve multiple purposes: configuration management, feature toggles, credentials storage, and communication between processes. Ruby applications commonly use environment variables for database connections, API keys, application settings, and deployment-specific configuration. The pattern separates configuration from code, following twelve-factor app principles.

# Typical configuration pattern
config = {
  database_url: ENV.fetch('DATABASE_URL'),
  redis_url: ENV.fetch('REDIS_URL', 'redis://localhost:6379'),
  log_level: ENV.fetch('LOG_LEVEL', 'info').downcase,
  worker_count: ENV.fetch('WORKER_COUNT', '2').to_i
}

The ENV object supports standard Hash operations with some restrictions. Keys and values must be strings, and certain system-specific variables may be read-only. Ruby provides methods for safe access, type conversion, and batch operations on environment variables.

Basic Usage

The ENV constant provides Hash-like access to environment variables. Use square bracket notation for direct access, returning nil for missing variables. The fetch method provides safer access with default values and required variable validation.

# Direct access - returns nil if missing
api_key = ENV['API_KEY']
port = ENV['PORT']

# Using fetch with defaults
port = ENV.fetch('PORT', '3000')
timeout = ENV.fetch('TIMEOUT', '30').to_i

# Required variables - raises KeyError if missing
database_url = ENV.fetch('DATABASE_URL')

Setting environment variables modifies the current process environment and affects child processes spawned afterward. Changes only persist for the current Ruby process and its children, not the parent shell or other processes.

# Setting variables
ENV['NODE_ENV'] = 'production'
ENV['DEBUG'] = 'false'

# Unsetting variables
ENV.delete('TEMP_TOKEN')
ENV['CACHE_ENABLED'] = nil  # Alternative deletion method

Environment variable names are case-sensitive on most systems. Ruby preserves the exact case when accessing variables. Convention uses UPPERCASE names with underscores for separation, though Ruby accepts any valid string as a key.

# Case-sensitive access
ENV['PATH']        # System PATH variable
ENV['path']        # Different variable (likely nil)
ENV['My_Variable'] # Valid but unconventional

The each method iterates over all environment variables, yielding key-value pairs. Other enumerable methods work with the ENV object, though they return arrays rather than maintaining the ENV interface.

# Iterating through environment variables
ENV.each do |key, value|
  puts "#{key}: #{value[0..50]}#{'...' if value.length > 50}"
end

# Finding variables matching patterns
app_vars = ENV.select { |key, _| key.start_with?('APP_') }
database_vars = ENV.to_h.select { |key, _| key.include?('DATABASE') }

Ruby converts all environment variable values to strings. Numbers, booleans, and complex data require explicit parsing. Common conversion patterns handle typical configuration types.

# Type conversion patterns
port = ENV.fetch('PORT', '3000').to_i
enabled = ENV.fetch('FEATURE_ENABLED', 'false') == 'true'
timeout = ENV.fetch('TIMEOUT', '30.5').to_f

# Array parsing
allowed_hosts = ENV.fetch('ALLOWED_HOSTS', '').split(',').map(&:strip)
tags = ENV.fetch('TAGS', '').split(' ')

# JSON configuration
config_json = ENV.fetch('CONFIG_JSON', '{}')
config = JSON.parse(config_json)

Error Handling & Debugging

Missing environment variables cause the most common configuration errors. The fetch method raises KeyError for missing required variables, providing clear error messages that identify the missing key.

begin
  database_url = ENV.fetch('DATABASE_URL')
  api_secret = ENV.fetch('API_SECRET')
rescue KeyError => e
  abort "Missing required environment variable: #{e.key}"
end

# Custom error handling with context
def required_env(key, description = nil)
  ENV.fetch(key)
rescue KeyError
  message = "Missing required environment variable: #{key}"
  message += " (#{description})" if description
  raise ArgumentError, message
end

database_url = required_env('DATABASE_URL', 'PostgreSQL connection string')

Type conversion errors occur when environment variables contain unexpected formats. Implement validation and provide meaningful error messages for configuration problems.

def parse_integer(key, default = nil)
  value = ENV[key] || default
  return nil if value.nil?
  
  Integer(value)
rescue ArgumentError
  raise ArgumentError, "Invalid integer value for #{key}: '#{value}'"
end

def parse_boolean(key, default = false)
  value = ENV[key]
  return default if value.nil?
  
  case value.downcase
  when 'true', '1', 'yes', 'on'
    true
  when 'false', '0', 'no', 'off'
    false
  else
    raise ArgumentError, "Invalid boolean value for #{key}: '#{value}'"
  end
end

# Usage with proper error handling
begin
  port = parse_integer('PORT', 3000)
  debug_mode = parse_boolean('DEBUG', false)
rescue ArgumentError => e
  $stderr.puts "Configuration error: #{e.message}"
  exit 1
end

Debugging environment variable issues requires visibility into current values and their sources. Create diagnostic methods that safely display configuration without exposing sensitive data.

# Safe configuration display
def display_config(sensitive_keys = ['PASSWORD', 'SECRET', 'KEY', 'TOKEN'])
  ENV.each do |key, value|
    display_value = if sensitive_keys.any? { |pattern| key.include?(pattern) }
                     '[REDACTED]'
                   elsif value.length > 100
                     "#{value[0..97]}..."
                   else
                     value
                   end
    puts "#{key}=#{display_value}"
  end
end

# Configuration validation with detailed reporting
class ConfigValidator
  def initialize
    @errors = []
    @warnings = []
  end

  def require_env(key, format: :string)
    value = ENV[key]
    if value.nil? || value.empty?
      @errors << "Missing required environment variable: #{key}"
      return nil
    end

    validate_format(key, value, format)
  end

  def validate_format(key, value, format)
    case format
    when :integer
      Integer(value)
    when :url
      URI.parse(value)
      value
    when :email
      raise ArgumentError unless value.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\z/i)
      value
    else
      value
    end
  rescue ArgumentError, URI::InvalidURIError
    @errors << "Invalid #{format} format for #{key}: '#{value}'"
    nil
  end

  def report
    unless @errors.empty?
      $stderr.puts "Configuration errors:"
      @errors.each { |error| $stderr.puts "  - #{error}" }
      exit 1
    end

    unless @warnings.empty?
      $stderr.puts "Configuration warnings:"
      @warnings.each { |warning| $stderr.puts "  - #{warning}" }
    end
  end
end

Environment variable precedence issues arise when multiple sources set the same variable. Document and validate the expected precedence order in configuration loading.

# Configuration precedence with debugging
class ConfigLoader
  PRECEDENCE_ORDER = [
    'command line arguments',
    'environment variables',
    'config file',
    'defaults'
  ].freeze

  def load_config
    config = load_defaults
    merge_config_file(config)
    merge_environment_variables(config)
    merge_command_line_args(config)
    
    log_final_config(config) if debug_mode?
    config
  end

  private

  def merge_environment_variables(config)
    ENV.each do |key, value|
      next unless key.start_with?('APP_')
      
      config_key = key.sub('APP_', '').downcase
      old_value = config[config_key]
      config[config_key] = value
      
      log_override(config_key, old_value, value, 'environment variable') if debug_mode?
    end
  end

  def log_override(key, old_value, new_value, source)
    puts "Config override: #{key} changed from '#{old_value}' to '#{new_value}' (#{source})"
  end
end

Production Patterns

Production environment variable management requires secure, scalable, and maintainable approaches. Organize variables by concern, use consistent naming conventions, and implement proper secret management practices.

# Structured configuration loading
class ProductionConfig
  REQUIRED_VARS = %w[
    DATABASE_URL
    REDIS_URL
    SECRET_KEY_BASE
  ].freeze

  OPTIONAL_VARS = {
    'PORT' => '3000',
    'WORKER_COUNT' => '2',
    'LOG_LEVEL' => 'info',
    'TIMEOUT' => '30'
  }.freeze

  def self.load
    config = {}
    
    # Load required variables
    REQUIRED_VARS.each do |var|
      config[var.downcase] = ENV.fetch(var)
    end
    
    # Load optional variables with defaults
    OPTIONAL_VARS.each do |var, default|
      config[var.downcase] = ENV.fetch(var, default)
    end
    
    # Type conversions
    config['port'] = config['port'].to_i
    config['worker_count'] = config['worker_count'].to_i
    config['timeout'] = config['timeout'].to_i
    
    validate_production_config(config)
    config
  end

  def self.validate_production_config(config)
    raise ArgumentError, "Port must be between 1 and 65535" unless (1..65535).include?(config['port'])
    raise ArgumentError, "Worker count must be positive" unless config['worker_count'] > 0
    raise ArgumentError, "Invalid log level" unless %w[debug info warn error].include?(config['log_level'])
  end
end

Database and external service configuration requires careful URL parsing and connection validation. Implement retry logic and health checks for production reliability.

# Database configuration with connection pooling
class DatabaseConfig
  def self.from_env
    url = ENV.fetch('DATABASE_URL')
    uri = URI.parse(url)
    
    {
      adapter: uri.scheme,
      host: uri.host,
      port: uri.port || default_port(uri.scheme),
      database: uri.path[1..-1],  # Remove leading slash
      username: uri.user,
      password: uri.password,
      pool_size: ENV.fetch('DB_POOL_SIZE', '5').to_i,
      checkout_timeout: ENV.fetch('DB_TIMEOUT', '5').to_f,
      ssl_mode: ENV.fetch('DB_SSL_MODE', 'prefer')
    }
  end

  def self.default_port(scheme)
    case scheme
    when 'postgresql' then 5432
    when 'mysql' then 3306
    when 'sqlite' then nil
    else raise ArgumentError, "Unsupported database scheme: #{scheme}"
    end
  end
end

# Redis configuration with failover
class RedisConfig
  def self.from_env
    primary_url = ENV.fetch('REDIS_URL')
    
    config = {
      url: primary_url,
      timeout: ENV.fetch('REDIS_TIMEOUT', '5').to_f,
      reconnect_attempts: ENV.fetch('REDIS_RECONNECT_ATTEMPTS', '3').to_i
    }
    
    # Sentinel configuration for high availability
    if ENV['REDIS_SENTINELS']
      config[:sentinels] = parse_sentinel_urls(ENV['REDIS_SENTINELS'])
      config[:role] = ENV.fetch('REDIS_ROLE', 'master')
    end
    
    config
  end

  def self.parse_sentinel_urls(urls_string)
    urls_string.split(',').map do |url|
      uri = URI.parse(url.strip)
      { host: uri.host, port: uri.port }
    end
  end
end

Feature flags and runtime configuration through environment variables enable dynamic behavior changes without code deployment. Implement type-safe flag parsing with audit logging.

# Feature flag management
class FeatureFlags
  FLAGS = {
    'NEW_CHECKOUT_FLOW' => :boolean,
    'ANALYTICS_SAMPLING_RATE' => :float,
    'MAX_UPLOAD_SIZE' => :integer,
    'ALLOWED_COUNTRIES' => :array,
    'MAINTENANCE_MODE' => :boolean
  }.freeze

  def self.load
    flags = {}
    
    FLAGS.each do |flag_name, type|
      env_key = "FEATURE_#{flag_name}"
      raw_value = ENV[env_key]
      
      flags[flag_name.downcase] = if raw_value
                                   parse_flag_value(raw_value, type)
                                 else
                                   default_value(type)
                                 end
    end
    
    log_active_flags(flags)
    flags
  end

  def self.parse_flag_value(value, type)
    case type
    when :boolean
      %w[true 1 yes on].include?(value.downcase)
    when :integer
      Integer(value)
    when :float
      Float(value)
    when :array
      value.split(',').map(&:strip)
    else
      value
    end
  rescue ArgumentError
    default_value(type)
  end

  def self.default_value(type)
    case type
    when :boolean then false
    when :integer then 0
    when :float then 0.0
    when :array then []
    else ''
    end
  end

  def self.log_active_flags(flags)
    active_flags = flags.select { |_, value| value && value != 0 && value != [] }
    return if active_flags.empty?
    
    puts "Active feature flags: #{active_flags.keys.join(', ')}"
  end
end

Multi-environment configuration management requires environment-specific variable loading with inheritance and override capabilities. Implement configuration validation across environments.

# Multi-environment configuration
class EnvironmentConfig
  ENVIRONMENTS = %w[development test staging production].freeze
  
  def self.load(env = nil)
    env ||= ENV.fetch('RAILS_ENV', 'development')
    raise ArgumentError, "Invalid environment: #{env}" unless ENVIRONMENTS.include?(env)
    
    config = load_base_config
    config.merge!(load_environment_config(env))
    
    validate_environment_requirements(config, env)
    config
  end

  def self.load_base_config
    {
      'app_name' => ENV.fetch('APP_NAME', 'MyApp'),
      'log_level' => ENV.fetch('LOG_LEVEL', 'info'),
      'session_timeout' => ENV.fetch('SESSION_TIMEOUT', '3600').to_i
    }
  end

  def self.load_environment_config(env)
    case env
    when 'development'
      {
        'database_url' => ENV.fetch('DATABASE_URL', 'sqlite3:db/development.db'),
        'cache_store' => ENV.fetch('CACHE_STORE', 'memory'),
        'debug_mode' => true
      }
    when 'test'
      {
        'database_url' => ENV.fetch('TEST_DATABASE_URL', 'sqlite3:db/test.db'),
        'cache_store' => 'null_store',
        'debug_mode' => false
      }
    when 'production'
      {
        'database_url' => ENV.fetch('DATABASE_URL'),
        'cache_store' => ENV.fetch('CACHE_STORE', 'redis'),
        'debug_mode' => false,
        'force_ssl' => ENV.fetch('FORCE_SSL', 'true') == 'true'
      }
    end
  end

  def self.validate_environment_requirements(config, env)
    case env
    when 'production'
      required_keys = %w[database_url secret_key_base]
      missing_keys = required_keys.select { |key| config[key].nil? || config[key].empty? }
      raise ArgumentError, "Missing production config: #{missing_keys.join(', ')}" unless missing_keys.empty?
    end
  end
end

Testing Strategies

Environment variable testing requires isolation between test cases and controlled variable setup. Use setup and teardown methods to manage test environment state without affecting other tests or the system environment.

# RSpec testing patterns
RSpec.describe 'Environment Variable Configuration' do
  around(:each) do |example|
    original_env = ENV.to_h
    example.run
    ENV.clear
    ENV.update(original_env)
  end

  describe 'required configuration' do
    it 'raises error when DATABASE_URL is missing' do
      ENV.delete('DATABASE_URL')
      
      expect { ProductionConfig.load }.to raise_error(KeyError, /DATABASE_URL/)
    end

    it 'loads configuration with all required variables' do
      ENV['DATABASE_URL'] = 'postgresql://localhost/test'
      ENV['REDIS_URL'] = 'redis://localhost:6379'
      ENV['SECRET_KEY_BASE'] = 'test-secret'
      
      config = ProductionConfig.load
      expect(config['database_url']).to eq('postgresql://localhost/test')
    end
  end

  describe 'optional configuration with defaults' do
    before do
      ENV['DATABASE_URL'] = 'postgresql://localhost/test'
      ENV['REDIS_URL'] = 'redis://localhost:6379'
      ENV['SECRET_KEY_BASE'] = 'test-secret'
    end

    it 'uses default port when not specified' do
      config = ProductionConfig.load
      expect(config['port']).to eq(3000)
    end

    it 'overrides default port when specified' do
      ENV['PORT'] = '4000'
      config = ProductionConfig.load
      expect(config['port']).to eq(4000)
    end
  end
end

Mock environment variables for testing different configuration scenarios. Create helper methods that simplify environment setup for complex test cases.

# Test helpers for environment management
module EnvironmentHelpers
  def with_env(vars)
    original_values = {}
    vars.each do |key, value|
      original_values[key] = ENV[key]
      if value.nil?
        ENV.delete(key)
      else
        ENV[key] = value.to_s
      end
    end
    
    yield
  ensure
    original_values.each do |key, value|
      if value.nil?
        ENV.delete(key)
      else
        ENV[key] = value
      end
    end
  end

  def with_production_env
    with_env(
      'RAILS_ENV' => 'production',
      'DATABASE_URL' => 'postgresql://prod:5432/app',
      'REDIS_URL' => 'redis://prod:6379',
      'SECRET_KEY_BASE' => 'prod-secret-key'
    ) do
      yield
    end
  end

  def with_minimal_env
    with_env(
      'DATABASE_URL' => nil,
      'REDIS_URL' => nil,
      'SECRET_KEY_BASE' => nil
    ) do
      yield
    end
  end
end

RSpec.configure do |config|
  config.include EnvironmentHelpers
end

# Usage in tests
RSpec.describe FeatureFlags do
  describe '#load' do
    it 'parses boolean flags correctly' do
      with_env('FEATURE_NEW_CHECKOUT_FLOW' => 'true') do
        flags = FeatureFlags.load
        expect(flags['new_checkout_flow']).to be true
      end
    end

    it 'handles invalid integer values gracefully' do
      with_env('FEATURE_MAX_UPLOAD_SIZE' => 'invalid') do
        flags = FeatureFlags.load
        expect(flags['max_upload_size']).to eq(0)
      end
    end
  end
end

Integration tests verify that environment variable configuration works correctly with external dependencies. Test configuration loading with realistic values and validate connection establishment.

# Integration testing with real environment setup
RSpec.describe 'Database Integration', type: :integration do
  before(:all) do
    @test_db_url = ENV.fetch('TEST_DATABASE_URL', 'sqlite3::memory:')
    @original_url = ENV['DATABASE_URL']
    ENV['DATABASE_URL'] = @test_db_url
  end

  after(:all) do
    ENV['DATABASE_URL'] = @original_url
  end

  it 'connects to database using environment configuration' do
    config = DatabaseConfig.from_env
    connection = establish_connection(config)
    
    expect(connection).to be_connected
    expect(connection.adapter_name.downcase).to include(URI.parse(@test_db_url).scheme)
  end

  it 'applies connection pool settings from environment' do
    with_env('DB_POOL_SIZE' => '10', 'DB_TIMEOUT' => '2.5') do
      config = DatabaseConfig.from_env
      
      expect(config[:pool_size]).to eq(10)
      expect(config[:checkout_timeout]).to eq(2.5)
    end
  end
end

# Testing environment variable precedence
RSpec.describe 'Configuration Precedence' do
  let(:config_file_path) { 'spec/fixtures/test_config.yml' }
  
  before do
    File.write(config_file_path, {
      'test' => {
        'port' => 2000,
        'worker_count' => 1
      }
    }.to_yaml)
  end

  after do
    File.delete(config_file_path) if File.exist?(config_file_path)
  end

  it 'environment variables override config file values' do
    with_env('PORT' => '3000') do
      config = ConfigLoader.new(config_file_path).load_config
      
      expect(config['port']).to eq('3000')  # From ENV
      expect(config['worker_count']).to eq(1)  # From config file
    end
  end
end

Test configuration validation and error handling to ensure production deployments fail fast with clear error messages. Verify that validation catches common configuration mistakes.

# Configuration validation testing
RSpec.describe ConfigValidator do
  subject(:validator) { ConfigValidator.new }

  describe '#require_env' do
    it 'validates URL format' do
      with_env('API_URL' => 'not-a-url') do
        validator.require_env('API_URL', format: :url)
        
        expect { validator.report }.to output(/Invalid url format/).to_stderr
                                  .and raise_error(SystemExit)
      end
    end

    it 'validates email format' do
      with_env('ADMIN_EMAIL' => 'invalid-email') do
        validator.require_env('ADMIN_EMAIL', format: :email)
        
        expect { validator.report }.to output(/Invalid email format/).to_stderr
                                  .and raise_error(SystemExit)
      end
    end

    it 'passes with valid configuration' do
      with_env(
        'API_URL' => 'https://api.example.com',
        'ADMIN_EMAIL' => 'admin@example.com'
      ) do
        validator.require_env('API_URL', format: :url)
        validator.require_env('ADMIN_EMAIL', format: :email)
        
        expect { validator.report }.not_to raise_error
      end
    end
  end
end

Common Pitfalls

Environment variable values are always strings in Ruby, regardless of shell appearance. Forgetting type conversion causes subtle bugs when comparing or performing arithmetic operations.

# Common mistake - treating ENV values as integers
port = ENV['PORT'] || 3000  # Wrong: may return "3000" (string)
if port > 8000
  puts "Using high port"  # String comparison, not numeric
end

# Correct approach - explicit conversion
port = ENV.fetch('PORT', '3000').to_i
if port > 8000
  puts "Using high port"  # Proper numeric comparison
end

# Boolean conversion pitfall
enabled = ENV['FEATURE_ENABLED'] || false  # Wrong: "false" is truthy
if enabled
  activate_feature  # Always runs if ENV var exists, even if set to "false"
end

# Correct boolean parsing
enabled = ENV.fetch('FEATURE_ENABLED', 'false') == 'true'
if enabled
  activate_feature  # Only runs when explicitly set to "true"
end

Missing environment variables cause nil return values, leading to NoMethodError exceptions when chaining methods. The fetch method with defaults prevents these runtime errors.

# Pitfall - chaining methods on potentially nil values
config_json = ENV['CONFIG_JSON']
config = JSON.parse(config_json)  # NoMethodError if CONFIG_JSON is nil

# Safe approach with validation
config_json = ENV.fetch('CONFIG_JSON', '{}')
config = JSON.parse(config_json)

# Another common mistake
database_host = ENV['DATABASE_HOST'].downcase  # NoMethodError if nil
database_port = ENV['DATABASE_PORT'].to_i      # NoMethodError if nil

# Defensive programming
database_host = ENV.fetch('DATABASE_HOST', 'localhost').downcase
database_port = ENV.fetch('DATABASE_PORT', '5432').to_i

Environment variable precedence confusion occurs when multiple sources set the same variable. System environment variables take precedence over Ruby assignments, causing unexpected behavior.

# Misleading behavior - system ENV trumps Ruby assignment
ENV['PATH'] = '/custom/path'
puts ENV['PATH']  # Still shows system PATH, not '/custom/path'

# Child process inheritance gotcha
ENV['DEBUG'] = 'true'
system('ruby -e "puts ENV[\'DEBUG\']"')  # Prints 'true' in child process

ENV.delete('DEBUG')
system('ruby -e "puts ENV[\'DEBUG\']"')  # Prints nothing - variable removed

String encoding issues arise when environment variables contain non-ASCII characters or when different systems use different default encodings. Ruby handles encoding transparently in most cases but problems occur with binary data.

# Potential encoding issues
api_key = ENV['API_KEY']  # May contain non-ASCII characters
if api_key.ascii_only?
  # Safe for ASCII-only protocols
else
  # Handle encoding explicitly
  api_key = api_key.force_encoding('UTF-8')
end

# Base64 encoded secrets
encoded_secret = ENV['SECRET_KEY']
begin
  secret = Base64.decode64(encoded_secret)
rescue ArgumentError => e
  raise "Invalid base64 in SECRET_KEY: #{e.message}"
end

Security pitfalls include accidentally logging sensitive environment variables and exposing secrets through error messages or debugging output.

# Security mistake - logging all environment variables
ENV.each { |k, v| Rails.logger.info "#{k}=#{v}" }  # Exposes secrets

# Safe logging approach
SENSITIVE_KEYS = %w[PASSWORD SECRET KEY TOKEN API_KEY].freeze

ENV.each do |key, value|
  display_value = if SENSITIVE_KEYS.any? { |pattern| key.include?(pattern) }
                   '[REDACTED]'
                 else
                   value
                 end
  Rails.logger.info "#{key}=#{display_value}"
end

# Error message exposure
begin
  database_url = ENV.fetch('DATABASE_URL')
  uri = URI.parse(database_url)
rescue URI::InvalidURIError => e
  # Bad - exposes URL in error message
  raise "Invalid DATABASE_URL: #{database_url} - #{e.message}"
  
  # Better - generic error message
  raise "Invalid DATABASE_URL format - check configuration"
end

Variable naming conflicts occur when environment variables override important system variables or when naming conventions clash between different applications sharing the same environment.

# Dangerous - overriding system variables
ENV['PATH'] = '/app/bin'  # Can break system commands
ENV['HOME'] = '/tmp'      # May break applications expecting user home

# Application variable conflicts
ENV['PORT'] = '3000'      # Your app's port
ENV['PORT'] = '4000'      # Another app tries to set different port

# Safe naming with prefixes
ENV['MYAPP_PORT'] = '3000'
ENV['MYAPP_DEBUG'] = 'true'
ENV['MYAPP_LOG_LEVEL'] = 'info'

# Namespace separation
class AppConfig
  PREFIX = 'MYAPP_'.freeze

  def self.env_key(key)
    "#{PREFIX}#{key.upcase}"
  end

  def self.get(key, default = nil)
    ENV.fetch(env_key(key), default)
  end

  def self.set(key, value)
    ENV[env_key(key)] = value.to_s
  end
end

Test environment pollution happens when tests modify environment variables without proper cleanup, causing test interdependence and flaky results.

# Problem - tests affecting each other
RSpec.describe 'Config A' do
  it 'sets up config' do
    ENV['FEATURE_X'] = 'true'
    # Test passes but ENV persists
  end
end

RSpec.describe 'Config B' do
  it 'assumes clean environment' do
    # Fails because FEATURE_X still set from previous test
    expect(ENV['FEATURE_X']).to be_nil
  end
end

# Solution - proper test isolation
RSpec.describe 'Configuration' do
  around(:each) do |example|
    original_env = ENV.to_h.dup
    example.run
  ensure
    ENV.clear
    ENV.update(original_env)
  end
end

Reference

ENV Object Methods

Method Parameters Returns Description
ENV[key] key (String) String or nil Direct access to environment variable
ENV[key] = value key (String), value (String) String Set environment variable
ENV.fetch(key) key (String) String Get required variable, raises KeyError if missing
ENV.fetch(key, default) key (String), default (String) String Get variable with default value
ENV.delete(key) key (String) String or nil Remove variable, returns previous value
ENV.key?(key) key (String) Boolean Check if variable exists
ENV.has_key?(key) key (String) Boolean Alias for key?
ENV.empty? None Boolean Check if no environment variables set
ENV.size None Integer Number of environment variables
ENV.clear None Hash Remove all environment variables
ENV.keys None Array<String> Array of all variable names
ENV.values None Array<String> Array of all variable values
ENV.each Block Hash Iterate over key-value pairs
ENV.select Block Array Filter variables by condition
ENV.reject Block Array Exclude variables by condition
ENV.to_h None Hash Convert to regular Hash
ENV.inspect None String String representation for debugging

Common Type Conversion Patterns

Type Pattern Example
Integer ENV.fetch(key, default).to_i ENV.fetch('PORT', '3000').to_i
Float ENV.fetch(key, default).to_f ENV.fetch('RATE', '1.5').to_f
Boolean ENV.fetch(key, 'false') == 'true' ENV.fetch('DEBUG', 'false') == 'true'
Array (comma) ENV.fetch(key, '').split(',') ENV.fetch('TAGS', '').split(',')
Array (space) ENV.fetch(key, '').split ENV.fetch('HOSTS', '').split
JSON JSON.parse(ENV.fetch(key, '{}')) JSON.parse(ENV.fetch('CONFIG', '{}'))
URI URI.parse(ENV.fetch(key)) URI.parse(ENV.fetch('DATABASE_URL'))

Standard Environment Variables

Variable Purpose Typical Values
PATH Executable search path /usr/bin:/bin:/usr/local/bin
HOME User home directory /home/username
USER Current username username
SHELL Default shell /bin/bash
TERM Terminal type xterm-256color
LANG System locale en_US.UTF-8
PWD Current working directory /path/to/directory
TMPDIR Temporary directory /tmp

Application Configuration Variables

Variable Purpose Example Value
DATABASE_URL Database connection postgresql://user:pass@host:5432/db
REDIS_URL Redis connection redis://localhost:6379/0
SECRET_KEY_BASE Application secret 64-character-hex-string
RAILS_ENV Application environment production
PORT Server port 3000
RACK_ENV Rack environment production
LOG_LEVEL Logging verbosity info
WORKER_COUNT Process count 2

Error Types and Handling

Error Class Cause Handling Strategy
KeyError Missing required variable Use ENV.fetch with meaningful error messages
ArgumentError Invalid type conversion Validate format before conversion
URI::InvalidURIError Malformed URL Parse and validate URL format
JSON::ParserError Invalid JSON string Provide default JSON and validate structure
NoMethodError Method called on nil Use fetch with defaults or check presence

Boolean Value Conventions

True Values False Values
"true" "false"
"1" "0"
"yes" "no"
"on" "off"
"enabled" "disabled"

Security Best Practices

  • Never log environment variables containing sensitive data
  • Use prefixed names to avoid system variable conflicts
  • Validate variable formats before use
  • Use fetch instead of direct access for required variables
  • Store secrets in specialized secret management systems when possible
  • Rotate secrets regularly and update environment configuration
  • Use least-privilege access patterns for service accounts