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
koflags): While not directly inskaffold.yamlforko, you can influenceko’s base image selection through environment variables or customkoconfigurations if needed, though Skaffold’s defaults are usually excellent. - Build Targets: For multi-module Go projects,
kocan build specific packages. You’d typically configure this within thekocommand itself or by adjusting yourskaffold.yamlto pointkoto the correct directory or main package. Skaffold can pass flags tokovia theargsfield under thekostanza 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.