Build and Publish Lightweight Go Binaries into Container Images with ko

Google’s project ko takes the pain out of containerizing your Go programs as lightweight OCI-compliant images and publishing them to your favorite repositories.

Vladimir Vivien
5 min readFeb 12, 2023

The build-time cycle, from source code to container image publication, can add up over time when developing cloud-native applications. This posts explores ko, an open source container image builder from Google, that quickly builds your Go binaries into lightweight containerized images and publish them to your favorite repositories.

Read more about project ko here.

ko provides a simple command-line tool to quickly build your Go binaries into containers. When your Go projects compiles into a static binary (with no OS dependencies, think CGO_ENABLED=0), you can use ko to build super lightweight container images without the hundreds of megabytes of OS froth.

Installing ko

There are several ways you can install ko.

Using Go:

go install github.com/google/ko@latest

Home brew:

brew install ko

ko also provides a GitHub Action setup that you can integrate in your CI/CD pipeline:

steps:
- uses: imjasonh/setup-ko@v0.6

Building and publishing an image

The ko build command wraps the go build command to compile the Go code, automatically generate a container image, and push the image to a local or remote repository.

This write up will compile and build an image for the following Go program, called timeapp, which simply prints the current time:

package main

import (
"fmt"
"time"
)

func main() {
fmt.Println(time.Now())
}

Configuring your repository

Before building, let us configure ko to set the location where the images are published. In general, ko uses environment variable KO_DOCKER_REPO to set the repository where the OCI images will be published.

For instance, the following configures ko to use the GitHub container repository:

KO_DOCKER_REPO=ghcr.io/vladimirvivien/services ko build .

Publish locally

Optionally, if you have a Docker daemon running locally, ko can build and publish your images there using KO_DOCKER_REPO=ko.local (or by specifying --local -L flag) as shown below:

KO_DOCKER_REPO=ko.local ko build .

# equivalent to
ko build --local .

Note that the ko build command uses the package’s import path (in this example, current path “.” ) the same way as go build . When the command is done, you will see your new image in the Docker daemon as shown below:

$> docker images

REPOSITORY TAG IMAGE ID CREATED SIZE
ko.local/timeapp-fc793bb2a14c1eb1 latest 47c3a0cf49d7 11 hours ago 3.1MB

ko automatically generates an image name using the program’s package name and the build hash. That behavior (and others) can be configured as we will see later.

Launching the image container

The image can be launched using your favorite OCI runtime tool. The snippet below shows the ko-built container started using Docker:

$> docker run --rm ko.local/timeapp-fc793bb2a14c1eb1:latest

2022-01-11 15:18:57.111766885

By default, ko sets the entry point to the container as /ko-app/<main-package-name>. This is illustrated below by launching the container using its default entry point:

$> docker run --rm ko.local/timeapp-fc793bb2a14c1eb1:latest /ko-app/timeapp

2022-01-11 15:20:51.158882465

Publishing to a remote registry

If you are using Docker and are already authenticated to your registry (i.e. with docker login), you are ready to build/push ko-generated images. If you are not using Docker, you can use command ko login to setup your username and password authentication to publish to a remote the registry:

ko login ghcr.com -u mine -p craft

Or, authenticate with a token stored in an environment variable:

echo $GITHUB_PAT | grep ko login ghcr.com -u mine --password-stdin

Once authenticated, ko will automatically publish your generated images to a specified remote registry. For instance, the following will build and publish the Go program binary (from earlier) to my GitHub Container Registry (ghcr.io) account in services namespace:

KO_DOCKER_REPO=ghcr.io/vladimirvivien/services ko build .

Publishing to a running kind cluster

One ko feature that will speed up your cloud-native development cycle is the ability to automatically build an image and load the container into a running kind cluster:

KO_DOCKER_REPO=kind.local ko build .

Once loaded, you can use kubectl to apply a YAML that uses that image:

kubectl apply -f some-file-referecing-image.ymal

Image configuration

ko provides several settings that influence how a container image is built and published. As was shown earlier, ko will generate a default name based on the program’s main package followed by a ko-generated MD5 hash suffix:

$> docker images

REPOSITORY TAG IMAGE ID CREATED SIZE
ko.local/timeapp-fc793bb...6804e latest 47c3a0cf49d7 11 hours ago 3.1MB

There are several command-line arguments that can control how the image name is generated.

-- preserve-import-paths

Flag --preserve-import-paths (or -P) generates the name with the entire program’s import path:

KO_DOCKER_REPO=ko.local ko build --preserve-import-paths .

A listing of the images shows the image name as the full import path for the main package:

$> docker images

REPOSITORY TAG IMAGE ID CREATED SIZE
ko.local/github.com/vladimirvivien/timeapp latest f650668c3d74 18 hours ago 3.33MB

--base-import-paths

The --base-import-paths (or -B) flag uses the registry path and the package name only when generating the image name, as shown below:

$> KO_DOCKER_REPO=ko.local ko build --base-import-paths .

$> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ko.local/timeapp latest f650668c3d74 18 hours ago 3.33MB

--bare

The --bare flag only includes the repository’s path in the name, without main package included:

$> KO_DOCKER_REPO=ko.local ko build --bare .

$> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ko.local latest f650668c3d74 18 hours ago 3.33MB

.ko.yaml configuration file

Another way ko can be configured via the .ko.yaml configuration file when placed in the build directory (or specify its location with environment variable KO_CONFIG_PATH). The following snippet shows a configuration file with build directives for the Go code:

builds:
- id: timeapp
main: ./timeapp
env:
- CGO_ENABLED=0
ldflags:
- -s -w
- -extldflags "-static"

Multi-platform images

ko has the ability to build and publish images for multiple platforms using the --platform flag. For instance, the example below will automatically cross-compile the code for all supported platforms (for the ko base image) and then produce the image:

$> KO_DOCKER_REPO=ko.local ko build -B --platform=all .

13:29:31 Building github.com/vladimirvivien/timeapp for linux/amd64
13:29:31 Building github.com/vladimirvivien/timeapp for linux/arm64
13:29:31 Building github.com/vladimirvivien/timeapp for linux/arm/v6
13:29:31 Building github.com/vladimirvivien/timeapp for linux/ppc64le
13:29:31 Building github.com/vladimirvivien/timeapp for linux/s390x
13:29:31 Building github.com/vladimirvivien/timeapp for linux/arm/v7
13:29:31 Building github.com/vladimirvivien/timeapp for linux/386
13:29:31 Building github.com/vladimirvivien/timeapp for linux/riscv64

You can target specific platform as show below:

KO_DOCKER_REPO=ko.local ko build -B --platform=linux/amd64,linux/arm64 .

If you are using .ko.yaml, you can include section defaultPlatforms: to specify the build target platforms:

defaultPlatforms:
- linux/arm64
- linux/amd64

Conclusion

If your Go development build cycle involves creating container images, there is no doubt you would welcome a speed improvement in your workflow. This posts shows how using ko can quickly build Go programs into containerized artifacts that are automatically pushed to a local or remote registry, providing a big productivity boost.

The features covered here are a small portion of what is possible with ko. If you are interested in learning about the project and start using it, visit its documentation website at https://ko.build/ 🎉

--

--