The easiest way to imagine this is that ENTRYPOINT and CMD both define lists of words, and the final command is constructed by just concatenating the two lists together. Docker has no way of knowing whether or not the first word of the CMD list is actually an executable or not.
All three of these run the exact same command when a container is started:
FROM busybox ENTRYPOINT ["/bin/ls"] CMD ["-lrt", "/"]
FROM busybox ENTRYPOINT [] CMD ["/bin/ls", "-lrt", "/"]
FROM busybox ENTRYPOINT ["/bin/ls", "-lrt", "/"] CMD []
My general experience has been that CMD should always be a well-formed command in its own right. There are two good reasons for this: it is frequently useful to docker run --rm -it imagename sh to see what came out of your image build process; and there is a useful pattern of setting ENTRYPOINT to a script that does some first-time setup and then exec "$@" to run the CMD. (Combine the two and you get a debugging shell after the first-time setup has run.)
# good CMD ["/app/myapp", "--foreground"]
# good ENTRYPOINT ["wait-for-it.sh", "db:5432", "--"] CMD ["/app/myapp", "--foreground"]
# hard to provide an alternate command or an ENTRYPOINT wrapper ENTRYPOINT ["/app/myapp"] CMD ["--foreground"]
For practical purposes ENTRYPOINT must be the JSON-array form and must not use a sh -c wrapper. As the documentation notes, if ENTRYPOINT is a bare command then the CMD is ignored; if it has an explicit sh -c wrapper the interactions with additional arguments aren't obvious.
# just prints an empty line FROM busybox ENTRYPOINT /bin/echo CMD ["one", "two", "three", "four"]
# just prints an empty line FROM busybox ENTRYPOINT ["/bin/sh", "-c", "/bin/echo"] CMD ["one", "two", "three", "four"]
In fact, ENTRYPOINT as a bare string doesn't block CMD per se; but the resulting /bin/sh -c 'some command' arg arg sets $0, $1, etc. inside the command in a way that's not obvious.
# prints "three" FROM busybox ENTRYPOINT echo $2 CMD ["one", "two", "three", "four"]
# prints "-c" FROM busybox ENTRYPOINT echo $1 CMD one two three four
CMDwas there from the very beginning (first mention at Jan 29, 2013), whileENTRYPOINTwas introduced at Jun 24, 2013. The initial release of Docker (according to Wikipedia) was at March 13, 2013, soENTRYPOINTwas introduced after the initial release. This means, that newENTRYPOINTdirective probably had to work so that the existing semantics ofCMDwas left untouched (backwards compatibility). This could explain whyCMDwithoutENTRYPOINTis a valid option and why in this case the semantics of its first argument is different.