Blog of Rob Galanakis (@robgalanakis)

Makefile and Dotenv

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).

This was originally posted on my consultancy’s blog, at https://lithic.tech/blog/2020-05/makefile-dot-env. If you have any questions, please leave a comment or get in touch!

Leave a Reply