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