Multistage builds
The idea behind multistage builds is to make it easy to build smaller container images by facilitating the exclusion of intermediate build files from the final product.
Smaller container images take less space on disk, meaning they take less time to download and deploy. During the compilation of software, it is common to need a compiler, several library dependencies, and intermediate objects that will not be needed during the execution of the program. Multistage builds allow you to define two or more docker build stages in the same Dockerfile
. They will be executed in order one after the other, and each "stage" will be able to copy files from the previous ones. This way, we can easily and in the same build process, build the software and then keep only the files that we actually need for the execution.
Usage
First create a new go project or have an existing go initialize project.
- To initialize a new go project as an example:
go mod init example.com/go-server
Take this Dockerfile
:
Dockerfile
:
FROM golang:1.18.3-stretch as builder
RUN mkdir -p /go/src/server
WORKDIR /go/src/server
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
RUN CGO_ENABLED=0 go build server.go
FROM alpine:edge
RUN mkdir /app
COPY --from=builder /go/src/server/server /app/server
CMD ["/app/server"]
and this code (golang):
server.go
:
package main
import (
"fmt"
"strings"
"net/http"
"github.com/pborman/uuid"
)
func main() {
http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
uuidWithHyphen := uuid.NewRandom()
uuid := strings.Replace(uuidWithHyphen.String(), "-", "", -1)
fmt.Fprintf(w, "Welcome to my website!\n")
fmt.Fprintf(w, uuid)
})
fmt.Print("Starting server in port 8080...\n")
http.ListenAndServe(":8080", nil)
}
go mod tidy
, which will download all the dependencies that are required in your source files and update go.mod
file with that dependency. In this case it will download github.com/pborman/uuid
.
The dockerfile
can be divided into two parts (or stages), each starting by the FROM
instruction:
FROM golang:1.18.3-stretch as builder
, uses the official golang image containing everything we need to compile the code. It is labeled asbuilder
. We copygo.mod and go.sum
and download the package dependencies to the "working directory". We copy the whole "working directory", including the code withCOPY . .
, and finally compile the code withRUN CGO_ENABLED=0 go build server.go
.FROM alpine:edge
, uses the minimal distributionalpine
. In the lineCOPY --from=builder /go/src/server/server /app/server
the compiled program and only the compiled program is copied from the previous stage (build
).
In order to test this build process, put the two files in the same directory and name them Dockerfile
and server.go
. Then run the command:
docker build . -t go-server
This will produce the image called go-server:latest
. To check the size of the image just run:
$ docker images go-server
REPOSITORY TAG IMAGE ID CREATED SIZE
go-server latest 173c922261a3 16 minutes ago 12.1MB
it should give you approximately 12MB, which is more than half (~7MB) is the compiled code.
If you pull the image golang:1.18.3-stretch
(the one we used for building the code) and check its size, you will see that it is approximately 890 MB
.
$ docker images golang:1.18.3-stretch
REPOSITORY TAG IMAGE ID CREATED SIZE
golang 1.18.3-stretch 6ee1deda35bd 12 days ago 890 MB
This same small image (go-server:latest
) is of course also achievable by other methods. You can build the code outside of docker and then copy it to the alpine
image. You can mount the code directory into the build image, build it and then again copy the compiled product into the alpine
image. But none of these methods are as easy and compact as this one.
Usage in Rahti 2
In order to test this in Rahti 2, one only needs to login in Rahti 2, select the correct project, and run:
oc new-build https://github.com/cscfi/multi-stage-build.git
NOTE: The code must be in a git repository and Rahti 2 must be able to clone it.
The end result will be an image called multi-stage-build
stored in the internal Rahti 2 registry of the project you selected. This image can then be used in a Rahti 2 deployment using the image stream option when deploying an image.