env_vars_dockerfile

Overview

Environment variables are a simple and popular option to avoid hard-coding credentials into your code or build files, while eliminating the risk of accidentally pushing sensitive information to Git. Most importantly, utilizing them in your programs is very easy.

Say, you want to retrieve the username and password of your firewall in a Python program. This is all you have to do:

import os

fw_username = os.getenv('FIREWALL_USER', None)
fw_password = os.getenv('FIREWALL_PASS', None)

Recently, I had to dockerize an API server that I have developed. This API server needed to authenticate to multiple other services to function. We just observed how it is done inside the code but how do you use them in a Dockerfile to pass them to the program?

Let’s quickly explain what environment variables are before jumping right into the solution.

Environment Variables

An environment variable is a key-value item that can influence how programs behave on a computer. They keep information that the operating system and other programs need. For example the PATH environment variable that contains a list of directories where executable files are located.

Execute env command in your shell to get a list of all defined environment variables.

Defining a new environment variable is possible in different ways but I’ll cover three ways here:

  1. inline with a command you want to run:
LOG_LEVEL=debug ./my_program

We just defined a temporary environment variable here that only affects the my_program and goes out of scope when the my_program exits.

  1. Session wide:
export LOG_LEVEL=debug

./my_program

Using the export command, we defined an environment variable which will be available throughout the life of our terminal session. This way, other processes can also use that environment variable.

  1. Defining environment variables in files that are run on system startup or login sessions:

An example of these files on Linux would be .bashrc and .profile files. Appending your environment variables to them will load them when you spawn or login to a shell (e.g. Bash)

export LOG_LEVEL=debug >> ~/.bashrc
# or
export LOG_LEVEL=debug >> ~/.profile

Using Environment Variables in Dockerfile and docker-compose

Let’s write a simple Dockerfile to demonstrate a few things:

Write a Dockerfile

FROM python:3.11.1

ARG FW_USER=${FW_USER:-""}
ARG FW_PASS=${FW_PASS:-""}

ENV FW_USER ${FW_USER}
ENV FW_PASS ${FW_PASS}

RUN apt-get update -y \
    && apt-get install -y --no-install-recommends \
    && gcc
    && g++

RUN apt-get -y clean

WORKDIR /app

COPY ./requirements.txt /app
COPY ./*.py /app

RUN pip3 install --no-cache-dir -r /app/requirements.txt

CMD [ "python3", "main.py", "0.0.0.0", "8080"]

The important lines here are the ones starting with ARG and ENV.

  • ARG: Defines a build-time variable that can be passed to the docker build command using the --build-arg option or through docker compose, more on that later.
  • ENV: Sets environment variables that are available to the container at runtime. In this case, the FW_USER and FW_PASS environment variables are set to the values of the FW_USER and FW_PASS build-time variables, respectively.

Now, we can write out our docker-compose file to define how Docker containers should be run.

Write a docker-compose file

As mentioned earlier docker-compose files define how Docker containers are run and connected together. As of this writing, the provided example is the new docker-compose format (notice the lack of version key on top):

services:
  api_server:
    image: my_program:1.0
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - "FW_USER=${FW_USER}"
      - "FW_PASS=${FW_PASS}"
    container_name: "my_program"
    volumes:
      - ./:/app
  proxy_server:
    image: nginx:latest
    container_name: nginx_proxy
    ports:
      - "80:8080"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf

What matters for this post are these line:

environment:
  - "FW_USER=${FW_USER}"
  - "FW_PASS=${FW_PASS}"

Please note that my example is using the docker compose plugin and differs from the docker-compose command; although the outcome is the same regardless of the command used.

Here is what happens when you run docker compose build:

  1. docker compose looks up the environment variables in the shell it is running on for FW_USER and FW_PASS
  2. They get passed to the Dockerfile
  3. Docker image gets built with our desired environment variables
  4. Environment variables get presented to the program when we start the container

Conclusion

In this post, we discussed what environment variables are and how to use them in our programs and Dockerfiles.

Although this method is relatively safe and way better than hard-coding credentials in your programs, it is still not the best option out there.

whenever possible, you should consider using identity and secret management solutions such as Hashicorp’s Vault.