Configuration files

The file

A config file is a hash with comments. YAML is the usual choice because a person edits it, and the shape here is one section named after the program:

app: name: demo port: 3000 debug: false

The section is not decoration: a file that also holds a database: block can be shared between programs, and each one reads its own key.

The script

Three layers in order: the defaults in code, the file over them, the environment over the file. Hash#merge is the layering, and the validation is the last line before the program starts:

require "yaml" DEFAULTS = { "name" => "app", "port" => 3000, "debug" => false }.freeze i = ARGV.index("--config") or abort("usage: ruby app.rb --config FILE") path = ARGV[i + 1] or abort("usage: ruby app.rb --config FILE") raw = YAML.safe_load_file(path) or abort("#{path}: empty config") settings = DEFAULTS.merge(raw.fetch("app") { abort("#{path}: missing app section") }) settings["port"] = Integer(ENV.fetch("APP_PORT", settings["port"])) abort("port must be positive") unless settings["port"].positive? puts "#{settings["name"]} on #{settings["port"]} (debug: #{settings["debug"]})"

safe_load_file opens and parses in one call, and the same safe rules as the YAML lesson apply. Integer() is the strict conversion: a value that is not a number raises instead of becoming zero.

Running it

ruby app.rb --config config.yml

demo on 3000 (debug: false)

The environment layer is a real override, not a second file:

APP_PORT=4000 ruby app.rb --config config.yml

demo on 4000 (debug: false)

A container sets APP_PORT and mounts the same config, and the code does not change between the laptop and the host. The precedence is documented in the README, which is the part that keeps the next reader from guessing.

Fail fast

A config error must stop the program before it opens a socket or writes a row. The abort calls above print to stderr and exit non-zero, which a supervisor and a CI job both read:

ruby app.rb --config broken.yml

broken.yml: missing app section

APP_PORT=-1 ruby app.rb --config config.yml

port must be positive

The message names the file or the setting, so the fix is a one-line edit. A program that starts with a bad config and fails on the first request wastes the time between the two.

What to watch for

Never commit the file that holds a password. Commit a config.example.yml with empty values, read the real one from a path in the environment, and the secret stays out of the repository.

A key that a config does not mention should come from the defaults, but a key that a config sets to null is a deliberate empty value. merge keeps the null and the default is gone; decide which one the setting means and validate it.

Next: HTTP clients

Article author: Arthur Isaev