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
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 (
).
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
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.
- Automatic loading in CommandBox with
commandbox-dotenv
. - Automatic loading for CommandBox servers with
commandbox-dotenv
. - Automatic loading for
docker-compose.yml
files. - Easy passing on to
docker run
anddocker-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).
Add Your Comment