In this tutorial I will explain multiple ways you can use to define what you want to run in the container when you start the container. You may think it is easy, since you know you can use the docker run command on your host and as an argument pass the command that has to run in the container. You also know you can use the CMD instruction in the Dockerfile, but this is not the full story, so let's see our options.
Note: The tutorial requires jq and recent Bash version installed on your machine.
If you want to be notified about other videos, you can subscribe to my channel:
Before we begin running containers let's learn about the facts this tutorial was built around.
The actual command running in the container is based on the SHELL, ENTRYPOINT and the CMD instructions in the Dockerfile.
Commands defined as arguments of the RUN instruction will run during the build and never in the container started from the final image.
The RUN, ENTRYPOINT and the CMD instructions can be written in the exec form and the shell form.
The arguments of the CMD or instruction will be the arguments of the command defined as
an ENTRYPOINT if that exists and CMD is written in the exec form.
a SHELL if that exists and CMD is written in the shell form.
shell form means we define the command as a string and exec form means we define the command as a json list like ["echo", "Hello"].
The SHELL instruction must be written in the exec form as there is no other shell to which you could pass the command defined as a shell.
Passing commands as an argument to docker run is always equivalent to writing the CMD instruction in the exec form so the command will always be the argument of the entrypoint and never the shell.
If you don't have a shell, variables are not evaluated, so echo $HOME would just show $HOME on the console and the value of the variable.
Using docker run allows you to use variables which will be evaluated outside the container and not inside.
Even though passing commands to docker run is a kind of exec form, you can manually defined the shell in that form as an argument of docker run.
The command defined as a SHELL doesn't actually have to be a shell. It can be any command.
We need to create a simple Dockerfile called Dockerfile.v1 like this:
FROM ubuntu:22.04CMD ["echo", "$HOME"]
Let's build the image and run the container.
docker build .--force-rm-f Dockerfile.v1 -t localhost/command:v1
docker run -i--name"command-v1-0""localhost/command:v1"
And the output is
$HOME
We basically told Docker to execute echo $HOME without evaluating the variable $HOME. If you want to get the same result in your terminal interactively, you have to use apostrophes around each part of the command.
'echo''$HOME'
Since echo is the same without apostrophes, you could just run the following:
echo'$HOME'
So the statement that the exec form will not evaluate variables is proven, but the interactive examples doesn't prove that we don't have a shell. Let's ask Docker what command it actually ran.
Note: The jq part is required only because the original output would container a quoted string.
It shows that the actual command running in the container is
echo'My home is $HOME'
In some shells like zshell echo is a built-in command, but in bash in our container it is at /usr/bin/echo. You can check this by running the following using the base image:
docker run -it--rm ubuntu:22.04 which echo
So Docker just ran the binary instead of using a shell. Now let's pass the command as an argument to docker run.
docker run -i--name"command-v1-1""localhost/rimelek/tutorial-docker-command:v1"\echo"My home is $HOME"
In this case the output on my machine is
My home is /Users/ta
because my home dir on Mac is indeed /Users/ta and the variable was interpreted on the host, not in the container. This is the only reason it worked, but it could not use a variable this way inside the container.
And finally, check the command that ran in the container:
Now we will try the shell form in a CMD instruction. We have actually two ways to do this. First we will quote the entire command as a single argument of the CMD instruction. The content of Dockerfile.v2 is the following:
FROM ubuntu:22.04CMD "echo \"My home is $HOME\""
docker build .--force-rm-f Dockerfile.v2 -t localhost/rimelek/tutorial-docker-command:v2
docker run -i--name"command-v2-0""localhost/rimelek/tutorial-docker-command:v2"
It will show you the folowing error message:
/bin/sh: 1: echo "My home is /root": not found
The fact is that the CMD instruction's argument is not just what you quote but it also includes the quote itself. It means that echo "My home is $HOME" was interpreted as a single executable filename which couldn't be found obsiously. Let's find out what the command was in the container:
If you try it with an argument passed to docker run, as I mentioned at the beginning of the tutorial, we will actually use the exec form, so what happens then?
docker run -i--name"command-v2-1""localhost/rimelek/tutorial-docker-command:v2"\"echo \"My home is $HOME\""
We will get an error message, but in this case not from the process inside the container because the exec form will be interpreted by Docker itself:
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "echo \"My home is /Users/ta\"": stat echo "My home is /Users/ta": no such file or directory: unknown.
time="2023-07-02T13:12:52+02:00" level=error msg="error waiting for container: "
The environment variable was evaluated, but Docker couldn't find "echo \"My home is $HOME\"" as a single executable file, so it could not start the container, therefore we don't have any output. You can run
docker logs command-v2-1
to confirm it. In any other case when Docker can start the container the error will be thrown by the the process inside the container wich will generate logs. You could check the running command in the container, but it won't be different from a working command because the original quotation mark will be lost in the output:
It clearly shows that the argument of the CMD instruction became the argument of /bin/sh -c which is the default value of the SHELL instruction in the Dockerfile. Passing the command as an argument of docker run would again evaluate the variable on the host:
docker run -i--name"command-v3-1""localhost/rimelek/tutorial-docker-command:v3"\echo"My home is $HOME"
The next example using Dockerfile.v4 shows how you can use shell scripts with the CMD instruction.
FROM ubuntu:22.04COPY src/hello.sh /# COPY --chmod is supported with buildkit instead of the below instructionRUN chmod +x /hello.sh
CMD /hello.sh
Note that we also use a RUN instruction written in the shell form which is what we usually do as opposed to how we use the CMD instruction in which we usually use the exec form, but during the build process we often need a shell to use variables and pipe run a chain of commands. In this example we will stick to the shell form in CMD, so hello.sh will be the argument of the default shell again, which is /bin/sh -c.
docker build .--force-rm-f Dockerfile.v4 -t localhost/rimelek/tutorial-docker-command:v4
docker run -i--name"command-v4-0""localhost/rimelek/tutorial-docker-command:v4"
The output:
Hello Docker
Let's check the content of hello.sh and how we got this output:
name="${*:-Docker}"echo"Hello $name"
You probably noticed that there is no shebang line (#!/bin/bash) at the beginning of the file. Although many shell script has one, it is not required if you always want to pass the file to a specific shell as argument. And this is what happens using the shell form.
The 5th example shows that we can override the default shell and that doesn't even have to be an actuall shell. That perfectly demonstrates that the SHELL, ENTRYPOINT and CMD instructions are all about what will be the argument of what and knowing that you can do whatever you want with this information.
As funny as it is, we will use the ls command in the SHELL instruction which will take hello.sh as an argument. You can guess that it will just show the file path when we start the container, but let's build the image.
docker build .--force-rm-f Dockerfile.v5 -t localhost/rimelek/tutorial-docker-command:v5
docker run -i--name"command-v5-0""localhost/rimelek/tutorial-docker-command:v5"
Since in the previous example the problem was the entrypoint, let's use one in Dockerfile.v6:
FROM ubuntu:22.04COPY src/hello.sh /RUN chmod +x /hello.sh
SHELL ["/bin/bash", "-c"]ENTRYPOINT /hello.shCMD my friend
Build the image and run the container:
docker build .--force-rm-f Dockerfile.v6 -t localhost/rimelek/tutorial-docker-command:v6
docker run -i--name"command-v6-0""localhost/rimelek/tutorial-docker-command:v6"
The output is:
Hello Docker
Shouldn't the CMD be the agument of the entrypoint? Why did we get "Hello Docker" instead of "Hello my friend"? Let's check what was running in the container:
Since we wrote the ENTRYPOINT instruction in the shell form, it behaved exactly as the CMD would have worked. It became the argument of the shell which changed the original entrypoint to the following:
/bin/bash -c /hello.sh
Then we also wrote the CMD instruction in shell form so the new command became:
/bin/bash -c'my friend'
It seems because everything is in shell form the modified command could become the argument of the modified entrypoint, but it doesn't make sense, since the bash shell does not take any more arguments when we define the shell script as a command instead of a script file, so everything after -c hello.sh will be ignored without throwing any error message. It isn't a big issue, since the ENTRYPOINT instruction is usually used in the exec form, but what happens if we pass the string "my friend" to docker run as an argument.
docker run -i--name"command-v6-1""localhost/rimelek/tutorial-docker-command:v6" my friend
Output:
Hello Docker
Something is still wrong, so let's check the command that was running in the container:
docker build .--force-rm-f Dockerfile.v7 -t localhost/rimelek/tutorial-docker-command:v7
docker run -i--name"command-v7-0""localhost/rimelek/tutorial-docker-command:v7"
And now we are back to the previous problem. We still don't have a shebang line in the script, so we get the error message:
exec /hello.sh: exec format error
This time my friend became the argument of hello.sh properly, but hello.sh still can't be interpreted because of the lack of the shebang line or any provided shell.
We need to use a new script with a shebang line that defines the shell in the first line. The new Dockerfile called Dockerfile.v8 will make sure the new hello-bash.sh will be copied:
So even by using the exec form, you can have a shell script if you have a shell defined in the first line.
Note: You would probably expect /bin/bash -c '/hello-bash.sh whoami' to be the actual running command in the container since I stated that the command worked because there was a shell. Your expectation is almost correct, but we can't get that command from the metadata given us by docker container ls. Up until now everything could be determined based on the content of the Dockerfile, but the shebang line is not known directly by Docker and in this case the shell script wasn't passed to the shell defined in the Dockerfile, so the final command will be different.
Let's pass the executable command as an argument of docker run:
docker run -i--name"command-v8-1""localhost/rimelek/tutorial-docker-command:v8"whoami
The output is still just root and the final command is still the same too.
If you change the command to be ps 1 you can get the running process from the container's point of view:
docker run --rm"localhost/rimelek/tutorial-docker-command:v8" ps 1
Output:
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 /bin/bash /hello-bash.sh ps 1
And now we have the bash shell and its aguments. It executes /hello-bash.sh and since we didn't use the -c option to define a command as a string, it won't ignore the rest of the arguments but recognizes the shell script as a file and passes the rest of the arguments to it.
Example 9 - Using a fake shell to make a file executable
At the beginning of this post I mentioned that we can also write the RUN instruction in two different forms and we already used it in the shell form. Depending on what the shell is, you might want to write it in the exec form, but first let's keep the shell form what we usually use. Since we don't really want to run the script during build, we will also change the shell again to chmod so instead of running it in a shell we will make it executable this way.
docker build .--force-rm-f Dockerfile.v9 -t localhost/rimelek/tutorial-docker-command:v9
docker run -i--name"command-v9-0""localhost/rimelek/tutorial-docker-command:v9"
The output is
root
Note that this example is just preparing ourself to the next one, so it won't give us any interesting output, but since we don't have an error message, we can be sure that the build ran correctly and we could also run a container from it. Let's see the command inside the container.
Now it will show an error message, because the /hello-bash.sh in the RUN instruction wasn't passed to the shell, because it was in the exec form. The entrypoint has nothing to do with the RUN instruction so we won't end up with an inficnite loop.
The error message is the following:
#7 [3/3] RUN ["/hello-bash.sh"]
#0 0.081 runc run failed: unable to start container process: exec: "/hello-bash.sh": permission denied
#7 ERROR: process "/hello-bash.sh" did not complete successfully: exit code: 1
------
> [3/3] RUN ["/hello-bash.sh"]:
#0 0.081 runc run failed: unable to start container process: exec: "/hello-bash.sh": permission denied
------
Dockerfile.v10:7
--------------------
5 | SHELL ["chmod", "+x"]
6 |
7 | >>> RUN ["/hello-bash.sh"]
8 |
9 | SHELL ["/bin/bash", "-c"]
--------------------
ERROR: failed to solve: process "/hello-bash.sh" did not complete successfully: exit code: 1
The last example will show a working version again, but in this case although we still write the RUN instruction in the exec form, we write it correctly containing the actual chmod command instead of relying on the SHELL. Let's see the content of Dockerfile.v11.
docker build .--force-rm-f Dockerfile.v11 -t localhost/rimelek/tutorial-docker-command:v11
docker run -i--name"command-v11-0""localhost/rimelek/tutorial-docker-command:v11"
The output is just root as in the previously working examples, but this RUN instruction wouldn't work if you wanted to use an environment variable in the filename for example, because that would work in a shell only. Without expecting any surprise, let's check the command in the container:
I hope you now you understand the instructions a little bit better. I used the Docker commands directly so you can copy and paste safely without worrying about what unexpected commands in a script, but you can also clone the Git repository from GitHub and use the scripts mentioned in the readme to have a colored, automatically executed test which you can run as many times as you want and use ./reset.sh to remove all the generated images and containers.
Scripts to demonstrate the ways to build the main process inside a Docker container
Building the main process inside Docker container
This source code was created for a Hungarian Youtube tutorial
For english links, please check the release list or the latest version of this readme
It demonstrates how you can build the main process (the first process with 1 as PID) inside a Docker container using SHELL, ENTRYPOINT and CMD and how the RUN instruction is similar to the others.
Note: The tutorial requires jq and recent Bash version installed on your machine.
Each Dockerfile has a version number. You can run the tests one by one using build.sh and run.sh passing the version as the first argument to each script.
After cloning the project you might want to switch to v2.0.0 in case there are newer commits:
git checkout v2.0.0
The output of ./test-all.sh would be something like this but colored (use the horizontal scrollbar to see the right side of the output):
Container Command Output
command-v1-0 echo 'My home is $HOME' My home is $HOME
command-v1-1 echo 'My home is /Users/ta' My home is /Users/ta
command-v2-0 /bin/sh -c '"echo \"My home is $HOME\""' /bin/sh: 1: echo "My home is /root": not found
command-v2-1 echo "My home is /Users/ta"
command-v3-0 /bin/sh -c 'echo "My home is $HOME"' My home is /root
command-v3-1 echo 'My home is /Users/ta' My home is /Users/ta
command-v4-0 /bin/sh -c /hello.sh Hello Docker
command-v4-1 /hello.sh exec /hello.sh: exec format error
command-v5-0 /bin/ls -l /hello.sh -rwxr-xr-x 1 root root 40 Jun 20 18:05 /hello.sh
command-v5-1 /hello.sh exec /hello.sh: exec format error
command-v6-0 /bin/bash -c /hello.sh /bin/bash -c 'my friend' Hello Docker
command-v6-1 /bin/bash -c /hello.sh my friend Hello Docker
command-v7-0 /hello.sh my friend exec /hello.sh: exec format error
command-v7-1 /hello.sh my friend exec /hello.sh: exec format error
command-v8-0 /hello-bash.sh whoami root
command-v8-1 /hello-bash.sh whoami root
command-v9-0 /hello-bash.sh whoami root
command-v9-1 /hello-bash.sh whoami root
command-v10-0
command-v10-1
command-v11-0 /hello-bash.sh whoami root
command-v11-1 /hello-bash.sh my friend Hello my friend