Docker CMD weirdness when ENTRYPOINT is a shell script - docker

Here's a simple Dockerfile
FROM centos:6.6
ENTRYPOINT ["/bin/bash", "-l", "-c"]
CMD ["echo", "foo"]
Unfortunately it doesn't work. Nothing is echo'd when you run the resulting container that's built.
If you comment out the ENTRYPOINT then it works. However, if you set the ENTRYPOINT to /bin/sh -c, then it fails again
FROM centos:6.6
ENTRYPOINT ["/bin/sh", "-c"]
CMD ["echo", "foo"]
I thought that was the default ENTRYPOINT for an container that didn't have one defined, why didn't that work?
Finally, this also works
FROM centos:6.6
ENTRYPOINT ["/bin/bash", "-l", "-c"]
CMD ["echo foo"]
Before I submit an issue, I wanted to see if I'm doing something obviously wrong?
I'm using rvm inside my container which sort of needs a login shell to work right.

Note that the default entry point/cmd for an official centos 6 image is:
no entrypoint
only CMD ["/bin/bash"]
If you are using the -c command, you need to pass one argument (which is the full command): "echo foo".
Not a series of arguments (CMD ["echo", "foo"]).
As stated in dockerfile CMD section:
If you use the shell form of the CMD, then the <command> will execute in /bin/sh -c:
FROM ubuntu
CMD echo "This is a test." | wc -
If you want to run your <command> without a shell then you must express the command as a JSON array and give the full path to the executable
Since echo is a built-in command in the bash and C shells, the shell form here is preferable.

Related

Dockerfile - exec form of ENTRYPOINT and shell form of CMD

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

How are CMD and ENTRYPOINT exec forms in a docker file parsed?

I am running Jupyter in a Docker container. The following shell form will run fine:
CMD jupyter lab --ip='0.0.0.0' --port=8888 --no-browser --allow-root /home/notebooks
But the following one on docker file will not:
ENTRYPOINT ["/bin/sh", "-c"]
CMD ["jupyter", "lab", "--ip='0.0.0.0'", "--port=8888", "--no-browser", "--allow-root", "/home/notebooks"]
The error is:
usage: jupyter [-h] [--version] [--config-dir] [--data-dir] [--runtime-dir] [--paths] [--json] [subcommand]
jupyter: error: one of the arguments --version subcommand --config-dir --data-dir --runtime-dir --paths is required
So obviously /bin/sh -c sees the jupyter argument, but not the following ones.
Interestingly,
CMD ["jupyter", "lab", "--ip='0.0.0.0'", "--port=8888", "--no-browser", "--allow-root", "/home/notebooks"]
will run fine, so it cannot be the number of arguments, or can it?
According to https://docs.docker.com/engine/reference/builder/#cmd, the shell form of CMD executes with /bin/sh -c. So from my point of view I see little difference in the 2 versions. But the reason must be how the exec forms are being evaluated when ENTRYPOINT and CMD are present at the same time.
At a very low level, Linux commands are executed as a series of "words". Typically your shell will take a command line like ls -l "a directory" and breaks that into three words ls -l a directory. (Note the space in "a directory": in the shell form that needs to be quoted to be in the same word.)
The Dockerfile CMD and ENTRYPOINT (and RUN) commands have two forms. In the form you've specified that looks like a JSON array, you are explicitly specifying how the words get broken up. If it doesn't look like a JSON array then the whole thing is taken as a single string, and wrapped in an sh -c command.
# Explicitly spelling out the words
RUN ["ls", "-l", "a directory"]
# Asking Docker to run it via a shell
RUN ls -l 'a directory'
# The same as
RUN ["sh", "-c", "ls -l 'a directory'"]
If you specify both ENTRYPOINT and CMD the two lists of words just get combined together. The important thing for your example is that sh -c takes the single next word and runs it as a shell command; any remaining words can be used as $0, $1, ... positional arguments within that command string.
So in your example, the final thing that gets run is more or less
ENTRYPOINT+CMD ["sh", "-c", "jupyter", ...]
# If the string "jupyter" contained "$1" it would expand to the --ip option
The other important corollary to this is that, practically, ENTRYPOINT can't be the bare-string format: when the CMD is appended to it you get
ENTRYPOINT some command
CMD with args
ENTRYPOINT+CMD ["sh", "-c", "some command", "sh", "-c", "with args"]
and by the same rule all of the CMD words get ignored.
In practice you almost never need to explicitly put sh -c or a SHELL declaration in a Dockerfile; use a string-form command instead, or put complex logic into a shell script.

How to access build args in ENTRYPOINT dockerfile

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.

Add arguments to entrypoint/cmd for different containers

I have this simple node.js image:
FROM node:12
USER root
WORKDIR /app
COPY package.json .
COPY package-lock.json .
RUN npm i --production
COPY . .
ENTRYPOINT node dist/main.js
ultimately, I just want to be able to pass different arguments to node dist/main.js like so:
docker run -d my-image --foo --bar=3
so that the executable when run is
node dist/main.js --foo --bar=3
I have read about CMD / ENTRYPOINT and I don't know how to do this, anybody know?
I would suggest writing a custom entrypoint script to handle this case.
In general you might find it preferable to use CMD to ENTRYPOINT in most cases. In particular, the debugging shell pattern of
docker run --rm -it myimage sh
is really useful, and using ENTRYPOINT to run your main application breaks this. The entrypoint script pattern I’m about to describe is also really useful in general and it’s easy to drop in if your main container process is described with CMD.
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["node", "dist/main.js"]
The script itself is an ordinary shell script that gets passed the CMD as command-line arguments. It will typically end with exec "$#" to actualy run the CMD as the main container process.
Since the entrypoint script is a shell script, and it gets passed the command from the docker run command line as arguments, you can do dynamic switching on it, and meet both your requirement to just be able to pass additional options to your script and also my requirement to be able to run arbitrary programs instead of the Node application.
#!/bin/sh
if [ $# = 1 ]; then
# no command at all
exec node dist/main.js
else
case "$1" of
-*) exec node dist/main.js "$#" ;;
*) exec "$#" ;;
esac
fi
This seems to work:
ENTRYPOINT ["node", "dist/main.js"]
CMD []
which appears to be equivalent to just:
ENTRYPOINT ["node", "dist/main.js"]
you can't seem to use single quotes - double quotes are necessary, and you have to use shell syntax..not sure why, but this style does not work:
ENTRYPOINT node dist/main.js

Default Docker entrypoint

I am creating an image from another image that set a specific entrypoint. However I want my image to have default one. How do I reset the ENTRYPOINT?
I tried the following Dockerfile:
FROM some-image
ENTRYPOINT ["/bin/sh", "-c"]
Unfortunately it doesn't work like the default entrypoint as it need the command to be quoted.
docker run myimage ls -l / # "-l /" arguments are ignored
file1 file2 file3 # files in current working directory
docker run myimage "ls -l /" # works correctly
How do I use commands without quoting?
To disable an existing ENTRYPOINT, set an empty array in your docker file
ENTRYPOINT []
Then your arguments to docker run will exec as a shell form CMD would normally.
The reason your ENTRYPOINT ["/bin/sh", "-c"] requires quoted strings is that without the quotes, the arguments to ls are being passed to sh instead.
Unquoted results in lots of arguments being sent to sh
"/bin/sh", "-c", "ls", "-l", "/"
Quoting allows the complete command (sh -c) to be passed on to sh as one argument.
"/bin/sh", "-c", "ls -l /"
This isn't really related to docker. Try running the following:
/bin/sh -c echo foo
/bin/sh -c "echo foo"
The -c means that /bin/sh only picks up one argument. So removing the -c from the entrypoint you define should fix it. This is more flexible than resetting the entry point; e.g. you can do this to use Software Collections:
ENTRYPOINT ["scl", "enable", "devtoolset-4", "--", "bash"]
Note: beware of ENTRYPOINT [].
As mentioned in moby/moby issue 3465 ("Reset properties inherited from parent image"), Brendon C. notes:
Looks like ENTRYPOINT [] and ENTRYPOINT [""] both invalidate the cache on each build when not using BuildKit.
Simple Dockerfile to demonstrate:
FROM jrottenberg/ffmpeg:4.3-alpine311 as base
ENTRYPOINT []
RUN echo "HERE!"
Steps 2 and 3 will never use cache. This is my workaround:
FROM jrottenberg/ffmpeg:4.3-alpine311 as base
ENTRYPOINT ["/usr/bin/env"]
RUN echo "HERE!"

Resources