Dockerizing a Go service with a multi-stage Dockerfile
The Docker images you build in a microservice system are very important. You will build many of them, and each one many, many times. These images will also be shipped back and forth over the wire, and they present a target for attackers. With this in mind, it makes sense to build images that have the following properties:
- Lightweight
- Present minimal attack surface
This can be done by using a proper base image. For example, Alpine is very popular due to its small footprint. However, nothing beats the scratch base image. With Go-based microservices, you can literally create an image that just contains your service binary. Let's continue peeling the onion and look into the Dockerfile of one of the services. Spoiler alert: they are all virtually identical, and just differ in terms of their service names.
We are using the multi-stage Dockerfile here. We will build the image using the standard Golang image. The arcane magic in the last line is what it takes to build a truly static and self-contained Golang binary that doesn't require a dynamic runtime library:
FROM golang:1.11 AS builder
ADD ./main.go main.go
ADD ./service service
# Fetch dependencies
RUN go get -d -v
# Build image as a truly static Go binary
RUN CGO_ENABLED=0 GOOS=linux go build -o /link_service -a -tags netgo -ldflags '-s -w' .
We then copy the final binary into a scratch base image and create the smallest and most secure image possible. We exposed the 7070 port, which is the port the service listens on:
FROM scratch
MAINTAINER Gigi Sayfan <the.gigi@gmail.com>
COPY --from=builder /link_service /app/link_service
EXPOSE 7070
ENTRYPOINT ["/app/link_service"]