In this series on Makefiles, we've covered how to use wildcards and using application presets rather than having to pass in values.
There's one more technique we use for controlling Makefiles:
Dotenv (.env files)
We love love love using .env
files to configure our applications. If you're not familiar with them, the gist is that they specify environment variable keys and values in a very simple file format. For example, to control the LOG_LEVEL
var, you can use:
LOG_LEVEL=info
And then load that at application startup (most languages have some sort of dotenv
package for this).
While our full configuration practices are a topic for another day, we have a few rules we use on all of our projects:
- All configuration through environment variables (do not load files, do not talk to external stores)
- Always use
.env
files. - Never check in
.env
files. - Only have one
.env
file (no cascading files based on environment). -
.env
takes precedence over values in the environment.
Some of these decisions are no doubt controversial, but the goal is that .env
files provide unambiguous, canonical storage of local environment variables, and non-local environments use whatever native mechanisms for injecting environment variables before starting the process (Heroku Config Vars, AWS Parameter Store hooked up to ECS, etc).
Make and .env
This ".env is the canonical storage of local environment" is so easy and standard, we can even set up Make to use it!
We put this block at the top of Makefile- it looks for a .env file, and if present, it will set Make variables from it:
ifneq (,$(wildcard ./.env))
include .env
export
endif
Where is this useful? Well, let's say you want to connect to your local application's database. We can put that database URL into the .env
file:
DATABASE_URL=postgres://appuser:pass@localhost:13005/myapp
(We always run our databases through docker-compose
and use different ports for different projects- you won't ever go back to :5432
once you start doing it this way)
Now in our Makefile, we can set up a target to connect via psql
. Note the cmd-exists-%
target from last week
ifneq (,$(wildcard ./.env))
include .env
export
endif
cmd-exists-%:
@hash $(*) > /dev/null 2>&1 || \
(echo "ERROR: '$(*)' must be installed and available on your PATH."; exit 1)
psql: cmd-exists-psql
psql "${DATABASE_URL}"
Now run it from the command line:
$ make psql
psql "postgres://appuser:pass@localhost:13005/myapp"
psql (12.2, server 11.4 (Debian 11.4-1.pgdg90+1))
Type "help" for help.
Docker too!
One more nice thing for this super-flexible .env
file! Docker and docker-compose can accept an --env-file
param, and by keeping everything in a single .env
, you can share all the same configuration between your local application, Make, and Docker build.
Here's an adjusted Makefile snippet, and an example Docker command:
ifneq (,$(wildcard ./.env))
include .env
export
ENV_FILE_PARAM = --env-file .env
endif
docker-server:
docker run --rm -it -p $(PORT):$(PORT) $(ENV_FILE_PARAM)
The $PORT
, of course, is defined in the .env
file :)
More next time
Later this week we'll talk about one more neat thing we do with Make to standardize even further. If you are writing any JavaScript in particular, it's life-changing.
Finally, we'll wrap up this series in one week by talking about why we are all-in on Make as a task runner (if we tried to sell you on it in the first post you probably would have ignored us).
Top comments (1)
In addition to using environment variables I can recommend the tool github.com/dotenv-linter/dotenv-li... - it’s a lightning-fast linter for .env files. Written in Rust.
Maybe it would be useful for you.