Skaffold with ko lets you build Go container images incredibly fast, often without a Docker daemon.

Let’s see it in action. Imagine you have a simple Go web server in main.go:

package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello from Go!")
	})

	fmt.Println("Server listening on :8080")
	http.ListenAndServe(":8080", nil)
}

And a skaffold.yaml file:

apiVersion: skaffold.dev/v2beta26
kind: Config
build:
  artifacts:
    - image: gcr.io/my-project/go-app
      ko: {}
  local:
    push: false
deploy:
  kubectl:
    manifests:
      - k8s/deployment.yaml

And a basic Kubernetes deployment k8s/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: go-app
  template:
    metadata:
      labels:
        app: go-app
    spec:
      containers:
        - name: go-app
          image: gcr.io/my-project/go-app
          ports:
            - containerPort: 8080

Now, run skaffold dev. Skaffold will detect the ko builder. ko will compile your Go application directly into a container image, bypassing the need for a Dockerfile and often avoiding the Docker daemon entirely. It’s a two-step process: ko first builds the Go binary, and then bundles that binary into a minimal container image (usually based on distroless or scratch images).

The primary problem ko solves is the overhead of traditional Docker builds for Go applications. Dockerfiles involve copying source code, running build commands within a container, and then packaging the result. This can be slow, especially for iterative development where you’re rebuilding frequently. ko streamlines this by directly producing the container artifact from the Go build output.

Internally, ko operates by invoking the Go toolchain (go build) to produce a static Go binary. This binary is then placed into a container image. For maximum efficiency and security, ko defaults to using minimal base images like gcr.io/distroless/static-debian11 or even scratch. This means the final image contains only your Go binary and any static assets it needs, drastically reducing image size and attack surface.

The skaffold.yaml configuration is straightforward. Under build.artifacts, you specify the image name. The ko: {} stanza tells Skaffold to use ko for this artifact. build.local.push: false means Skaffold will pull the image locally after ko builds it, rather than pushing it to a remote registry immediately. This is ideal for local development.

The key levers you control are:

  • Image Name: The fully qualified name of your container image (e.g., docker.io/library/my-app, gcr.io/my-project/my-app).
  • Base Image (via ko flags): While not directly in skaffold.yaml for ko, you can influence ko’s base image selection through environment variables or custom ko configurations if needed, though Skaffold’s defaults are usually excellent.
  • Build Targets: For multi-module Go projects, ko can build specific packages. You’d typically configure this within the ko command itself or by adjusting your skaffold.yaml to point ko to the correct directory or main package. Skaffold can pass flags to ko via the args field under the ko stanza if you need more granular control.

A common misconception is that ko always replaces Docker entirely. While it often bypasses the Docker daemon for building the Go binary into an image, if your workflow requires other Dockerfile-based images (e.g., for external services, build tools, or complex multi-stage builds), you’ll still need Docker installed and running. Skaffold can orchestrate builds using both ko and docker builders within the same skaffold.yaml.

When ko builds your Go application, it doesn’t just copy your source code into an image. Instead, it compiles your Go program into a static binary. This binary is then executed within a minimal container environment. This means that any dependencies your Go program has at runtime must be statically linked or included in the minimal base image. For most Go applications that don’t rely on dynamic system libraries, this works seamlessly with distroless or scratch images.

After fixing the build, you’ll likely encounter issues with your application not starting correctly in Kubernetes due to misconfigured ports or readiness/liveness probes.

Want structured learning?

Take the full Skaffold course →