This post is a companion piece to a talk I gave at GopherCon 2023.

You don’t need a Linux distro in your Docker image to run a statically linked Go binary. Docker has a special base image called scratch that is empty. It allows your binary to run directly against the kernel while remaining isolated from the host system.

While we still need a distro to build the binary, we can use scratch as the base image for the final stage. Docker only includes the files that are in the final stage in the image. This means that we can make our image much smaller, allowing them to be shipped and downloaded faster. It also reduces the attack surface of the image, because there is no shell available to run commands and exploits.

Let’s take a standard Dockerfile for a Go web app and see how we can use scratch to make it smaller, easier to ship, and more secure.

FROM goland:1.21-alpine

COPY ./webapp/* .

RUN go build -o /webapp

EXPOSE 8080
CMD ["/webapp"]

The image size of the generated Docker image is 298 MB, even for a simple 16 line Go program.

We can split our Dockerfile into two stages: one for building the binary and one for running it.

# STAGE 1: Build
FROM goland:1.21-alpine AS build

COPY ./webapp/* .

RUN go build -o /webapp

# STAGE 2: Run
FROM scratch

COPY --from=build /webapp /webapp

EXPOSE 8080
CMD ["/webapp"]

This Docker file uses the Alpine distro in the first stage to build the binary. Then the binary is copied into the second stage which uses scratch as the base image. The resulting image is 6.72 MB. That is a 98% reduction in size.