What are shell form and exec form?

Expanding on the top rated answer in this thread, These are the recommended forms to use for each instruction:

  • RUN: shell form
  • ENTRYPOINT: exec form
  • CMD: exec form

Shell features, variable substitution, signal trapping and forwarding, command and entrypoint concatenation are explained with examples in the below.

Variable substitution

In the shell form, Dockerfile instructions will inherit environment variables from the shell, such as $HOME and $PATH:

FROM alpine:latest

# Shell: echoes "/root" as set by the shell
RUN echo $HOME
# Exec: echoes "$HOME" because nothing is set
RUN ["echo", "$HOME"]

However, both forms behave the same when it comes to environment variables set by the ENV instruction in the Dockerfile:

FROM alpine:latest
ENV VERSION=1.0.0

# Shell: echoes "1.0.0" because Docker does the substitution
RUN echo $VERSION

# Exec: echoes "1.0.0" because Docker does the substitution
RUN ["echo", "$VERSION"]

Shell features

The main thing you lose with the exec form is all the useful shell features: sub commands, piping output, chaining commands, I/O redirection, and more. These kinds of commands are only possible with the shell form:

FROM ubuntu:latest

# Shell: run a speed test
RUN apt-get update \
 && apt-get install -y wget \
 && wget -O /dev/null http://speedtest.wdc01.softlayer.com/downloads/test10.zip \
 && rm -rf /var/lib/apt/lists/*

# Shell: output the default shell location
CMD which $(echo $0)

RUN instruction can make use of shell form for all the other features mentioned above and to reduce overall size of the image. Every Dockerfile directive will generate an intermediate container, committed into an intermediate image. Chaining multiple RUN instructions can help reduce the overall image size.

Signal trapping & forwarding

Most shells do not forward process signals to child processes, which means the SIGINT generated by pressing CTRL-C may not stop a child process:

# Note: Alpine's `/bin/sh` is really BusyBox `ash`, and when
#   `/bin/sh -c` is run it replace itself with the command rather
#   than spawning a new process, unlike Ubuntu's `bash`, meaning
#   Alpine doesn't exhibit the forwarding problem

FROM ubuntu:latest

# Shell: `bash` doesn't forward CTRL-C SIGINT to `top`. so it will not stop.
ENTRYPOINT top -b

# Exec: `top` traps CTRL-C SIGINT and stops
ENTRYPOINT ["top", "-b"]

This is the main reason to use the exec form for both ENTRYPOINT and SHELL.

Docker docs has more examples on this.. Signal trapping is also needed for docker stop command to work and for any cleanup task that needs to be done before stopping the container.

CMD as ENTRYPOINT parameters

Below concatenation will only work in exec form. It will not work in shell form.

FROM alpine:latest

# Exec: default container command
ENTRYPOINT ["sleep"]

# Exec: passed as parameter to the sleep command. At the end 'sleep 2s' will be run on the container.
CMD ["2s"]

However if you need to use some shell features in the exec form for CMD then below can be done.

CMD ["sh", "-c", "echo ${SLEEP_DURATION}"]

This does variable substitution to echo contents of SLEEP_DURATION environmental variable in the container while retaining the feature of exec form.


Source:

Above quoted examples are from this blog: source

Official docs now has lot more info on these differences - docker docs for entrypoint and docker docs for CMD


These following explanation are from the Kubernetes In Action book(chapter 7).

Firstly, They have both two different forms:

  • shell form—For example, ENTRYPOINT node app.js

  • exec form—For example, ENTRYPOINT ["node","app.js"]

Actually The difference is whether the specified command is invoked inside a shell or not. I want to explain main difference between them with an example, too.

ENTRYPOINT ["node", "app.js"]

This runs the node process directly (not inside a shell), as you can see by listing the processes running inside the container:

$ docker exec 4675d ps x  
PID TTY      STAT   TIME    COMMAND
1    ?        Ssl    0:00   node app.js   
12   ?        Rs     0:00   ps x

ENTRYPOINT node app.js

If you’d used the shell form (ENTRYPOINT node app.js), these would have been the container’s processes:

$ docker exec -it e4bad ps x  
PID TTY      STAT   TIME    COMMAND    
1    ?        Ss     0:00   /bin/sh -c node app.js
7    ?        Sl     0:00   node app.js   
13   ?        Rs+    0:00   ps x

As you can see, in that case, the main process (PID 1) would be the shell process instead of the node process. The node process (PID 7) would be started from that shell. The shell process is unnecessary, which is why you should always use the exec form of the ENTRYPOINT instruction.


The docker shell syntax (which is just a string as the RUN, ENTRYPOINT, and CMD) will run that string as the parameter to /bin/sh -c. This gives you a shell to expand variables, sub commands, piping output, chaining commands together, and other shell conveniences.

RUN ls * | grep $trigger_filename || echo file missing && exit 1

The exec syntax simply runs the binary you provide with the args you include, but without any features of the shell parsing. In docker, you indicate this with a json formatted array.

RUN ["/bin/app", "arg1", "arg2"]

The advantage of the exec syntax is removing the shell from the launched process, which may inhibit signal processing. The reformatting of the command with /bin/sh -c in the shell syntax may also break concatenation of your entrypoint and cmd together.

The entrypoint documentation does a good job covering the various scenarios and explaining this in more detail.

Tags:

Docker

Shell

Exec