Skip to content
ExponaBLOG
RUBY ON RAILS · DAY 037 / 100

Rails environments and configuration: a practical guide

Settings per environment.

Expona Admin6 min read

Introduction

Your Rails application behaves differently in development than it does in production, and that's by design. Rails environments and configuration: a practical guide walks us through how to manage those differences safely and predictably. We'll explore the built-in mechanisms Rails gives us—environment files, environment variables, and custom configuration—so that our team ships code that works locally, passes CI/CD, and runs reliably on servers.

Understanding config/environments files

Rails ships with three default environments: development, test, and production. Each has its own configuration file in the config/environments/ directory. These files run early in the Rails boot sequence and set framework-level defaults for that specific context.

In development, we want verbose logging, asset serving on the fly, and detailed error pages. In production, we want caching enabled, compiled assets only, and minimal information leaked in error responses. The environment file is where these decisions live.

Here's a typical production configuration that our team uses:

ruby
Rails.application.configure do
  config.cache_classes = true
  config.eager_load = true
  config.consider_all_requests_local = false
  config.action_controller.perform_caching = true
  config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
  config.log_level = :info
  config.log_tags = [:request_id]
  config.active_record.dump_schema_after_migration = false
end

The cache_classes flag tells Rails to load all classes once at startup rather than reloading them on each request. eager_load ensures all code paths are exercised before serving traffic, catching boot-time errors early. consider_all_requests_local prevents detailed exception pages from being shown to users. By toggling these in the environment file, we avoid hardcoding these decisions in our application code.

Development configuration looks very different:

ruby
Rails.application.configure do
  config.cache_classes = false
  config.eager_load = false
  config.consider_all_requests_local = true
  config.action_controller.perform_caching = false
  config.log_level = :debug
  config.active_record.dump_schema_after_migration = true
end

Notice how cache_classes is false. This means when we edit a file and refresh the browser, Rails reloads that class without restarting the server. consider_all_requests_local is true, so we see full stack traces and local variables in the browser when an error occurs. These settings make local development fast and debuggable.

The test environment file is lean because it runs in isolation:

ruby
Rails.application.configure do
  config.cache_classes = true
  config.eager_load = true
  config.consider_all_requests_local = true
  config.action_controller.perform_caching = false
  config.log_level = :warn
end

Test caching is on (tests run once per suite), but caching itself is off (we don't want state bleeding between tests). Logging is quiet to avoid noise in test output.

Environment variables for deploy-specific values

Configuration files are committed to version control. Secrets, API keys, and environment-specific endpoints are not. This is where environment variables come in.

In production, we might deploy to AWS with one RDS endpoint. In staging, we use a different endpoint. In development, everyone connects to localhost. The environment variable pattern lets us inject these values at runtime without changing code.

Rails provides ENV to read environment variables anywhere in the application:

ruby
# config/environments/production.rb
Rails.application.configure do
  config.active_record.database_url = ENV['DATABASE_URL']
end

When Rails boots in production, it reads the DATABASE_URL environment variable that was set by the deployment platform (Heroku, ECS, Lambda, or your own infrastructure). The same code runs in staging; the only difference is the environment variable value.

For local development, we use dotenv to load a .env file. This file is never committed:

ruby
# Gemfile
gem 'dotenv-rails', groups: [:development, :test]
ruby
# .env (not committed)
DATABASE_URL=postgres://localhost/myapp_dev
REDIS_URL=redis://localhost:6379/1
API_KEY=sk_test_abc123

When Rails boots in development, dotenv loads these into ENV automatically. The code path is identical to production, but the values come from a local file instead of the deployment platform.

We keep credentials that truly must be secret—like signing keys and OAuth tokens—in Rails encrypted credentials, but that's a separate topic. For environment-specific URLs and settings that vary per deployment target, environment variables are the standard approach.

config.x for custom settings

Sometimes we need application-level configuration that's not a Rails framework setting. Maybe we have feature flags, API rate limits, or business logic thresholds that differ between environments. The config.x namespace exists for exactly this.

In our environment file, we define custom settings under config.x:

ruby
# config/environments/production.rb
Rails.application.configure do
  config.x.max_retry_attempts = 5
  config.x.api_rate_limit = 1000
  config.x.feature_flags = { new_checkout: true, beta_pricing: false }
end
ruby
# config/environments/development.rb
Rails.application.configure do
  config.x.max_retry_attempts = 2
  config.x.api_rate_limit = 999999
  config.x.feature_flags = { new_checkout: true, beta_pricing: true }
end

Then, anywhere in the application, we read these values via Rails.configuration.x:

ruby
class PaymentProcessor
  def retry_payment
    attempt = 0
    loop do
      break if attempt >= Rails.configuration.x.max_retry_attempts
      # retry logic
      attempt += 1
    end
  end
end

config.x is a clean way to keep business logic settings out of environment variables and centralized in one place. The values are loaded at boot time, so there's no performance cost to reading them.

For truly dynamic feature flags—ones that can change without redeploying—use a database or feature flag service. But for static, environment-specific settings, config.x is simpler and safer.

Keep production defaults safe

The most common configuration mistake we see is shipping insecure production defaults. A developer writes code that "just works" in development, but if the environment variables aren't set in production, the application silently behaves incorrectly.

For example, if we forget to set REDIS_URL in production and fall back to an in-memory cache, our app will appear to work until the server restarts and all cached data vanishes. If we forget to set logging, we'll have no observability. If we forget to disable verbose error messages, we'll leak information to users.

The pattern we follow is: assume production is the safe, secure state. Development and test are permissive. This way, if a value is missing, the application either refuses to start or behaves conservatively.

ruby
# config/environments/production.rb
Rails.application.configure do
  # Fail fast if required config is missing
  raise "REDIS_URL not set" if ENV['REDIS_URL'].blank?
  raise "DATABASE_URL not set" if ENV['DATABASE_URL'].blank?
  
  config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
  config.active_record.database_url = ENV['DATABASE_URL']
  
  # Explicit, restrictive settings
  config.consider_all_requests_local = false
  config.action_controller.perform_caching = true
  config.log_level = :info
end

By raising early if required environment variables are missing, we catch deployment problems at boot time, not after requests fail. This is much better than discovering the issue hours later in production logs.

Common mistakes

Hardcoding values in config/environments files. Environment files are committed to version control. Any secret or environment-specific value should be in an environment variable, not in the file. We've seen API keys committed this way—never do it.

Not testing the production environment locally. We should run RAILS_ENV=production bundle exec rails server locally at least once before deploying. We'll catch configuration bugs immediately instead of in production.

Assuming development and production are similar. They're not. Production caches aggressively, reloads less, and logs less. If a bug only happens in production, it's often because of these differences. Test in production-like environments early and often.

Mixing secrets and configuration. A configuration value is something non-sensitive that changes per environment, like a database URL. A secret is something sensitive, like an API key or signing key. Use environment variables for both for now, but plan to use Rails credentials or a secret management service for secrets.

Forgetting to validate environment variables at boot. If your app requires STRIPE_API_KEY to function, check for it in the environment file and fail loudly if it's missing. Don't wait for the first request to hit an endpoint that needs it.

Key takeaways

  • Environment files (config/environments/development.rb, etc.) set Rails framework defaults per context. Production should be secure and conservative; development should be permissive and fast.
  • Environment variables allow deploy-specific values—URLs, keys, credentials—to be injected at runtime without changing code. Use dotenv locally and the platform's environment variable mechanism in production.
  • config.x is a clean namespace for custom, non-secret, environment-specific application settings. It's loaded at boot time and accessible throughout the app.
  • Always assume production is the safe state. If a required environment variable is missing, fail at boot rather than silently degrading behavior.
  • Test your application in production-like environments before deploying. Run RAILS_ENV=production locally to catch configuration surprises early.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles