skip to Main Content

When we build an image from a Dockerfile, each step in the Dockerfile, results into creation of an intermediate container created from the image from its previous step and running the instruction of that step.
However when it comes to the ultimate step of CMD, the newly created image by running the CMD command on top of penultimate step will also have the same startup command CMD right?

Example:

Step 1. FROM alpine
Step 2. RUN apk --update add redis 
Step 3. EXPOSE 6379
Step 4. CMD ["redis-server"]

Step 1 FROM Alpine will have Image I1

Step 2 will start a container C1 using I1 and the RUN command as startup creating new Image I2 (removing intermediate container C1)

Step 3 will use image I2 to run a new container C2 with EXPOSE command as startup and create a new image I3 (removing intermediate container C2)

Step 4 will use image I3 to start a new container C3 and run startup command CMD that will create a new image I4 (removing intermediate container C3)

Finally I4 will be presented as the final image. Now this image I4 will also the same start up command of CMD as the image I3.

My question is why do we have to run container C3 and create image I4? Why not just leave it at image I3 with startup command CMD which will run when a user tried to create a container using I3

2

Answers


  1. Chosen as BEST ANSWER

    I found the solution to the question:

    To start with the image I3 and I4 do not have the same start up command. Let me explain:

    Actually whenever each step executes, it takes the Image created from the previous step and creates a container with that image. Then it executes the command in the current step inside this running container using docker exec modifying the filesystem snapshot without modifying the start up command. This then gets used as a base image for the next step.

    However this the behavior is slightly different for the ultimate step of CMD. In this step, docker again takes the image generated from the penultimate step and creates a container. Then instead of executing the instruction in the CMD step... it simply sets the startup command for the container and takes an image snapshot and shuts down the intermediate container.

    So the difference is that while in other steps the filesystem snapshot gets updated, in the ultimate step of CMD, only the startup command gets updated.


  2. It doesn’t run these containers, this only happens for RUN steps. There’s also no new layers added, the only steps that can add filesystem layers are RUN, COPY, and ADD. This is a modification to the image’s config json metadata. The image manifest contains a reference to this config json, so you get a new image digest for every config change. And with the classic build process, the easy way to generate that metadata is to create a container, but as the output here shows, it’s not being run, status is only created:

    $ DOCKER_BUILDKIT=0 docker build --no-cache --rm=false -f df.intermediate .
    Sending build context to Docker daemon  20.99kB
    Step 1/4 : FROM alpine
    latest: Pulling from library/alpine
    ba3557a56b15: Pull complete
    Digest: sha256:a75afd8b57e7f34e4dad8d65e2c7ba2e1975c795ce1ee22fa34f8cf46f96a3be
    Status: Downloaded newer image for alpine:latest
     ---> 28f6e2705743
    Step 2/4 : RUN apk --update add redis
     ---> Running in dadc1e8d5044
    fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/main/x86_64/APKINDEX.tar.gz
    fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/community/x86_64/APKINDEX.tar.gz
    (1/1) Installing redis (6.0.11-r0)
    Executing redis-6.0.11-r0.pre-install
    Executing redis-6.0.11-r0.post-install
    Executing busybox-1.32.1-r3.trigger
    OK: 8 MiB in 15 packages
     ---> 32a040f16a0c
    Step 3/4 : EXPOSE 6379
     ---> Running in 7272af442111
     ---> 6d8a0569afdf
    Step 4/4 : CMD ["redis-server"]
     ---> Running in 7853be69241a
     ---> 8027f666c955
    Successfully built 8027f666c955
    
    $ docker container ls -a
    CONTAINER ID   IMAGE                           COMMAND                  CREATED          STATUS                     PORTS                                        NAMES
    7853be69241a   6d8a0569afdf                    "/bin/sh -c '#(nop) …"   9 seconds ago    Created                                                                 awesome_mclean
    7272af442111   32a040f16a0c                    "/bin/sh -c '#(nop) …"   9 seconds ago    Created                                                                 pensive_dewdney
    dadc1e8d5044   28f6e2705743                    "/bin/sh -c 'apk --u…"   11 seconds ago   Exited (0) 9 seconds ago                                                nice_greider
    

    Importantly, images are json that reference other layers and config metadata, and you don’t copy all the layers, you only create additional references to the same layers, so there’s only a few kb to track another json file that points to another config file. You can see that when pushing to a registry:

    $ docker push localhost:5000/test/intermediate:latest
    The push refers to repository [localhost:5000/test/intermediate]
    c2b5fcbe3ebc: Pushed
    cb381a32b229: Pushed
    latest: digest: sha256:f8171dae4c1e77313ce3a6c7496e4c641a5636f55ce1bb15fe4dd6f114680239 size: 739
    

    And looking at the history section of the image config, it shows those steps as not having any layer data (empty_layer: true):

    $ regctl image inspect localhost:5000/test/intermediate
    {            
      "created": "2021-03-18T13:39:41.050804558Z",
      "architecture": "amd64",
      "os": "linux",                                     
      "config": {                                    
        "ExposedPorts": {
          "6379/tcp": {}                                 
        },       
        "Env": [                                        
          "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
        ],                                               
        "Cmd": [                                            
          "redis-server"
        ]                
      },                                                                  
      "rootfs": {
        "type": "layers",                                             
        "diff_ids": [
          "sha256:cb381a32b2296e4eb5af3f84092a2e6685e88adbc54ee0768a1a1010ce6376c7",       
          "sha256:c2b5fcbe3ebc0a260ba84a593ab0b7fd9f584242a98170bb0d0b5b89c353d2f0"
        ]        
      }, 
      "history": [                                                         
        {                 
          "created": "2021-02-17T21:19:54.634967707Z",                                       
          "created_by": "/bin/sh -c #(nop) ADD file:80bf8bd014071345b1c0364eeb0a5e48f3fb0d87f9c31cb990e57caa652b59b8 in / "
        },
        {                                                                  
          "created": "2021-02-17T21:19:54.811094842Z",
          "created_by": "/bin/sh -c #(nop)  CMD ["/bin/sh"]",                              
          "empty_layer": true
        },
        {
          "created": "2021-03-18T13:39:40.595419392Z",
          "created_by": "/bin/sh -c apk --update add redis"
        },                                                
        {        
          "created": "2021-03-18T13:39:40.818725501Z",
          "created_by": "/bin/sh -c #(nop)  EXPOSE 6379",
          "empty_layer": true
        },
        {              
          "created": "2021-03-18T13:39:41.050804558Z",
          "created_by": "/bin/sh -c #(nop)  CMD ["redis-server"]",
          "empty_layer": true            
        }                     
      ]                       
    }
    

    Docker tracks each of these separately because it impacts caching. Only when you start from the same previous state is it valid to reuse a cached step from a previous build.

    Note that there are other ways to generate this metadata, and buildkit does this without creating or running containers:

    $ docker build --no-cache --progress=plain -f df.intermediate .
    #1 [internal] load build definition from df.intermediate
    #1 sha256:f3ec60f1563d5476385c136badb851fd417c0ae26caf5f61c3e931b8b8b08b86
    #1 transferring dockerfile: 121B done
    #1 DONE 0.0s      
                          
    #2 [internal] load .dockerignore
    #2 sha256:586943d6f8ffae008b8774450d592ae867dbda8f418150cb140924142d97a698
    #2 transferring context: 49B done
    #2 DONE 0.0s                 
                      
    #3 [internal] load metadata for docker.io/library/alpine:latest
    #3 sha256:d4fb25f5b5c00defc20ce26f2efc4e288de8834ed5aa59dff877b495ba88fda6
    #3 DONE 0.0s                             
                            
    #4 [1/2] FROM docker.io/library/alpine   
    #4 sha256:665ba8b2cdc0cb0200e2a42a6b3c0f8f684089f4cd1b81494fbb9805879120f7
    #4 resolve docker.io/library/alpine:latest done  
    #4 DONE 0.0s 
                 
    #5 [2/2] RUN apk --update add redis  
    #5 sha256:655ace4fb184eee9174a1a3911e13e4086750316878b55acc3f07cd5dd0ff2d6
    #5 0.510 fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/main/x86_64/APKINDEX.tar.gz
    #5 0.724 fetch https://dl-cdn.alpinelinux.org/alpine/v3.13/community/x86_64/APKINDEX.tar.gz
    #5 0.946 (1/1) Installing redis (6.0.11-r0)  
    #5 0.960 Executing redis-6.0.11-r0.pre-install 
    #5 1.042 Executing redis-6.0.11-r0.post-install
    #5 1.048 Executing busybox-1.32.1-r3.trigger
    #5 1.060 OK: 8 MiB in 15 packages   
    #5 DONE 1.2s   
    
    #6 exporting to image
    #6 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
    #6 exporting layers 0.0s done 
    #6 writing image sha256:8126afb6ee91f75f6d5ce4c59ae5b66981a708715a217a36e98ccf423c6d025d done
    #6 DONE 0.0s 
    

    In the above, you can see #5 executes the RUN, and then it immediately writes the result.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search