The local Kubernetes loop problem
You write a line of Go code, save the file, and wait. You run a Docker build, push the image to a registry, apply a Kubernetes manifest, and tail the logs. Five minutes later, you realize you missed a comma. You repeat the cycle. This is the classic Kubernetes development tax. The cluster expects immutable artifacts, but your code changes every few minutes. Skaffold exists to erase that tax. It watches your filesystem, rebuilds the container, updates the deployment, and restarts the pod without you typing a single command.
What Skaffold actually does
Think of Skaffold as a bridge between your local editor and a remote cluster. Normally, Kubernetes deployment is a linear pipeline. You compile code, bake it into a container image, tag it, push it to a registry, and tell Kubernetes to use that tag. Changing the source code means repeating the entire chain. Skaffold intercepts that chain and turns it into a loop.
It tracks file changes, triggers a lightweight build, updates the image tag in your manifests on the fly, and applies the changes to the cluster. The cluster stays live. Your code updates in seconds. Under the hood, Skaffold handles the plumbing: it manages Docker or BuildKit, runs kubectl apply, sets up port forwarding, and tails logs. You get a local development experience that targets a real Kubernetes environment.
Skaffold is a loop manager. Keep your manifests declarative and let it handle the iteration.
The minimal configuration
Every Skaffold project starts with a single configuration file. Place skaffold.yaml in your project root. The file tells Skaffold where your source lives, how to build the image, and which Kubernetes manifests to apply.
Here is the simplest valid configuration for a Go project:
# skaffold.yaml
apiVersion: skaffold/v4
kind: Config
metadata:
name: go-webapp
build:
artifacts:
- image: localhost:5000/go-webapp
context: .
docker:
dockerfile: Dockerfile
deploy:
kubectl:
manifests:
- k8s/deployment.yaml
The build section points to your Dockerfile and sets the image name. The deploy section tells Skaffold to use kubectl and apply everything in the k8s/ directory. Run skaffold dev in your terminal and the loop starts immediately.
Keep the config flat. Skaffold reads it once and caches the structure.
Walking through the dev loop
When you run skaffold dev, the tool performs a first-pass build. It reads the Dockerfile, compiles your Go code, creates the container image, and tags it with a unique identifier. It then applies your Kubernetes manifests, replacing any hardcoded image tags with the newly generated one. Once the pod is running, Skaffold switches to watch mode.
Change a file in your editor and save it. Skaffold detects the filesystem event. It rebuilds the image, leveraging Docker layer caching to skip unchanged steps. It updates the deployment manifest with the new image tag and triggers a rolling update. Kubernetes terminates the old pod and starts a new one. The new pod pulls the fresh image and starts your Go binary. The entire cycle usually takes ten to twenty seconds for a small Go service.
Go builds are fast, but Docker cache invalidation can still slow you down. If you change a dependency in go.mod, every layer after the dependency copy step rebuilds. Skaffold does not change how Docker caches work. It just automates the trigger.
Watch the logs, not the terminal spinner. The cluster tells you when it is ready.
A realistic Go setup
Production-ready Go services use multi-stage Dockerfiles to keep images small. Skaffold works seamlessly with this pattern. You also want faster iteration than a full rebuild provides. Skaffold supports file synchronization, which copies changed files into a running container without restarting the pod. This is useful for debugging or when your binary supports hot reloading.
Here is a standard multi-stage Dockerfile for a Go HTTP service:
# Dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Copy dependency manifests first to leverage Docker cache
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build the binary with static linking for minimal runtime image
RUN CGO_ENABLED=0 GOOS=linux go build -o /server .
FROM alpine:3.19
WORKDIR /app
# Copy only the compiled binary from the builder stage
COPY --from=builder /server .
EXPOSE 8080
CMD ["./server"]
The Kubernetes deployment expects a standard spec. Here is a minimal deployment manifest:
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-webapp
spec:
replicas: 1
selector:
matchLabels:
app: go-webapp
template:
metadata:
labels:
app: go-webapp
spec:
containers:
- name: server
image: localhost:5000/go-webapp
ports:
- containerPort: 8080
Now update skaffold.yaml to enable sync. This tells Skaffold to copy changed Go files into the running container instead of rebuilding the image every time:
# skaffold.yaml
apiVersion: skaffold/v4
kind: Config
metadata:
name: go-webapp
build:
artifacts:
- image: localhost:5000/go-webapp
context: .
docker:
dockerfile: Dockerfile
sync:
manual:
- src: "internal/**/*.go"
dest: /app
- src: "cmd/**/*.go"
dest: /app
deploy:
kubectl:
manifests:
- k8s/deployment.yaml
The sync block maps local source directories to the container path. When you save a .go file, Skaffold copies it into the pod. Your Go binary does not automatically restart unless you add a file watcher or use a framework that supports hot reloading. For most Go services, a quick rebuild is still faster than debugging sync edge cases.
Trust the build cache. Let Docker handle layer invalidation.
Common friction points
Skaffold abstracts complexity, but it does not hide Kubernetes or Docker constraints. You will hit friction when your environment fights the tool.
Missing .dockerignore files are the most common slowdown. If your context includes node_modules, .git, or build artifacts, Docker uploads megabytes of unnecessary data. The build hangs, and Skaffold waits. Add a .dockerignore with *.git, *.dockerignore, and vendor/ to keep the context lean.
Kubernetes resource limits can break the loop. If your deployment sets a tight memory limit, the pod might crash during a rolling update when the new image pulls. The cluster reports error from server: container "server" in pod "go-webapp-xyz" is waiting to start: image can't be pulled. Skaffold will retry, but the pod stays in a crash loop until you relax the limits or fix the image.
Port forwarding conflicts happen when another process occupies the local port. Skaffold forwards the container port to your machine by default. If you already run a service on that port, you get listen tcp 127.0.0.1:8080: bind: address already in use. Change the local port in your skaffold.yaml under portForward or stop the conflicting process.
Go build cache and Docker cache do not share state. If you delete your local ~/go/pkg/mod cache, Docker still thinks the go mod download layer is valid. The build fails inside the container with go: missing go.sum entry for module. Clear the Docker build cache with docker builder prune when dependency layers misbehave.
The compiler rejects this with undefined: variable if you reference a missing symbol. Skaffold will surface the error, but it will not fix the typo. Read the output. Fix the code. Save the file.
Errors are signals, not failures. Fix the root cause and keep the loop running.
When to reach for Skaffold
Development tooling is a spectrum. Pick the right tool for your current stage.
Use Skaffold when you need a full local-to-cluster loop with automatic image building, manifest patching, and rolling updates. Use manual kubectl apply when you are deploying to production or need strict control over rollout steps and rollback procedures. Use Ko when you want zero-container-registry builds that compile Go directly to OCI images on the fly without a Docker daemon. Use plain Docker Compose when you are debugging locally without Kubernetes overhead or network policies.
Match the tool to the environment. Don't overcomplicate the loop.