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.
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.
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