Chapter 5 - Building Docker Images

Yusuf IsahYusuf Isah
5 min read

Introduction

To truly harness the power of Docker, we need to create our own custom images. This chapter will take you into the world of image creation, covering the essential topics of building images from Dockerfiles, tagging images for easy management, and utilizing multi-stage builds for efficient development. Whether you're building a simple web application or a complex microservices architecture, understanding how to build Docker images is a crucial step in unlocking the full potential of containerization.

In this chapter, we will be containerizing a simple Go application called "greetings-app". All the files and directories you need to follow along with this tutorial can be found on my GitHub repository. Simply log into your GitHub repository and search for either "yusbuntu/greetings-tanzania" or "yusbuntu/greetings-egypt". Copy the URL of whichever one you choose and clone the repository on your local machine using the "git clone [url_of_repository_you_want_to_clone]" command.

Building Images From a Dockerfile

After navigating into the project directory of the repository you just cloned, you should see the files and directory below after running the ls command:

The greetings-app which is colorized green, is the Go executable that is created after building the application. You can run this executable by simply typing on your terminal:

./greetings-app

Next, go to your browser and type localhost:8080/ to see the application in its full glory :). To terminate the application, hold the ctrl button and press the c button on your keyboard. Now that you have seen what the greetings-app looks like, let's build a Docker image of this application using a Dockerfile. But first, let's take a look at the contents of our Dockerfile:

nano Dockerfile

In summary, this Dockerfile is building a Docker image for a Go application named greetings-app that listens on port 8080. The image is built using the official Golang 1.18 image as the base, and the resulting image contains the compiled Go binary and exposes the necessary port.

To build the Docker image, use the following command:

docker build -t greetings-app .

The -t flag tags the image with a name (greetings-app), and the . at the end specifies the build context, typically the current directory.

Tagging Images

Tagging images is a crucial part of managing and deploying Docker images. Tags help you identify different versions of your images. To add a tag to an image, you can use the docker tag command:

docker tag greetings-app greetings-app:v1.0

This command tags the greetings-app image with the version v1.0.

Finally, let's start a container using the image:

docker run -d -p 8080:8080 greetings-app:v1.0

In the command above, the-d flag runs the container in detached or daemon mode. The -p flag exposes port 8080 on the container to port 8080 on the host machine. When you run docker ps, you should see your container running. To stop the container, use the docker stop [container_ID] command.

You can also pass environment variables to the greetings-app container. To do this, use the -e flag. For example:

docker run -d -e GREETING="Hello Docker Champ!" -p 8080:8080 greetings-app:v1.0

Multi-stage Builds

Multi-stage builds are an advanced feature in Docker that allow you to use multiple "FROM" statements in your Dockerfile. This helps in reducing the final image size by copying only the necessary files from intermediate/temporary stages. The image we created earlier is about 984MB in size. This is not ideal for production due to the reasons below:

  • Slower Deployment: Larger images take longer to download and deploy, which means slower startup times for your application.

  • Increased Storage: Larger images consume more storage space on your servers, which can lead to increased costs.

  • Security Risks: Larger images may contain unnecessary packages or dependencies, increasing the attack surface for potential security vulnerabilities.

  • Resource Intensive: Larger images typically require more resources (CPU, memory) to run, which can impact performance and scalability.

To solve these problems, let's re-build the Docker image for our greetings app using a multi-stage build. The Dockerfile we will use to achieve this is titled Dockerfile.multi in the project directory. Before building the Docker image, let's take a look at its contents:

The goal of this multi-stage build is to:

  • Use the Golang image to build the application, taking advantage of its build tools and dependencies.

  • Use a smaller, more light-weight Debian image for the final stage, which will run the application.

  • Keep the final image size small by only copying the necessary files from the Build Stage.

This approach allows for efficient and optimized image creation, reducing the final image size and improving deployment times.

To build the Docker image from this Dockerfile, simply run:

docker build -t greetings-app:v2.0 -f Dockerfile.multi .

Lets breakdown the components of the command above:

  • docker build: This is the command to build a Docker image.

  • -t greetings-app:v2.0: This option tags the image with the name greetings-app and version v2.0. This makes it easy to identify and reference the image later.

  • -f Dockerfile.multi: This option specifies the Dockerfile to use for the build. In this case, it's Dockerfile.multi instead of the default Dockerfile. This allows you to have multiple Dockerfiles for different build scenarios.

  • .: This is the context directory for the build. The . refers to the current directory, which means Docker will look for the Dockerfile.multi file in the current directory and use the files in the current directory as the build context.

After building the image, If you run docker images you will notice that this new image is only about 78.8MB in size. That is a massive size difference compared to the 984MB size of the previous image.

As an exercise, start a container using this new image.

Conclusion

In this chapter, we explored the fundamentals of building Docker images from Dockerfiles, and tagging them for versioning. We also highlighted the importance of a small image size, and utilized multi-stage builds for efficient image creation. Through consistent practice, you'll become proficient in creating high-quality Docker images that meet the unique needs of your applications.

Feel free to leave comments and share this article. Follow my blog for more insights on Docker!

0
Subscribe to my newsletter

Read articles from Yusuf Isah directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yusuf Isah
Yusuf Isah

Hello. I am a DevOps enthusiast from Nigeria. I am also passionate about Technical Writing. As a passionate DevOps enthusiast, I'm dedicated to bridging the gap between development and operations teams. With a strong foundation in Linux, Git, Docker, and Kubernetes, I excel in creating efficient, scalable, and reliable software delivery pipelines. With a keen eye for detail and a passion for continuous learning, I stay up-to-date with industry trends and best practices. My goal is to collaborate with like-minded professionals, share knowledge, and drive innovation in the DevOps space. I look forward to sharing with you, all I've learned so far in my DevOps journey.