Create the smallest and secured golang docker image based on scratch

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 1c1309ff8e0d 10 days ago 779MB

Ouch 779 MB 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 bbab7aea1231 7 weeks ago 269MB

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 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.
RUN adduser -D -g '' appuser
WORKDIR $GOPATH/src/mypackage/myapp/
COPY . .
# Fetch dependencies.
# Using go get.
RUN go get -d -v
# Using go mod.
# RUN go mod download
# Build the binary.
RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/backoffice
############################
# 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 our static executable.
COPY --from=builder /go/bin/hello /go/bin/hello
# Use an unprivileged user.
USER 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.11.5
FROM golang@sha256:8dea7186cf96e6072c23bcbac842d140fe0186758bcc215acb1745f584984857 as builder

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 our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
# Use an unprivileged user.
USER 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:8dea7186cf96e6072c23bcbac842d140fe0186758bcc215acb1745f584984857 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
RUN adduser -D -g '' appuser
WORKDIR $GOPATH/src/mypackage/myapp/
COPY . .
# Fetch dependencies.
# Using go get.
RUN go get -d -v
# Using go mod.
# RUN go mod download
# 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 our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
# Use an unprivileged user.
USER 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:8dea7186cf96e6072c23bcbac842d140fe0186758bcc215acb1745f584984857 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
RUN adduser -D -g '' appuser
WORKDIR $GOPATH/src/mypackage/myapp/
COPY . .
# Fetch dependencies.
# Using go get.
RUN go get -d -v
# Using go mod.
# RUN go mod download
# 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 our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
# Use an unprivileged user.
USER 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:8dea7186cf96e6072c23bcbac842d140fe0186758bcc215acb1745f584984857 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
RUN adduser -D -g '' appuser
WORKDIR $GOPATH/src/mypackage/myapp/
COPY . .
# Fetch dependencies.
# Using go mod with go 1.11
RUN go mod download
# 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 our static executable
COPY --from=builder /go/bin/hello /go/bin/hello
# Use an unprivileged user.
USER appuser
# Run the hello binary.
ENTRYPOINT ["/go/bin/hello"]
If you have any advice about security, please let me know :)

You can find a working example on github