go-docker-multi-stage-build
go-docker-multi-stage-build copied to clipboard
Complete guide to multi-stage Docker build for Go to build minimal Go image, with labels metadata included
Multi-Stage Docker build for Go
TL;DR
Dockerize your golang app easily with the new multi-stage builds from Docker 17.05. Reduce deployment steps and produce smaller, optimized builds.
Audience:
- You want to know how to Dockerize your
Golangapp - You want your Docker image to be as small as possible
- You want to know how multi-stage docker build works and the pros
References:
- The example can be found in the Github repo here
Highlights
- You will first build a docker image using only the
Dockergolang base image, and observe the outcome. For simplicity, our program will just output "hello, go" - Then, you will learn how to build a more optimized docker image, but requires separate commands
- Finally, we will demonstrate how multi-stage build can simplify our process
Why label-schema.org?
It's important to tag your images with sufficient information because:
- you want to know where is the source code for the docker build
- you want to know which branch/git commit it is using
- you want to know when was the last build
- you want to know how to rebuild it
- you want to know who last build it
- you want to know what this docker image do, how to run it and what is the environment variables
Guide
Setup
You need to have golang and a minimum version of docker 17.05 installed in order to run this demo. You can check the version of your dependencies as shown below:
Validating go version:
$ go version
go version go1.9 darwin/amd64
Validating Docker version:
$ docker version
Client:
Version: 17.06.1-ce
API version: 1.30
Go version: go1.8.3
Git commit: 874a737
Built: Thu Aug 17 22:53:38 2017
OS/Arch: darwin/amd64
Server:
Version: 17.06.1-ce
API version: 1.30 (minimum version 1.12)
Go version: go1.8.3
Git commit: 874a737
Built: Thu Aug 17 22:54:55 2017
OS/Arch: linux/amd64
Experimental: true
The golang program
The main.go contains our application logic. It does nothing but print Hello, go!.
package main
import "log"
func main() {
log.Println("Hello, go!")
}
Now that we have our application, let's dockerize it!
Method 1: Using the Golang image
The steps in Dockerfile.00 is as follow:
- We select the
golang:1.9image - We create a workdir called hello-world
- We copy the file into the following directory
- We get all the dependencies required by our application
- We compile our application to produce a static binary called
app - We run our binary
FROM golang:1.9
WORKDIR /go/src/github.com/alextanhongpin/hello-world
COPY main.go .
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
CMD ["/go/src/github.com/alextanhongpin/hello-world/app"]
Let's build an image called alextanhongpin/hello-world-00 out of it. You can use your Github username instead when building the image.
$ docker build -t alextanhongpin/hello-world-00 -f Dockerfile.00 .
Sending build context to Docker daemon 2.016MB
Step 1/6 : FROM golang:1.9
---> 5e2f23f821ca
Step 2/6 : WORKDIR /go/src/github.com/alextanhongpin/hello-world
---> Using cache
---> d36bf8436458
Step 3/6 : COPY main.go .
---> Using cache
---> 2fa05dc652bc
Step 4/6 : RUN go get -d -v
---> Using cache
---> bb0f73ac82d1
Step 5/6 : RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
---> Using cache
---> 8b32d3f4cfd0
Step 6/6 : CMD /go/src/github.com/alextanhongpin/hello-world/app
---> Running in 440d47e71346
---> 2669fc5303bf
Removing intermediate container 440d47e71346
Successfully built 2669fc5303bf
Successfully tagged alextanhongpin/hello-world-00:latest
We will run our docker image to validate that it is working:
$ docker run alextanhongpin/hello-world-00
Hello, go!
Let's take a look at the image size that is produced:
docker image list | grep hello-world
alextanhongpin/hello-world-00 latest 2669fc5303bf 42 seconds ago 729MB
We have a 729MB image for a simple Hello, go!! What can we do to minimize it? That brings us to next step...
Method 2: Build locally
The reduce the size, we can try to compile our main.go locally and copy the executable to an alpine image - the size should be smaller since it contains only our executable, but without the go runtime. Let's compile our main.go:
$ CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
Dockerfile.01 contains the step to build our second image:
FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY app .
CMD ["/app"]
All it does is copy our compiled binary to an alpine image. We will build the image with the following command:
$ docker build -t alextanhongpin/hello-world-01 -f Dockerfile.01 .
Sending build context to Docker daemon 2.017MB
Step 1/4 : FROM alpine:latest
---> 7328f6f8b418
Step 2/4 : RUN apk --no-cache add ca-certificates
---> Using cache
---> 70fb51eb7cf7
Step 3/4 : COPY app .
---> b2a128947460
Removing intermediate container 79ec202de604
Step 4/4 : CMD /app
---> Running in fa74b21e353a
---> d678076674fa
Removing intermediate container fa74b21e353a
Successfully built d678076674fa
Successfully tagged alextanhongpin/hello-world-01:latest
Let's validate it again as we did before and view the change in the size:
$ docker run alextanhongpin/hello-world-01
Hello, go!
Let's take a look at the image size:
docker image list | grep hello-world
alextanhongpin/hello-world-01 latest d678076674fa 45 seconds ago 6.55MB
alextanhongpin/hello-world-00 latest 2669fc5303bf 5 minutes ago 729MB
We can see that the size has reduced dramatically from 729MB to 6.55MB. This however, involves two different step - compiling the binary locally and create a docker image. The next section will demonstrate how you can reduce this to a single step.
Method 3: Using multi-stage build
Multi-stage buil is a new feature in Docker 17.05 and allows you to optimize your Dockerfiles. With it, we can reduce our build into a single step. This is how our Dockerfile will look like:
FROM golang:1.9 as builder
WORKDIR /go/src/github.com/alextanhongpin/hello-world
COPY main.go .
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alextanhongpin/hello-world/app .
CMD ["./app"]
Let's build and observe the magic:
$ docker build -t alextanhongpin/hello-world .
Sending build context to Docker daemon 2.018MB
Step 1/10 : FROM golang:1.9 as builder
---> 5e2f23f821ca
Step 2/10 : WORKDIR /go/src/github.com/alextanhongpin/hello-world
---> Using cache
---> d36bf8436458
Step 3/10 : COPY main.go .
---> Using cache
---> 2fa05dc652bc
Step 4/10 : RUN go get -d -v
---> Using cache
---> bb0f73ac82d1
Step 5/10 : RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
---> Using cache
---> 8b32d3f4cfd0
Step 6/10 : FROM alpine:latest
---> 7328f6f8b418
Step 7/10 : RUN apk --no-cache add ca-certificates
---> Using cache
---> 70fb51eb7cf7
Step 8/10 : WORKDIR /root/
---> Using cache
---> a7a3eea586d3
Step 9/10 : COPY --from=builder /go/src/github.com/alextanhongpin/hello-world/app .
---> Using cache
---> e723f2ddc2eb
Step 10/10 : CMD ./app
---> Using cache
---> 71995c167901
Successfully built 71995c167901
Successfully tagged alextanhongpin/hello-world:latest
$ docker run alextanhongpin/hello-world
Hello, go!
You can now build your golang image in a single step. The output is shown below:
$ docker image list | grep hello-world
alextanhongpin/hello-world-01 latest d678076674fa 4 minutes ago 6.55MB
alextanhongpin/hello-world-00 latest 2669fc5303bf 8 minutes ago 729MB
alextanhongpin/hello-world latest 71995c167901 12 hours ago 6.54MB
Adding Label Schema
There is a standardised naming conventions for labels in Docker image, which can be found here. In our Dockerfile, we specify the ARG to be passed during the build process.
FROM golang:1.9 as builder
WORKDIR /go/src/github.com/alextanhongpin/hello-world
COPY main.go .
RUN go get -d -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alextanhongpin/hello-world/app .
# Metadata params
ARG VERSION
ARG BUILD_DATE
ARG VCS_URL
ARG VCS_REF
ARG NAME
ARG VENDOR
# Metadata
LABEL org.label-schema.build-date=$BUILD_DATE \
org.label-schema.name=$NAME \
org.label-schema.description="Example of multi-stage docker build" \
org.label-schema.url="https://example.com" \
org.label-schema.vcs-url=https://github.com/alextanhongpin/$VCS_URL \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vendor=$VENDOR \
org.label-schema.version=$VERSION \
org.label-schema.docker.schema-version="1.0" \
org.label-schema.docker.cmd="docker run -d alextanhongpin/hello-world"
CMD ["./app"]
We create a simple Makefile to ease storing the variables:
VERSION := $(shell git rev-parse HEAD)
BUILD_DATE := $(shell date -R)
VCS_URL := $(shell basename `git rev-parse --show-toplevel`)
VCS_REF := $(shell git log -1 --pretty=%h)
NAME := $(shell basename `git rev-parse --show-toplevel`)
VENDOR := $(shell whoami)
print:
@echo VERSION=${VERSION}
@echo BUILD_DATE=${BUILD_DATE}
@echo VCS_URL=${VCS_URL}
@echo VCS_REF=${VCS_REF}
@echo NAME=${NAME}
@echo VENDOR=${VENDOR}
build:
docker build -t alextanhongpin/hello-go --build-arg VERSION="${VERSION}" \
--build-arg BUILD_DATE="${BUILD_DATE}" \
--build-arg VCS_URL="${VCS_URL}" \
--build-arg VCS_REF="${VCS_REF}" \
--build-arg NAME="${NAME}" \
--build-arg VENDOR="${VENDOR}" .
Running $ make print output:
VERSION=a8dd38b765470fe69ee1127519a586512942f318
BUILD_DATE=Tue, 27 Mar 2018 11:50:42 +0800
VCS_URL=go-docker-multi-stage-build
VCS_REF=a8dd38b
NAME=go-docker-multi-stage-build
VENDOR=alextan
Running $ make build will now inject those variables into the Docker image during the build process:
docker build -t alextanhongpin/hello-go --build-arg VERSION="a8dd38b765470fe69ee1127519a586512942f318" \
--build-arg BUILD_DATE="Tue, 27 Mar 2018 11:51:16 +0800" \
--build-arg VCS_URL="go-docker-multi-stage-build" \
--build-arg VCS_REF="a8dd38b" \
--build-arg NAME="go-docker-multi-stage-build" \
--build-arg VENDOR="alextan" .
Sending build context to Docker daemon 96.77kB
Step 1/17 : FROM golang:1.9 as builder
---> a6c306bd0b2f
Step 2/17 : WORKDIR /go/src/github.com/alextanhongpin/hello-world
---> Using cache
---> 13d8a2ac5144
Step 3/17 : COPY main.go .
---> Using cache
---> 3db9ab323851
Step 4/17 : RUN go get -d -v
---> Using cache
---> 1c4a3363c51c
Step 5/17 : RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
---> Using cache
---> 16c60c3ee194
Step 6/17 : FROM alpine:latest
---> 3fd9065eaf02
Step 7/17 : RUN apk --no-cache add ca-certificates
---> Using cache
---> 09eef72b03f8
Step 8/17 : WORKDIR /root/
---> Using cache
---> caaa69a4ea86
Step 9/17 : COPY --from=builder /go/src/github.com/alextanhongpin/hello-world/app .
---> Using cache
---> 4f152587c422
Step 10/17 : ARG VERSION
---> Using cache
---> 238fd64c8894
Step 11/17 : ARG BUILD_DATE
---> Using cache
---> d6e82c21c2b7
Step 12/17 : ARG VCS_URL
---> Using cache
---> 4483ad9a0ebc
Step 13/17 : ARG VCS_REF
---> Using cache
---> ca3fa7d5de18
Step 14/17 : ARG NAME
---> Using cache
---> ad4d30434177
Step 15/17 : ARG VENDOR
---> Using cache
---> b5720f32c236
Step 16/17 : LABEL org.label-schema.build-date=$BUILD_DATE org.label-schema.name=$NAME org.label-schema.description="Example of multi-stage docker build" org.label-schema.url="https://example.com" org.label-schema.vcs-url=https://github.com/alextanhongpin/$VCS_URL org.label-schema.vcs-ref=$VCS_REF org.label-schema.vendor=$VENDOR org.label-schema.version=$VERSION org.label-schema.docker.schema-version="1.0" org.label-schema.docker.cmd="docker run -d alextanhongpin/hello-world"
---> Running in e4aad0b65c5f
Removing intermediate container e4aad0b65c5f
---> fbca9a419ee3
Step 17/17 : CMD ["./app"]
---> Running in 450ea5afb993
Removing intermediate container 450ea5afb993
---> 0f8e678167f6
Successfully built 0f8e678167f6
Successfully tagged alextanhongpin/hello-go:latest
To verify the labels are injected into the image, you can just docker inspect the image.
# Inspect the labels by iterating through them, and printing them each in a new line
$ docker inspect --format='{{range $k, $v := .Config.Labels}}{{$k}}={{$v}}{{println}}{{end}}' alextanhongpin/hello-go
Output:
org.label-schema.build-date=Tue, 27 Mar 2018 11:51:16 +0800
org.label-schema.description=Example of multi-stage docker build
org.label-schema.docker.cmd=docker run -d alextanhongpin/hello-world
org.label-schema.docker.schema-version=1.0
org.label-schema.name=go-docker-multi-stage-build
org.label-schema.url=https://example.com
org.label-schema.vcs-ref=a8dd38b
org.label-schema.vcs-url=https://github.com/alextanhongpin/go-docker-multi-stage-build
org.label-schema.vendor=alextan
org.label-schema.version=a8dd38b765470fe69ee1127519a586512942f318