How to build a production Docker Image for Go
One of the many reasons I love Go is how easy it is to build and containerise.
However, it’s also important to ensure your production application has the smallest attack surface possible, is portable, and small in size. Let’s talk through how you might do that.
Let’s see how we might build that.
FROM golang:1.23.2-bookworm as builder
COPY . /workdir
WORKDIR /workdir
ENV CGO_CPPFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-all"
ENV GOFLAGS="-buildmode=pie"
RUN go build -ldflags "-s -w" -trimpath ./cmd/app
FROM gcr.io/distroless/base-debian12:nonroot
COPY --from=builder /workdir/app /bin/app
USER 65534
ENTRYPOINT ["/bin/app"]
Let’s go through it line by line. Here it is as a gist.
-
FROM golang:1.23.2-bookworm as builder
: This uses the official Go image version 1.23 as the base image and names the stage “builder.” Here we are building a multistage docker image. Because GO compiles to a binary, we don’t actually need Go to run it and can throw the build dependencies away. 1.23.2 is the latest at the time of writing, but if you’re unsure you can check here to find the latest release and tags. -
COPY . /workdir
: This copies the current directory into the /workdir directory inside the container. WORKDIR /workdir This sets the working directory inside the container to /workdir. -
ENV CGO_CPPFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-all"
: This sets an environment variable for the C compiler, with specific flags for hardening the code. You can read more about these flags here. -
ENV GOFLAGS="-buildmode=pie"
: This sets an environment variable for the Go build process, instructing it to build a Position Independent Executable (PIE), adding an extra layer of security to the compiled binary. You can read more about that here -
FROM http://gcr.io/distroless/base-debian12:nonroot
: This setups the second image in our multi-stage build. This time, we are using a distroless base image. You can read about the advantage of distroless here. We are using non-root to limit the permissions our container has. -
COPY --from=builder /workdir/app /bin/app
This copies the compiled binary from the builder stage to the /bin directory in the final image. You’ll need to update “app” to be the path to your main.go. -
USER 65534
: This sets the user ID for running subsequent commands and for when the container is run. 65534 is often associated with a user that has minimal privileges, enhancing security. -
ENTRYPOINT ["/bin/app"]
: This specifies the command to run when the container starts. Again, you’ll need to change it depending on how you named your app.
That’s it! Who knew you could do so much with so few lines!