The default golang image is great! It allows you to quickly build and test your golang projects. But it has a few draw backs, it is a massive 964 MB even the slimmed down alpine based image is 327 MB, not only that but having unused binaries and packages opens you up to security flaws.

LETS GET BUILDING!

Multi-Stage

Using a multi-stage image will allow you to build smaller images by dropping all the packages used to build the binaries and only including the ones required during runtime.

# Create a builder stage
FROM golang:alpine as builder

RUN apk update
RUN apk add --no-cache git ca-certificates \
    && update-ca-certificates

COPY . .

# Fetch dependencies
RUN go mod download
RUN go mod verify

# Build the binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-w -s" \
    -o /go/bin/my-docker-binary

# Create clean image
FROM alpine:latest

# Copy only the static binary
COPY --from=builder /go/bin/my-docker-binary \
    /go/bin/my-docker-binary

# Run the binary
ENTRYPOINT ["/go/bin/my-docker-binary"]

Great now we have an image thats 20 MB thats a 95% reduction! Remember these are production images so we use -ldflags="-w -s" to turn off debug information -w and Go symbols -s.

Scratch Image and Lowest Privilege User

Now to get rid of all those unused packages. Instead of using the alpine image as our final stage we will use the scratch image which has literally nothing!

Will will take this opportunity to also create a non-root user. Add the following snippet to your builder stage

ENV USER=appuser
ENV UID=10001 

RUN adduser \    
    --disabled-password \    
    --gecos "" \    
    --home "/nonexistent" \    
    --shell "/sbin/nologin" \    
    --no-create-home \    
    --uid "$\{UID\}" \    
    "$\{USER\}"

We will need to copy over the ca-certificates to the final stage, this is only required if you are making https calls and we will also need to copy over the passwd and group files to use our appuser. Finally we need get the stage to use our user.

# Copy over the necessary files
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

# Use our user!
USER appuser:appuser

So finally your Dockerfile should look something like this:

# Create a builder stage
FROM golang:alpine as builder

RUN apk update
RUN apk add --no-cache git ca-certificates \
    && update-ca-certificates

ENV USER=appuser
ENV UID=10001 

RUN adduser \    
    --disabled-password \    
    --gecos "" \    
    --home "/nonexistent" \    
    --shell "/sbin/nologin" \    
    --no-create-home \    
    --uid "${UID}" \    
    "${USER}"

COPY . .

# Fetch dependencies
RUN go mod download
RUN go mod verify

# Build the binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-w -s" \
    -o /go/bin/my-docker-binary

# Create clean image
FROM scratch

# Copy only the static binary
COPY --from=builder \
    /go/bin/my-docker-binary \
    /go/bin/my-docker-binary
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

# Use our user!
USER appuser:appuser

# Run the binary
ENTRYPOINT ["/go/bin/my-docker-binary"]