Recently I’ve been writing a service in Go to enhance the projects dashboard on Bitbucket – if you haven’t heard we launched Atlassian Connect for Bitbucket as a way for anyone to build add-ons for three millions of Bitbucket users out there. Like many other Gophers I’ve been happily deploying my Go services using Docker. The process is smooth and pleasurable if not for one thing: the size of the official default Go image.

After all is said and done my application – which by itself would comprise of around ~6MB
of static binary in size, becomes a whopping 642MB
when using the default Go Docker image. Our internal Docker registry handles that with no problems but it seems such a waste of space.
Recently I found this clear article detailing a Go, Docker workflow with clear instructions and snippets showing how to statically compile an application and shrink it to 1%
of the size. The technique is elegant and simple enough but because my development system is OSX that approach needs to be modified with an extra layer of complexity. I need to manage cross-compilation of my Go project across OS architectures (OSX, Linux). I did some research and attempts at using gonative, but ended up going the Docker route to solve everything.
While working through the problem I remembered an older article from Xebia that did something smart: perform the build and link step inside Docker containers and store the (now compatible) binary in a scratch
image. The scratch
image is the smallest possible Docker image and it’s generally used to build base images or to contain single binaries.
So that’s what I set out to replicate with the new insight from the former article. I ended up with a streamlined process which automates everything smoothly:
- Write a multi-purpose
Makefile
to both setup the build environment inside Docker and statically compile the Go application (read more about it below). - Create a
Dockerfile
to build the statically linked Go binary (called “Dockerfile.build
“). - Run it and extract the Linux binary using “
docker cp
“. - Create a bare bones
Dockerfile
that adds the binary to a “scratch
” Docker image, plus the needed static web application files (“Dockerfile.static
“). - Profit! Run application using Docker.
Here a breakdown of the steps in detail.
Write a multi-purpose Makefile
The Makefile
will be capable of doing several things:
- Collect the dependencies needed by our Go application.
- Assemble the right Docker container to build our statically linked Go binary.
- Build our Go program.
- Inject the binary and the application static assets into a minimal Docker image.
The interesting bit here is that the same Makefile
will be used both to create the build container and as configuration inside the container for the compilation command (if you want you’re free to split the two logical uses in separate Makefiles but I found it delightfully efficient to keep only one).
Here’s how the Makefile
looks like:
default: builddocker
setup:
go get golang.org/x/oauth2
go get golang.org/x/oauth2/jwt
go get google.golang.org/api/analytics/v3
buildgo:
CGO_ENABLED=0 GOOS=linux go build -ldflags "-s" -a -installsuffix cgo -o main ./go/src/bitbucket.org/durdn/project-name
builddocker:
docker build -t durdn/build-project-name -f ./Dockerfile.build .
docker run -t durdn/build-project-name /bin/true
docker cp `docker ps -q -n=1`:/main .
chmod 755 ./main
docker build --rm=true --tag=durdn/project-name -f Dockerfile.static .
run: builddocker
docker run
-p 8080:8080 durdn/project-name
The golang
Docker image expects the Go code to be stored in “./go/src/...
” The build flags specify you want a static binary. The builddocker
step does the following:
- Build a container (tagged
durdn/build-project-name
) with the Go tool chain and the dependencies included. - The build step will compile the Go application statically.
- Generate a container from the resulting image:
docker run durdn/build-project-name /bin/true
. - Extract Linux static binary generated:
docker cp $(docker ps -q -n=1):/main .
- Make it executable:
chmod 755 ./main
. - Copy the binary and the static assets into a minimal image.
Run the Makefile
with the simple:
make builddocker
Build the static Linux binary in a container
The Makefile
uses two separate Dockerfiles as already mentioned. Let’s have a look at the Dockerfile.build
:
FROM golang
ADD Makefile /
WORKDIR /
RUN make setup
ADD ./collector /go/src/bitbucket.org/durdn/project-name/collector
ADD ./dashboard /go/src/bitbucket.org/durdn/project-name/dashboard
RUN make buildgo
CMD ["/bin/bash"]
This simple Dockefile
allows us to build the static Go binary calling make
. If you want to kick off the build manually you can simply type:
docker build -t durdn/app-name -f ./Dockerfile.build .
This will generate the cross-compiled binary executable as ./main
inside the container.
Create tiny Go Docker image
The last step is to create a minimal Docker container and put our binary into it. For this we we can use the very tiny tianon/true
or the scratch
image mentioned before. This is the magical step that allows to shrink the application image hundredfold.
The Dockerfile.static
for this step is pretty straight forward:
# Create a minimal container to run a Golang static binary
FROM tianon/true
MAINTAINER Nicola Paolucci "npaolucci@atlassian.com"
EXPOSE 8080
COPY certs/certs /etc/ssl/certs/ca-certificates.crt
COPY dashboard/config.json /config.json
COPY dashboard/properties.json /properties.json
ADD dashboard/dashboards /dashboards
ADD dashboard/public /public
ADD dashboard/widgets /widgets
ADD main /
ENV PORT=8080
CMD ["/main"]
Run it like this:
docker build --rm --tag=durdn/project-name -f Dockerfile.static .
As explained in the Docker workflow mentioned before, the certificates are needed if we want the application to run smoothly in a cross architecture setting.
The ADD
and COPY
lines here are for adding the configuration files and the web application folders that contain standard CSS, HTML and JavaScript files.
After the build command the application can be started as you would expect with:
docker run -p 8080:8080 da-dashboard --config=config.json
Conclusions
The end result is beautiful, a Docker image weighting 8.6MB
including all the static assets. I know it’s a small thing but it makes me feel so accomplished.
Find an example of the setup in this small Git repository.
Liked this piece and want more Go content? Check out my recent article on Learning Go with flash cards.
In any case if you found this interesting at all and want more why not follow me at @durdn or my awesome team at @atlassiandev?