DEV Community

Cover image for Shrinking docker image size over 100x in just 2 steps
Shubham Tiwary
Shubham Tiwary

Posted on

Shrinking docker image size over 100x in just 2 steps

Smaller image size helps you:

  • make faster builds on your CI
  • testing containers becomes faster
  • less cost for build jobs / container space



. . .

Here's some quick hacks I found after playing around for a while with docker images

In this example, I'm shrinking an image of about ~900 MB to around ~8 MB (as tested on my docker)

For reference, I'll take this simple sample Dockerfile:

FROM golang
WORKDIR /app
COPY main.go .
RUN go build -v -o server
ENTRYPOINT [ "/app/server" ]
Enter fullscreen mode Exit fullscreen mode

Which simply builds a go program, and runs it.

This sizes around 946 MB after building


1 - Base Image as Alpine

Alpine is a very small Linux distro used for containers

So most images you want to run, will have an alpine base equivalent of them. Where to find them?

For example, for the golang image from dockerhub:

golang dockerhub

You can choose the tags here to choose an alpine image.

So the Dockerfile with alpine base will be:

FROM golang:1.24-alpine  #just any alpine tag from the tag list
WORKDIR /app
COPY go.mod .
COPY main.go .
RUN go build -v -o server -buildvcs=false 
ENTRYPOINT [ "/app/server" ]
Enter fullscreen mode Exit fullscreen mode

With this, the image build takes around 353MB size (3x smaller already)


2 - Multi Stage builds

I like to think of this like a:

multistage rocket, where there's multiple sections and after certain intervals, the sections push the rest, cutting itself off, one by one, until there's one final portion left.

Similarly, suppose in our go-program,

  1. we need the go compiler to build it first of all
  2. once its build, only the executable is needed (compiler or dependencies not needed during runtime may be dropped off)

Let's look at this with a Dockerfile:


# Stage 1: build
FROM golang:1.24-alpine AS builder-stage
WORKDIR /app
COPY go.mod .
COPY main.go .
RUN go build -v -o server -buildvcs=false 


# Stage-2: run
FROM scratch
WORKDIR /app
COPY --from=builder-stage /app/server /app
ENTRYPOINT [ "/app/server" ]
Enter fullscreen mode Exit fullscreen mode
  1. In stage-1 -> we build the "server" executable and it will be placed at /app/server (call this "builder" stage
  2. Then in 2nd stage -> take that executable from builder stage and simply run it.

In the 2nd stage, a base image called "scratch" is used, it is even more lightweight than alpine. But only used for cases like this, where there's literally no dependencies needed, just to run.

After this build, only the 2nd stage remains as final image.

Once we build this, the image size shrinks to 8.15MB! (almost 300x)


Limitations

This seems too good to be true, and it is yeah. Not for all cases can we optimize up to this extent.

Some cases, we simply need all the dependencies in run time as in compile time too (ex: interpreted languages like Python - although there's build tools for that, but limited), so it's tricky to use multi stage builds.

Repositories like Dockerhub also have compression on their end to optimize the size during deployments.

Even in the case of base image as alpine, some tools might not have great support in alpine


Trying it yourself

To replicate the following on your own,

Here's command to build the Dockerfile:

docker build -t <image-name:tag> .
Enter fullscreen mode Exit fullscreen mode

And the go program I used for this (a very simple HTTP server):

package main

import (
    "fmt"
    "log"
    "net/http"
)

func greet(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "hello world\n")
}

func main() {
    http.HandleFunc("/", greet)

    fmt.Printf("Starting server at port 8090\n")
    if err := http.ListenAndServe(":8090", nil); err != nil {
        log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)