Better Dockerfiles

April 9th, 2021 on ols.wtf

Earlier on this week, my team ran a hack day around standardising our golden path for deploying applications to our internal Kubernetes clusters. As part of this we were going to create a boiler plate repo that a development squad could fork and replace with their own application. We needed a mock application that could sit in place, and so naturally we wrote one with Golang.

Because this was supposed to be a hacky Hello, World! application meant primarily to demonstrate the surrounding infrastructure, the Dockerfile for running this application within a container was relatively simple:

  • Use golang:latest
  • Build the app with go build
  • EXPOSE the port
  • Run the app using ENTRYPOINT

This is all well and good, but then when we look at the size of this image, things get a little concerning

redacted/ols/sup    0.1 6afb0d242fd3    2 days ago  894MB

Nearly 900MB for app that literally does this

% PORT=1337 MESSAGE="Hello, this is a nice message" go run main.go &
% curl localhost:1337
Hello, this is a nice message% 

As well as this obvious issue with size, there are also a number of problems with this current Dockerfile. Namely that the container runs as root and we’re using latest as our base image. If we want this to be a golden example of what to do, then how can we have so many not-to-do things in our Dockerfile? Let’s go about fixing that.

Add a maintainer

Starting simple, add a maintainer so that people know who to contact if there are issues with the configuration

LABEL maintainer="Oliver Leaver-Smith <my@work.email>"

Don’t use latest

We discourage using the latest tag of an image, purely because you can’t be sure what is going to change between builds. While the temptation to use just a specific tag is great (such as golang:1.16.3), what’s better is to use the digest to ensure that no funny business can happen with people masquerading as that tag from a nefarious container registry.

# FROM golang:1.16.3 AS builder
FROM golang@sha256:3fc96f3fc8a5566a07ac45759bad6381397f2f629bd9260ab0994ef0dc3b68ca AS builder

Create an unprivileged user

We don’t (or shouldn’t) run our applications as root on virtual machines or bare metal, and so there is no reason we should run our applications as root within containers.

ENV USER=app
RUN adduser \
    --disabled-password \
    --gecos "" \
    --home "/app" \
    --shell "/sbin/nologin" \
    --uid "10001" \
    "${USER}"

Use a scratch container

As part of the build process, we get a static binary. As such, we don’t need the whole shebang we get with the golang image, and so we should use as small an image as possible. Using scratch is an option, but I have taken to using gcr.io/distroless/static from Google, as it contains just enough to run static binaries such as Go applications. We need to copy over the binary, as well as the user config we created earlier.

FROM gcr.io/distroless/static

COPY --from=builder /app/main /app/main
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group

USER app:app

Now when we look at the size of our container we can rest assured that it’s not going to weigh down our Kubernetes cluster with all the additional kruft that isn’t required. And we are safe in the knowledge that it’s at least a little bit more safe and secure than it was before.

redacted/ols/sup    0.2 eeefd04061c5    2 days ago  14.2MB

More importantly, we’re setting a good example to those that come after us looking for an easily reproducable way to deploy to Kubernetes.


Do you have a comment to make on this content? Start a discussion in my public inbox by emailing ~ols/public-inbox@lists.sr.ht. You can see the inbox here.