There are many options for building and deploying Go applications using containers and believe me, I have tried most of them on my journey to find the right balance of fast build times, small containers, and security. After trying lots of different options, I believe I have landed on the optimal solution.
My first priority is security. I want to make sure the container only contains the things I need to run my application and runs as a non-priveleged user. Distroless containers provide a great mix of minimal and secure.
But, why do I need these at all? Haven’t you heard of
scratch
containers?
Scratch containers are great for getting up and running but as the application grows you find yourself needing access to operating system functionality, like file systems, time zones, etc. Distroless has some of these core pieces but strips out the rest.
For our Go containers we will be building the binary outside the container for performance. We do this so that we can take advantage of Github Actions caching of dependencies to great reduce build times, then copy the built binary into the container. Our build command looks like this for x86 processes.
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bin/my-app-linux-amd64 main.go
Next, we need to define the Docker container that will accept our binary. We pull in the latest alpine
image and copy over the mime types, time zone data, and certificate authorities to the distroless container. Note: The distroless container has many of these already baked in but they can be out of date which throws flags on security scanners. Finally, we change the user to an unprivileged user provided by the Distroless container.
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM alpine:latest AS gobuilder
RUN apk update --no-cache && apk upgrade --no-cache \
&& apk add --no-cache mailcap git tzdata ca-certificates
FROM --platform=$TARGETPLATFORM gcr.io/distroless/static-debian11 AS result
ARG TARGETOS TARGETARCH
# copy mime types from the base image so they are up to date
COPY --from=gobuilder /etc/mime.types /etc/
# copy the latest time zone data from the base to the resulting image
COPY --from=gobuilder /usr/share/zoneinfo /usr/share/zoneinfo
# copy the latest ca certificates to the image
COPY --from=gobuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# copy the built application to the resulting container
COPY ./bin/my-app-${TARGETOS}-${TARGETARCH} /my-app
# switch to an unprivileged user
USER nonroot:nonroot
# the application running in the container should always expose port 8080
EXPOSE 8080
# set the default entry point to as the application binary
ENTRYPOINT [ "/my-app" ]
# set the deafult command to run the api server
CMD [ "run" ]
That’s it! Now we can build the application, copy it into a docker container, and deploy it to our cluster. Hopefully this helps you and your team build minimal and secure containers.