Favor Config Files over Env Vars

Yoav Nordmann
Israeli Tech Radar
Published in
8 min readSep 20, 2023

I don’t know how to say this so I’ll just say it as I see it: I hate environment variables. I really do. Even to the point that I judge applications on whether they use env vars or configuration files.

Reading the “12FactorApp”, specifically the issue on environment variables, I remember to this day thinking, this is not right. I agree with the idea as stated: “Config varies substantially across deploys, code does not”, but env vars? Is that the best we can do?

Why Not?

In the following sections, I will try and convince you why using environment variables is bad for you, your health, and even more so, your apps’ health.

The list as to why not is, as I see it, as follows:

  1. Lack of Visibility
  2. Lack of Ownership, Lack of Isolation
  3. Lack of Complex Structures
  4. Lack of CLI Support

Lack of Visibility

Env Vars have no structure. You cannot group them in any way other than by way of prefix or suffix. As if you could call that grouping.

In order to overcome that problem, sometimes, on rare occasions, they are grouped in the code. An endless list of env vars grouped into smaller groups of vars that belong together. That’s nice. Better than nothing right?

The problem arises when one wants to check the working variables in the running environment. In Linux, the command env will do just that. Out comes a long list of env vars of the whole system, and now try figuring out what belongs to which group. Chaininggrep to that command can minimize your list, but it can cause problems as well.

This is running the env command on my Linux machine, just OS stuff. Enjoy!

SHELL=/bin/bash
SESSION_MANAGER=local/endor:@/tmp/.ICE-unix/1354,unix/endor:/tmp/.ICE-unix/1354
WINDOWID=94371854
COLORTERM=truecolor
XDG_CONFIG_DIRS=/home/yoavn/.config/kdedefaults:/etc/xdg
XDG_SESSION_PATH=/org/freedesktop/DisplayManager/Session1
NVM_INC=/home/yoavn/.nvm/versions/node/v16.17.0/include/node
LC_ADDRESS=en_IL
JAVA_HOME=/home/yoavn/.sdkman/candidates/java/current
DOTNET_ROOT=/usr/share/dotnet
LC_NAME=en_IL
GRADLE_HOME=/home/yoavn/.sdkman/candidates/gradle/current
MEMORY_PRESSURE_WRITE=c29tZSAyMDAwMDAgMjAwMDAwMAA=
SDKMAN_CANDIDATES_DIR=/home/yoavn/.sdkman/candidates
DESKTOP_SESSION=plasma
LC_MONETARY=en_IL
KITTY_PID=4957
GTK_RC_FILES=/etc/gtk/gtkrc:/home/yoavn/.gtkrc:/home/yoavn/.config/gtkrc
XCURSOR_SIZE=24
GTK_MODULES=canberra-gtk-module
XDG_SEAT=seat0
PWD=/home/yoavn/comet/comet-backend
XDG_SESSION_DESKTOP=KDE
XDG_SESSION_TYPE=x11
SYSTEMD_EXEC_PID=1602
XAUTHORITY=/tmp/xauth_BpHxyY
DESKTOP_STARTUP_ID=endor;1695201056;939427;1602_TIME815116
MOTD_SHOWN=pam
GTK2_RC_FILES=/etc/gtk-2.0/gtkrc:/home/yoavn/.gtkrc-2.0:/home/yoavn/.config/gtkrc-2.0
HOME=/home/yoavn
LC_PAPER=en_IL
LANG=en_US.UTF-8
XDG_CURRENT_DESKTOP=KDE
MEMORY_PRESSURE_WATCH=/sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/app-org.kde.latte\x2ddock@autostart.service/memory.pressure
KITTY_WINDOW_ID=3
XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat0
INVOCATION_ID=776ed7431ba44a86a8029ce468fb0604
MANAGERPID=1273
DOTNET_BUNDLE_EXTRACT_BASE_DIR=/home/yoavn/.cache/dotnet_bundle_extract
KDE_SESSION_UID=1000
NVM_DIR=/home/yoavn/.nvm
XDG_SESSION_CLASS=user
TERMINFO=/usr/lib/kitty/terminfo
TERM=xterm-kitty
LC_IDENTIFICATION=en_IL
USER=yoavn
KDE_SESSION_VERSION=5
PAM_KWALLET5_LOGIN=/run/user/1000/kwallet5.socket
MAVEN_HOME=/home/yoavn/.sdkman/candidates/maven/current
SDKMAN_DIR=/home/yoavn/.sdkman
DISPLAY=:0
SHLVL=1
NVM_CD_FLAGS=
LC_TELEPHONE=en_IL
LC_MEASUREMENT=en_IL
XDG_VTNR=2
SDKMAN_CANDIDATES_API=https://api.sdkman.io/2
XDG_SESSION_ID=2
XDG_RUNTIME_DIR=/run/user/1000
DEBUGINFOD_URLS=https://debuginfod.archlinux.org
LC_TIME=en_IL
QT_AUTO_SCREEN_SCALE_FACTOR=0
JOURNAL_STREAM=8:37166
XCURSOR_THEME=Breeze_Snow
KDE_FULL_SESSION=true
PATH=/home/yoavn/.nvm/versions/node/v16.17.0/bin:/home/yoavn/.sdkman/candidates/maven/current/bin:/home/yoavn/.sdkman/candidates/java/current/bin:/home/yoavn/.sdkman/candidates/gradle/current/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/home/yoavn/.dotnet/tools:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/usr/lib/rustup/bin
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
SDKMAN_PLATFORM=linuxx64
KDE_APPLICATIONS_AS_SCOPE=1
NVM_BIN=/home/yoavn/.nvm/versions/node/v16.17.0/bin
KITTY_INSTALLATION_DIR=/usr/lib/kitty
LC_NUMERIC=en_IL
OLDPWD=/home/yoavn/comet/comet-backend/scripts
_=/usr/bin/env

Lack of Ownership, Lack of Isolation

Continuing with the example from the section before: How do you know what env var is for your app, for the other app, and which one belongs to the operation system? You just don’t know!

You could always look at the documentation, right? True, one could. But not always is the documentation updated, and more often than not, sometimes it is non-existent altogether.

A tale of two applications. Each one has a logger, and each one defines it’s logging level using an env var called: APP_LOGGING_LEVEL. Great. Now, for one app you are changing the logging level. Can you guess what happens to the logging level of the other app?

Cool right? This is the stuff nightmares are made of!

Now come the cool guys and say: “In K8s you should run each app in its own POD anyway”. And yes, theoretically you are right, you should. But not all deployments are on K8s! I remember vividly working a this one customer, where due to a specific env var in the system I could always only run a specific micro-service as a global env var was micro-service specific.

Lack of Complex Structures

Using env vars, you are limited to strings. No lists, no structures such as JSON, and not even numbers. Strings. This means you need to handle conversions in the code, which makes the code always so much more fun to maintain in the long run.

Actually, I have seen once an app that saved a JSON struct as a string within an env var. If you need me to explain why this is bad practice, stick to env vars. Let’s just say that this bad practice was just a symptom of a much larger problem of software engineering practices which were non-existent there.

Lack of CLI Support

Usually, you need to set a lot of env vars for a quite standard app. Starting with DB connections, some feature flags, logging configurations, and much more. All of these have to be set in order for the application to run. Most of the time, what I have seen is that all of these configurations are being saved in the IDE.

While this is a good thing, try thinking of a new employee coming to work. He comes, clones the git repository, compiles the project and tries to run the application only to find out he’s missing env vars. So he turns to his team lead and asks about this, to which the team leader says:

”Oh yeah, sure, I forgot. I’ll send you the list. Which IDE are you using? VSCode? I’m using IDEA, so you’re gonna have to figure out how this works for you.”

Not the answer I was hoping for…

See, I have this tick. I do not use the IDE for EVERYTHING. I use it for development, but I prefer using the CLI as much as I can. Years of programming have taught me, that the IDE does what it can to make your app work. Many times you do not even know what the inner build mechanisms are behind your IDE. It just works. And then when you get to the deployment, all hell breaks loose. Sound familiar?

Try running your application from the command line. I had already huge shell scripts that I needed to run before running an app, just to set the env vars. I felt like pre-2000 all over again!

Config Files to the rescue

Let’s pause this rant for a second and get back to the basics. What are env vars for? Let[‘s go back to where we started. The “12FactorApp”.

The twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy to change between deploys without changing any code; unlike config files, there is little chance of them being checked into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard.

As stated in the beginning. I agree with the concept, but not the implementation of it. Let’s look at all my stated drawbacks of using env vars and see how they compare using configuration files:

Visibility: The configuration file is structured. I agree, the older properties files used in Java are not ideal either, but they are definitely more structured than env vars, yet still, I would not advocate for using them. Today one would use YAML or JSON as config files. TOML is another option, even HOCON files are an option.

Ownership, Isolation: There is never a dispute as to which parameter belongs to which application, as they are separated by files. Unless you have made such a mess that even god has forsaken your application, but that is another story. Each app with its own config file. Period. And this is why isolation is built in, to begin with.

Support Complex Structures: I don’t think I need to explain the capabilities of JSON or YAML files in terms of value types. Numbers, Strings, Lists, and even text are supported.

CLI Support: let's show a short example shall we?

java -jar app.jar --spring.config.location=~/app/config/application.yml

vs

MYSQL_HOST=mysql \
MYSQL_PORT=3306 \
MYSQL_DB=myappdb \
LOGGING_LEVEL=SEVERE \
FEATURE_FLAG_A=something \
...
java -jar app.jar

Which would you prefer?

MYSQL_HOST=mysql 
MYSQL_PORT=3306
MYSQL_DB=myappdb
MYSQL_USERNAME=foo
MYSQL_PASSWORD=bar
LOGGING_APPENDER=file
LOGGING_LEVEL=SEVERE
FEATURE_FLAG_A=something
FEATURE_FLAG_B=more

VS

{
"mysql": {
"host": "mysql",
"port": 3306,
"db": "myappdb",
"username": "foo",
"password": "bar"
},
"logging": {
"appender": "file",
"level": "severe"
},
"features": {
"a": "something",
"b": "more"
}
}

There is More

Not only, in my opinion, does the config file approach handle the above examples in a better and more elegant way, but there is even more:

Configuration Files Stacking

Most if not all modern config file libraries support the stacking of configuration files. And this is true power!

You can have a base config file with all of the configuration parameters set. And then you can have purpose-specific config files that inherit the base file and change only what is needed.

What you gain using this method is multifold:

  1. Documentation: All of your possible parameters exist in this one file and are always present. You might even want to purposefully NOT CHECK the existence of a specific config param, and have your app fail fast if something is amiss.
  2. Default Values: Your default values are stated in the base config file in one place. You should not have to search through your code in order to find out what your default values are.
  3. Purpose Specific Change: The specific purpose file contains only what is necessary for this purpose. e.g. DEV, STAGING, LOCAL, you get the picture. There is no need to copy the whole base file and then maintain multiple files until they are not in sync anymore and all hell breaks loose.

Python & Config Files

I would argue that env var and python are quite synonymous. The standard way of external configuration in Python is using env vars, right?

But see here, even Python has some libraries for configuration file management:

And the coup de la resistance: Ever heard of Python’s own configparser? It seems the language is maturing quite well!

Conclusion

As you can see, I am very opinionated on this subject, I admit. Over two decades of programming have made me a bitter man with strong convictions on how a software project should be structured and how it should behave.

I am sure You can find more reasons as to why not env vars, as you can find reasons as to why config files are bad and long live env vars. That’s fine. There is a lot of leeway in individual and group opinions, seeing benefits and drawbacks on different topics. This is just my opinion, and it has served me well.

I’ll leave you with this minor personal experience: For some reason, for all the clients and customers I’ve worked for, onboarding and project setup for the ones using env vars was considerably longer than for those using config files.

There is no one correct way to do it, there are endless possibilities for a solution.

--

--

Yoav Nordmann
Israeli Tech Radar

I am a Backend Tech Lead and Architect for Distributed Systems and Data. I am passionate about new technologies, knowledge sharing and open source.