Managing environment variables with direnv


Following the Twelve-Factors app, most web applications are storing configuration in environment variables rather than in a repository. It is possible to store secrets securely in a repository with git-crypt but that will likely be the topic of another article.

While in CI and on servers loading those environment variables happen automatically, loading them on your computer is problematic. The most popular approach is dotenv: you store your environment variables in a .env file and a library in your project will look for it and load them. A .env file sample:

REDIS_URL=blabla
# Make sure this is not used in other environments
APP_SECRET=hunter2
TRIAL_DAYS_LENGTH=14

And, for example in the JavaScript implementation, they can be loaded by adding require('dotenv').config() to your project, as early as possible. As you can see, this adds a dependency to your project that is only used in development: in CI variables are likely to be set in the CI itself and in production by whatever provisioning tool you are using.

Enter direnv.

It starts the same way as dotenv: you write your variables in a .envrc file which is almost identical to the example above: entries have an export keyword first.

export REDIS_URL=blabla
# Make sure this is not used in other environments
export APP_SECRET=hunter2
export TRIAL_DAYS_LENGTH=14

What sets it apart is how it loads the variables. You first need to install direnv, which is a cross-platform binary available on most package managers as well as set up a hook in your bash/zsh/fish config. This might sound complex but you only need to do this setup once per computer and it actually takes less than a minute. The hook is fast enough that it is imperceptible - no need to worry about terminal slowdown.

Now that you have the direnv hook, if you cd to a directory with a .envrc you will see:

$ direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

As mentioned in the message, direnv will not automatically load the variables found: you first need to allow the .envrc file. You will need to run direnv allow every time the .envrc is modified. Since direnv loads the variable automatically, this is needed for security as any repo could run commands on your machine otherwise.

And that's it you're done. Every time you will cd in a directory, it will automatically load variables found in .envrc and unload them when moving out of the directory:

$ cd my-repo/
direnv: loading .envrc
direnv: export +AWS_ACCESS_KEY_ID +AWS_SECRET_ACCESS_KEY +RUST_LOG +SENTRY_DSN

$ cd ..
direnv: unloading

You get the benefits of environment variables for configuration without having to add a dependency to your project. I was actually using direnv before hearing about dotenv: https://github.com/Keats/dbmigrate/pull/14 and I still don't really see myself using dotenv over it. I'm guessing the advantage of dotenv is if you are launching things from an IDE directly, using Docker or at companies restricting what can be installed on a machine.

If this is not your case, I highly recommend direnv.