• Home
  • LLMs
  • Python
  • Docker
  • Kubernetes
  • Java
  • All
  • About
Docker | Build Images
  1. Notes
  2. Build an image
  3. Build cache
  4. Example of an unoptimized Dockerfile
  5. Example of an optimized Dockerfile
  6. Multi-stage builds
  7. Debug a failing build

  1. Notes
    See these pages for more information:
    Docker CLI: docker build (build an image from a Dockerfile)
    https://docs.docker.com/reference/cli/docker/buildx/build/
    https://docs.docker.com/build/building/multi-stage/

    Docker introduced new backend for builing images (buildkit) which doesn't expose intermediate containers.
    To show intermediate containers, set the environment variable DOCKER_BUILDKIT before the docker build command:
  2. Build an image
    The 'docker build' command has the following syntax:

    Let's use this Dockerfile:

    To build an image from a Dockerfile:

    The option '-t IMAGE_NAME:IMAGE_TAG' instructs Docker to create an image with the specified image name (e.g., ubuntu-nginx) and image tag (e.g., latest).

    The character '.' at the end of the docker build command specifies the current directory. You can specify any path you want. It represents the path where Docker looks for all files and directories managed in the Dockerfile.

    By default, Docker will look for a Dockerfile with the name 'Dockerfile' in the current directory. You can use the '-f' option to specify an alternate location and a custom Dockerfile name.

    You can add a ".dockerignore" file where you can list files and directories that you want to be excluded when building the image. Therefore, these files and directories are not sent to the builder, which can help improve build speed (especially when using a remote Docker host).

    The build command produces the following output:

    In this case it's the first time the image is built. All the steps are executed and no cache was used by Docker.

    To list the images (we can see the image we created "ubuntu-nginx" + the parent image "ubuntu"):
  3. Build cache
    For each build command in the Dockerfile, Docker will generate a new filesystem layer. An image is a combination of all filesystem layers created by the build commands of the Dockerfile. Each layer is mapped to a specific build command in the Dockerfile.

    The size of each filesystem layer depends on the command executed (e.g., adding files, installing libraries, etc.). The final image size is the sum of the sizes of all its filesystem layers. Adding commands to delete files created in previous layers does not reduce the final image size, because each layer is immutable and retained in the image history.

    For a new created image, Docker will execute all the build commands in the Dockerfile.

    When building an already created image (assuming the cache was not cleared), Docker uses, for each command in the Dockerfile, the corresponding filesystem layer from its cache, only if:
    • The command and its parent were not updated.

    • The command and its parent are not generating a new filesystem layer (e.g., new files added or updated, etc.).

    If any of the above conditions are not met, Docker will automatically run all build commands in the Dockerfile that are below this updated build command.

    Allowing Docker to use cached layers when rebuilding images can significantly reduce build time.

    Best practices: A build command that might create a new filesystem layer every time it's executed (e.g., update OS, library) should be placed at the bottom of the Dockerfile. Another option, when possible, is to place that command in a parent Dockerfile and use the generated image as a parent image for the descendant Dockerfiles. In such case case, the parent image can be re-built only when needed.

    Best practices: When possible, multiple build commands should be combined in one single build command (using && operator).

    Following best practices when designing Dockerfiles helps:
    • speed up the creation of Docker images by leveraging the cache.

    • reduce the size of Docker images.

    • speed up copying/downloading Docker images by skipping layers that have already been copied/downloaded.
    Let's execute again the build command. The output should be similar to the following:

    As you can see, Docker verify for each build step if there is a cached layer, and if it does exists, it will use it (see ---> Using cache).

    Let's change the Dockerfile a bit (I will change the order of the instructions):

    Let's execute the build command. The output should be similar to the following:

    As you can notice, the instruction of the step 2 was changed and hence Docker is building a new image layer. Docker will then execute the subsequent steps and won't use the local cache even if the remaining steps didn't change at all.

    Note: You can use the --no-cache option to instruct Docker not to use the cache from previous builds.
  4. Example of an unoptimized Dockerfile
    The example of the Dockerfile we used above is unoptimized:

    Let's inspect the layers of the ubuntu image (ubuntu:latest):

    Let's inspect the layers of the new image (ubuntu-nginx:latest):

    Note that the image 'ubuntu-nginx:latest' inherit all the layers of the parent image 'ubuntu:latest'.

    The five new layers are the ones created by executing the build steps in the Dockerfile.

    Let's check the size of the layers of the new image (ubuntu-nginx:latest):

    Check the image size:

    Note that deleting temporary files in later layers won't reduce the image size. Let's demonstrate this by updating the Dockerfile above:


    Let's build the new Dockerfile:

    Let's inspect the layers of the new image (ubuntu-nginx:latest):

    You can notice that a new layer (deef6270e...) was added for the command rm -rf /var/lib/apt/lists/*

    Let's check the size of the layers of the new image (ubuntu-nginx:latest):

    Let's check the size of the new image:

    The image size remains the same because the sizes of the filesystem layers are additive.
  5. Example of an optimized Dockerfile
    Let's optimize a bit the Dockerfile we used above:

    Note that I have organized the commands and chained some together using the && operator.
    All chained commands will be executed, and they all need to succeed (exit with a return code of 0).
    The && operator instructs Docker to execute the subsequent command in the chain only if the preceding command is successful.

    Build the Dockerfile:

    Let's inspect the layers of the new image:

    First note that the build steps were reduced to only three steps. This result to only two new layers created for the new image instead of five with the non-optimized Dockerfile.

    Note that the steps, in the Dockerfile, to create the mtitek group and user are placed before the installation instructions of curl and nginx. This is to show that the instructions that are more likely to generate the same exact result, no matter how many time they are executed, should be placed at the top of the Dockerfile. This will save time when building the image frequently.

    Note also the instruction rm -rf /var/lib/apt/lists/*, which is useful to save disk space on the final image. The size of the image is now 141MB instead of 226MB.

    Let's check the size of the layers of the new image (ubuntu-nginx-optimized:latest):

  6. Multi-stage builds
    Multi-stage builds feature allow you to add multiple FROM instructions in the Dockerfile. The FROM instructions are basicaly the same as the regular single FROM instructions that we have used previously.

    Each FROM instruction define a build stage that has its base image and a set of instructions to build images layers. A FROM instruction should have a name which can be used by other FROM instructions as a reference. The FROM instruction that references another one, will be able to copy artifacts created within that staging build.

    A staging build can be considered as a temporary build that we can use in the next staging builds to copy only the artifacts that we need and discards everything else. The goal is to have an optimized final stage (in term of disk space) that in theory will be the one that we will use for the image we want to create.

    Let's use this Dokerfile (not a very useful one but it's simple enough to demonstrate the multi-stage builds feature):

    Let's build the Dockerfile:

    Let's have a look at the image history:

    As you can see, the image does contain only the layers of the base image (alpine) and the layer of the COPY instruction.

    Notes:
    • Assigning a name to a stage build is recommended but not mandatory. Instead of using the name of the stage build you can reference it by its number. The first FROM instruction in the Dockerfile has the number 0 and the following will be numbered 1,2,3, and so on. To reference the first build stage, you do: COPY --from=0 ...

    • It's also possible to copy artifacts from external images (kind of using the image as a stage):

    • You can also use a stage build as a base image for the FROM instruction (FROM stage0 AS stage1):
  7. Debug a failing build
    Let's use this Dockerfile above (notice the typo with the useradd command):

    Let's build the Dockerfile:

    In this case we can see the build complains about the command 'userad'.

    In some cases the error might not be very clear from the build output, so a nice feature in Docker is that you can use an intermediate layer of the image to start a container and debug any failing command.

    For example, we can run a container from the created layer '1e4467b07108' (see above) and execute commands directly in the container:
© 2025  mtitek