To understand images and their layers, you can always inspect the image history.
Let’s inspect the history of the last Docker image by running the following command:

As you can see, there are several layers, and every layer has associated commands. You can also see when the layers were created and the size of the disk space occupied by each. Some layers do not occupy any disk space, as they haven’t added anything new to the filesystem, such as CMD and EXPOSE directives. These perform some functions, but they do not write anything to the filesystem. While commands such as apk add write to the filesystem, you can see them taking up disk space.
Every layer modifies the old layer in some way, so every layer is just a delta of the filesystem configuration.
In the next section, we will deep dive into Dockerfiles and find out how we can build Docker images and see what the layered architecture looks like.
Understanding Dockerfiles, components, and directives
A Dockerfile is a simple file that constitutes a series of steps to build a Docker image. Each step is known as a directive. There are different kinds of directives. Let’s look at a simple example to understand how this works.
We will create a simple NGINX container by building the image from scratch rather than using the one available on Docker Hub. NGINX is very popular web server software that you can use for a variety of applications; for example, it can serve as a load balancer or a reverse proxy.
Start by creating a Dockerfile:
$ vim Dockerfile
FROM ubuntu:bionic
RUN apt update && apt install -y curl
RUN apt update && apt install -y nginx
CMD [“nginx”, “-g”, “daemon off;”]
Let’s look at each line and directive one by one to understand how this Dockerfile works:
• The FROM directive specifies what the base image for this container should be. This means we are using another image as the base and will be building layers on top of it. We use the ubuntu:bionic package as the base image for this build since we want to run NGINX on Ubuntu.
- The RUN directives specify the commands we need to run on a particular layer. You can run more than one command by separating them with &&. We want to run multiple commands in a single line if we’re going to club dependent commands in a single layer. Every layer should meet a particular objective. In the preceding example, the first RUN directive is used to install curl, while the next RUN directive is used to install nginx.
- You might be wondering why we have apt update before every installation. This is required, as Docker builds images using layers. So, one layer should not have implicit dependencies on the previous one. In this example, if we omit apt update while installing nginx, and if we want to update the nginx version without changing anything in the directive containing apt update (that is, the line that installs curl), when we run the build, apt update will not run again, so your nginx installation might fail.
• The CMD directive specifies a list of commands that we need to run when the built image runs as a container. This is the default command that will be executed, and its output will end up in the container logs. Your container can contain one or more CMD directives. For a long-running process such as NGINX, the last CMD should contain something that will not pass control back to the shell and continue to run for the container’s lifetime. In this case, we run nginx -g daemon off;, which is a standard way of running NGINX in the foreground.
Some directives can easily be confused with each other, such as ENTRYPOINT and CMD or CMD and RUN. These also test how solid your Docker fundamentals are, so let’s look at both.