Multi-Stage Docker Builds for Kubernetes

Tin Rabzelj
31 October, 2017

cover

Building and compiling apps inside a Docker image can produce very large images. To reduce the size, build pipeline can be split into multiple stages, where the final image only contains built binaries. This article shows how to set up a Docker image with services developed in Go, maintain multiple services inside a single image and how to use the image with Kubernetes.

Prerequisites

Install kubectl, Docker, and Minikube to run Kubernetes locally.

Run Minikube, or configure kubectl for some other provider.

$ minikube start [--vm-driver=<driver>]

Developing services

Install golang/dep, a tool for dependency management. Using it will simplify building Docker images.

$ go get -u github.com/golang/dep/cmd/dep

Create first directory, and initialize dep inside it.

$ mkdir first && cd first
$ dep init

Create first/main.go, and write a simple HTTP server.

package main

import (
	"fmt"
	"net/http"
)

func main() {
	r := http.NewServeMux()
	r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "first")
	})
	http.ListenAndServe(":3000", r)
}

Create another service inside second directory, which returns a string "second".

You can develop any number of services in a similar fashion. Each in a separate subdirectory.

Build "all-in-one" image

Create a Dockerfile inside project's root directory.

# Build stage
FROM golang:1.9.2-alpine3.6 AS build
# Support CGO and SSL
RUN apk --no-cache add gcc g++ make ca-certificates
WORKDIR /go/src/app
# Copy each service
COPY first first
COPY second second
# Compile them
RUN go install ./...

# Production build stage
FROM alpine:3.6
WORKDIR /usr/bin
# Copy built binaries
COPY --from=build /go/bin .

Each FROM instruction begins a new stage. The first stage is named "build". In the final stage, binaries get copied from the first stage by setting the --from=build.

For this example, the image will be pushed to Docker Hub.

Log-in with your Docker account.

$ docker login

Run the following command; replacing <username> with your Docker Hub username and <image> with repository's name.

$ docker build -t <username>/<image>

Push it to Docker Hub.

$ docker push <username>/<image>

Deploy to Kubernetes

Create first.yaml file; replacing <username> and <image>.

The spec.containers[].command field sets the default executable, in this case "first". To run another service, you'd simply change this field to some other service, which was compiled in the same Docker image.

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: tutorial
  labels:
    app: tutorial
spec:
  selector:
    matchLabels:
      app: tutorial
  replicas: 3
  template:
    metadata:
      labels:
        app: tutorial
    spec:
      containers:
      - name: tutorial
        image: docker.io/<username>/<image>
        command: ["first"]
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: tutorial
spec:
  type: LoadBalancer
  selector:
    app: tutorial
  ports:
  - port: 3000

Create resources.

$ kubectl apply -f first.yaml

Try calling the service.

$ curl $(minikube service tutorial --url)
first

Wrapping up

Using multi-stage Docker builds can help you drastically reduce the size of Docker images. It's also helpful to keep multiple services inside a single image, because it makes your build pipeline easier to maintain.

Newsletter

Get awesome articles delivered right to your doorstep

Protected by reCAPTCHA - Privacy - Terms

Comments

Sign in to post a comment