Environment Variables
Master Ruby's environment variable handling with the ENV hash. Learn to access system environment variables, manage configuration securely, and implement environment-specific behavior for development, testing, and production deployments.
What are Environment Variables?
Environment Variables: Key-value pairs that exist in the operating system environment. They provide a way to configure applications without changing code, store sensitive information securely, and adapt behavior based on deployment environment.
Why Use Environment Variables?
- Security: Keep secrets out of source code
- Flexibility: Different configurations per environment
- Deployment: Easy configuration changes without code updates
- Twelve-Factor Apps: Follow modern application principles
Common Environment Variables
Accessing Environment Variables
Basic ENV Access
# Access environment variables
puts ENV["HOME"] # User's home directory
puts ENV["PATH"] # System PATH
puts ENV["USER"] # Current username
# Check if variable exists
puts ENV["DATABASE_URL"] # nil if not set
puts ENV.key?("NODE_ENV") # true/false
# Get with default value
database_url = ENV["DATABASE_URL"] || "sqlite3://localhost/development.db"
port = ENV["PORT"] || "3000"
# More elegant default handling
def env_var(name, default = nil)
ENV[name] || default
end
host = env_var("HOST", "localhost")
debug_mode = env_var("DEBUG", "false") == "true"
# Fetch with error handling
begin
api_key = ENV.fetch("API_KEY")
rescue KeyError
puts "API_KEY environment variable is required"
exit 1
end
# Fetch with default
redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379")
# Environment-specific behavior
case ENV["RAILS_ENV"]
when "development"
puts "Running in development mode"
when "production"
puts "Running in production mode"
when "test"
puts "Running in test mode"
else
puts "Unknown environment"
end
Iterating Through Environment Variables
# Print all environment variables
ENV.each do |key, value|
puts "#{key}: #{value}"
end
# Find variables matching a pattern
app_vars = ENV.select { |key, value| key.start_with?("APP_") }
app_vars.each { |key, value| puts "#{key}: #{value}" }
# Get all variable names
puts "Environment variables:"
ENV.keys.sort.each { |key| puts " #{key}" }
# Filter sensitive information
safe_vars = ENV.reject { |key, value| key.include?("SECRET") || key.include?("PASSWORD") }
safe_vars.each { |key, value| puts "#{key}: #{value}" }
# Convert to hash for manipulation
env_hash = ENV.to_h
database_vars = env_hash.select { |key, value| key.start_with?("DATABASE_") }
# Check for required variables
required_vars = ["DATABASE_URL", "SECRET_KEY_BASE", "RAILS_ENV"]
missing_vars = required_vars.reject { |var| ENV.key?(var) }
if missing_vars.any?
puts "Missing required environment variables: #{missing_vars.join(', ')}"
exit 1
end
Environment Variable Types
Environment variables are always strings. Convert them appropriately:
# String to integer
port = ENV["PORT"]&.to_i || 3000
# String to boolean
debug = ENV["DEBUG"] == "true"
verbose = %w[true yes 1 on].include?(ENV["VERBOSE"]&.downcase)
# String to array
allowed_hosts = ENV["ALLOWED_HOSTS"]&.split(",") || ["localhost"]
# String to float
timeout = ENV["TIMEOUT"]&.to_f || 30.0
Setting Environment Variables
Setting Variables in Ruby
# Set environment variables at runtime
ENV["MY_APP_CONFIG"] = "production"
ENV["DEBUG_MODE"] = "true"
# Set multiple variables
ENV.update({
"APP_NAME" => "MyRubyApp",
"APP_VERSION" => "1.0.0",
"LOG_LEVEL" => "info"
})
# Delete environment variables
ENV.delete("TEMP_VAR")
# Set variables conditionally
ENV["DEFAULT_TIMEOUT"] ||= "30" # Only set if not already present
# Temporary environment changes
original_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "test"
# ... do something in test environment ...
ENV["RAILS_ENV"] = original_env # Restore
# Using a block for temporary changes
def with_env(changes)
original = changes.map { |key, _| [key, ENV[key]] }.to_h
begin
ENV.update(changes)
yield
ensure
original.each { |key, value| value ? ENV[key] = value : ENV.delete(key) }
end
end
# Usage
with_env("RAILS_ENV" => "test", "DEBUG" => "true") do
# Code runs with temporary environment
puts ENV["RAILS_ENV"] # "test"
end
# Environment is restored automatically
Shell Commands and Scripts
# Unix/Linux/macOS shell commands
# Set for current session
export DATABASE_URL="postgresql://localhost/myapp"
export RAILS_ENV="development"
export DEBUG="true"
# Set for single command
DATABASE_URL="postgresql://localhost/test" rails console
# Set multiple variables for single command
RAILS_ENV="test" DEBUG="true" bundle exec rspec
# Add to shell profile (~/.bashrc, ~/.zshrc)
echo 'export API_KEY="your-secret-key"' >> ~/.bashrc
# Load environment from file
source .env # or
. .env
# Windows Command Prompt
set DATABASE_URL=postgresql://localhost/myapp
set RAILS_ENV=development
# Windows PowerShell
$env:DATABASE_URL = "postgresql://localhost/myapp"
$env:RAILS_ENV = "development"
Environment Variable Persistence
- Shell Session: Variables last until session ends
- Shell Profile: Add to ~/.bashrc or ~/.zshrc for persistence
- .env Files: Use with dotenv gem for project-specific variables
- System-wide: Add to /etc/environment (Linux) or system settings
- Docker/Containers: Set in Dockerfile or docker-compose.yml
.env Files and dotenv Gem
Using dotenv Gem
# Add to Gemfile
gem 'dotenv-rails', groups: [:development, :test]
# Install
bundle install
# Create .env file in project root
# .env
DATABASE_URL=postgresql://localhost/myapp_development
REDIS_URL=redis://localhost:6379/0
SECRET_KEY_BASE=your-secret-key-here
API_KEY=your-api-key
DEBUG=true
LOG_LEVEL=debug
ALLOWED_HOSTS=localhost,127.0.0.1
SMTP_HOST=smtp.example.com
SMTP_PORT=587
# Load in Rails application
# config/application.rb (Rails automatically loads .env files with dotenv-rails)
# Load manually in non-Rails apps
require 'dotenv/load'
# Or load explicitly
require 'dotenv'
Dotenv.load('.env')
# Load environment-specific files
Dotenv.load('.env.local', '.env') # .env.local overrides .env
# Verify required variables
Dotenv.require_keys("DATABASE_URL", "SECRET_KEY_BASE")
# Access variables normally
puts ENV["DATABASE_URL"]
puts ENV["API_KEY"]
Environment-Specific .env Files
# File structure
.env # Default environment variables
.env.local # Local overrides (gitignored)
.env.development # Development-specific
.env.test # Test-specific
.env.production # Production-specific
# .env (shared defaults)
APP_NAME=MyRubyApp
LOG_LEVEL=info
TIMEOUT=30
# .env.development
DATABASE_URL=postgresql://localhost/myapp_development
REDIS_URL=redis://localhost:6379/0
DEBUG=true
LOG_LEVEL=debug
# .env.test
DATABASE_URL=postgresql://localhost/myapp_test
REDIS_URL=redis://localhost:6379/1
DEBUG=false
LOG_LEVEL=warn
# .env.production
DATABASE_URL=postgresql://prod-host/myapp_production
REDIS_URL=redis://prod-redis:6379/0
DEBUG=false
LOG_LEVEL=error
# Custom loading logic
require 'dotenv'
class EnvironmentLoader
def self.load
env = ENV["RAILS_ENV"] || "development"
files = [
".env.#{env}.local",
".env.local",
".env.#{env}",
".env"
].select { |file| File.exist?(file) }
Dotenv.load(*files)
end
end
EnvironmentLoader.load
Security Best Practices
- Never commit secrets: Add .env* to .gitignore
- Use .env.example: Template file without actual secrets
- Rotate secrets regularly: Change API keys and passwords
- Principle of least privilege: Only grant necessary permissions
- Environment isolation: Different secrets per environment
Configuration Management Patterns
Configuration Class Pattern
class AppConfig
class << self
def database_url
ENV.fetch("DATABASE_URL") { "sqlite3://localhost/development.db" }
end
def redis_url
ENV.fetch("REDIS_URL") { "redis://localhost:6379/0" }
end
def debug_mode?
%w[true yes 1 on].include?(ENV["DEBUG"]&.downcase)
end
def port
ENV.fetch("PORT") { "3000" }.to_i
end
def timeout
ENV.fetch("TIMEOUT") { "30" }.to_f
end
def allowed_hosts
ENV.fetch("ALLOWED_HOSTS") { "localhost" }.split(",").map(&:strip)
end
def api_key
ENV.fetch("API_KEY") do
raise "API_KEY environment variable is required"
end
end
def log_level
level = ENV.fetch("LOG_LEVEL") { "info" }.downcase
%w[debug info warn error fatal].include?(level) ? level : "info"
end
def environment
ENV.fetch("RAILS_ENV") { ENV.fetch("RACK_ENV") { "development" } }
end
def production?
environment == "production"
end
def development?
environment == "development"
end
def test?
environment == "test"
end
end
end
# Usage
puts "Starting app on port #{AppConfig.port}"
puts "Debug mode: #{AppConfig.debug_mode?}"
puts "Environment: #{AppConfig.environment}"
Structured Configuration
require 'ostruct'
class Config
def self.load
OpenStruct.new(
app: OpenStruct.new(
name: ENV.fetch("APP_NAME") { "MyRubyApp" },
version: ENV.fetch("APP_VERSION") { "1.0.0" },
host: ENV.fetch("HOST") { "localhost" },
port: ENV.fetch("PORT") { "3000" }.to_i,
timeout: ENV.fetch("TIMEOUT") { "30" }.to_f
),
database: OpenStruct.new(
url: ENV.fetch("DATABASE_URL"),
pool_size: ENV.fetch("DB_POOL_SIZE") { "5" }.to_i,
timeout: ENV.fetch("DB_TIMEOUT") { "5000" }.to_i
),
redis: OpenStruct.new(
url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/0" },
timeout: ENV.fetch("REDIS_TIMEOUT") { "1" }.to_f
),
logging: OpenStruct.new(
level: ENV.fetch("LOG_LEVEL") { "info" },
file: ENV["LOG_FILE"], # Optional
syslog: ENV["SYSLOG"] == "true"
),
features: OpenStruct.new(
debug: ENV["DEBUG"] == "true",
analytics: ENV.fetch("ANALYTICS") { "true" } == "true",
notifications: ENV.fetch("NOTIFICATIONS") { "true" } == "true"
),
external: OpenStruct.new(
api_key: ENV.fetch("API_KEY"),
api_url: ENV.fetch("API_URL") { "https://api.example.com" },
webhook_secret: ENV["WEBHOOK_SECRET"]
)
)
end
end
# Initialize configuration
CONFIG = Config.load
# Usage
puts "App: #{CONFIG.app.name} v#{CONFIG.app.version}"
puts "Running on #{CONFIG.app.host}:#{CONFIG.app.port}"
puts "Database: #{CONFIG.database.url}"
puts "Debug mode: #{CONFIG.features.debug}"
Practical Examples
Database Connection Manager
require 'uri'
class DatabaseManager
def self.connection_params
url = ENV.fetch("DATABASE_URL") do
raise "DATABASE_URL environment variable is required"
end
uri = URI.parse(url)
{
adapter: adapter_from_scheme(uri.scheme),
host: uri.host,
port: uri.port,
database: uri.path[1..-1], # Remove leading slash
username: uri.user,
password: uri.password,
pool: ENV.fetch("DB_POOL_SIZE") { "5" }.to_i,
timeout: ENV.fetch("DB_TIMEOUT") { "5000" }.to_i,
encoding: ENV.fetch("DB_ENCODING") { "utf8" },
ssl: ENV["DB_SSL"] == "true"
}
end
def self.adapter_from_scheme(scheme)
case scheme
when "postgresql", "postgres"
"postgresql"
when "mysql", "mysql2"
"mysql2"
when "sqlite3", "sqlite"
"sqlite3"
else
raise "Unsupported database scheme: #{scheme}"
end
end
def self.test_connection
params = connection_params
puts "Testing connection to #{params[:adapter]} database..."
puts "Host: #{params[:host]}:#{params[:port]}"
puts "Database: #{params[:database]}"
puts "Username: #{params[:username]}"
# Test actual connection here
end
end
# Usage
begin
DatabaseManager.test_connection
rescue => e
puts "Database connection failed: #{e.message}"
exit 1
end
Multi-Service Configuration
class ServiceConfig
SERVICES = %w[database redis email storage monitoring].freeze
def self.validate!
errors = []
# Check required environment variables
required_vars = [
"DATABASE_URL",
"SECRET_KEY_BASE",
"RAILS_ENV"
]
missing = required_vars.reject { |var| ENV.key?(var) }
errors << "Missing required variables: #{missing.join(', ')}" if missing.any?
# Validate email service if enabled
if email_enabled?
email_vars = ["SMTP_HOST", "SMTP_PORT", "SMTP_USERNAME", "SMTP_PASSWORD"]
missing_email = email_vars.reject { |var| ENV.key?(var) }
errors << "Email enabled but missing: #{missing_email.join(', ')}" if missing_email.any?
end
# Validate storage service
if storage_service == "s3"
s3_vars = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_BUCKET"]
missing_s3 = s3_vars.reject { |var| ENV.key?(var) }
errors << "S3 storage selected but missing: #{missing_s3.join(', ')}" if missing_s3.any?
end
if errors.any?
puts "Configuration errors:"
errors.each { |error| puts " - #{error}" }
exit 1
end
end
def self.email_enabled?
ENV.fetch("EMAIL_ENABLED") { "false" } == "true"
end
def self.storage_service
ENV.fetch("STORAGE_SERVICE") { "local" }
end
def self.monitoring_enabled?
ENV.fetch("MONITORING_ENABLED") { "false" } == "true"
end
def self.print_summary
puts "Service Configuration Summary:"
puts " Environment: #{ENV['RAILS_ENV']}"
puts " Email: #{email_enabled? ? 'Enabled' : 'Disabled'}"
puts " Storage: #{storage_service.upcase}"
puts " Monitoring: #{monitoring_enabled? ? 'Enabled' : 'Disabled'}"
puts " Debug mode: #{ENV['DEBUG'] == 'true' ? 'On' : 'Off'}"
end
end
# Validate configuration on startup
ServiceConfig.validate!
ServiceConfig.print_summary