In Dockerfile, does a CMD instruction add a new layer?

1k Views Asked by At

I checked many documentation sites and even a book. None of the resources clear this up for me.

The KB from Docker says yes, is it wrong?

https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

# syntax=docker/dockerfile:1
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

Each instruction creates one layer:

  • FROM creates a layer from the ubuntu:18.04 Docker image.

  • COPY adds files from your Docker client’s current directory.

  • RUN builds your application with make.

  • CMD specifies what command to run within the container.

I want the list of instructions that don't create a layer. I know all create an intermediate layer, but I am concerned about the final layers in the image.

2

There are 2 best solutions below

0
On

The short answer is: no.

Here's an example of the python image history:

$ regctl image config python --format '{{jsonPretty .History}}'
[
  {
    "created": "2023-11-21T05:21:24.536066751Z",
    "created_by": "/bin/sh -c #(nop) ADD file:39d17d28c5de0bd629e5b7c8190228e5a445d61d668e189b7523e90e68f78244 in / "
  },
  {
    "created": "2023-11-21T05:21:25.128983079Z",
    "created_by": "/bin/sh -c #(nop)  CMD [\"bash\"]",
    "empty_layer": true
  },
  {
    "created": "2023-11-21T09:52:48.60112971Z",
    "created_by": "/bin/sh -c set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tca-certificates \t\tcurl \t\tgnupg \t\tnetbase \t\tsq \t\twget \t; \trm -rf /var/lib/apt/lists/*"
  },
  {
    "created": "2023-11-21T09:53:05.826622089Z",
    "created_by": "/bin/sh -c apt-get update && apt-get install -y --no-install-recommends \t\tgit \t\tmercurial \t\topenssh-client \t\tsubversion \t\t\t\tprocps \t&& rm -rf /var/lib/apt/lists/*"
  },
  {
    "created": "2023-11-21T09:54:02.653610372Z",
    "created_by": "/bin/sh -c set -ex; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tautoconf \t\tautomake \t\tbzip2 \t\tdpkg-dev \t\tfile \t\tg++ \t\tgcc \t\timagemagick \t\tlibbz2-dev \t\tlibc6-dev \t\tlibcurl4-openssl-dev \t\tlibdb-dev \t\tlibevent-dev \t\tlibffi-dev \t\tlibgdbm-dev \t\tlibglib2.0-dev \t\tlibgmp-dev \t\tlibjpeg-dev \t\tlibkrb5-dev \t\tliblzma-dev \t\tlibmagickcore-dev \t\tlibmagickwand-dev \t\tlibmaxminddb-dev \t\tlibncurses5-dev \t\tlibncursesw5-dev \t\tlibpng-dev \t\tlibpq-dev \t\tlibreadline-dev \t\tlibsqlite3-dev \t\tlibssl-dev \t\tlibtool \t\tlibwebp-dev \t\tlibxml2-dev \t\tlibxslt-dev \t\tlibyaml-dev \t\tmake \t\tpatch \t\tunzip \t\txz-utils \t\tzlib1g-dev \t\t\t\t$( \t\t\tif apt-cache show 'default-libmysqlclient-dev' 2>/dev/null | grep -q '^Version:'; then \t\t\t\techo 'default-libmysqlclient-dev'; \t\t\telse \t\t\t\techo 'libmysqlclient-dev'; \t\t\tfi \t\t) \t; \trm -rf /var/lib/apt/lists/*"
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "ENV PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    "comment": "buildkit.dockerfile.v0",
    "empty_layer": true
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "ENV LANG=C.UTF-8",
    "comment": "buildkit.dockerfile.v0",
    "empty_layer": true
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "RUN /bin/sh -c set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tlibbluetooth-dev \t\ttk-dev \t\tuuid-dev \t; \trm -rf /var/lib/apt/lists/* # buildkit",
    "comment": "buildkit.dockerfile.v0"
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "ENV GPG_KEY=7169605F62C751356D054A26A821E680E5FA6305",
    "comment": "buildkit.dockerfile.v0",
    "empty_layer": true
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "ENV PYTHON_VERSION=3.12.0",
    "comment": "buildkit.dockerfile.v0",
    "empty_layer": true
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "RUN /bin/sh -c set -eux; \t\twget -O python.tar.xz \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz\"; \twget -O python.tar.xz.asc \"https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc\"; \tGNUPGHOME=\"$(mktemp -d)\"; export GNUPGHOME; \tgpg --batch --keyserver hkps://keys.openpgp.org --recv-keys \"$GPG_KEY\"; \tgpg --batch --verify python.tar.xz.asc python.tar.xz; \tgpgconf --kill all; \trm -rf \"$GNUPGHOME\" python.tar.xz.asc; \tmkdir -p /usr/src/python; \ttar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz; \trm python.tar.xz; \t\tcd /usr/src/python; \tgnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\"; \t./configure \t\t--build=\"$gnuArch\" \t\t--enable-loadable-sqlite-extensions \t\t--enable-optimizations \t\t--enable-option-checking=fatal \t\t--enable-shared \t\t--with-lto \t\t--with-system-expat \t\t--without-ensurepip \t; \tnproc=\"$(nproc)\"; \tEXTRA_CFLAGS=\"$(dpkg-buildflags --get CFLAGS)\"; \tLDFLAGS=\"$(dpkg-buildflags --get LDFLAGS)\"; \tmake -j \"$nproc\" \t\t\"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \t\t\"LDFLAGS=${LDFLAGS:-}\" \t\t\"PROFILE_TASK=${PROFILE_TASK:-}\" \t; \trm python; \tmake -j \"$nproc\" \t\t\"EXTRA_CFLAGS=${EXTRA_CFLAGS:-}\" \t\t\"LDFLAGS=${LDFLAGS:--Wl},-rpath='\\$\\$ORIGIN/../lib'\" \t\t\"PROFILE_TASK=${PROFILE_TASK:-}\" \t\tpython \t; \tmake install; \t\tbin=\"$(readlink -ve /usr/local/bin/python3)\"; \tdir=\"$(dirname \"$bin\")\"; \tmkdir -p \"/usr/share/gdb/auto-load/$dir\"; \tcp -vL Tools/gdb/libpython.py \"/usr/share/gdb/auto-load/$bin-gdb.py\"; \t\tcd /; \trm -rf /usr/src/python; \t\tfind /usr/local -depth \t\t\\( \t\t\t\\( -type d -a \\( -name test -o -name tests -o -name idle_test \\) \\) \t\t\t-o \\( -type f -a \\( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \\) \\) \t\t\\) -exec rm -rf '{}' + \t; \t\tldconfig; \t\tpython3 --version # buildkit",
    "comment": "buildkit.dockerfile.v0"
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "RUN /bin/sh -c set -eux; \tfor src in idle3 pydoc3 python3 python3-config; do \t\tdst=\"$(echo \"$src\" | tr -d 3)\"; \t\t[ -s \"/usr/local/bin/$src\" ]; \t\t[ ! -e \"/usr/local/bin/$dst\" ]; \t\tln -svT \"$src\" \"/usr/local/bin/$dst\"; \tdone # buildkit",
    "comment": "buildkit.dockerfile.v0"
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "ENV PYTHON_PIP_VERSION=23.2.1",
    "comment": "buildkit.dockerfile.v0",
    "empty_layer": true
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "ENV PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/c6add47b0abf67511cdfb4734771cbab403af062/public/get-pip.py",
    "comment": "buildkit.dockerfile.v0",
    "empty_layer": true
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "ENV PYTHON_GET_PIP_SHA256=22b849a10f86f5ddf7ce148ca2a31214504ee6c83ef626840fde6e5dcd809d11",
    "comment": "buildkit.dockerfile.v0",
    "empty_layer": true
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "RUN /bin/sh -c set -eux; \t\twget -O get-pip.py \"$PYTHON_GET_PIP_URL\"; \techo \"$PYTHON_GET_PIP_SHA256 *get-pip.py\" | sha256sum -c -; \t\texport PYTHONDONTWRITEBYTECODE=1; \t\tpython get-pip.py \t\t--disable-pip-version-check \t\t--no-cache-dir \t\t--no-compile \t\t\"pip==$PYTHON_PIP_VERSION\" \t; \trm -f get-pip.py; \t\tpip --version # buildkit",
    "comment": "buildkit.dockerfile.v0"
  },
  {
    "created": "2023-10-16T00:14:53Z",
    "created_by": "CMD [\"python3\"]",
    "comment": "buildkit.dockerfile.v0",
    "empty_layer": true
  }
]

Note all the lines that say "empty_layer": true. Those indicate the step being run did not create a layer, and that's visible when the layers themself are listed:

$ regctl manifest get python --platform local --format '{{jsonPretty .Layers}}'
[
  {
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
    "digest": "sha256:90e5e7d8b87a34877f61c2b86d053db1c4f440b9054cf49573e3be5d6a674a47",
    "size": 49582225
  },
  {
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
    "digest": "sha256:27e1a8ca91d35598fbae8dee7f1c211f0f93cec529f6804a60e9301c53a604d0",
    "size": 24049172
  },
  {
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
    "digest": "sha256:d3a767d1d12e57724b9f254794e359f3b04d4d5ad966006e5b5cda78cc382762",
    "size": 64130771
  },
  {
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
    "digest": "sha256:711be5dc50448ab08ccab0b44d65962f36574d341749ab30651b78ec0d4bfd1c",
    "size": 211066535
  },
  {
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
    "digest": "sha256:7ad48fee40035670dcaf937f0ac03b16dc5ac98f001dc04c2c84cf56af728d04",
    "size": 6391205
  },
  {
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
    "digest": "sha256:a319993f7bddcfa1ff27981884048de4356a15b667935ca420480c40030b48cd",
    "size": 22506963
  },
  {
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
    "digest": "sha256:c5bc2fe650d8f5c0670e0f23abba8fa89bd0e849855a2e60384a4069edc47df9",
    "size": 244
  },
  {
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
    "digest": "sha256:0303a8131ddc42ea2be42f92ce5c5e6deb404f51e7758728fc26deaf811665cb",
    "size": 2676803
  }
]

That shows only 8 filesystem layers while there were 17 steps in the history. Steps like ENV, CMD, and LABEL that don't create filesystem changes do not create a new layer.

Another way to see the same result is to compare the output of docker inspect $image --format '{{json .RootFS.Layers}}' to docker history $image and see the difference in lines (the values from regctl show the image on the registry and give visibility to fields like empty_layer).

0
On

Every line in the Dockerfile will add a layer, including CMD. You could refer to the terminology of layers:

Layer

Images are composed of layers. Each layer is a set of filesystem changes. Layers do not have configuration metadata such as environment variables or default arguments - these are properties of the image as a whole rather than any particular layer.

You can see that a layer is, in fact, just changes made to the file system. So, if a line in your Dockerfile actually makes a difference to the image, it will add a layer.

Of course, there are still some lines that won't change the Docker image, for example, Parser directives:

Parser directives are optional and affect the way in which subsequent lines in a Dockerfile are handled. Parser directives do not add layers to the build and will not be shown as a build step.

However, things like comments really do nothing for the final image. As they do not change the image, they do not add a layer.

You can also use docker history <Image ID> to see the layers on an actual image.