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