Rails Integration
Use KeyEnv with Ruby on Rails applications for secure secrets management.
Rails Integration
Integrate KeyEnv with your Ruby on Rails application to securely manage secrets like database credentials, API keys, and Rails secret key base.
Features
- Load secrets before Rails initializes
- Environment-aware configuration (development, test, production)
- Works with Rails credentials and encrypted secrets
- Supports database.yml, storage.yml, and other config files
- Built-in caching for serverless environments
Installation
Add the KeyEnv gem to your Gemfile:
# Gemfile
gem 'keyenv'Then install:
bundle installQuick Start
Create an initializer to load secrets at boot:
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
require 'keyenv'
client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
client.load_env(
project_id: ENV['KEYENV_PROJECT'],
environment: Rails.env
)
endNow use secrets in your configuration:
# config/application.rb
config.secret_key_base = ENV['SECRET_KEY_BASE']Rails Initializer Setup
Basic Initializer
The simplest approach loads all secrets into ENV at startup:
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
require 'keyenv'
# Map Rails.env to KeyEnv environment
keyenv_environment = case Rails.env
when 'development' then 'development'
when 'test' then 'development'
when 'staging' then 'staging'
when 'production' then 'production'
else Rails.env
end
client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
count = client.load_env(
project_id: ENV['KEYENV_PROJECT'],
environment: keyenv_environment
)
Rails.logger.info "KeyEnv: Loaded #{count} secrets for #{keyenv_environment}" if defined?(Rails.logger)
endWith Caching (Serverless)
For serverless environments like AWS Lambda, enable caching:
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
require 'keyenv'
client = KeyEnv.new(
token: ENV['KEYENV_TOKEN'],
cache_ttl: 300 # Cache for 5 minutes
)
client.load_env(
project_id: ENV['KEYENV_PROJECT'],
environment: Rails.env
)
endEarly Loading with boot.rb
For secrets needed before initializers run (like database credentials), load in boot.rb:
# config/boot.rb
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup'
require 'bootsnap/setup' if defined?(Bootsnap)
# Load KeyEnv secrets before Rails initializes
if ENV['KEYENV_TOKEN'].present?
require 'keyenv'
rails_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
client.load_env(
project_id: ENV['KEYENV_PROJECT'],
environment: rails_env
)
endEnvironment-Aware Configuration
Automatic Environment Detection
Map Rails environments to KeyEnv environments:
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
require 'keyenv'
def detect_keyenv_environment
# Check explicit override first
return ENV['KEYENV_ENV'] if ENV['KEYENV_ENV'].present?
# Map Rails.env to KeyEnv environment
case Rails.env
when 'development', 'test'
'development'
when 'staging', 'review'
'staging'
when 'production'
'production'
else
Rails.env
end
end
client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
client.load_env(
project_id: ENV['KEYENV_PROJECT'],
environment: detect_keyenv_environment
)
endPlatform-Specific Detection
Detect environment from deployment platforms:
# config/initializers/keyenv.rb
def detect_keyenv_environment
return ENV['KEYENV_ENV'] if ENV['KEYENV_ENV'].present?
# Heroku
if ENV['HEROKU_APP_NAME'].present?
return ENV['HEROKU_APP_NAME'].include?('staging') ? 'staging' : 'production'
end
# Render
return 'production' if ENV['RENDER'].present?
# Railway
return ENV['RAILWAY_ENVIRONMENT'] if ENV['RAILWAY_ENVIRONMENT'].present?
# Fly.io
return 'production' if ENV['FLY_APP_NAME'].present?
# Fall back to Rails.env mapping
case Rails.env
when 'development', 'test' then 'development'
when 'staging' then 'staging'
else 'production'
end
endDatabase Configuration
Using database.yml with ERB
Reference secrets in database.yml:
# config/database.yml
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("DB_POOL", 5) %>
development:
<<: *default
database: myapp_development
test:
<<: *default
database: myapp_test
production:
<<: *default
url: <%= ENV['DATABASE_URL'] %>Store in KeyEnv:
DATABASE_URL=postgres://user:password@host:5432/myapp_production
DB_POOL=25Individual Database Credentials
For more control, use individual variables:
# config/database.yml
production:
adapter: postgresql
encoding: unicode
host: <%= ENV['DB_HOST'] %>
port: <%= ENV.fetch('DB_PORT', 5432) %>
database: <%= ENV['DB_NAME'] %>
username: <%= ENV['DB_USER'] %>
password: <%= ENV['DB_PASSWORD'] %>
pool: <%= ENV.fetch('DB_POOL', 5) %>Store in KeyEnv:
DB_HOST=your-db-host.amazonaws.com
DB_PORT=5432
DB_NAME=myapp_production
DB_USER=myapp
DB_PASSWORD=secure-passwordStorage Configuration
Active Storage with S3
# config/storage.yml
amazon:
service: S3
access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
region: <%= ENV.fetch('AWS_REGION', 'us-east-1') %>
bucket: <%= ENV['AWS_S3_BUCKET'] %>Store in KeyEnv:
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=secret...
AWS_REGION=us-east-1
AWS_S3_BUCKET=myapp-productionActive Storage with Google Cloud
# config/storage.yml
google:
service: GCS
project: <%= ENV['GCS_PROJECT'] %>
credentials: <%= ENV['GCS_CREDENTIALS'] %>
bucket: <%= ENV['GCS_BUCKET'] %>Rails Credentials Integration
Hybrid Approach
Use KeyEnv for deployment secrets and Rails credentials for development:
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
require 'keyenv'
client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
client.load_env(
project_id: ENV['KEYENV_PROJECT'],
environment: Rails.env
)
else
# Fall back to Rails credentials in development
Rails.logger.info 'KeyEnv: Using Rails credentials (KEYENV_TOKEN not set)'
endAccessing Secrets
Create a helper that checks ENV first, then falls back to credentials:
# config/initializers/secrets.rb
module Secrets
class << self
def fetch(key, default = nil)
ENV[key.to_s.upcase] || Rails.application.credentials.dig(key) || default
end
def [](key)
fetch(key)
end
end
endUsage:
Secrets.fetch(:stripe_secret_key)
Secrets[:aws_access_key_id]Configuration Patterns
Action Mailer
# config/environments/production.rb
config.action_mailer.smtp_settings = {
address: ENV.fetch('SMTP_HOST', 'smtp.gmail.com'),
port: ENV.fetch('SMTP_PORT', 587).to_i,
user_name: ENV['SMTP_USERNAME'],
password: ENV['SMTP_PASSWORD'],
authentication: :plain,
enable_starttls_auto: true
}Redis / Sidekiq
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') }
end
Sidekiq.configure_client do |config|
config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') }
endAction Cable
# config/cable.yml
production:
adapter: redis
url: <%= ENV.fetch('REDIS_URL', 'redis://localhost:6379/1') %>
channel_prefix: myapp_productionComplete Example
Here's a complete Rails configuration using KeyEnv:
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
require 'keyenv'
KEYENV_ENV_MAP = {
'development' => 'development',
'test' => 'development',
'staging' => 'staging',
'production' => 'production'
}.freeze
keyenv_env = ENV.fetch('KEYENV_ENV') { KEYENV_ENV_MAP.fetch(Rails.env, Rails.env) }
begin
client = KeyEnv.new(
token: ENV['KEYENV_TOKEN'],
timeout: 10,
cache_ttl: Rails.env.production? ? 300 : 0
)
count = client.load_env(
project_id: ENV['KEYENV_PROJECT'],
environment: keyenv_env
)
Rails.logger.info "KeyEnv: Loaded #{count} secrets for #{keyenv_env}" if defined?(Rails.logger)
rescue KeyEnv::Error => e
Rails.logger.error "KeyEnv: Failed to load secrets - #{e.message}" if defined?(Rails.logger)
raise if Rails.env.production? # Fail hard in production
end
end# config/database.yml
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("DB_POOL", 5) %>
development:
<<: *default
database: myapp_development
test:
<<: *default
database: myapp_test
production:
<<: *default
url: <%= ENV['DATABASE_URL'] %># config/environments/production.rb
Rails.application.configure do
# Secret key base
config.secret_key_base = ENV['SECRET_KEY_BASE']
# Force SSL
config.force_ssl = true
# Action Mailer
config.action_mailer.smtp_settings = {
address: ENV['SMTP_HOST'],
port: ENV.fetch('SMTP_PORT', 587).to_i,
user_name: ENV['SMTP_USERNAME'],
password: ENV['SMTP_PASSWORD'],
authentication: :plain,
enable_starttls_auto: true
}
# Logging
config.log_level = ENV.fetch('LOG_LEVEL', 'info').to_sym
end# config/initializers/stripe.rb
Stripe.api_key = ENV['STRIPE_SECRET_KEY']# config/initializers/sentry.rb
if ENV['SENTRY_DSN'].present?
Sentry.init do |config|
config.dsn = ENV['SENTRY_DSN']
config.environment = Rails.env
config.traces_sample_rate = 0.1
end
endSetting Up Your Deployment
Required Environment Variables
Set these in your deployment platform (not in KeyEnv):
| Variable | Description |
|---|---|
KEYENV_TOKEN | Your KeyEnv service token |
KEYENV_PROJECT | Your KeyEnv project ID |
KEYENV_ENV | Override environment detection (optional) |
Secrets to Store in KeyEnv
Store these secrets in KeyEnv for each environment:
| Secret | Description | Example |
|---|---|---|
SECRET_KEY_BASE | Rails secret key | rails secret output |
DATABASE_URL | Database connection | postgres://user:pass@host/db |
REDIS_URL | Redis connection | redis://localhost:6379/0 |
SMTP_PASSWORD | Email password | app-specific-password |
AWS_ACCESS_KEY_ID | AWS credentials | AKIA... |
AWS_SECRET_ACCESS_KEY | AWS secret | ... |
STRIPE_SECRET_KEY | Stripe API key | sk_live_... |
SENTRY_DSN | Sentry DSN | https://[email protected]/... |
Generate a Rails Secret Key
rails secretLocal Development
Without KeyEnv
For local development without KeyEnv, the initializer gracefully skips loading:
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
# ... KeyEnv loading
end
# Falls back to local environment variables or Rails credentialsUsing the CLI
Generate a .env file for local development:
keyenv pull --env development --output .envThen use dotenv to load it:
# Gemfile (development group)
group :development, :test do
gem 'dotenv-rails'
endThe dotenv gem loads .env automatically before Rails initializers.
Testing
Test Environment Setup
Skip KeyEnv in tests by not setting KEYENV_TOKEN:
# spec/rails_helper.rb
# Test environment variables
ENV['SECRET_KEY_BASE'] = 'test-secret-key-base'
ENV['DATABASE_URL'] = 'postgres://localhost/myapp_test'Or use a separate test environment in KeyEnv:
# config/initializers/keyenv.rb
keyenv_env = case Rails.env
when 'test' then 'test' # Use KeyEnv test environment
# ...
endRSpec Configuration
# spec/support/keyenv.rb
RSpec.configure do |config|
config.before(:suite) do
# Ensure test secrets are available
ENV['SECRET_KEY_BASE'] ||= 'test-secret'
ENV['STRIPE_SECRET_KEY'] ||= 'sk_test_fake'
end
endTroubleshooting
"KEYENV_TOKEN not set"
In production, ensure KEYENV_TOKEN is set in your deployment platform's environment variables.
"Project not found"
Verify KEYENV_PROJECT matches your project ID in the KeyEnv dashboard.
"Environment not found"
Check that the environment name exists in your KeyEnv project and matches your Rails environment mapping.
Secrets not loading in database.yml
If database credentials aren't available when Rails initializes:
- Move KeyEnv loading to
config/boot.rb(before database.yml is processed) - Or use
DATABASE_URLwhich Rails handles specially
Debugging
Enable debug logging:
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
require 'keyenv'
client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
begin
secrets = client.export_secrets(
project_id: ENV['KEYENV_PROJECT'],
environment: Rails.env
)
Rails.logger.debug "KeyEnv: Found #{secrets.count} secrets"
secrets.each { |s| Rails.logger.debug "KeyEnv: - #{s.key}" }
client.load_env(project_id: ENV['KEYENV_PROJECT'], environment: Rails.env)
rescue KeyEnv::Error => e
Rails.logger.error "KeyEnv: #{e.class} - #{e.message}"
raise
end
endPerformance
load_env makes a single API call at startup. Secrets are cached in ENV for the lifetime of the process. There's no runtime overhead after initialization.
For serverless environments, use cache_ttl to avoid repeated API calls on warm starts.