Does Docker EXPOSE make a new layer?

3.9k Views Asked by At

I have been playing around with creating docker files and reading the documentation and I was wondering this question: Does adding an EXPOSE command to my Dockerfile add a layer? (and if it does why would I care/ does it matter where it is placed inside the file?)

It is not specifly said in the documentation.

I understand that RUN, COPY, and ADD create layers because they modify the file system. However, does the use of EXPOSE generate a layer since it simply adds metadata to the container?

5

There are 5 best solutions below

0
On BEST ANSWER

I realised i could test this myself. and I've found that adding EXPOSE does not add a new file system layer, but it does add a layer none the less, also it does matter which order you make your docker files for your cache layers.

basically: every command creates a new layer, every command that changes the file system creates a filesystem layer.

FROM ...
EXPOSE 80
COPY smthing .

is different from:

FROM ...
COPY smthing .
EXPOSE 80

When executed multiple times (say in a development environment).

in the first example the EXPOSE command is cached and is not executed even if the smthing file changes. If the something file changes, docker build will only re-executed this command (rest is taken from cache).

In the second example. if the smthing file changes, the EXPOSE command will also be rebuild. (since everything after the copy command is invalidated and re executed on docker build).

Would i change the EXPOSE port the first case would have to re-execute the copy command, where the second example wouldn't.

But both would lead to the exact same end result file-system layer wise.

docker inspect imageName #shows the file system layer
docker history imageName #shows all the layers
0
On

No, layers are not created for steps that do not modify the filesystem. This includes EXPOSE, ENTRYPOINT, CMD, LABEL, and ENV. That's visible by inspecting the nginx image on Hub. The image config includes the history of all steps run in the Dockerfile:

$ regctl image config --format '{{jsonPretty .History}}' --platform local nginx
[
  {
    "created": "2023-11-21T05:21:37.108578179Z",
    "created_by": "/bin/sh -c #(nop) ADD file:d261a6f6921593f1e0b3f472ab1b1822e2c6deb0b369200f0b3370556bfad017 in / "
  },
  {
    "created": "2023-11-21T05:21:37.490120886Z",
    "created_by": "/bin/sh -c #(nop)  CMD [\"bash\"]",
    "empty_layer": true
  },
  {
    "created": "2023-11-21T09:05:08.890208867Z",
    "created_by": "/bin/sh -c #(nop)  LABEL maintainer=NGINX Docker Maintainers <[email protected]>",
    "empty_layer": true
  },
  {
    "created": "2023-11-21T09:05:08.973984294Z",
    "created_by": "/bin/sh -c #(nop)  ENV NGINX_VERSION=1.25.3",
    "empty_layer": true
  },
  {
    "created": "2023-11-21T09:05:09.055690549Z",
    "created_by": "/bin/sh -c #(nop)  ENV NJS_VERSION=0.8.2",
    "empty_layer": true
  },
  {
    "created": "2023-11-21T09:05:09.13626085Z",
    "created_by": "/bin/sh -c #(nop)  ENV PKG_RELEASE=1~bookworm",
    "empty_layer": true
  },
  {
    "created": "2023-11-21T09:05:31.351851262Z",
    "created_by": "/bin/sh -c set -x     && groupadd --system --gid 101 nginx     && useradd --system --gid nginx --no-create-home --home /nonexistent --comment \"nginx user\" --shell /bin/false --uid 101 nginx     && apt-get update     && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates     &&     NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62;     NGINX_GPGKEY_PATH=/usr/share/keyrings/nginx-archive-keyring.gpg;     export GNUPGHOME=\"$(mktemp -d)\";     found='';     for server in         hkp://keyserver.ubuntu.com:80         pgp.mit.edu     ; do         echo \"Fetching GPG key $NGINX_GPGKEY from $server\";         gpg1 --keyserver \"$server\" --keyserver-options timeout=10 --recv-keys \"$NGINX_GPGKEY\" && found=yes && break;     done;     test -z \"$found\" && echo >&2 \"error: failed to fetch GPG key $NGINX_GPGKEY\" && exit 1;     gpg1 --export \"$NGINX_GPGKEY\" > \"$NGINX_GPGKEY_PATH\" ;     rm -rf \"$GNUPGHOME\";     apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/*     && dpkgArch=\"$(dpkg --print-architecture)\"     && nginxPackages=\"         nginx=${NGINX_VERSION}-${PKG_RELEASE}         nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE}         nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE}         nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE}         nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE}     \"     && case \"$dpkgArch\" in         amd64|arm64)             echo \"deb [signed-by=$NGINX_GPGKEY_PATH] https://nginx.org/packages/mainline/debian/ bookworm nginx\" >> /etc/apt/sources.list.d/nginx.list             && apt-get update             ;;         *)             echo \"deb-src [signed-by=$NGINX_GPGKEY_PATH] https://nginx.org/packages/mainline/debian/ bookworm nginx\" >> /etc/apt/sources.list.d/nginx.list                         && tempDir=\"$(mktemp -d)\"             && chmod 777 \"$tempDir\"                         && savedAptMark=\"$(apt-mark showmanual)\"                         && apt-get update             && apt-get build-dep -y $nginxPackages             && (                 cd \"$tempDir\"                 && DEB_BUILD_OPTIONS=\"nocheck parallel=$(nproc)\"                     apt-get source --compile $nginxPackages             )                         && apt-mark showmanual | xargs apt-mark auto > /dev/null             && { [ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark; }                         && ls -lAFh \"$tempDir\"             && ( cd \"$tempDir\" && dpkg-scanpackages . > Packages )             && grep '^Package: ' \"$tempDir/Packages\"             && echo \"deb [ trusted=yes ] file://$tempDir ./\" > /etc/apt/sources.list.d/temp.list             && apt-get -o Acquire::GzipIndexes=false update             ;;     esac         && apt-get install --no-install-recommends --no-install-suggests -y                         $nginxPackages                         gettext-base                         curl     && apt-get remove --purge --auto-remove -y && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list         && if [ -n \"$tempDir\" ]; then         apt-get purge -y --auto-remove         && rm -rf \"$tempDir\" /etc/apt/sources.list.d/temp.list;     fi     && ln -sf /dev/stdout /var/log/nginx/access.log     && ln -sf /dev/stderr /var/log/nginx/error.log     && mkdir /docker-entrypoint.d"
  },
  {
    "created": "2023-11-21T09:05:31.757621042Z",
    "created_by": "/bin/sh -c #(nop) COPY file:01e75c6dd0ce317d516928a17584d111cd082840c01e58be0afc851b33adb916 in / "
  },
  {
    "created": "2023-11-21T09:05:31.847217434Z",
    "created_by": "/bin/sh -c #(nop) COPY file:caec368f5a54f70a844a13005eb2255bed778809b3672d516e719ce2f4bce123 in /docker-entrypoint.d "
  },
  {
    "created": "2023-11-21T09:05:31.932790643Z",
    "created_by": "/bin/sh -c #(nop) COPY file:3b1b9915b7dd898a0e32f7eb9715a35c9feab914022efff68ba990bc1ec7d169 in /docker-entrypoint.d "
  },
  {
    "created": "2023-11-21T09:05:32.015013482Z",
    "created_by": "/bin/sh -c #(nop) COPY file:57846632accc89753f45cbc00cb9e6223d991e1d31297eec3395a7ca58eed6a6 in /docker-entrypoint.d "
  },
  {
    "created": "2023-11-21T09:05:32.101911945Z",
    "created_by": "/bin/sh -c #(nop) COPY file:9e3b2b63db9f8fc702e2dc2bdd0943be0d990c028cddcf1c159f5556a8ba3030 in /docker-entrypoint.d "
  },
  {
    "created": "2023-11-21T09:05:32.182571426Z",
    "created_by": "/bin/sh -c #(nop)  ENTRYPOINT [\"/docker-entrypoint.sh\"]",
    "empty_layer": true
  },
  {
    "created": "2023-11-21T09:05:32.278509322Z",
    "created_by": "/bin/sh -c #(nop)  EXPOSE 80",
    "empty_layer": true
  },
  {
    "created": "2023-11-21T09:05:32.393152592Z",
    "created_by": "/bin/sh -c #(nop)  STOPSIGNAL SIGQUIT",
    "empty_layer": true
  },
  {
    "created": "2023-11-21T09:05:32.482668371Z",
    "created_by": "/bin/sh -c #(nop)  CMD [\"nginx\" \"-g\" \"daemon off;\"]",
    "empty_layer": true
  }
] 

That shows 16 steps run, but 9 of those steps have the "empty_layer": true defined.

To see the layers associated with the image, we can find those listed in the image manifest:

$ regctl manifest get --format '{{jsonPretty .Layers}}' --platform local nginx
[                                                                                                                                                                                                                  
  {                                                 
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",                                                                                                                                              
    "digest": "sha256:1f7ce2fa46ab3942feabee654933948821303a5a821789dddab2d8c3df59e227",                                                                                                                           
    "size": 29149908                                                                                     
  },                                                                                                     
  {                                                 
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",                                                                                                                                              
    "digest": "sha256:9b16c94bb68628753a94b89ddf26abc0974cd35a96f785895ab011d9b5042ee5",                                                                                                                           
    "size": 41378367                                                                                     
  },                                                                                                                                                                                                               
  {                                                 
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",                                                                                                                                              
    "digest": "sha256:9a59d19f9c5bb1ebdfef2255496b1bb5d658fdccc300c4c1f0d18c73f1bb14b5",                                                                                                                           
    "size": 625                                                                                          
  },                                                                                                                                                                                                               
  {                                                 
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",                                                                                                                                              
    "digest": "sha256:9ea27b074f71d5766a59cdbfaa15f4cd3d17bffb83fed066373eb287326abbd3",                                                                                                                           
    "size": 959                                     
  },                                                                                                                                                                                                               
  {                                                 
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",                                                                                                                                              
    "digest": "sha256:c6edf33e2524b241a0b191d0a0d2ca3d8d4ae7470333b059dd97ba30e663a1a3",                                                                                                                           
    "size": 371                                     
  },                                                
  {                                                 
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",                                                                                                                                              
    "digest": "sha256:84b1ff10387b26e2952f006c0a4fe4c6f3c0743cb08ee448bb7157220ad2fc8f",                                                                                                                           
    "size": 1214                                    
  },                                                
  {                                                 
    "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",                                                                                                                                              
    "digest": "sha256:51735783196785d1f604dc7711ea70fb3fab3cd9d99eaeff991c5afbfa0f20e8",                                                                                                                           
    "size": 1403                                    
  }                                                 
]

In there, only 7 layers (16 steps - 9 empty layer steps) are listed, each containing the compressed tar diff of filesystem changes included in that layer. When images are pushed/pulled to a registry, those 7 layers are all that are transmitted or stored. And when the image is run, those 7 layers, plus a single read/write layer for the container, are assembled into the overlay filesystem.

0
On

No, directive EXPOSE does not add a new layer. This information will be stored permanently in the image and container config and could be retrieved via:

docker inspect --format '{{.Config.ExposedPorts}}' <image_id>

But you may still be wondering why there is a line in the output that says that the new image was created for this command. Consider this Dockerfile:

FROM alpine
EXPOSE 8000

In the end, Docker produces such output:


Step 1/2 : FROM alpine
 ---> 965ea09ff2eb
Step 2/2 : EXPOSE 8000
 ---> Running in 6c8fae4f3499
Removing intermediate container 6c8fae4f3499
 ---> 067aa2abe94f
Successfully built 067aa2abe94f
Successfully tagged envtest:latest

In the same time docker history outlines:


IMAGE               CREATED              CREATED BY                                      SIZE                COMMENT
067aa2abe94f        About a minute ago   /bin/sh -c #(nop)  EXPOSE 8000                  0B                  
965ea09ff2eb        7 weeks ago          /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B

Every line in the Dockerfile causes image creation on the top of the current image, thus image has a link to the parent image. Afterward, the immediate container will be initialized based on this new image then your command will be executed within and result committed to the image.

As a general rule, if the command does not lead to changes in the filesystem the new layer won't be created. I would recommend using dive for exploring each layer in a docker image.

3
On

Yes, every instruction in a Dockerfile generates a new layer for the resulting image.

However, layers created via EXPOSE are empty layers. That is, their size is 0 bytes.

While they don't impact you storage-wise, they do count for leveraging layer cache while building or pulling/pushing images from a registry.

A good way to understand an image's layers is to use the docker history command. For instance, given the following Dockerfile:

FROM scratch

EXPOSE 4000
EXPOSE 3000

do

docker build -t test/image .

If you then docker history test/image you'll see:

IMAGE               CREATED             CREATED BY                           SIZE                COMMENT
ab9f435de7bc        4 seconds ago       /bin/sh -c #(nop)  EXPOSE 4000/tcp   0 B                 
15e09691c313        5 seconds ago       /bin/sh -c #(nop)  EXPOSE 3000/tcp   0 B     

If you switch the order of the EXPOSE statements and build again, you'll see the layer cache being ignored.

4
On

All instructions create new layers, but instructions that do not change the filesystem will create a layer that is empty.

It's worth looking into how Docker's filesystem layering works which you can read about here or here for AUFS.

Essentially new layers on the file system are made of those files that have changed from the layer below them, it's like a stack of diffs. As such, if there is no change, there is no layer to make. Mostly...

Every instruction in a Dockerfile will create an image layer, but for AUFS (in the case of EXPOSE) that layer will be empty (no difference between it and the one below it).