Using variable interpolation in string in Docker - bash

I am having trouble creating and using variables in a Dockerfile - I build a Docker image via a Dockerfile with this command:
$ docker build --build-arg s=scripts/a.sh -t a .
(So because I use --build-arg, $s will be an available argument in the Dockerfile, and this part works)
The Dockerfile is like so:
ARG s
RUN echo $s
RUN useradd -ms /bin/bash newuser
USER newuser
WORKDIR /home/newuser
ENV fn=$(filename $s) # fails on this line
COPY $s .
ENTRYPOINT ["/bin/bash", "/home/newuser/$fn"]
The problem I have is that the Docker build is failing on the line indicated above.
Error response from daemon: Syntax error - can't find = in "$s)". Must be of the form: name=value
If I change that line to this:
RUN fn=$(filename $s)
I get this error:
Error: Command failed: docker build --build-arg s=scripts/a.sh -t a .
The command '/bin/sh -c fn=$(filename $s)' returned a non-zero code: 127
Anyone know the correct way to
Create a variable inside the docker file
Use string interpolation with that variable so that I can reference the variable in the ENTRYPOINT arguments like so:
ENTRYPOINT ["/bin/bash", "/home/newuser/$var"]
Even if I do this:
ARG s
ARG sname
RUN echo $s # works as expected
RUN echo $sname # works as expected
RUN useradd -ms /bin/bash newuser
USER newuser
WORKDIR /home/newuser
COPY $s . # works as expected (I believe)
ENTRYPOINT /bin/bash /home/newuser/$sname # does not work as expected
even though I am using the "non-JSON" version of ENTRYPOINT, it still doesn't seem to pick up the value for the $sname variable.

I would avoid using variable in ENTRYPOINT at all. It's tricky and requires a deep understanding of what is going on. And is easy to break it by accident. Just consider one of the following.
Create link with the known name to your start script.
RUN ln -s /home/newuser/$sname /home/newuser/docker_entrypoint.sh
ENTRYPOINT ["/home/newuser/docker_entrypoint.sh"]
or write standalone entrypoint script that runs what you need.
But if you want to know how and why solutions in your questions work just keep reading.
First some definitions.
ENV - is environment variable available during buildtime (docker build) and runtime (docker run)
ARG - is environment variable available only during buildtime
If you look at https://docs.docker.com/engine/reference/builder/#environment-replacement you see the list of dockerfile instructions that support those environment variables directly. This is why COPY "picks up the variable" as you said.
Please note that there is no RUN nor ENTRYPOINT. How does it work?
You need to dig into the documentation. First RUN (https://docs.docker.com/engine/reference/builder/#run). There are 2 forms. The first one executes command through the shell and this shell has access to buildtime environment variables.
# this works because it is called as /bin/sh -c 'echo $sname'
# the /bin/sh replace $sname for you
RUN echo $sname
# this does NOT work. There is no shell process to do $sname replacement
# for you
RUN ["echo", "$sname"]
Same thing applies to the ENTRYPOINT and CMD except only runtime variables are available during container start.
# first you need to make some runtime variable from builtime one
ENV sname $sname
# Then you can use it during container runtime
# docker runs `/bin/sh -c '/bin/bash /home/newuser/$sname'` for you
# and this `/bin/sh` proces interprets `$sname`
ENTRYPOINT /bin/bash /home/newuser/$sname
# but this does NOT work. There is no process to interpolate `$sname`
# docker runs what you describe.
ENTRYPOINT ["/bin/bash", "/home/newuser/$sname"]
edit 2017-04-03: updated links to the docker documentations and slight rewording to avoid confusion that I sense from other answers and comments.

I requested #Villem to answer, and his answer is much more definitive, but the following will work (just is not a stable solution). His answer is basically saying that this answer is not a good way to do it:
ARG s # passed in via --build-arg s=foo
ARG sname # passed in via --build-arg sname=bar
RUN echo $s
RUN echo $sname
ENV sname $sname # this is the key part
RUN useradd -ms /bin/bash newuser
USER newuser
WORKDIR /home/newuser
COPY $s .
ENTRYPOINT /bin/bash /home/newuser/$sname # works, but is not stable!
don't ask me why the COPY command picks up the variable that was declared via ARG, but that the ENTRYPOINT command does not seem to pick up the variable declared via ARG, but only picks up the variable declared via ENV. At least, this appears to be the case.

I spent a lot of time to find out that.
Don't works !
ARG install="bundle install --jobs=4"
FROM ruby:2.6.3-alpine
RUN eval $install
But this works...
FROM ruby:2.6.3-alpine
ARG install="bundle install --jobs=4"
RUN eval $install
Then to build the docker image:
docker build -t server --no-cache --build-arg install="bundle install --without development test" .`

I wanted both variable substitution and arguments passing.
Let's say our Dockerfile has:
ENV VARIABLE=replaced
And we want to run this:
docker run <image> arg1 arg2
I obviously wanted this output:
replaced arg1 arg2
I eventually found this one:
ENTRYPOINT [ "sh", "-c", "echo $VARIABLE $0 $#" ]
It works!!!
But I feel SOOOO dirty!
Obviously, in real life, I wanted to do something more useful:
docker run <image> --app.options.additional.second=true --app.options.additional.third=false
ENTRYPOINT [ "sh", "-c", "java -Xmx$XMX $0 $#", \
"-jar", \
"/my.jar", \
"--app.options.first=true" ]
Why a so complicated answer?
"ENTRYPOINT java..." would not pass docker arguments to the entrypoint => "ENTRYPOINT [..." is mandatory for that
"ENTRYPOINT [..." will NOT call the shell, so no variable substitution is done at all => "sh -c" is mandatory for that
"sh -c" only take the FIRST argument passed to it and split it in command+arguments => so everything must be in the first argument of "sh -c" for variables to be visible by the command
Docker arguments are passed as extra array entries of "ENTRYPOINT [..." => so the "$#" is necessary to "copy" the remainings arguments of the "sh -c" into the "echo ..." command to be executed (and as a bonus, we can reuse the additional array entries of ENTRYPOINT[] to place forced arguments in a readable way in the Dockerfile)
"$#" removes the first argument => so explicit "$0" must be used before "$#"
Fiou...
I added a comment to this issue, for Docker developers to see what we are forced to do and perhaps change their mind to make ENTRYPOINT[] replace environment variables: https://github.com/moby/moby/issues/4783#issuecomment-442466609

Related

pass arguments in dockerfile?

I want to pass an argument to my dockerfile such that I should be able to use that argument in my run command but it seems I am not able to do so
I am using a simple bash file that will trigger the docker build and docker run
FROM openjdk:8 AS SCRATCH
WORKDIR /
ADD . .
RUN apt install unzip
RUN unzip target/universal/rule_engine-1.0.zip -d target/universal/rule_engine-1.0
COPY target/universal/rule_engine-1.0 .
ENV MONGO_DB_HOST="host.docker.internal"
ENV MONGO_DB_PORT="27017"
EXPOSE 9000
ARG path
CMD target/universal/rule_engine-1.0/bin/rule_engine -Dconfig.file=$path
above is my dockerfile
and below is my bash file which will access this dockerfile
#!/bin/bash
# change the current path to rule engine path
cd /Users/zomato/Documents/Intern_Project/rule_engine
sbt dist
ENVIR=$1
config=""
if [ $ENVIR == "local" ]
then
config=conf/application.conf
elif [ $ENVIR == "staging" ]
then
config=conf/staging.conf
else
config=conf/production.conf
fi
echo $config
docker build --build-arg path=$config -t rule_engine_zip .
docker run -i -t -p 9000:9000 rule_engine_zip
but when i access the dockerfile through bash script which will set config variable I am not able to set path variable in last line of dockerfile to the value of config.
ARG values won't be available after the image is built, so
a running container won’t have access to those values. To dynamically set an env variable, you can combine both ARG and ENV (since ENV can't be overridden):
ARG PATH
ENV P=${PATH}
CMD target/universal/rule_engine-1.0/bin/rule_engine -Dconfig.file=$P
For further explanation, I recommend this article, which explains the difference between ARG and ENV in a clear way:
As you can see from the above image, the ARG values are available only during the image build.

Docker image env variables overwritten by local machine

Why is it that when checking the env for an image I create, I get the image environment variables listed as expected, but when I try to access one of those env variables (i.e. $PATH), I'm getting my local machines environment variable output instead?
I believe I misunderstand how docker environment variables work. I'm attempting to run some commands against a docker container and am seeing what I consider unexpected behavior. I have created a simple example to try to demonstrate.
Dockerfile:
FROM node:12.13.0
ENV PATH="${PATH}:/custom-path/goes-here"
Commands:
docker build . -tag env-test
docker run env-test /bin/bash -c "env"
docker run env-test /bin/bash -c "$PATH"
Expected Output from final two commands.
docker run env-test /bin/bash -c "env".
...
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/custom-path/goes-here
...
docker run evn-test /bin/bash -c "echo $PATH"
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/custom-path/goes-here
Actual Output from final two commands
docker run env-test /bin/bash -c "env".
...
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/custom-path/goes-here
...
docker run evn-test /bin/bash -c "echo $PATH"
/Users/local-machine-user/Downloads/google-cloud-sdk/bin:/Users/local-machine-user/.nvm/versions/node/v12.16.1/bin:/Library/Frameworks/Python.framework/Versions/3.7/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/local-machine-user/Downloads/google-cloud-sdk/bin:/Library/Frameworks/Python.framework/Versions/3.7/bin
The output of running echo $PATH against the created image is returning my local machines $PATH variable. What?
The primary thing I'm trying to do is execute a script against the docker image that requires those environment variables I set in the image, but the script fails because the environment variables the script uses end up being for my local machine and not the ones specified in the image.
Say you're trying to run your third example
docker run env-test /bin/bash -c "echo $PATH"
The first thing that happens here is that your local shell processes this command and does its usual set of expansions. Environment variable references in double quotes are expanded, for example. Once it's built the final command line, then the shell executes it.
A generally useful trick is to just put echo at the front of the command
echo docker run env-test /bin/bash -c "echo $PATH"
This will show you the command that would have been run, but not actually run it.
To make this work you need to cause your local shell to not expand environment variables, so that the shell you're launching in the container can do it. Either single quotes or backslash escaping will work for this
docker run env-test /bin/sh -c 'echo $PATH'
docker run env-test /bin/sh -c "echo \$PATH"
The primary thing I'm trying to do is execute a script against the docker image that requires those environment variables I set in the image
The best way to approach this is probably to write a normal shell script and COPY it into your image. This saves both layers of quoting and confusion around which shell is processing things like variables. If you can't modify the image, an alternative is to bind-mount a script from the host.
# If the script is in the image
docker run --rm env-test path-echoer.sh
# If not
docker run --rm -v $PWD:/scripts env-test /scripts/path-echoer.sh
You should escape the dollar sign when using $PATH in a string - "echo \$PATH"
What happens is that when running this line:
docker run evn-test /bin/bash -c "echo $PATH"
Bash first translate $PATH, then passes that string into the docker container. So the command that is ran inside the container is:
docker run evn-test /bin/bash -c "echo /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

'docker run' ignores first command appended to ENTRYPOINT ["/bin/bash", "-c", ". foo.sh"] but not ["bash", "foo.sh"]

I am trying to run a docker image which executes a bash script and passes run-time arguments to that bash script. I have found that when I build the image using the recommended ENTRYPOINT ["/bin/bash", "-c", ". foo.sh"] entrypoint, the first argument appended to the docker run command doesn't get picked up by my script, but when I build the image with ENTRYPOINT ["bash", "foo.sh"], it does.
A toy version of the shell script looks like this:
#!/bin/bash
echo you got "$#" args
ARG1=${1:-foo}
ARG2=${2:-bar}
ARG3=${3:-1}
ARG4=${4:-$(date)}
echo "$ARG1"
echo "$ARG2"
echo "$ARG3"
echo "$ARG4"
so basically the script expects up to 4 command line arguments that each have default values.
The original Dockerfile I tried looks like this:
FROM ubuntu
COPY foo.sh foo.sh
ENTRYPOINT ["/bin/bash", "-c", ". foo.sh"]
and was based on a number of resources I found for how to properly execute a shell script using the exec form of ENTRYPOINT recommended by docker.
After building this image with docker build -t foo ., I run it with docker run -it foo first second third fourth and I get the following output:
you got 3 args
second
third
fourth
Tue Jul 2 13:14:52 UTC 2019
so clearly the first argument appended to the docker run command is dropped somewhere along the line, and the only arguments that get ingested by the shell command are the second, third, and fourth.
I spent ages trying to diagnose the issue, and so far haven't figured out why this is happening. The best I've come up with is somewhat of a hacky workaround, after discovering that changing the entrypoint to simply ENTRYPOINT ["bash", "pilates.sh"] produces the desired results.
I would love to know the following: 1. Why does the original entrypoint drop the first run-time argument? 2. Why does the second entrypoint work any differently than the first?
When you run bash -c 'something' foo bar baz, "foo" becomes the zero'th parameter (i.e. $0)
You need to insert a dummy param in there, perhaps
ENTRYPOINT ["/bin/bash", "-c", ". foo.sh", "bash"]
This is documented in the bash man page, in the description of the -c option.
A Docker container runs a single process, specified in your case by the ENTRYPOINT setting; when that process exits the container exits. Here this means that there’s no surrounding shell environment you need to update, so there’s no need to run your script using the . built-in; and once you’re just running a simple command, there’s also no need to wrap your command in a sh -c wrapper.
That yields a Dockerfile like
FROM ubuntu
COPY foo.sh foo.sh
RUN chmod +x foo.sh # if it’s not executable already
ENTRYPOINT ["./foo.sh"]
This also avoids the issue with sh -c consuming its first argument noted in #GlennJackman’s answer.
I needed to access environment variables in container startup command. This works since Docker 1.121
SHELL [ "/bin/sh", "-c", "exec my_app \"$#\"" ]
ENTRYPOINT
The blank ENTRYPOINT will become $0 and all other arguments will be passed as-is.
Example
FROM busybox
ENV MY_VAR=foobar
SHELL [ "/bin/sh", "-c", "printf '[%s]\n' \"${MY_VAR}\" \"$#\"" ]
ENTRYPOINT
docker run foo/bar a b c
[foobar]
[a]
[b]
[c]

Unable to pass variables to run docker container [duplicate]

If I set an environment variable, say ENV ADDRESSEE=world, and I want to use it in the entry point script concatenated into a fixed string like:
ENTRYPOINT ["./greeting", "--message", "Hello, world!"]
with world being the value of the environment varible, how do I do it? I tried using "Hello, $ADDRESSEE" but that doesn't seem to work, as it takes the $ADDRESSEE literally.
You're using the exec form of ENTRYPOINT. Unlike the shell form, the exec form does not invoke a command shell. This means that normal shell processing does not happen. For example, ENTRYPOINT [ "echo", "$HOME" ] will not do variable substitution on $HOME. If you want shell processing then either use the shell form or execute a shell directly, for example: ENTRYPOINT [ "sh", "-c", "echo $HOME" ].
When using the exec form and executing a shell directly, as in the case for the shell form, it is the shell that is doing the environment variable expansion, not docker.(from Dockerfile reference)
In your case, I would use shell form
ENTRYPOINT ./greeting --message "Hello, $ADDRESSEE\!"
After much pain, and great assistance from #vitr et al above, i decided to try
standard bash substitution
shell form of ENTRYPOINT (great tip from above)
and that worked.
ENV LISTEN_PORT=""
ENTRYPOINT java -cp "app:app/lib/*" hello.Application --server.port=${LISTEN_PORT:-80}
e.g.
docker run --rm -p 8080:8080 -d --env LISTEN_PORT=8080 my-image
and
docker run --rm -p 8080:80 -d my-image
both set the port correctly in my container
Refs
see https://www.cyberciti.biz/tips/bash-shell-parameter-substitution-2.html
I tried to resolve with the suggested answer and still ran into some issues...
This was a solution to my problem:
ARG APP_EXE="AppName.exe"
ENV _EXE=${APP_EXE}
# Build a shell script because the ENTRYPOINT command doesn't like using ENV
RUN echo "#!/bin/bash \n mono ${_EXE}" > ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
# Run the generated shell script.
ENTRYPOINT ["./entrypoint.sh"]
Specifically targeting your problem:
RUN echo "#!/bin/bash \n ./greeting --message ${ADDRESSEE}" > ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]
I SOLVED THIS VERY SIMPLY!
IMPORTANT: The variable which you wish to use in the ENTRYPOINT MUST be ENV type (and not ARG type).
EXAMPLE #1:
ARG APP_NAME=app.jar # $APP_NAME can be ARG or ENV type.
ENV APP_PATH=app-directory/$APP_NAME # $APP_PATH must be ENV type.
ENTRYPOINT java -jar $APP_PATH
This will result with executing:
java -jar app-directory/app.jar
EXAMPLE #2 (YOUR QUESTION):
ARG ADDRESSEE="world" # $ADDRESSEE can be ARG or ENV type.
ENV MESSAGE="Hello, $ADDRESSEE!" # $MESSAGE must be ENV type.
ENTRYPOINT ./greeting --message $MESSAGE
This will result with executing:
./greeting --message Hello, world!
Please verify to be sure, whether you need quotation-marks "" when assigning string variables.
MY TIP: Use ENV instead of ARG whenever possible to avoid confusion on your part or the SHELL side.
For me, I wanted to store the name of the script in a variable and still use the exec form.
Note: Make sure, the variable you are trying to use is declared an environment variable either from the commandline or via the ENV directive.
Initially I did something like:
ENTRYPOINT [ "${BASE_FOLDER}/scripts/entrypoint.sh" ]
But obviously this didn't work because we are using the shell form and the first program listed needs to be an executable on the PATH. So to fix this, this is what I ended up doing:
ENTRYPOINT [ "/bin/bash", "-c", "exec ${BASE_FOLDER}/scripts/entrypoint.sh \"${#}\"", "--" ]
Note the double quotes are required
What this does is to allow us to take whatever extra args were passed to /bin/bash, and supply those same arguments to our script after the name has been resolved by bash.
man 7 bash
-- A -- signals the end of options and disables further
option processing. Any arguments after the -- are treated
as filenames and arguments. An argument of - is
equivalent to --.
In my case worked this way: (for Spring boot app in docker)
ENTRYPOINT java -DidMachine=${IDMACHINE} -jar my-app-name
and passing the params on docker run
docker run --env IDMACHINE=Idmachine -p 8383:8383 my-app-name
I solved the problem using a variation on "create a custom script" approach above. Like this:
FROM hairyhenderson/figlet
ENV GREETING="Hello"
RUN printf '#!/bin/sh\nfiglet -W \${GREETING} \$#\n' > /runme && chmod +x /runme
ENTRYPOINT ["/runme"]
CMD ["World"]
Run like
docker container run -it --rm -e GREETING="G'Day" dockerfornovices/figlet-greeter Alec
If someone wants to pass an ARG or ENV variable to exec form of ENTRYPOINT then a temp file created during image building process might be used.
In my case I had to start the app differently depending on whether the .NET app has been published as self-contained or not.
What I did is I created the temp file and I used its name in the if statement of my bash script.
Part of my dockerfile:
ARG SELF_CONTAINED=true #ENV SELF_CONTAINED=true also works
# File has to be used as a variable as it's impossible to pass variable do ENTRYPOINT using Exec form. File name allows to check whether app is self-contained
RUN touch ${SELF_CONTAINED}.txt
COPY run-dotnet-app.sh .
ENTRYPOINT ["./run-dotnet-app.sh", "MyApp" ]
run-dotnet-app.sh:
#!/bin/sh
FILENAME=$1
if [ -f "true.txt" ]; then
./"${FILENAME}"
else
dotnet "${FILENAME}".dll
fi
Here is what worked for me:
ENTRYPOINT [ "/bin/bash", "-c", "source ~/.bashrc && ./entrypoint.sh ${#}", "--" ]
Now you can supply whatever arguments to the docker run command and still read all environment variables.

Setting environment variables when running docker in detached mode

If I include the following line in /root/.bashrc:
export $A = "AAA"
then when I run the docker container in interactive mode (docker run -i), the $A variable keeps its value. However if I run the container in detached mode I cannot access the variable. Even if I run the container explicitly sourcing the .bashrc like
docker run -d my_image /bin/bash -c "cd /root && source .bashrc && echo $A"
such line produces an empty output.
So, why is this happening? And how can I set the environment variables defined in the .bashrc file?
Any help would be very much appreciated!
The first problem is that the command you are running has $A being interpreted by your hosts shell (not the container shell). On your host, $A is likely black, so your effectively command becomes:
docker run -i my_image /bin/bash -c "cd /root && source .bashrc && echo "
Which does exactly as it says. We can escape the variable so it is sent to the container and properly evaluated there:
docker run -i my_image /bin/bash -c "echo \$A"
But this will also be blank because, although the container is, the shell is not in interactive mode. But we can force it to be:
docker run -i my_image /bin/bash -i -c "echo \$A"
Woohoo, we finally got our desired result. But with an added error from bash because there is no TTY. So, instead of interactive mode, we can just set a psuedo-TTY:
docker run -t my_image /bin/bash -i -c "echo \$A"
After running some tests, it appears that when running a container in detached mode, overidding the default environment variables doesnt always happen the way we want, depending on where you are in the Dockerfile.
As an exemple if, running a container in a detached container like so:
docker run **-d** --name image_name_container image_name
Whatever ENV variables you defined within the Dockerfile takes effect everywhere (read the rest and you will understand what the everywhere means).
example of a simple dockerfile (alpine is just a lighweight linux distribution):
FROM alpine:latest
#declaring a docker env variable and giving it a default value
ENV MY_ENV_VARIABLE dummy_value
#copying two dummy scripts into a place where i can execute them straight away
COPY ./start.sh /usr/sbin
COPY ./not_start.sh /usr/sbin
#in this script i could do: echo $MY_ENV_VARIABLE > /test1.txt
RUN not_start.sh
RUN echo $MY_ENV_VARIABLE > /test2.txt
#in this script i could do: echo $MY_ENV_VARIABLE > /test3.txt
ENTRYPOINT ["start.sh"]
Now if you want to run your container in detached and override some ENV variables, like so:
docker run **-d** -e MY_ENV_VARIABLE=new_value --name image_name_container image_name
Surprise! The var MY_ENV_VARIABLE is only overidden inside the script that is run in the ENTRYPOINT (and i checked, same thing happens if your replace ENTRYPOINT with CMD). It would also be overidden in a subscript that you could call from this start.sh script. But the MY_EV_VARIABLE variables that are called within a RUN dockerfile command or within the dockerfile itself do not get overidden.
In other words we would have $MY_ENV_VARIABLE being replaced by the value dummy_value and new_value depending on if you are in the ENTRYPOINT or not.

Resources