I need to pass optional, runtime parameter to a command in Docker.
The idea is that if PARAM env variable is set when docker is being run - it should be passed to java command as --key VALUE , and when runtime parameter is not set - it shoulddn't pass anything - in particular it shouldn't pass --key parameter name.
I.e. it should run following command if PARAM env variable is set:
/bin/java -jar artifact.jar --key $PARAM
And following if it's not:
/bin/java -jar artifact.jar
I wanted to use :+ syntax, but it's resolved during build time, which means it won't be affected by runtime env variable.
docker build -t test-abc . && docker run -e "PARAM=oooo" test-abc
FROM openjdk:17
ENV PARAM=${PARAM:+"--key $PARAM"}
ENTRYPOINT /bin/java -jar artifact.jar $PARAM
Prepare a script that will correctly handle arguments:
#!/bin/bash
args=()
if [[ -v PARAM ]]; then
args=(--key "$PARAM")
fi
/bin/java -jar artifact.jar "${args[#]}" "$#"
And add it:
ADD entrypoint.sh /entrypoint.sh
CMD chmod +x /entrypoint.sh
ENTRYPOINT /entrypoint.sh
If you are using ENTRYPOINT here, and you rewrite it in JSON-array syntax, then you can pass additional arguments to your program after the docker run image-name. (Technically they replace the CMD.)
# Dockerfile: do use JSON-array syntax; do not attempt to handle options;
# must use ENTRYPOINT for "container-as-command" layout
ENTRYPOINT ["java", "-jar", "artifact.jar"]
docker run test-abc --key oooo
You can also write the parameter expansion directly inside the command string. This requires you to use shell-syntax ENTRYPOINT or CMD, and so will essentially forbid you from providing extra options you don't set up this way. The ${VARIABLE:+value} syntax is supported in the POSIX specification so this should work with all Linux distribution base images, even if they don't include bash.
ENTRYPOINT java -jar artifact.jar ${PARAM:=--key "$PARAM"}
BashFAQ/050 has some more syntax suggestions, some of which are bash-specific, but the more detailed ones do require a good understanding of shell splitting and expansions.
Related
I am looking at Docker's documentation to understand what would be behavior of ENTRYPOINT defined in exec form and CMD defined in shell form.
The example in the docs only shows something like exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd which does not tell me anything.
For example, what if we had:
ENV JAVA_OPTS '-XX:+UseG1GC -Xms512m -Xmx1536m'
ENTRYPOINT ["java"]
CMD $JAVA_OPTS -jar app.jar
Would the problem with signal propagation exist here (in other words, would any extra subshell be spawned here)?
If either ENTRYPOINT or CMD are not JSON arrays, they are interpreted as strings and converted to a length-3 array ["/bin/sh", "-c", "..."].
The resulting two lists are concatenated.
So in your example, the final command list would be
["java", "/bin/sh", "-c", "$JAVA_OPTS -jar app.jar"]
or in Bourne shell syntax
java /bin/sh -c '$JAVA_OPTS -jar app.jar'
This passes the shell interpreter /bin/sh as an argument to java; that almost certainly is not what you intend.
If the CMD is anything other than a complete command, it must use the JSON-array syntax, which in turn means it can't use any shell features and it can't expand environment variable references. This would include both the "container-as-command" pattern where ENTRYPOINT is the command to run and CMD its arguments, and the antipattern you show here where ENTRYPOINT is the interpreter only (and you have to repeat the -jar app.jar option in a docker run command override).
I prefer a setup where CMD is always a complete shell command. If you have an ENTRYPOINT at all, it's a script that does some startup-time setup and then runs exec "$#" to run the command passed as arguments. This can accept either form of CMD.
# ENTRYPOINT ["./docker-entrypoint.sh"] # optional
CMD java $JAVA_OPTS -jar app.jar # in a single shell-format CMD
It took me a while but I finally figured out what is the point of /bin/sh -c in this use-case. We can for example use tini as an entrypoint
ENTRYPOINT ["tini", "--"]
and then a shell for of CMD, but we need to use exec in order to replace a subshell, that is
CMD exec java $JAVA_OPTS -jar app.jar
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?
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.)
I am trying to deploy an app in payara micro based on payara dockerimage and I need to pass one arguement snapshotversion in ENTRYPOINT(basically i want to access the build args in ENTRYFORM) exec form, as exec form of ENTRYPOINT is preferred: my docker file is as follows:
FROM payara/micro:5.193.1
ARG snapshotversion
ENV snapshotvs=$snapshotversion
RUN jar xf payara-micro.jar
COPY /service/war/target/app-emailverification-service-war-${snapshotversion}.war ${DEPLOY_DIR}/
COPY ojdbc6.jar ${PAYARA_HOME}/
COPY --chown=payara domain.xml /opt/payara/MICRO-INF/domain/domain.xml
RUN cd /opt/payara/MICRO-INF/domain && ls -lrt
#ENTRYPOINT ["java", "-jar", "/opt/payara/payara-micro.jar", "--deploy", "/opt/payara/deployments/app-service-war-$snapshotvs.war", "--domainConfig", "/opt/payara/MICRO-INF/domain/domain.xml","--addLibs", "/opt/payara/ojdbc6.jar"]
ENTRYPOINT java -jar /opt/payara/payara-micro.jar --deploy /opt/payara/deployments/app-service-war-$snapshotvs.war --domainConfig /opt/payara/MICRO-INF/domain/domain.xml --addLibs /opt/payara/ojdbc6.jar
The commented ENTRYPOINT does not work. Container logs says invalid deployment. What am i missing here? Also how can I use CMD with this. Can someone post an example.
The commented line doesn't work, because it is an exec form of ENTRYPOINT, which doesn't invoke shell (/bin/sh -c), so variable substitution doesn't happening.
If you want to use an exec form and environment variables you need to specify it directly:
ENTRYPOINT ["sh", "-c", "your command with env variable"]
To your question about how can you use CMD with this, for example like this:
ENTRYPOINT ["sh", "-c"]
CMD ["your command with env variable"]
You mentioned, that you want to use build args in ENTRYPOINT instruction. It's not really possible, because nor ARG nor ENV are expanded in ENTRYPOINT or CMD: https://docs.docker.com/engine/reference/builder/#environment-replacement, https://docs.docker.com/engine/reference/builder/#scope
Also you could take a look at great page with best practices for writing Dockerfile and ENTRYPOINT instructions specifically.
Two suggestions that complement each other:
If you're COPYing a file into the image, you can give it a fixed name inside the image. That avoids this problem.
WORKDIR /opt/payara
COPY service/war/target/app-emailverification-service-war-${snapshotversion}.war deployments/app-service.war
If you have a particularly long or involved command that you're trying to make be the main container process, wrap it in a shell script. You want to make sure to exec the main container process to avoid some trouble around signal handling (resulting in docker stop pausing for 10 seconds and then hard-killing your actual process).
#!/bin/sh
exec java \
-jar /opt/payara/payara-micro.jar \
--deploy /opt/payara/deployments/app-service.war \
--domainConfig /opt/payara/MICRO-INF/domain/domain.xml \
--addLibs /opt/payara/ojdbc6.jar
COPY launch.sh ./
RUN chmod +x launch.sh
CMD ["/opt/payara/launch.sh"]
In this second case, it's a shell script, so you can have ordinary shell variable substitutions.
I've a dockerfile where I use a custom entrypoint.sh. In this file I want to use the ARGS which I pass from docker-compose to the dockerfile.
The problem is that I don't get the content of the variable to the dockerfile I just get the variable name.
For example:
ARGS ENVIROMENT=production
ENTRYPOINT ["/var/www/html/entrypoint.sh"]
CMD ["${ENVIROMENT}"]
entrypoint.sh
#!/bin/sh
cd /var/www/html
composer update
echo $1;
The echo $1 show "${Enviroment}" instead of "production" what I expect.
Ouch ! You've hit a sensible point of Docker with this question.
But first, let me clarify some point here :
First of all, you have a typo in your example. It's ARG not ARGS
ARG allows you to define a build-time variable. Meaning that this variable will only be usefull when doing a docker image build command. You'll then be able to override it with --build-arg. For example :
docker image build --build-arg ENVIROMENT=integration ...`
At the opposite, ENV allows you to define an environment variable which can be used during runtime.
You can find all the info you need in the official documentation for env and arg
Now, back to the point...
To make it simple:
Do not use both ENTRYPOINT and CMD when you want to pass some environment variable to your entrypoint from your cmd. It's just a pain. Really.
When you want to use a environment variable inside CMD, then you'll have to either use bash format, or to prefix the command with sh -c for exec format :
CMD ["sh", "-c", "echo ${GREETINGS}"]
#or
CMD echo ${GREETINGS}
Here is a Dockerfile that works with both syntax (just uncomment the CMD you want to use) :
FROM debian:8
ENV GREETINGS="hello world"
#CMD ["sh", "-c", "echo ${GREETINGS}"]
#CMD echo ${GREETINGS}
You can find more detailled info on those issues :
Issue 5509
Issue 34772
I would like to complete Marc abouchacra's answer.
What is still missing is how to use the ARG command.
A possible solution could be:
ARG ENVIRONMENT=production
ENV ENVIRONMENT=$ENVIRONMENT
CMD exec /var/www/html/entrypoint.sh $ENVIRONMENT
The exec is there to make sure your entrypoint.sh is the process with the PID=1.
As per Docker documentation:
There can only be one CMD instruction in a Dockerfile. If you list more than one CMD then only the last CMD will take effect.
I wish to execute a simple bash script(which processes docker environment variable) before the CMD command(which is init in my case).
Is there any way to do this?
Use a custom entrypoint
Make a custom entrypoint which does what you want, and then exec's your CMD at the end.
NOTE: if your image already defines a custom entrypoint, you may need to extend it rather than replace it, or you may change behavior you need.
entrypoint.sh:
#!/bin/sh
## Do whatever you need with env vars here ...
# Hand off to the CMD
exec "$#"
Dockerfile:
COPY entrypoint.sh /entrypoint.sh
RUN chmod 755 /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Docker will run your entrypoint, using CMD as arguments. If your CMD is init, then:
/entrypoint.sh init
The exec at the end of the entrypoint script takes care of handing off to CMD when the entrypoint is done with what it needed to do.
Why this works
The use of ENTRYPOINT and CMD frequently confuses people new to Docker. In comments, you expressed confusion about it. Here is how it works and why.
The ENTRYPOINT is the initial thing run inside the container. It takes the CMD as an argument list. Therefore, in this example, what is run in the container is this argument list:
# ENTRYPOINT = /entrypoint.sh
# CMD = init
["/entrypoint.sh", "init"]
# or shown in a simpler form:
/entrypoint.sh init
It is not required that an image have an ENTRYPOINT. If you don't define one, Docker has a default: /bin/sh -c.
So with your original situation, no ENTRYPOINT, and using a CMD of init, Docker would have run this:
/bin/sh -c 'init'
^--------^ ^--^
| \------- CMD
\--------------- ENTRYPOINT
In the beginning, Docker offered only CMD, and /bin/sh -c was hard-coded as the ENTRYPOINT (you could not change it). At some point along the way, people had use cases where they had to do more custom things, and Docker exposed ENTRYPOINT so you could change it to anything you want.
In the example I show above, the ENTRYPOINT is replaced with a custom script. (Though it is still ultimately being run by sh, because it starts with #!/bin/sh.)
That ENTRYPOINT takes the CMD as is argument. At the end of the entrypoint.sh script is exec "$#". Since $# expands to the list of arguments given to the script, this is turned into
exec "init"
And therefore, when the script is finished, it goes away and is replaced by init as PID 1. (That's what exec does - it replaces the current process with a different command.)
How to include CMD
In the comments, you asked about adding CMD in the Dockerfile. Yes, you can do that.
Dockerfile:
CMD ["init"]
Or if there is more to your command, e.g. arguments like init -a -b, would look like this:
CMD ["init", "-a", "-b"]
Dan's answer was correct, but I found it rather confusing to implement. For those in the same situation, here are code examples of how I implemented his explanation of the use of ENTRYPOINT instead of CMD.
Here are the last few lines in my Dockerfile:
#change directory where the mergeandlaunch script is located.
WORKDIR /home/connextcms
ENTRYPOINT ["./mergeandlaunch", "node", "keystone.js"]
Here are the contents of the mergeandlaunch bash shell script:
#!/bin/bash
#This script should be edited to execute any merge scripts needed to
#merge plugins and theme files before starting ConnextCMS/KeystoneJS.
echo Running mergeandlaunch script
#Execute merge scripts. Put in path to each merge script you want to run here.
cd ~/theme/rtb4/
./merge-plugin
#Launch KeystoneJS and ConnextCMS
cd ~/myCMS
exec "$#"
Here is how the code gets executed:
The ENTRYPOINT command kicks off the mergeandlaunch shell script
The two arguments 'node' and 'keystone.js' are passed along to the shell script.
At the end of the script, the arguments are passed on to the exec command.
The exec command then launched my node program the same way the Docker command CMD would do.
Thanks to Dan for his answer.
Although I found I had to do something like this within the Dockerfile:
WORKDIR /
COPY startup.sh /
RUN chmod 755 /startup.sh
ENTRYPOINT sh /startup.sh /usr/sbin/init
NOTE: I named the script startup.sh as opposed to entrypoint.sh
The key here was that I needed to provide 'sh' otherwise I kept getting "no such file..." errors coming out of 'docker logs -f container_name'.
See:
https://github.com/docker/compose/issues/3876