Dotenv, Makefile & rules for a nice DX

August 23, 2023

You must already be aware of the Twelve-Factor App, and I want talk about the 3rd one:

III. Config
Store config in the environment

This has been widely adopted and this is a great practice.
However, the .env file has also been adopted to configure those variables. It creates some frustrating and unsuspected behaviors.

I've done my fair share of mistakes in the past with these environment variables and after going back and forth, I established a list of rules.

Rules

Rule #1: Always store configuration in the environment variables

This one is obvious, with the tooling we have today to deploy applications (k8s, docker, etc), it's crazy not to use environment variables for the configuration.

Rule #2: Allows developers to use a .env file

It's a good reminder that your tooling should support a .env file and use it, when there is one.

Rule #3: Never include the .env file in the versionning

Instead, you can keep a .env.dist in your versionning.

Rule #4: Avoid conditional .env files (.env.local, .env.dev)

It's working, but it will over-complexify the system and each of your tool needs to support it.

Rule #5: Existing environment variables must have precedence over values in .env

This is a tricky one. It seems logical, but we need to carefully check if this is really the case.
Makefile, for example, does not play nice with this one.

Rule #6: Automatically check if all required environment variables are at least declared

Before starting your application, you should check if the required variables are defined.

Makefile

Since make ignores .env by default (and for good reasons), I've added this hack in my Makefiles:

Loading variables in the right order

I want the variables to be loaded in this order, from highest priority to lowest priority:

  • real environment variables
  • .env
  • Makefile default values
  • docker-compose.yml default values

To test the correct order of precedence of my solution, I've created this repository.

With this setup, I can override any default variable from docker-compose.yml or Makefile in my local .env.
At the same time, If I need to run a target only once with a different variable, I can still do it like this: FOO=bar make up.

This is really a Developer eXperience oriented decision, I want developers of a project to have control over the way it's configured locally. With any decent CI, it's not an issue.