Published on

Art of building small containers

4 min read

Authors
banner

In this article, we will learn how to build small docker containers by understanding builder pattern and multistage builds in detail and go over what benefits they provide.

Spoilers we will reduce our golang container size from over 850mb to just under 12mb!

I've also made a video, if you'd like to follow along. Slides from the video can be found here

Problem

Docker images are often much bigger than they have to be which ends up impacting our deployments, security and dev experience.

Optimizing a build can be complex as it's hard to keep your image clean and eventually, it gets messy, and hard to follow.

We also end up shipping unnecessary assets like tooling, dev dependencies, runtime or compiler in our releases.

Let's assume we have a simple hello world in Go and we'd like to dockerize it and deploy on Kubernetes

Here's our very simple hello world project

├── Dockerfile
└── main.go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", helloHandler)
	http.ListenAndServe(":8080", nil)
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, World!")
}

and our Dockerfile

FROM golang:1.16.5
WORKDIR /app
COPY main.go .
RUN go build main.go
CMD ./main
$ docker build -t default .
$ docker images

default     latest    afac261974d0   32 seconds ago     868MB

Woah, why is our hello world image over ~850mb!

meme

Solutions

Let's look at some possible solutions for reducing our image size

Smaller base images

One simple solution is to just use a smaller base image when we are building our containers.

Example

GO

1.16.5              862MB
1.16.5-alpine       302MB  <---

Node

16                  907MB
16-slim             176MB
16-alpine           112MB  <---

To do this we can update the FROM statement in our Dockerfile

FROM golang:1.16-alpine

Builder pattern with multistage builds

builder pattern

Builder pattern simply describes a way to build your docker containers by splitting the build process into two or multiple stages to reduce any unnecessary assets from the production image.

The first image is a builder image, which basically builds our code by taking advantage of having all the necessary build utilities available.

The second image is our runtime or release image in which we will just copy over the built binary and deploy it, hence the size reduction!

Multistage builds just allow us to define all our stages in a single Dockerfile as opposed to splitting into multiple Dockerfiles like we had to do before multistage feature was available

Here's how it reflects in our Dockerfile

FROM golang:1.16.5 as builder
WORKDIR /app
COPY main.go .
RUN go build main.go

FROM alpine as production
COPY --from=builder /app/main .
EXPOSE 8080
CMD ./main
$ docker build -t multistage .
$ docker images

multistage     latest    iucs2934758r   18 seconds ago     12.5MB

Note: we can also use scratch or my new favourite distroless containers from Google

TLDR

  • Derive from a base image with the whole runtime or SDK
  • Copy our source code
  • Install dependencies
  • Produce build artifact
  • Copy the built artifact to a much smaller release image

Benefits

Here are the benefits we get from building small containers

Performance

Some of the benefits of building and deploying small docker containers are:

  • Faster push and pull from the container registry
  • Small and optimized builds

Cost effective

We can now push our new docker image with a fraction of the cost and space required for the original.

Here's an interesting example from one from my client running around 18 microservices on Kubernetes

Default

18 microservices x ~800mb x 5 deploys cycles month x 12 months

~864GB/year

Optimized

18 microservices x ~25mb x 5 deploys cycles month x 12 months

~27GB/year

Security

Security is an essential part of any application especially if you're working in a highly regulated industry such as healthcare, finance, etc.

Smaller images reduces a lot of attack surface for vulnerabilities, here's a quick scan from AWS ECR

scan results

Next steps

Now we can deploy our tiny docker containers on Kubernetes/OpenShift fast and efficiently.

Feel free to reach out to me on Twitter if you have any questions.

© 2024 Karan Pratap Singh