CrackedRuby CrackedRuby

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

  1. Application defaults defined in code
  2. Configuration file defaults (if used)
  3. Environment-specific configuration files
  4. Local configuration files (.env.local)
  5. Environment variables from process environment
  6. 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])