A common question that comes up when dealing with docker is how to configure it without using a web-based administrator. Configuration files are somewhat new for the CFML world. What are our options for configuration and secrets when we want to avoid using web-based administrators (both for convenience and security).

A powerful tool in our configuration / security toolbelt when using Docker is to use environment variables.

Why Environment Variables

Environment variables are configuration values specific to the container (or environment) they are running in. They may or may not be secret. The time to reach for an environment variable is when you may want a different value or set of behaviors between development and production. This also enables reuse of our images since we can change behaviors and values on the fly.

We can see some examples of these configuration environment variables in the ortussolutions/commandbox Docker image:

  • PORT
    • The port which your server should start on. The default is 8080.
  • BOX_INSTALL
    • When set to true, the box install command will be run before the server is started to ensure any dependencies configured in your box.json file are installed.

In our CFML code, we can use environment variables through the Java System object:

var system = createObject( "java", "java.lang.System" );
var allEnvVarsStruct = system.getEnv();
var port = system.getEnv( "PORT" );

Another great example of environment variables in Docker is the MySQL image. Here are a few of their available environment variables:

  • MYSQL_ROOT_PASSWORD
    • Set the root password for the server.
  • MYSQL_DATABASE
    • Create a new database with the given name.

Here is another example of how environment variables can be simple values, like a password, or can also kick off complex actions, like creating a new database.

As you can see with these examples, an environment variable may be optional and is not necessarily a secret. But environment variables really shine when it comes to secrets. By definition, environment variables only exist in a single environment, so they are not stored in shared areas like source control.

Hopefully you can see how environment variables are valuable tools for configuration and secret management, but you may be confused how to use them in a volatile and ephemeral environment like containers and Docker. How do we set environment variables for our containers? With a traditional server, we would SSH in to the server and type the command export PORT=8080 in our terminal and it would be available as long as the server was up. While we could do that with our containers, we really shouldn't need us to interact with our containers to make them useful. With the constant spinning up and down of containers, we would be spending way too much time trying to keep environment variables set. So what are our options?

Environment Variables in Docker

docker

Docker lets you pass environment variables through the -e or --env flags when creating a new container with docker run. This gives you a chance to provide different secrets and / or configuration while using the same base image. My favorite example of this is the mysql image:

docker run -d \
    --name coolblog \
    -p 33306:3306 \
    -e MYSQL_ROOT_PASSWORD=my-secret-pw \
    -e MYSQL_DATABASE=coolblog \
    mysql

The above command creates a new MySQL server, sets the root password, and creates a coolblog database, all while using the base image. I use this constantly when I need a test database.

Passing environment variables through the command line is nice but can become overwhelming with many environment variables. docker run also takes a --env-file flag. The file should be formatted as a Java properties file. It can be named anything you like, but I'd suggest using the name .env. This name is a common convention and also has some synergy with CommandBox and docker-compose (which we'll get to later).

Here's our same example above with a .env file:

// .env
MYSQL_ROOT_PASSWORD=my-secret-pw
MYSQL_DATABASE=coolblog
```sh
docker run -d \
    --name coolblog \
    -p 33306:3306 \
    --env-file .env \
    mysql

IMPORTANT: Note about creating .env files. By default, these are able to be committed to source control. You do NOT want to do this. Even after removing secrets from source control, it is still possible to find them. Please add .env to your .gitignore file immediately.

Additionally, consider creating a .env.example file that contains only the keys of your .env file and is committed to source control. This file will serve as a template for others to know what keys need to be specified.

Dockerfile

As you work with Docker, you will often extend a base image and add your own customizations. The following is a very basic example of a custom Dockerfile:

FROM ortussolutions/commandbox

In your custom Dockerfile, you can declare environment variables using the ENV keyword:

FROM ortussolutions/commandbox

ENV PORT 8888

The environment variables declared in your Dockerfile can be used throughout the rest of the Dockerfile commands and inside your containers. You can access the value of an environment variable in your Dockerfile by wrapping it in braces (${PORT}).

docker-compose

If you are working with docker-compose, you have a few more options for defining your environment variables.

First, inside each service you can use any defined environment variables using the familiar ${KEY} syntax.

You can define new environment variables for you services inside an environment key, either as an array or a dictionary:


web:
  image: ortussolutions/commandbox
  environment:
    - PORT=8888
    # If you don't pass a value, the environment variable will be passed through.
    # It will be as if you wrote `BOX_INSTALL=${BOX_INSTALL}`.
    - BOX_INSTALL 

db:
  image: mysql
  environment:
    MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
    MYSQL_DATABASE: ${DB_DATABASE}

redis:
  image: redis
  env_file:
    # An env file can be passed just as with `docker run`
    - ./Docker/redis.env

In the example above, you see that I chose to use an existing environment variable to specify the MySQL root password and database. If I put the actual value in the docker-compose.yml file here, I would be committing secrets to source control, which is a very bad idea. So how do we provide these values? Let's talk about the .env file.

docker-compose will look for an .env file (not to be confused with the env_file property) in the same directory as the docker-compose.yml file. All the values in this properties file will be loaded and available in the docker-compose.yml file. These values are not automatically available in your containers. You need to either pass through the keys you want in your services explicitly, or you can pass the .env file to the env_file key:


web:
  # Environment variables can be used in any property
  image: ortussolutions/commandbox:${TAG}
  env_file:
    # This passes all the of the values in the `.env` file to the container
    - .env

Lastly, you can pass in environment variables using the -e flag using the docker-compose run command. You probably won't find too much of a use for this, but it's worth mentioning.

Revisiting the .env file

I want to take a moment to stress the value behind using the .env properties file convention. Here's the benefits you get for using it.

  1. Automatic loading in CommandBox with commandbox-dotenv.
  2. Automatic loading for CommandBox servers with commandbox-dotenv.
  3. Automatic loading for docker-compose.yml files.
  4. Easy passing on to docker run and docker-compose services

There are four great reasons to just stick to a .env file.

Once again, this is important enough to mention twice: please remember to ignore .env in your .gitignore file. Also consider adding a .env.example file with all the keys and none of the values that you keep in sync with your .env file. Your coworkers and future self will thank you.

Wrap-up

Whether you are new to Docker, a Docker expert, or even sticking with a traditional server setup, using environment variables will make your code more dynamic, more portable, and more ready for your eventual transition to Docker (or whatever comes next).