Docker Images: A Deep Dive into Container Technology

Roman Glushach
7 min readJul 31, 2023
Docker Images

Docker image is a read-only template that defines the structure and contents of a Docker container. A Docker image consists of one or more layers, each layer representing a set of changes or instructions that are applied to the previous layer. For example, a Docker image might have a base layer that contains the operating system, a second layer that installs some software packages, a third layer that copies some files from the host machine, and so on. Each layer is identified by a unique hash, and the order of the layers determines the final state of the image.

When you run a Docker container from an image, Docker creates a thin writable layer on top of the image layers, called the container layer. This layer stores any changes that are made to the container during its lifetime, such as creating, modifying, or deleting files. The container layer is ephemeral, meaning that it is discarded when the container is stopped or deleted. The image layers, on the other hand, are persistent and immutable, meaning that they are not modified or deleted unless you explicitly do so.

What are Docker images?

A Docker image is a file that contains all the information and instructions to create and run a container. A Docker image consists of multiple layers, each layer representing a change or an addition to the image. For example, a layer can be created by installing a package, copying a file, or running a command.

A Docker image also has metadata, such as the image name, tags, labels, environment variables, exposed ports, volumes, entrypoint, and command. The metadata provides information about the image and how to use it.

Each layer is identified by a unique ID or a digest, which is a hash of the layer’s content. Each layer also has a parent layer, except for the base layer, which has no parent. The base layer is the lowest layer in the image hierarchy, and it usually contains the operating system or a minimal runtime environment.

The layers in a Docker image are read-only, which means they cannot be modified once they are created. However, when a container is created from an image, a new writable layer is added on top of the image layers. This layer is called the container layer, and it contains any changes made to the container during its lifetime. The container layer is deleted when the container is removed.

The advantage of using layers is that they allow reuse and sharing of common components among different images and containers. For example, if you have two images that are based on the same base layer, you only need to store the base layer once on your disk. Similarly, if you have two containers that are based on the same image, you only need to store the image layers once on your disk.

Another benefit of using layers is that they enable fast and efficient delivery of images. When you pull an image from a registry (such as Docker Hub), you only download the layers that you don’t already have on your disk. This reduces the bandwidth and storage requirements for downloading images.

How do Docker images work?

Docker images are based on a concept called union file systems (UFS). UFS allows multiple file systems to be mounted on top of each other, forming a single coherent file system. Each file system can have its own properties and permissions, and can hide or override files from the lower file systems. UFS enables Docker to create layered images that share common files and only store the differences between them.

Docker uses a UFS called overlay2 by default, which supports up to 128 layers per image. Overlay2 works by using two directories for each layer: one for the actual files (called lowerdir) and one for metadata (called upperdir). The lowerdir contains the files from the previous layer, while the upperdir contains the changes or instructions for the current layer. Overlay2 also uses a third directory called workdir to prepare the files before they are merged into the upperdir. Finally, overlay2 uses a fourth directory called merged to present the unified view of all the layers as a single file system.

When you run a Docker container from an image, overlay2 creates a new directory for the container layer (called diff) and mounts it on top of the image layers using the merged directory. Any changes that you make to the container are stored in the diff directory, while the image layers remain unchanged. When you stop or delete the container, overlay2 unmounts the diff directory and deletes it along with any changes.

Docker images use a copy-on-write strategy to minimize disk space and improve performance. This means that when a file is modified in the writable layer of a container, only the modified part of the file is copied from the image layer to the container layer. The rest of the file remains unchanged in the image layer. This way, multiple containers can share the same image layers without duplicating them.

How to Create a Docker Image?

There are two main ways to create a Docker image:

  • Dockerfile
  • using an existing image as a base

Using a Dockerfile

A Dockerfile is a text file that contains the commands to build a Docker image. A Dockerfile has a specific syntax and structure that follows these rules:

  • Each line in a Dockerfile represents a command or an instruction that is executed in order
  • Each instruction creates a new layer in the image
  • Each instruction has a keyword (in uppercase) followed by arguments
  • Comments start with # and are ignored by the Docker engine
  • The first instruction must be FROM, which specifies the base image to use
  • The last instruction should be CMD or ENTRYPOINT, which specifies the default command to run when the container is created

Here is an example of a simple Dockerfile that creates an image based on Ubuntu and installs Python:

FROM ubuntu:latest # Use Ubuntu as the base image

RUN apt-get update && apt-get install -y python # Update the package list and install Python

WORKDIR /app # Set the working directory to /app

COPY hello.py . # Copy the hello.py file from the current directory to /app

CMD ["python", "hello.py"] # Run hello.py when the container is created

To build an image from this Dockerfile, you need to use the docker build command. The docker build command takes the path to the directory where the Dockerfile is located as an argument. You can also use the -t option to give a name and a tag to the image. For example:

docker build -t my-python-image .

This will create an image called my-python-image with the latest tag. You can also specify a different tag, such as:

docker build -t my-python-image:1.0 .

This will create an image called my-python-image with the 1.0 tag.

You can list all the images on your machine using the docker images command:

docker images

This will show you something like this:

REPOSITORY TAG      IMAGE   ID              CREATED         SIZE
my-python-image latest 7a8f3a9c0e0f 10 seconds ago 133MB
my-python-image 1.0 7a8f3a9c0e0f 10 seconds ago 133MB
ubuntu latest f643c72bc252 2 weeks ago 72.9MB

You can see that the my-python-image has two tags, latest and 1.0, but they have the same image ID, which means they are the same image.

You can run the image using the docker run command:

docker run my-python-image

This will create a container based on the image and run the hello.py script, which prints “Hello, world!” to the standard output.

You can also use the -it option to run the container in interactive mode, which allows you to enter commands in the container’s shell:

docker run -it my-python-image

This will create a container based on the image and open a bash shell in the /app directory. You can then execute any command you want, such as:

root@7a8f3a9c0e0f:/app# python hello.py
Hello, world!
root@7a8f3a9c0e0f:/app# ls
hello.py
root@7a8f3a9c0e0f:/app# exit

To exit the container, you need to type exit or press Ctrl+D.

Using an Existing Image as a Base

Another way to create a Docker image is to use an existing image as a base and add your own changes on top of it. This is useful when you want to customize or extend an image that already has some functionality or configuration that you need.

For example, suppose you want to create an image that has Python and Flask installed. Instead of starting from scratch with Ubuntu, you can use the official Python image as a base and install Flask on top of it.

To do this, you need to create a Dockerfile that uses the Python image as the base and adds the Flask installation as a layer. For example:

FROM python:latest # Use Python as the base image

RUN pip install flask # Install Flask using pip

WORKDIR /app # Set the working directory to /app

COPY app.py . # Copy the app.py file from the current directory to /app

EXPOSE 5000 # Expose port 5000 for Flask

CMD ["python", "app.py"] # Run app.py when the container is created

The app.py file is a simple Flask application that returns “Hello, Flask!” when accessed:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
return "Hello, Flask!"

if __name__ == "__main__":
app.run(host="0.0.0.0")

To build an image from this Dockerfile, you need to use the docker build command with a name and a tag for the image. For example:

docker build -t my-flask-image .

This will create an image called my-flask-image with the latest tag.

You can run the image using the docker run command with the -p option to map port 5000 of the container to port 5000 of your machine:

docker run -p 5000:5000 my-flask-image

This will create a container based on the image and run the app.py script, which starts a Flask server on port 5000. You can access the Flask application from your browser by visiting http://localhost:5000.

Best Practices

Conclusion

Docker image is a read-only template that defines the structure and contents of a Docker container. A Docker image consists of one or more layers, each of which represents a set of changes or instructions that are applied to the base layer.

--

--

Roman Glushach

Senior Software Architect & Engineer Manager at Freelance