Chapter 3: Working with Docker Images
Docker images are foundational to the world of Docker and containerization. They are read-only templates composed of layered filesystems used to create Docker containers. Essentially, an image is a lightweight, stand-alone, executable package that includes everything needed to run a piece of software.
In this chapter, we will embark on an in-depth exploration of Docker images. We’ll start by understanding what Docker images are, followed by the differences between Docker images and Docker containers. We will then delve into various aspects of working with Docker images, including how to build images using a Dockerfile, how to inspect and manage them, and how to push them to Docker Hub or any other Docker image registry.
The chapter will further cover the importance of managing Docker image sizes, as smaller images often lead to faster deployment times and reduced security vulnerabilities. We will learn about tagging images, managing image versions, reducing image size, and the significance of Docker image IDs.
By the end of this chapter, you’ll have a thorough understanding of Docker images and will be equipped with practical skills to create, manage, and optimize Docker images efficiently. The concepts and skills acquired in this chapter are vital for anyone aiming to effectively leverage Docker for development and deployment of applications.
What is a Docker Image?
A Docker image is the basis of Docker containers. It is a read-only template that contains a set of instructions for creating a Docker container. In essence, a Docker image is a lightweight, stand-alone, executable software package that encapsulates everything needed to run a piece of software, including the application code, a runtime environment, libraries, environment variables, and configuration files.
Let’s delve deeper into the key elements of Docker images:
Read-only Templates: Docker images are read-only, meaning that once created, they can’t be modified. However, they can be used as a foundation to create new images with additional layers.
Layers: Docker images are built up from a series of read-only layers. Each layer corresponds to a Dockerfile instruction. The layers are stacked on top of each other. When you change a Docker image — for instance, update an application to a new version — a new layer gets built. Therefore, rather than replacing the whole image or entirely rebuilding, as you may do with a virtual machine, only that layer is updated.
Lightweight: The layering mechanism makes Docker images lightweight. Layers are shared among images, meaning that if you have multiple images on your system that are based on the same base image, Docker will only keep one copy of the base image on your system.
Dockerfile: Docker images are created from Dockerfiles. A Dockerfile is a simple text file that contains the commands or instructions needed to create a Docker image. These instructions include identification of the base image, the addition of new files, execution of various commands, opening of specific network ports, and more.
Image Registry: Docker images are typically stored in an image registry. Docker Hub is a public registry that anyone can use, and Docker is configured to look for images on Docker Hub by default. You can also host your own Docker registries and will find it especially useful if you want to keep your images private.
A Docker image thus includes everything needed to run an application as a lightweight, stand-alone entity that can run virtually anywhere. Docker images can be shared and used by others and form the foundation of Docker containers. In the next sections, we will learn how to create, manage, and use Docker images.
Docker Images vs. Docker Containers
Understanding the distinction between Docker Images and Docker Containers is crucial to understanding Docker. These terms are sometimes used interchangeably, but they represent different concepts in the Docker ecosystem.
Docker Image
A Docker image, as we’ve previously discussed, is a read-only template that contains a set of instructions for creating a Docker container. It’s like a blueprint for a container. It includes the application code, runtime environment, libraries, environment variables, and configuration files needed to run the application. Images are created with the docker build
command, either manually or by using a Dockerfile.
Docker images are static, unchanging entities. They are versioned and unmodifiable after creation; changes result in new images being created. Docker images are stored in a Docker registry such as Docker Hub or in a private registry.
Docker Container
A Docker container, on the other hand, is a running instance of a Docker image. It’s the live, running application that’s been launched from an image. When you run the image, Docker adds a read-write layer on top of the image, in which your application runs. You can think of a Docker container as a running Docker image brought to life with the application process.
Containers are created from Docker images with the docker run
command. They can be started, stopped, deleted and moved, and can interact with other containers and the host system. Each container is isolated but can connect with other containers and its host machine.
By understanding these differences, you can more effectively build, run, and manage both Docker Images and Docker Containers.
Working with Docker Images
Working with Docker images involves several operations, including pulling images from a Docker registry, listing images, removing images, and more. In this section, we’ll go over these operations in detail.
Pulling Docker Images
The first step in using a Docker image is to have it on your machine. Docker images are typically stored in Docker registries. Docker Hub, a public registry, is used by default. You can pull an image from Docker Hub using the docker pull
command:
docker pull <image-name>
For example, to pull the latest Ubuntu image, you would use:
docker pull ubuntu
If you want to pull a specific version of an image, you can specify a tag. For example, to pull Ubuntu 18.04, you would use:
docker pull ubuntu:18.04
Listing Docker Images
After pulling the image, you can list all Docker images on your machine using the docker images
or docker image ls
command:
docker images
This command will return a list of all images on your system, along with their repository name, tag, image ID, creation date, and size.
Removing Docker Images
If you have an image that you no longer need, you can remove it using the docker rmi
or docker image rm
command:
docker rmi <image-id>
To remove an image, you’ll need to use the image ID, which you can find by using the docker images
command. Note that you won’t be able to remove an image that is being used by an existing container.
These are the most common tasks when working with Docker images. In the next sections, we’ll learn how to create our own images, tag and push them to a Docker registry, manage their size, and more.
Creating Docker Images with a Dockerfile
A Dockerfile is a text file that contains a list of commands that the Docker daemon uses to build an image. It’s like a script that automates the creation of Docker images.
Here’s an example of a basic Dockerfile:
# Use an official Python runtime as a parent image
FROM python:3.7-slim
# Set the working directory in the container to /app
WORKDIR /app
# Add the current directory contents into the container at /app
ADD . /app
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Make port 80 available to the world outside this container
EXPOSE 80
# Define environment variable
ENV NAME World
# Run app.py when the container launches
CMD ["python", "app.py"]
Let’s break down what’s happening in this Dockerfile:
FROM python:3.7-slim
: This line sets the base image. In this case, we’re using the slim variant of the official Python 3.7 image.
WORKDIR /app
: This line sets the working directory for any instructions that follow it in the Dockerfile. In this case, we’re setting it to /app
.
ADD . /app
: This line copies new files, directories or remote file URLs from <src>
and adds them to the filesystem of the image at the path <dest>
.
RUN pip install --no-cache-dir -r requirements.txt
: This line uses the RUN
command to install the Python dependencies the Python application requires.
EXPOSE 80
: This line tells Docker that the container will listen on the specified network ports at runtime. In this case, it’s port 80.
ENV NAME World
: This line sets an environment variable. In this case, we’re setting NAME
to World
.
CMD ["python", "app.py"]
: This line provides defaults for an executing container. In this case, it’s saying that we want the container to run app.py
with Python when it launches.
You can build an image from a Dockerfile using the docker build
command:
docker build -t my-python-app .
In the above command, -t my-python-app
assigns the name my-python-app
to the image, and the .
specifies that Docker should look for the Dockerfile in the current directory.
Once the image is built, you can run it to create a container:
docker run -p 4000:80 my-python-app
The -p 4000:80
option maps the host port 4000 to the container’s port 80.
In this way, Dockerfiles allow you to define your application environments in a version-controlled and human-readable format.
Building and Inspecting Docker Images
Building a Docker image is a crucial step in the Docker workflow. As previously explained, this is achieved with the docker build
command.
Let’s take a deeper look into this process and also explore how to inspect a Docker image once it’s built.
Building Docker Images
The docker build
command is used to build an image from a Dockerfile and a context. The build’s context is the set of files in the specified PATH or URL. The Docker build process is executed in a sequence of steps called build layers.
Here’s an example of how to build an image:
docker build -t my-app:1.0 .
In the above command:
-t my-app:1.0
tags the image with the namemy-app
and version1.0
..
sets the build context to the current directory. Docker will look for the Dockerfile in this location.
The Dockerfile specifies what goes on in the environment inside your container. This includes specifying the base image, defining the file system and network, and launching the application.
Inspecting Docker Images
Once you have built a Docker image, you might want to inspect its details. This is where the docker inspect
command comes in. The docker inspect
command provides detailed information on Docker objects such as images, containers, volumes, and networks.
For example, if you want to inspect a Docker image, you can use:
docker inspect my-app:1.0
The command will return a JSON object that includes various details about the image, such as its ID, creation time, Docker version, size, virtual size, labels, environment variables, and more. This can be particularly useful for debugging or when you need specific information about an image (like the image’s IP address or the configuration of its volumes).
Remember that the docker inspect
command can also be used to inspect other Docker objects such as containers, networks, and volumes.
In summary, building and inspecting Docker images are essential skills when working with Docker. The ability to create your own images from a Dockerfile and to inspect these images will provide you with flexibility and control over your Docker environments.
Tagging and Pushing Docker Images
Tagging and pushing Docker images are crucial aspects of managing and distributing Docker images. Let’s understand them in detail:
Tagging Docker Images
Tagging is a way to add a version number to your Docker images. It can also be used to label images with meaningful names, like ‘production’ or ‘test’. By default, Docker tags images with the ‘latest’ tag.
Here’s how you can tag a Docker image:
docker tag source_image:tag target_image:tag
In the above command, source_image:tag
identifies the image that you want to tag, and target_image:tag
is the new identifier for your image. For instance, to tag a local image with the ID ‘abcdefg’ as ‘my_image:1.0’, you would use:
docker tag abcdefg my_image:1.0
Now, you can refer to the image by the tag ‘my_image:1.0’ as well as by its ID.
Pushing Docker Images
Once you have tagged your Docker image, you can push it to a Docker registry, which is a storage and distribution system for named Docker images. The default registry can be Docker Hub, a public registry hosted by Docker, but you can also use third-party registries or configure your own private registry.
Here’s how you can push a Docker image to a registry:
docker push target_image:tag
Before pushing an image, you need to authenticate to the registry using the docker login
command.
For instance, if you want to push the ‘my_image:1.0’ image to Docker Hub, you would use:
docker login
docker push my_image:1.0
The docker login
command prompts for a username and password. If you are pushing to Docker Hub, these are your Docker Hub username and password. Once you’re logged in, you can push your image.
In summary, tagging Docker images allows you to manage different versions of your images or to label them according to your needs, while pushing Docker images allows you to distribute them to your team, to your clients, or to the community. Both are fundamental aspects of the Docker image lifecycle.
Managing Docker Image Size
When building Docker images, it’s important to keep an eye on the size of your images. Large Docker images take longer to transfer and deploy, use more disk space, and can be less secure because they may include unnecessary tools and libraries that can be exploited by attackers. Therefore, keeping your Docker images as small as possible is a good practice.
Here are some strategies to manage the size of your Docker images:
1. Use Minimal Base Images:
When possible, use minimal base images. These images are stripped down to the bare minimum, reducing their size. For instance, the Alpine Linux image is only 5MB, while the Debian-based Python image is closer to 900MB.
For example, instead of:
FROM python:3.7
Consider using:
FROM python:3.7-alpine
2. Reduce the Number of Layers:
Each command in a Dockerfile creates a new layer in the Docker image. Reducing the number of layers can help to reduce the size of the image.
You can reduce layers by combining commands. For instance, instead of:
RUN apt-get update
RUN apt-get install -y package1 package2 package3
You can write:
RUN apt-get update && apt-get install -y \
package1 \
package2 \
package3
3. Clean Up After Installations:
When using package managers to install software, clean up unnecessary files after the installation to keep the image size down.
For instance, when using apt-get
:
RUN apt-get update && apt-get install -y \
package1 \
package2 \
package3 \
&& rm -rf /var/lib/apt/lists/*
4. Use Multi-Stage Builds:
Multi-stage builds are a way to optimize Dockerfiles while keeping them easy to read and maintain. They allow you to use one Docker image to build an application (the builder), and then copy the built application into a smaller, final image.
Here’s an example of a multi-stage Dockerfile:
# Builder stage
FROM python:3.7 as builder
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
# Final stage
FROM python:3.7-alpine
WORKDIR /app
COPY --from=builder /app .
CMD ["python", "app.py"]
In the above Dockerfile, the first stage uses the python:3.7
image to build the Python application. Then, the second stage uses the python:3.7-alpine
image, which is much smaller, and only the built application is copied into this image.
Remember to use docker image ls
to check the size of your Docker images, and docker history <image>
to see the size of each layer in an image.
Keeping Docker images as small as possible can save you a lot of resources in the long run and can make your applications more secure and efficient.
Understanding Docker Image IDs
Docker Image IDs are unique identifiers associated with each Docker image. They serve a crucial role in managing and working with Docker images, allowing Docker to keep track of different images and their layers. Let’s delve deeper into understanding these IDs.
What is a Docker Image ID?
Each Docker image has an Image ID, a SHA256 hash that uniquely identifies the image. Docker generates this ID when you build an image, and it is based on the contents of the image. Therefore, even a small change in the Dockerfile or the image’s layers will result in a new ID.
To view an image’s ID, use the docker images
command:
docker images
The output will display information about your images, including their repository, tag, ID, creation date, and size. The Image ID column shows the unique ID for each image.
Image IDs and Tags
While an Image ID is a long alphanumeric hash, Docker also allows you to assign human-readable tags to your images, providing a convenient way to refer to them. However, multiple tags can point to the same image ID, and a single tag can also be moved from one Image ID to another.
For instance, if you have two versions of an application and both are based on the same Docker image, they will share the same Image ID, but they can have different tags.
Layers and Image IDs
Docker images consist of multiple layers, and each layer has its own ID. These layers are stacked on top of each other to form the final image. When you make changes to an image and rebuild it, Docker reuses unchanged layers from cache, improving the build speed.
The Image ID corresponds to the final layer in the image. Thus, you can consider an Image ID as a pointer to a specific combination of image layers.
In summary, Docker Image IDs play a vital role in Docker’s content-addressable identity scheme, facilitating efficient image storage, layer sharing, and quick image lookup. They’re essential for managing and maintaining your Docker environment.
Exercise & Labs
Exercise 1: Docker Image Creation and Inspection
Create a simple Dockerfile that uses the latest Python 3 image (python:3
) and runs the command python -V
to print out the Python version. Build this Dockerfile into an image with the tag python-test
.
Inspect the python-test
image using the docker inspect
command. Look at the output and identify the following properties:
-
- Image ID
- Creation time
- Docker version
- Size
Exercise 2: Docker Image Tagging and Pushing
Tag the python-test
image you created in Exercise 1 with a new tag python-test:v2
.
Create a Docker Hub account if you haven’t already, log in via the command line, and push your newly tagged image to Docker Hub.
Exercise 3: Docker Image Size Management
Compare the sizes of the python:3
image and the python:3-alpine
image using the docker images
command. Note the difference in size.
Modify the Dockerfile from Exercise 1 to use the python:3-alpine
image and rebuild the image. Compare the sizes of the new image and the original python-test
image.
Exercise 4: Docker Image IDs and Layers
Run docker history python-test
to view the layers of the python-test
image. Identify the number of layers and their sizes.
Modify your Dockerfile from Exercise 3 to add another command: RUN echo "Hello, Docker!" > hello.txt
. Rebuild the image and check the history again. Identify the new layer and its size.
Working with Docker Containers
UP NEXT