Dive into Docker's Multi-Stage Builds

Dive into Docker's Multi-Stage Builds

Introduction: Docker has revolutionized how developers build, ship, and run applications by providing a consistent environment across different platforms. One of the powerful features Docker offers is multi-stage builds, which allow you to optimize your Docker images and improve performance. In this article, we'll walk through the process of setting up a Task Manager application in Docker using multi-stage builds, step-by-step.

Understanding the Mechanism of Multi-Stage Builds in Docker

Multi-stage builds in Docker streamline the image creation process by dividing it into multiple stages, each with its own set of instructions. The initial stages typically handle building dependencies and compiling application code, while subsequent stages copy only the necessary artifacts from previous stages.

This results in smaller and more efficient final images, as unnecessary files and dependencies are discarded along the way. By using lightweight base images in later stages, multi-stage builds reduce image size, improve performance, and enhance security by minimizing the attack surface. Overall, multi-stage builds provide a powerful solution for optimizing Docker images, particularly for complex applications with diverse dependencies.

Note: For demonstration purposes, the tutorial utilizes a task manager application written in the Go language. However, the concepts and techniques discussed can be applied to optimize Docker images for any application, regardless of the programming language used.

Step 1: Clone the Repository

  1. Open your terminal or command prompt.

  2. Run the following command to clone the repository:

     git clone https://github.com/HemanthGangula/task-manager-in-docker
    

    This command will create a task-manager-in-docker directory containing the necessary files for building the Docker image.

  3. You also have the option to create your own folder named "task_manager." Below, you'll find the code for the files "task_manager.go" and "Dockerfile."

  4. task_mangager.go

  5.      package main
    
         import (
                 "bufio"
                 "fmt"
                 "os"
                 "strconv"
                 "strings"
         )
    
         type Task struct {
                 ID       int
                 Name     string
                 Complete bool
         }
    
         var tasks []Task
         var taskIdCounter int
    
         func main() {
                 reader := bufio.NewReader(os.Stdin)
                 for {
                         fmt.Println("Task Manager")
                         fmt.Println("1. Add Task")
                         fmt.Println("2. List Tasks")
                         fmt.Println("3. Mark Task as Complete")
                         fmt.Println("4. Exit")
                         fmt.Print("Enter your choice: ")
    
                         choice, _ := reader.ReadString('\n')
                         choice = strings.TrimSpace(choice)
    
                         switch choice {
                         case "1":
                                 fmt.Print("Enter task name: ")
                                 taskName, _ := reader.ReadString('\n')
                                 taskName = strings.TrimSpace(taskName)
                                 addTask(taskName)
                         case "2":
                                 listTasks()
                         case "3":
                                 fmt.Print("Enter task ID to mark as complete: ")
                                 taskIDInput, _ := reader.ReadString('\n')
                                 taskIDInput = strings.TrimSpace(taskIDInput)
                                 taskID, err := strconv.Atoi(taskIDInput)
                                 if err != nil {
                                         fmt.Println("Invalid task ID")
                                         continue
                                 }
                                 markTaskAsComplete(taskID)
                         case "4":
                                 fmt.Println("Exiting...")
                                 os.Exit(0)
                         default:
                                 fmt.Println("Invalid choice. Please try again.")
                         }
                 }
         }
    
         func addTask(name string) {
                 taskIdCounter++
                 task := Task{
                         ID:       taskIdCounter,
                         Name:     name,
                         Complete: false,
                 }
                 tasks = append(tasks, task)
                 fmt.Println("Task added successfully.")
         }
    
         func listTasks() {
                 if len(tasks) == 0 {
                         fmt.Println("No tasks.")
                         return
                 }
                 fmt.Println("Tasks:")
                 for _, task := range tasks {
                         completeStatus := "Incomplete"
                         if task.Complete {
                                 completeStatus = "Complete"
                         }
                         fmt.Printf("ID: %d, Name: %s, Status: %s\n", task.ID, task.Name, completeStatus)
                 }
         }
    
         func markTaskAsComplete(id int) {
                 found := false
                 for i, task := range tasks {
                         if task.ID == id {
                                 tasks[i].Complete = true
                                 fmt.Println("Task marked as complete.")
                                 found = true
                                 break
                         }
                 }
                 if !found {
                         fmt.Println("Task not found.")
                 }
         }
    
  6. task_manager.go

  7.      ###########################################
         # BASE IMAGE FOR BUILD STAGE
         ###########################################
    
         FROM ubuntu:latest AS build
    
         # Install Go
         RUN apt-get update && apt-get install -y golang-go
    
         # Set the environment variable to disable Go modules
         ENV GO111MODULE=off
    
         # Set the working directory inside the container
         WORKDIR /app
    
         # Copy the local package files to the container's workspace
         COPY . .
    
         # Build the Go app
         RUN CGO_ENABLED=0 go build -o /app/task_manager .
    
         ###########################################
         # FINAL IMAGE
         ###########################################
    
         FROM scratch
    
         # Copy the executable from the build stage to the final image
         COPY --from=build /app/task_manager /app/task_manager
    
         # Set the entrypoint for the container
         ENTRYPOINT ["/app/task_manager"]
    

Step 2: Build the Docker Image

  1. Navigate into the cloned directory:

     cd task-manager-in-docker
    
  2. Run the Docker build command to build the Docker image:

     docker build -t task-manager .
    

    This command reads the instructions from the Dockerfile in the directory and builds the Docker image named task-manager.

  3. Step 3: View the Docker Image

  4. Once the build process is complete, you can view the list of Docker images by running:

     docker images
    

    This command will display the task-manager image among the list of Docker images on your system.

  5. In the image below, before implementing Multi-Stage Builds in Docker, the image named 'taskmanager' had a size of 869MB. After utilizing Multi-Stage Builds to create another image named 'taskmanager_multistage', the image size drastically reduced to 1.82MB.

  6. I've deployed my Docker image on Docker Hub.

  7. Explore my Docker image here:

Uncover the possibilities and ignite your imagination!

In conclusion, this article has showcased the process of optimizing your project using Docker multi-stage builds, using a task manager application written in Go as an example. By harnessing multi-stage builds, you can craft smaller, more efficient Docker images, leading to expedited build times and heightened performance. Integrating Docker into your development workflow not only streamlines the deployment process but also simplifies application management across various environments, empowering your development journey.

Did you find this article valuable?

Support Hemanth by becoming a sponsor. Any amount is appreciated!