How to build more secure apps using multi-stage builds with a runtime base image
Some images will allow you to separate the build stages such that you can compile the app with one image and then copy the final binary onto a minimal runtime base image such as static or glibc-dynamic to significantly reduce the size of the final container image. This technique produces images that are ultra-light, hardened, performant, and secure.
Go is a great test case to demonstrate the value of using a multi-stage build to slim down the final app. Go is a compiled language that is not optimized for use as a runtime. For this reason, it is recommended to use a multi-stage build. We will compile the binary using the Go image, then copy the binary into a minimal runtime base image.Knowing which runtime base image to use depends on how your Go project was compiled:
Static binaries are self-contained and do not rely on shared system libraries or runtime dependencies on the host system. As such, they are fully portable and avoid compatibility issues with C libraries. If the static Go binary is mounted on a static base image, it will produce a tiny image, that is as minimal as it gets.In the example below, the flagCGO_ENABLED=0 is added to the Dockerfile to ensure that the compiled binary is statically linked. CGO is a Go tool that enables the creation of Go packages that call C code. When CGO is disabled, the resulting binary is statically linked.
In your project directory, save the code below to a Dockerfile:
Dockerfile example
Copy
# Pull Go image and set it as builderFROM reg.mini.dev/go:latest AS builder# Set CGO_ENABLED to 0 to create a static binaryENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64# Copy project directory content to app directory in the containerCOPY . /app# Compile your Go appRUN cd /app && go build -o go-minimus .# Pull static image to use as a runtime base imageFROM reg.mini.dev/static:latest# Copy go-minimus binary from the /app directory in the builder container to /usr/bin/ in the final static containerCOPY --from=builder /app/go-minimus /usr/bin/# Set the container entrypoint to run the go-minimus binary when the container is startedENTRYPOINT ["/usr/bin/go-minimus"]
In your project directory, save the code below to a file and name ithello-minimus.go.This is a very simple script which prints “Hello from Minimus!” to the terminal. You can use your own script instead.
Copy
package mainimport "fmt"func main() { fmt.Println("Hello from Minimus!")}
In your project directory, create a go.mod file. This file declares the modules and dependencies required by the project. In our case, the module set is only needed for testing purposes, so it’s very simple.
Copy
module minimus.dev/hello_minimusgo 1.19
Your project directory should now contain 3 files:
Dockerfile
hello-minimus.go
go.mod
From your project directory run the following command to build the custom image hello-go:
Copy
docker build -t hello-go .
The period . specifies the current directory as the build context.
If your Go app needs to link to database drivers or other C integrations, you can compile a dynamically linked binary by setting the CGO_ENABLED=1 flag. Make sure the required C libraries are available in both your build and runtime environments.In the example below, we will use the Minimus Glibc-Dynamic image as a runtime base for a simple web application.
In a new project directory, save the code below to a new Dockerfile:
Dockerfile example
Copy
# Pull Go image and set it as builderFROM reg.mini.dev/go AS builder# Set CGO_ENABLED=1 to compile a dynamically linked binaryENV CGO_ENABLED=1 GOOS=linux GOARCH=amd64# Copy the project directory content to app directory in the containerCOPY . /app# Compile Go applicationRUN cd /app && go build -o Hello-Minimus-Web .# Pull dynamic image to use as a runtime baseFROM reg.mini.dev/glibc-dynamic:latest# Copy Go binary from the /app directory in the builder container to /usr/bin/ in the final dynamic containerCOPY --from=builder /app/Hello-Minimus-Web /usr/bin/EXPOSE 8080# Set entrypoint to run the binary when the container is startedENTRYPOINT ["/usr/bin/Hello-Minimus-Web"]
Download the example filehello-minimus-web.go and save it to your project directory. This is a very simple script that creates a webpage with several tabs so you can navigate between them.
In your project directory, create a go.mod file. This file declares the modules and dependencies required by the project. In our case, the module set is only needed for testing purposes, so it’s very simple.
Copy
module minimus.dev/hello_minimusgo 1.19
Your project directory should now contain 3 files:
Dockerfile
hello-minimus-web.go
go.mod
From your project directory run the following command to build the image:
Copy
docker build -t hello-go-web .
The period . specifies the current directory as the build context.
Spin up the image we just created with this command: