Today I learned: How to make very small containers for golang binaries

John Jainschigg - June 17, 2020 -

TL;DR – Official Go Docker container images tend to be beefy. The standard image on Docker Hub is called golang (docker pull golang), and tossing in a Go program (such as for interactive execution) will bring it up above 800MB. But building images from scratch (for example, using the scratch container) using compiled binaries that don’t need complex, multi-layered OS and language environment support, will keep things slimmer.

I’ve been building microservices applications for demos lately. A lot of the work has been in Node.js, because it’s easy. But this past weekend, I started learning Go (because ‘all the cool kids,’ obviously) and so, there I was, figuring out how to containerize Go programs.

It turns out this is easy, too. But I was surprised to discover how large the resulting container images were. Suppose we have a minimal program, hello.go, like:

package main

import (

func main() {

… which prints the array containing its arguments. Running this on the command line with:

$ go run hello.go hi there
[/tmp/go-build289681080/b001/exe/hello hi there]

… gets you the standard argument array, beginning with the executable path.

Now you can put this into a golang container using the following Dockerfile…

FROM golang

COPY . .

CMD ["go","run","hello.go","hi","there"]

… then build it …

$ docker build -f Dockerfile.golang --tag hello:1.1 .
Sending build context to Docker daemon  2.073MB
Step 1/3 : FROM golang
 ---> 5fbd6463d24b
Step 2/3 : COPY . .
 ---> f72803dfaac0
Step 3/3 : CMD ["go","run","hello.go","hi","there"]
 ---> Running in 39e7765bec67
Removing intermediate container 39e7765bec67
 ---> f96c3d2c0861
Successfully built f96c3d2c0861
Successfully tagged hello:1.1

… and run it, and get the expected result:

$ docker run hello:1.1
[/tmp/go-build847833630/b001/exe/hello hi there]

But when you check with docker images, you see that the container is relatively yuge.

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello               1.1                 f96c3d2c0861        3 minutes ago       812MB

‘Kayso, you can make a much, much smaller container by changing your Dockerfile to this:

FROM scratch

COPY ./hello /go/bin/hello

CMD ["/go/bin/hello","hi","there"]

… and using the so-called ‘scratch’ image, which is basically an empty container without a shell.

But the scratch image also contains no Go language environment, so you need to compile your application into an executable first:

$ go build hello.go

… which gets you a hello binary in your local directory that you can execute like this …

$ ./hello hi there
[./hello hi there]

… and get back the (drum roll) expected result.

Looking back at the Dockerfile (called, in this case Dockerfile.scratch), you can see all it’s doing is copying the binary (hello) into a directory it creates in the container (/go/bin) and then running it from there. So build …

$ docker build -f Dockerfile.scratch --tag hello:1.2 .
Sending build context to Docker daemon  2.073MB
Step 1/3 : FROM scratch
Step 2/3 : COPY ./hello /go/bin/hello
 ---> Using cache
 ---> fbc88299067f
Step 3/3 : CMD ["/go/bin/hello","hi","there"]
 ---> Running in dd925ee8a3ab
Removing intermediate container dd925ee8a3ab
 ---> 4a049a401c79
Successfully built 4a049a401c79
Successfully tagged hello:1.2

… and run …

$ docker run hello:1.2
[/go/bin/hello hi there]

… and (by this time, I figure you’re not surprised) … the expected result. But look at the container image:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello               1.2                 4a049a401c79        33 seconds ago      2.07MB

That’s a pretty substantial size reduction!

Flipping from shell-based execution to no-shell-in-sight execution naturally comes with many potential gotchas. In this case, for example, if you were to build the arguments passed by your CMD out of expressions that required evaluation/expansion by a shell, things wouldn’t work as planned in a no-shell container. You’ll need to construct code carefully to run in such a stripped-back environment.

Now, be advised: I haven’t thought about side-effects, security, stability, gotchas that would no doubt be obvious to a seasoned Go dev, and I’d love to hear about them in the comments. But as we say in the artisanal code-creation atelier: “It works on my machine.” 

Today I Learned (TIL) is an intermittent journal of somewhat half-baked solutions to “speed bump” problems encountered by Mirantis Technical Marketing folks working well beyond our native spheres of expertise for research purposes. None of what we write about here has been checked by grown-ups, or is appropriate for production without further validation. Use at own risk. Comments, cautions, and suggestions for improvement are very welcome! Email (Twitter: @jjainschigg).

From Virtualization to Containerization
Learn how to move from monolithic to microservices in this free eBook
Download Now
Radio Cloud Native – Week of May 11th, 2022

Every Wednesday, Nick Chase and Eric Gregory from Mirantis go over the week’s cloud native and industry news. This week they discussed: Docker Extensions Artificial Intelligence shows signs that it's reaching the common person Google Cloud TPU VMs reach general availability Google buys MobileX, folds into Google Cloud NIST changes Palantir is back, and it's got a Blanket Purchase Agreement at the Department of Health and Human …

Radio Cloud Native – Week of May 11th, 2022
Where do Ubuntu 20.04, OpenSearch, Tungsten Fabric, and more all come together? In the latest Mirantis Container Cloud releases!

In the last several weeks we have released two updates to Mirantis Container Cloud - versions 2.16 and 2.17, which bring a number of important changes and enhancements. These are focused on both keeping key components up to date to provide the latest functionality and security fixes, and also delivering new functionalities for our customers to take advantage of in …

Where do Ubuntu 20.04, OpenSearch, Tungsten Fabric, and more all come together? In the latest Mirantis Container Cloud releases!
Monitoring Kubernetes costs using Kubecost and Mirantis Kubernetes Engine [Transcript]

Cloud environments & Kubernetes are becoming more and more expensive to operate and manage. In this demo-rich workshop, Mirantis and Kubecost demonstrate how to deploy Kubecost as a Helm chart on top of Mirantis Kubernetes Engine. Lens users will be able to visualize their Kubernetes spend directly in the Lens desktop application, allowing users to view spend and costs efficiently …

Monitoring Kubernetes costs using Kubecost and Mirantis Kubernetes Engine [Transcript]
The Definitive Guide to Container Platforms
Getting started with Kubernetes part 2: Creating K8s objects with YAML

Thursday, December 30, 2021 at 10:00 AM PST
Manage your cloud-native container environment with Mirantis Container Cloud

Wednesday, January 5 at 10:00 am PST