Skaffold with Bazel lets you run local Kubernetes development with hermetic builds, meaning your builds are isolated and repeatable, but it’s not about avoiding Dockerfiles.
Let’s watch it in action. Imagine we have a simple Go application and a Kubernetes deployment for it.
// main.go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from Skaffold+Bazel!")
})
fmt.Println("Starting server on :8080")
http.ListenAndServe(":8080", nil)
}
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-app:latest # Skaffold will manage this
ports:
- containerPort: 8080
Now, let’s define how Bazel builds our Go app and packages it for Kubernetes.
# BUILD.bazel
load("@io_bazel_rules_go//go:def.bzl", "go_binary")
go_binary(
name = "my_app_bin",
srcs = ["main.go"],
deps = [],
)
# This rule tells Skaffold how to build the Docker image.
# Note: This is NOT a standard Dockerfile. Bazel uses its own rules.
load("@io_bazel_rules_docker//container:container.bzl", "container_image")
container_image(
name = "my_app_image",
base = "@alpine//image", # Use a minimal base image
tars = {
"/app": "my_app_bin", # Package the built binary into the image
},
cmd = ["/app"], # The command to run when the container starts
)
And here’s our skaffold.yaml to tie it all together:
# skaffold.yaml
apiVersion: skaffold/v2beta15
kind: Config
build:
artifacts:
- image: my-app # This name matches the k8s/deployment.yaml
bazel:
target: //:my_app_image # The Bazel target that builds the container image
local:
push: false # Don't push to a registry, use local Docker daemon
deploy:
kubectl:
manifests:
- k8s/deployment.yaml
profiles:
- name: dev
build:
artifacts:
- image: my-app
bazel:
target: //:my_app_image
portForward:
- resourceType: deployment
resourceName: my-app
port: 8080
localPort: 8080
# This is where the magic happens for fast dev loops
sync:
- manual:
src: "main.go"
dest: "/app" # Syncs the source file into the running container
When you run skaffold dev, Skaffold consults skaffold.yaml. It sees the bazel build type for the my-app artifact. It then executes bazel build //:my_app_image. Bazel, in its hermetic environment, compiles your Go code and packages the resulting binary into a container image according to the container_image rule. This image is tagged locally as my-app:latest (or a specific tag managed by Skaffold).
After the build, Skaffold applies the Kubernetes manifests. If the deployment already exists, it updates the image tag. Then, for the dev profile, it attempts to sync changes. When you modify main.go, Skaffold detects the change. Instead of rebuilding the entire image, it uses kubectl cp (or similar mechanisms) to copy the updated main.go file directly into the running container. The application inside the container then restarts itself (this requires your application to have a mechanism for this, like a simple exec command that re-runs the binary or a built-in hot-reloader). This file sync bypasses the full Bazel build for rapid iteration.
The core problem this solves is the slow feedback loop in local Kubernetes development. Traditionally, you’d build a Docker image, push it (even locally), redeploy, and then wait. Bazel provides hermetic and fast builds, ensuring that what you build locally is exactly what you’ll get in production. Skaffold then orchestrates this Bazel build and integrates it with Kubernetes deployment and, crucially, file synchronization for near-instantaneous code updates without full rebuilds.
The sync directive in skaffold.yaml is the key to the "fast dev loop" part. When sync is configured, Skaffold watches your source files. On a change, it doesn’t trigger a Bazel build. Instead, it copies the modified file into the running container and signals the process within the container to reload. This often involves the application itself watching for changes to its source files and restarting its server process. For Go, you might add a simple file watcher and exec call to your main function for this to work seamlessly, or rely on frameworks that support this.
The most surprising thing is how sync interacts with Bazel. You’d expect Skaffold to trigger a Bazel build on every file change, but the sync configuration tells it to skip the build phase and directly update the running container. This works because Bazel’s role here is to produce the initial container image, and Skaffold’s sync handles the iterative updates to the code within that running container. The hermetic build from Bazel is still important for ensuring consistency between your local build and what gets deployed, but the development workflow leverages sync for speed.
The next concept you’ll grapple with is how to make your application inside the container actually reload itself after a file sync.