Ruby Logo

Environment Variables

Master environment variable management with ENV, dotenv gem, and secure configuration practices.

Home Ruby Environment Variables

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

PATH
System executable directories
HOME
User's home directory
USER
Current username
RAILS_ENV
Rails environment (development/production)
DATABASE_URL
Database connection string
API_KEY
Third-party service credentials

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

Quick Navigation

Related Topics

Video Tutorial

Watch and learn environment variables

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!