How to create the smallest possible Docker image for your Golang application.

This blog post will guide you to create the ultimate smallest possible Docker image for your Golang application using the Build flow tool Habitus!

In the container ecosystem, there's a lot of chatter about security and best practices to build the ultimate container image. The main goal is to create an image which is slim, secure, speedy, stable and set.

alt

I didn't have time to create a slim image, so I created a fat one instead.

Shortcuts are evil and we need to aim for slim images instead of fat ones which cause problems (security, performance, maintaining) in the long run.

Let get started!

Scratch for the win!

The first step is to understand how to create a Docker image with no base like ubuntu or alpine for example. We want the bare minimal. The goal is to isolate our process with NO dependencies or stuff we don't need. The scratch base image is your answer.

You can use Docker’s reserved, minimal image, scratch, as a starting point for building containers. Using the scratch image signals to the build process that you want the next command in the Dockerfile to be the first filesystem layer in your image.

Each Docker image references a list of read-only layers that represent filesystem differences. Layers are stacked on top of each other to form a base for a container’s root filesystem.

Behold!

The smallest possible Docker image for an executable. The executable should be a static build.

A static build is a compiled version of a program which has been statically linked against libraries. In computer science, linking means taking one or more objects generated by compilers and assembling them into a single executable program.

Steps to take:

  • Download the smallest hello world app in the world.
  • Create a Dockerfile
# start from scratch
FROM scratch

# copy our static linked executable
COPY helloworld helloworld

# tell how to run this container 
CMD ["./helloworld"]  
  • Build the image $ docker build . -t helloworld:smallest
Sending build context to Docker daemon 3.072 kB  
Step 1/3 : FROM scratch  
 --->
Step 2/3 : COPY myapp myapp  
 ---> Using cache
 ---> c3e978eab3c8
Step 3/3 : CMD ./myapp  
 ---> Using cache
 ---> cc6eb6cc3479
Successfully built cc6eb6cc3479  
  • Run the container $ docker run helloworld:smallest
Hi World  
  • Check the size of each layer $ docker history helloworld:smallest
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT  
cc6eb6cc3479        9 minutes ago       /bin/sh -c #(nop)  CMD ["./myapp"]              0 B  
c3e978eab3c8        11 minutes ago      /bin/sh -c #(nop) COPY file:863b4441410c89...   142 B  

A docker image of 142 Bytes. Eat that!

The two-stage rocket build.

The 1 million dollar question is? How to build our Golang application, get the executable and put it inside a container in one command?

Unfortunately, you can't do this in an automated fashion using the standard Docker tooling. Luckily we create a project called Habitus to automate this process.

alt

Basically, we need two stages. Build the artefact using $ go build and copy the executable into our final image. Let create both stages using Dockerfiles and glue them together with the Habitus rocket!

Let start with a simple HTTP service written in Golang:

main.go

package main

import (  
"fmt"
"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {  
    fmt.Fprintf(w, "hello world!")
}

func main() {  
    http.HandleFunc("/", handler)
    fmt.Println("Simple Helloworld server is running on port 8080")
    http.ListenAndServe(":8080", nil)
}

We need to build the executable first before we can run it as an isolated process using containers. Enter stage #1!

Stage #1

The responsibility of this stage is to build an image which can build your Golang executable and extract the artefact.

Dockerfile.builder

# start a golang base image, version 1.8
FROM golang:1.8

#switch to our app directory
RUN mkdir -p /go/src/helloworld  
WORKDIR /go/src/helloworld

#copy the source files
COPY main.go /go/src/helloworld

#disable crosscompiling 
ENV CGO_ENABLED=0

#compile linux only
ENV GOOS=linux

#build the binary with debug information removed
RUN go build  -ldflags '-w -s' -a -installsuffix cgo -o helloworld  

To build it manually run this command to build it.
$ docker build -f Dockerfile.builde -t builder:latest .

Copy the compiled artifact to your local disk
$ docker container cp [id_of_container]:/go/src/helloworld/helloworld helloworld

Stage #2

The responsibility of this stage is to copy the artefact into the smallest possible image.

Dockerfile.production

# start with a scratch (no layers)
FROM scratch

# copy our static linked library
COPY helloworld helloworld

# tell we are exposing our service on port 8080
EXPOSE 8080

# run it!
CMD ["./helloworld"]  

To build it manually run this command to build it.
$ docker build -f Dockerfile.production -t helloworld:latest .

Building your rocket to create the smallest possible Docker image

With Habitus you need a build.yml to tell which steps are necessary for the Docker build flow. Habitus give you the power to handle complex build flow without getting into bash hell mess.

build.yml

build:  
  version: 2016-03-14 # version of the build schema.
  steps:
    builder:
      name: builder
      dockerfile: Dockerfile.builder
      artifacts:
        - /go/src/helloworld/helloworld
    production:
      name: helloworld:latest
      dockerfile: Dockerfile.production
      depends_on:
        - builder

Build everything in one command! Easy as it gets.

$ habitus

017/04/05 10:14:01 ▶ Using '/Users/danielvangils/Desktop/Cloud 66/projects/go_projects/src/github.com/cloud66/helloworld/build.yml' as build file  
2017/04/05 10:14:01 ▶ Collecting artifact information  
2017/04/05 10:14:01 ▶ Building 2 steps  
2017/04/05 10:14:01 ▶ Step 0 - builder: builder  
2017/04/05 10:14:01 ▶ Step 1 - production: helloworld:latest  
2017/04/05 10:14:01 ▶ Parallel build for builder  
2017/04/05 10:14:01 ▶ Building builder  
2017/04/05 10:14:01 ▶ Parsing and converting 'Dockerfile.builder'  
2017/04/05 10:14:01 ▶ Writing the new Dockerfile into Dockerfile.builder.generated  
2017/04/05 10:14:01 ▶ Building the builder image from Dockerfile.builder.generated  
Step 1/7 : FROM golang:1.8  
 ---> 9ad50708c1cb
...
Step 7/7 : RUN go build  -ldflags '-w -s' -a -installsuffix cgo -o helloworld  
 ---> Running in 93778ba1c98c
 ---> b9736e1bf07c
Removing intermediate container 93778ba1c98c  
Successfully built b9736e1bf07c  
2017/04/05 10:14:12 ▶ Building container based on the image  
2017/04/05 10:14:12 ▶ Starting container 3278c37f63092f70e3ef157268d3bc770f0333b550c9a10ec0527940dc677833 to fetch artifact permissions  
2017/04/05 10:14:13 ▶ Permissions for /go/src/helloworld/helloworld is 755  
2017/04/05 10:14:13 ▶ Stopping the container 3278c37f63092f70e3ef157268d3bc770f0333b550c9a10ec0527940dc677833  
2017/04/05 10:14:14 ▶ Copying artifacts from 3278c37f63092f70e3ef157268d3bc770f0333b550c9a10ec0527940dc677833  
2017/04/05 10:14:14 ▶ Copying from /go/src/helloworld/helloworld to /Users/danielvangils/Desktop/Cloud 66/projects/go_projects/src/github.com/cloud66/helloworld/helloworld  
2017/04/05 10:14:14 ▶ Setting file permissions for /Users/danielvangils/Desktop/Cloud 66/projects/go_projects/src/github.com/cloud66/helloworld/helloworld to 755  
2017/04/05 10:14:14 ▶ Removing built container 3278c37f63092f70e3ef157268d3bc770f0333b550c9a10ec0527940dc677833  
2017/04/05 10:14:14 ▶ Parallel build for helloworld:latest  
2017/04/05 10:14:14 ▶ Building helloworld:latest  
2017/04/05 10:14:14 ▶ Parsing and converting 'Dockerfile.production'  
2017/04/05 10:14:14 ▶ Writing the new Dockerfile into Dockerfile.production.generated  
2017/04/05 10:14:14 ▶ Building the helloworld:latest image from Dockerfile.production.generated  
Step 1/4 : FROM scratch  
 ---> 
...
Step 4/4 : CMD ./helloworld  
 ---> Using cache
 ---> 31dd0a6f2cce
Successfully built 31dd0a6f2cce  

$ docker images | grep helloworld

helloworld  latest  31dd0a6f2cce  24 minutes ago  3.9 MB  

Now we got the smallest possible image with only one layer and it only contains our executable. You can even compress more to use upx to compress the executable even more, up to 40%.

To show all the layers, run the $ docker history command.

$ docker history helloworld

IMAGE             COMMENT                                          SIZE  
31dd0a6f2cce      /bin/sh -c #(nop)  CMD ["./helloworld"]          0 B  
bc5aa13f774e      /bin/sh -c #(nop)  EXPOSE 8080/tcp               0 B  
33a1c4891bb1      /bin/sh -c #(nop)  COPY file:bc24b3193d1b79...   3.9 MB  

Summary

Creating the smallest possible Docker image for your Golang application is easy with Habitus. Integrate Habitus with your CI/CD pipeline gives you control to create isolated processes with the minimal attack surface.

Good to know we support Habitus in our Buildgrid solution.

Happy welding your containers

Daniël van Gils

Daniël van Gils is a developer advocate at Cloud 66. He helps other developers craft web apps and container based architectures with ♥, to deploy on any server or public cloud.

Amsterdam & London
Subscribe and get updates

Have feedback? Please get in touch @cloud66 on Twitter.

Everything you need to build, manage and maintain containers in production on your own servers and any cloud

Try Cloud 66 — 14 Days Free Trial, No credit card required