Create the smallest and secured golang docker image based on scratch

Cyrille H
7 min readMar 1, 2018

--

When we are building a docker Image, the first idea is using the default official image.

Dockerfile begin with :

FROM golang
FROM nginx
FROM openjdk

There is an official Docker image for Go.

$ docker image list
golang latest ed081345a3da 4days ago 803MB

Ouch 803MB just for an empty image … this is crazy 😾

There is lightweight Alpine Docker image for Go.

Check the Alpine linux page for more informations.

FROM golang:alpine

This is smaller but too large for a production image with nothing

$ docker image list
golang alpine 57ce7b9daa9b 7 weeks ago 359MB

Use Multi-stage builds

Thanks to docker multi-stage builds, we can build our application in a docker alpine image an produce a small image with only a binary in a scratch image.

OK, it’s time to build a smaller image with multi-stage build

Before that we gonna see docker scratch image, a Zero Bytes image. Perfect for embedding our go static binary.

############################
# STEP 1 build executable binary
############################
FROM golang:alpine AS builder
# Install git.
# Git is required for fetching the dependencies.
RUN apk update && apk add --no-cache git
WORKDIR $GOPATH/src/mypackage/myapp/
COPY . .
# Fetch dependencies.# Using go get.
RUN go get -d -v
# Build the binary.
RUN go build -o /go/bin/hello
############################
# STEP 2 build a small image
############################
FROM scratch
# Copy our static executable.
COPY --from=builder /go/bin/hello /go/bin/hello
# Run the hello binary.
ENTRYPOINT ["/go/bin/hello"]

Oh cool only 21.2MB with everything i need for my go app.

$ docker image list
hello latest bbab7aea1234 3 hours ago 21.2MB

But we can optimize it, by removing debug informations and compile only for linux target and disabling cross compilation.

With go < 1.10

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o /go/bin/hello

With go ≥1.10

RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/hello

Now we have a small image only 13.6MB, almost ready for production

$ docker image list
hello latest bbab7aea1234 2hours ago 13.6MB

Let’s build a more secure docker image

Just some reminders :

OK, let’s do that with scratch image.

We have to create a new user on the builder image and copy the /etc/passwd and /etc/group file from the builder to te scratch image. Finally we can use unprivileged user “appuser” to launch the binary.

############################
# STEP 1 build executable binary
############################
FROM golang:alpine AS builder
# Install git.
# Git is required for fetching the dependencies.
RUN apk update && apk add --no-cache git
# Create appuser.
ENV USER=appuser
ENV UID=10001
# See https://stackoverflow.com/a/55757473/12429735RUN
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR $GOPATH/src/mypackage/myapp/
COPY . .
# Fetch dependencies.# Using go get.
RUN go get -d -v
# Using go mod.
# RUN go mod download
# RUN go mod verify
# Build the binary.
RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/hello
############################
# STEP 2 build a small image
############################
FROM scratch
# Import the user and group files from the builder.
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Copy our static executable.
COPY --from=builder /go/bin/hello /go/bin/hello
# Use an unprivileged user.
USER appuser:appuser
# Run the hello binary.
ENTRYPOINT ["/go/bin/hello"]

You can find a working example on github

Always pull image by digest

Using a trusted docker image like golang:alpine is not always enough for security. People can intercept your request to provide a modified docker image. The best solution : using digest (thks Patrik Iselind for this advice)

############################
# STEP 1 build executable binary
############################
#FROM golang:alpine AS builder

# golang alpine 1.13.5
FROM golang@sha256:0991060a1447cf648bab7f6bb60335d1243930e38420bee8fec3db1267b84cfa as builder

Always pull trusted image

Using a trusted docker image like golang:alpine is necessary.

############################
# ALWAYS PULL TRUSTED CONTENT
############################
export DOCKER_CONTENT_TRUST=1 && docker pull golang@sha256:0991060a1447cf648bab7f6bb60335d1243930e38420bee8fec3db1267b84cfa

Always export with port > 1024 as possible

OK, now we have a more secure image. But if we expose our docker with a port < 1024, we need some privileges for that.

OK so let’s expose always our binary with a port > 1024

# Import the user and group files from the builder.
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Copy our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
# Use an unprivileged user.
USER appuser:appuser
# Port on which the service will be exposed.
EXPOSE 9292
# Run the hello binary.
ENTRYPOINT ["/go/bin/hello"]

Add SSL ca certificates

Perfect, but to be secure we need to expose our services with SSL isn’t it ?

By default scratch image is not provided with SSL CA certificates. But with multi-step we can provide it.

############################
# STEP 1 build executable binary
############################
FROM golang@sha256:0991060a1447cf648bab7f6bb60335d1243930e38420bee8fec3db1267b84cfa as builder
# Install git + SSL ca certificates.
# Git is required for fetching the dependencies.
# Ca-certificates is required to call HTTPS endpoints.
RUN apk update && apk add --no-cache git ca-certificates && update-ca-certificates
# Create appuser
ENV USER=appuser
ENV UID=10001
# See https://stackoverflow.com/a/55757473/12429735RUN
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR $GOPATH/src/mypackage/myapp/
COPY . .
# Fetch dependencies.# Using go get.
RUN go get -d -v
# Using go mod.
# RUN go mod download
# RUN go mod verify
# Build the binary
RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/hello
############################
# STEP 2 build a small image
############################
FROM scratch
# Import from builder.
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Copy our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
# Use an unprivileged user.
USER appuser:appuser
# Run the hello binary.
ENTRYPOINT ["/go/bin/hello"]

You can find a working example on github

Add zoneinfo for timezones

By default scratch image is not provided with zoneinfo for timezones. But with multi-step we can provide it.

############################
# STEP 1 build executable binary
############################
FROM golang@sha256:0991060a1447cf648bab7f6bb60335d1243930e38420bee8fec3db1267b84cfa as builder
# Install git + SSL ca certificates.
# Git is required for fetching the dependencies.
# Ca-certificates is required to call HTTPS endpoints.
RUN apk update && apk add --no-cache git ca-certificates tzdata && update-ca-certificates
# Create appuser
ENV USER=appuser
ENV UID=10001
# See https://stackoverflow.com/a/55757473/12429735RUN
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR $GOPATH/src/mypackage/myapp/
COPY . .
# Fetch dependencies.# Using go get.
RUN go get -d -v
# Using go mod.
# RUN go mod download
# RUN go mod verify
# Build the binary
RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/hello
############################
# STEP 2 build a small image
############################
FROM scratch
# Import from builder.
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Copy our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
# Use an unprivileged user.
USER appuser:appuser
# Run the hello binary.
ENTRYPOINT ["/go/bin/hello"]

You can find a working example on github

Go 1.11 Fetch dependencies with go mod

Go 1.11 includes preliminary support for versioned modules. If you want to know more about go mod you can see the doc.

############################
# STEP 1 build executable binary
############################
FROM golang@sha256:0991060a1447cf648bab7f6bb60335d1243930e38420bee8fec3db1267b84cfa as builder
# Install git + SSL ca certificates.
# Git is required for fetching the dependencies.
# Ca-certificates is required to call HTTPS endpoints.
RUN apk update && apk add --no-cache git ca-certificates && update-ca-certificates
# Create appuser
ENV USER=appuser
ENV UID=10001
# See https://stackoverflow.com/a/55757473/12429735RUN
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR $GOPATH/src/mypackage/myapp/
COPY . .
# Fetch dependencies.# Using go mod with go 1.11
RUN go mod download
RUN go mod verify
# Build the binary
RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/hello
############################
# STEP 2 build a small image
############################
FROM scratch
# Import from builder.
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
# Copy our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
# Use an unprivileged user.
USER appuser:appuser
# Run the hello binary.
ENTRYPOINT ["/go/bin/hello"]

GoogleContainer distroless

This is a very interesting base container named “distroless” from Google, there is a version for statically compiled applications like Go that do not require libc and contains :

  • ca-certificates
  • A /etc/passwd entry for a root user
  • A /tmp directory
  • tzdata

If you need to debug, the :debug image provides a shell “busybox”.

If you have any advice about security, please let me know :)

You can find a working example on github

--

--