Configuration and environment

Defaults in code

A program that needs a setting should run without one. The defaults are a frozen hash in the code, and everything else is a layer on top of it:

DEFAULTS = { "APP_MODE" => "development", "PORT" => "3000" }

The order is fixed: defaults, then the file, then the environment. The environment wins because that is what a container and a CI job can set without editing a file, and it is the layer a deploy changes.

A .env file

A .env file is a convenience for a laptop: one KEY=VALUE per line, comments with #. A dozen lines of Ruby parse it, which is less than a dependency:

def load_dotenv(path) return {} unless File.exist?(path) File.readlines(path).filter_map do |line| line = line.strip next if line.empty? || line.start_with?("#") key, value = line.split("=", 2) [key, value] end.to_h end

split("=", 2) is the detail that makes it work: a password or a URL with an equals sign keeps everything after the first one. The real dotenv gem adds quoting and interpolation; for the three settings a script has, this is enough.

The loader

The three layers in one line, then one line of output that proves which value won:

settings = DEFAULTS.merge(load_dotenv(".env")).merge(ENV) token = settings.key?("TOKEN") ? "set" : "missing" puts "mode=#{settings["APP_MODE"]} port=#{settings["PORT"]} token=#{token}"

The token is printed as set or missing, never as its value. A script that prints the secret once puts it in a shell history, a log and a screenshot.

Running it

Without a file, the defaults answer:

ruby app.rb

mode=development port=3000 token=missing

With a .env next to the script, the file wins over the defaults:

ruby app.rb

mode=production port=8080 token=set

And the environment wins over the file, which is what a container sets:

APP_MODE=test PORT=9999 ruby app.rb

mode=test port=9999 token=set

The same three commands are the whole twelve-factor idea for a small script: the build is the same everywhere, and the environment decides which configuration it gets.

What to watch for

Do not commit .env. Commit .env.example with the keys and empty values, and add .env to .gitignore; the example is the documentation and the secret stays out.

ENV values are strings. A port read from the environment and used as a number needs Integer(ENV.fetch("PORT")), or the first comparison fails with a message about comparing a String with an Integer.

Next: Logging in depth

Article author: Arthur Isaev

Banner image