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"]