Nowadays, containers have become the standard for ensuring portability and consistency across environments. However, without the right approach, container images can become excessively large, slow, and insecure.
One of the biggest culprits behind slow container performance and bloated images is an inefficient build process. When dependencies required only during the build stage are included in the final container image, it results in unnecessary files and tools being carried over. This not only increases security risks but also leads to larger image sizes and slower application performance.
With the multi-stage build technique, applications can be built in separate stages, ensuring that only the necessary artifacts are included in the final image. This approach offers several key benefits in terms of efficiency, image size, and security.
How Multi-Stage Build Works
Multi-stage builds divide the build process into multiple stages within a single Dockerfile, ensuring that only the necessary components are included in the final docker image.
Let me explain this with an analogy. Imagine preparing a dinner for someone special ❤️
-
Preparation Stage (Build Stage 1)
- You chop vegetables, meat, and other ingredients.
- Tools like a knife, cutting board, and any other stuff are used at this stage.
-
Cooking Stage (Build Stage 2)
- With all the ingredients ready, you start cooking the dish in a big pan.
- You no longer need the knife and cutting board at this point.
-
Serving Stage (Final Stage)
- You plate the dish and serve it to your special someone.
- All the cooking tools and raw ingredients are no longer needed.
Without multi-stage builds, it’s like serving your special dish along with the big pan, knife, cutting board, and all the leftover ingredients. Definitely not the best dinner experience! 💔
Implementation in Dockerfile
In the context of the containers, multi-stage builds follow the same concept.
- Build Stages (Intermediate Stages) → These stages handle the application build process, such as installing dependencies, compiling code, and preparing the necessary artifacts.
- Final Stage → This stage contains only what is needed to run the application, excluding all build dependencies.
Here is a simple “Hello world” program written in Go.
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
This is an example of building the application without using multi-stage builds.
FROM golang:1.21.4-alpine AS builder
WORKDIR /app
COPY main.go .
RUN go build -o hello-world main.go
CMD ["./hello-world"]
Problem:
- The final image includes the entire Go build environment, making it much larger than necessary.
- Unused build tools and dependencies remain in the image, increasing security risks.
Without multi-stage builds, the Docker image size reaches 249MB!
Now, let's implement multi-stage builds to build the application.
FROM golang:1.21.4-alpine AS builder
WORKDIR /app
COPY main.go .
RUN go build -o hello-world main.go
FROM alpine:3.21
WORKDIR /root
COPY --from=builder /app/hello-world .
CMD ["./hello-world"]
Improvements:
- The final image only contains the compiled binary (
hello-world
), reducing the image size significantly. - The Go build environment is no longer included, improving security and efficiency.
As you can see, the docker image is now only 9.64MB, a massive reduction in size!
Summary
Multi-stage builds ensure that only the necessary components are included in the final Docker image, just like serving a meal without the cooking tools and leftover ingredients. This approach offers several key benefits:
- Smaller Docker Image Size → Reduces storage needs and speeds up image pull and push operations to the container registry.
- Enhanced Security → Eliminates unnecessary tools and dependencies, minimizing attack surfaces.
- Faster Build Times → Optimized caching improves efficiency, especially in CI/CD pipelines.
Top comments (0)