When I first started out in system administration, the use of environment variables was frowned upon. At that point environment variables were a global variable. Accessible and mutable by all applications on the system, on common deployments. If you were doing a jail or LXC container, that was slightly different. As containers came into the picture environment variables became common practice. Containers were a function, taking in a common set of parameters to configure the application.

As we strive to develop items locally, we want an easily usable configuration method. This will work across IDEs and environments. A common pattern I've seen in Kotlin and JDK is a properties file. These work, but switching to containers you need to pass in the properties, or provide them as a volume mount. They can be packaged in a shadow jar, but that is not an ideal situation. Configuration changes then ripple to another release. For local development you also need to modify the property configuration. Outside of the JDK IDE ecosphere it can be painful. If you work in a monorepository patterns, these configurations may be utilized by other applications.

I've used a common pattern for several years. Utilizing my recent helper library for environment configuration. A common standard is the env file popularized by nodejs and docker. It looks like the following.

myDataBaseHost=localhost
myDataBase=postgres
myDataBasePort=5432
myDataBaseUser=postgres
myDataBasePassword=postgres

Luckily there is an Intellij plugin to help with this. The EnvFile plugin. With this plugin during any run configuration you can add an env file. This will load the file, and set the configured values as environment variables during the execution.

Creating the Configuration

You can store the file at whatever location you wish. I usually place my configuration at the repository root ./local.env.

We never want to commit this.

.gitignore

local.env

local.env.tmpl

myDataBaseHost=
myDataBase=
myDataBasePort=
myDataBaseUser=
myDataBasePassword=

I provide a template file suffixed with .tmpl. This is meant to be a skeleton, so new users can easily tell the items that need to be set. If you are spinning up a local database via docker, that can be stored in the template file. More sensitive items, i.e. api keys should be omitted.

local.env

myDataBaseHost=localhost
myDataBase=postgres
myDataBasePort=5432
myDataBaseUser=postgres
myDataBasePassword=postgres

Local env just mirrors the example file noted above.

Shell / Command Line

The next portion is to enable this to work on the command line. Direnv will automatically load the variables when you change directories. This will scope the variables to each project you're working on. Following is a sample of .envrc, this script loads the .env file and sets them. It will unload them when exiting that directory.

Important: This is based strictly on local.env. Adjust file name as needed

#!/usr/bin/env zsh

for line in $(cat local.env); do
    export $line
done

In the above screen shot. I have changed directory into my kotlin-frm project. All the requisite environment files are loaded. As noted by the +pgDataBaseHost. Then when you change out of the directory it's unloaded from the environment.

*Important: If you make any changes to local.env you will need to run direnv allow

Docker / Docker Compose

With docker you can pass any variables via a ${myDataBaseHost} option. This will allow you to automatically pass variables from local.env -> docker compose -> container.  

If you adjust local.env and restart the container, it will now automatically pull in the configuration. The same can be said for the application. By having the application pulling from the environment i.e.

This flow has allowed me to iterate very quickly. I have several work spaces that automatically listen to the same configuration file.

Application Configuration

While there are a number of libraries out there for configuration. I wanted a simple option that just loaded environment variables into an object or data class, ensuring that they matched the proper type.

object ApplicationConfig {
   val dbHost : String by Environment("myDataBaseHost")
   ....
}

Adjusting via local.env will be automatically propagated to the application.

From the EnvFile plugin page

Pain Points

The difficulty with this is Gradle. The gradle during Intellij runs is not aware of any env files. So you will need to provide a default configuration without environment variables.

One Entry Point

With this the local.env file is the entry point for the scope of the application. Adjusting this single file will propagate automatically through the application and container environment.