How to use environment variables in CMD for ENTRYPOINT arguments in Dockerfile?

Viewed 612

I have a Dockerfile where I start a executable with default arguments like this:

ENTRYPOINT ["executable", "cmd"]
CMD ["--param1=1", "--param2=2"]

This works fine and I can run the container with default arguments:

docker run image_name

or with custom arguments:

docker run image_name --param1=a --param2=2

Now i would like to have a default parameter depend on a environment variable or default to the deafult value (1) like this:

--param1='${PARAM1:-1}'

I Understand that

ENTRYPOINT ["executable", "cmd"]
CMD ["--param1='${PARAM1:-1}'", "--param2=2"]

does not work since CMD is in exec form and does not invoke a command shell and cannot substitute environment variables.

But if I use CMD in shell form:

ENTRYPOINT ["executable", "cmd"]
CMD "--param1='${PARAM1:-1}' --param2=2"

I get no such option: -c

So my question is:

How get I archive environment variable substitution within the default arguments in CMD for my ENTRYPOINT?

2 Answers

One way would be to lose the CMD and wrap all the defaults up in a custom entrypoint. I try to avoid doing this, but sometimes it seems like the cleanest way, and you can be a lot more flexible:

Dockerfile:

    COPY 'my-entrypoint.sh' '/somewhere/in/path/my-entrypoint'
    ENTRYPOINT ['my-entrypoint']

my-entrypoint.sh

    #!/bin/sh
  
    ARGS="${@}"
    if [ -z "${ARGS}" ]; then
        ARGS="--param1=${PARAM1:-1} --param2=2"
    fi
  
    executable cmd $ARGS

You can't do this the way you describe, for the reasons you've laid out in the question. The ENTRYPOINT and CMD simply get concatenated together to form a single command line, and if either or both of those parts is a string rather than a JSON array it gets automatically converted to sh -c 'the string'.

ENTRYPOINT ["executable", "cmd"]
CMD "--param1='${PARAM1:-1}' --param2=2"

# Equivalently:
ENTRYPOINT ["executable", "cmd", "/bin/sh", "-c", "\"--param1=...\""]
CMD []

There are two techniques I'd suggest to work around this problem, though both require potentially substantial changes in the setup.

In Docker and Kubernetes, it turns out to generally be more convenient to pass options via environment variables than on the command line. This means your application needs to know to look for those variables, and supply some of the defaults you describe here. Some argument-parsing libraries support this out-of-the-box, but not all. Python's standard argparse library, for example, doesn't directly have environment-variable support, but you can still easily support them:

import argparse
import os

parser = argparse.ArgumentParser()
parser.add_argument('param1', default=os.environ.get('PARAM1', '1'))

args = parser.parse_args()
print(args.param1)
# Uses --param1 option, or else $PARAM1 variable, or else default "1"

The other approach I generally recommend is to make CMD a well-formed shell command; don't try to split the command between CMD and ENTRYPOINT. This avoids the problem of Docker inserting the sh -c wrapper in the middle of the line.

# no ENTRYPOINT
CMD executable cmd --param1="${PARAM1:-1}" --param2=2

The ENTRYPOINT pattern that I do find useful is to use a wrapper script to provide defaults and do other first-time setup. If that script is a Bourne shell script and ends with exec "$@", then it will run the CMD as the main container process.

#!/bin/sh
# docker-entrypoint.sh

# In Docker specifically, default $PARAM1 to "docker", not "1".
: ${PARAM1:=docker}

# Run the main container command.
exec "$@"
ENTRYPOINT ["/docker-entrypoint.sh"] # must be a JSON array
CMD executable cmd --param2=2

(There is no requirement to have an ENTRYPOINT. Making ENTRYPOINT be an interpreter and putting the script name in CMD doesn't bring any benefit, and makes it harder to run debugging commands like docker run --rm my-image ls -l /app.)

Related