Depending on your target architecture, you may wish to build your Go binaries with Docker to maintain a reproducible build, limit your build size, and minimize the attack vectors for your service. Using multistage Docker builds can help us to accomplish this task.
Consider the following simple package that logs a debug message to the screen:
package main
import "go.uber.org/zap"
func main() {
zapLogger: = zap.NewExample()
defer zapLogger.Sync()
zapLogger.Debug("Hi Gophers - from our Zap Logger")
}
If we want to build this and execute it within a Docker container while minimizing dependencies, we can use a multistage Docker build. To do so, we can perform the following steps:
- Initialize the current directory as the root of a module by executing the following:
go mod init github.com/bobstrecansky/HighPerformanceWithGo/11-deploying-go-code/multiStageDockerBuild
- Add the vendor repositories by executing the following:
go mod vendor
We now have all of the required vendor packages (in our case, the Zap logger) available in our repository. This can be seen in the following screenshot:
- Build our zapLoggerExample Docker container. We can build our container using the following Dockerfile:
# Builder - stage 1 of 2 FROM golang:alpine as builder COPY . /src WORKDIR /src RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -o zapLoggerExample # Executor - stage 2 of 2 FROM alpine:latest WORKDIR /src/ COPY --from=builder /src/zapLoggerExample . CMD ["./zapLoggerExample"]
In this Dockerfile example, we are using a multistage Docker build to build and execute our binary. In stage 1 of 2 (the builder stage), we use a golang alpine image as a base. We copy all of our files from our current directory into the /src/ directory on the Docker container, we make /src/ our working directory, and we build our Go binary. Disabling cgo, building for our Linux architecture, and adding the vendor directory we created in step 1 can all help minimize build size and time.
In stage 2 of 2 (the executor stage), we use a basic alpine Docker image, make /src/ our working directory, and copy the binary we built in the first stage to this Docker container. We then execute our logger as the final command within this Docker build.
- After we have our necessary dependencies together, we can build our Docker container. We do this by performing the following command:
docker build -t zaploggerexample .
- Once we have our Docker container built, we can execute it by performing the following command:
docker run -it --rm zaploggerexample
In the following screenshot, you can see our build and execution steps being completed:
Building our Go programs in multistage Docker containers can be helpful in creating reproducible builds, limiting binary size, and minimizing the attack vectors for our services by using only the bits and pieces we need.