Django
,Python
,Today I Learned
On environment variables and dotenv files
Brett Cannon recently vented some frustrations about .env
files.
I still hate .env files and their lack of a standard
https://mastodon.social/@brettcannon@fosstodon.org/112056455108582204
Brett’s thread and our conversation reminded me that my rule for working with dotenv files is to have my environment load them instead of my Python app trying to read from the .env
file directly.
What is a .env
(dotenv) file?
A .env
(aka dotenv) is a file that contains a list of key-value pairs in the format of {key}=value
.
At a basic level, this is what a bare minimum .env
file might look for in a Django project.
# .env
DEBUG=true
SECRET_KEY=you need to change this
My go-to library for reading ENV variables is environs
. While the environs
library can read directly from a dotenv file, don’t do that. I never want my program to read from a file in production because I don’t want a physical file with all of my API keys and secrets.
Most hosting providers, like Fly.io, have a command line interface for setting these key-value pairs in production to avoid needing a physical dotenv file.
Instead, we should default to assuming that the ENV variables will bet in our environment, and we should fall back to either a reasonable default value or fail loudly.
Using the environs
library, my Django settings.py
file tends to look like this:
# settings.py
import environs
env = environs.Env()
# this will default to False if not set.
DEBUG = env.bool("DJANGO_DEBUG", default=False)
# this will error loudly if not set
SECRET_KEY = env.str("SECRET_KEY")
# everything else...
I lean on Docker Compose for local development when I’m building web apps because I might have three to five services running. Compose can read a dotenv file and register them into environment variables.
.envrc
files aren’t .env
files
On my macOS, when I’m not developing in a container, I use the direnv
application to read an .envrc
file which is very similar to a dotenv file.
A .envrc
is very similar to a .env
file, but to register the values into memory, you have to use Bash’s export
convention. If you don’t specify export
, the environment variables won’t be available in your existing Bash environment.
# .envrc
export DEBUG=true
export SECRET_KEY=you need to change this
I’m a fan of direnv
because the utility ensures that my environment variables are only set while I am in the same folder or sub-folders that contain the .envrc
file. If I move to a different folder location or project, direnv
will automatically unload every environment variable that was previously set.
This has saved me numerous times over the years when I have run a command that might upload a file to s3 and ensure that I’m not uploading to the wrong account because an environment variable is still set from another project.
Clients are generally understanding, but overriding static media for one client with another client’s files is not a conversation I want to have with any client.
direnv
is excellent insurance against forgetting to unset an environment variable.
Seeding a .env
file
I prefer to ship an example .env.example
file in my projects with reasonable defaults and instructions for copying them over.
# .env.example
DEBUG=true
SECRET_KEY=you need to change this
If you are a casey/just
justfile
user, I like to ship a just bootstrap
recipe that checks if a .env
file already exists. If the .env
file does not exist, it will copy the example in place.
My bootstrap
recipe typically looks like this:
# justfile
bootstrap *ARGS:
#!/usr/bin/env bash
set -euo pipefail
if [ ! -f ".env" ]; then
echo ".env created"
cp .env.example .env
fi
How do we keep dotenv files in sync?
One pain point when working with dotenv files is keeping new environment variables updated when a new variable has been added.
Thankfully, modenv is an excellent utility that can do precisely this. I run modenv check
and will compare the .env*
files in the existing folder. It will tell us which files are missing an environment variable when it exists in one but not one of the other files.
I use modenv check -f
to sync up any missing keys with a blank value. This works well to sync up any new environment variables added to our .env.example
file with our local .env
file.
Alternatives
I recently wrote about Using Chamber with Django and managing environment variables, which dives into using Chamber, another tool for managing environment variables.
If you are working with a team, the 1Password CLI’s op run
command is an excellent way to share environment variables securely. The tool is straightforward and can be integrated securely with local workflows and CI with just a few steps.
Wednesday March 13, 2024