Local Dev Workflow
In this tutorial you will learn the complete code-build-push-deploy-iterate loop using kinder’s built-in local registry at localhost:5001. No external registry, no authentication, no push delays — just a plain Docker push that the cluster can immediately pull from. You will build a Go HTTP server, deploy it, access it via port-forward, change the code, push a new version, and verify the update — all without leaving your local machine.
Prerequisites
Section titled “Prerequisites”- kinder installed
- Docker (or Podman) installed and running
kubectlinstalled and on PATH- A text editor
Step 1: Create the cluster
Section titled “Step 1: Create the cluster”kinder create clusterStep 2: Verify the registry is running
Section titled “Step 2: Verify the registry is running”Confirm that the registry container is up:
docker ps --filter name=kind-registryExpected output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESa3f7b2c91d4e registry:2 "/entrypoint.sh /etc…" 2 minutes ago Up 2 minutes 0.0.0.0:5001->5000/tcp kind-registryThe kind-registry container listens on localhost:5001 on your host. Any image you push to it is immediately available to all nodes in the cluster.
Step 3: Create a simple application
Section titled “Step 3: Create a simple application”Create a new directory for the project and add two files:
mkdir myapp && cd myappmain.go:
package main
import ( "fmt" "net/http")
func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello from v1!") }) http.ListenAndServe(":8080", nil)}Dockerfile:
FROM golang:1.23-alpine AS builderWORKDIR /appCOPY main.go .RUN go build -o server main.go
FROM alpine:latestCOPY --from=builder /app/server /serverEXPOSE 8080CMD ["/server"]Step 4: Build and push v1
Section titled “Step 4: Build and push v1”Build the image and tag it with the local registry address:
docker build -t localhost:5001/myapp:v1 .Push it to the local registry:
docker push localhost:5001/myapp:v1Expected output:
The push refers to repository [localhost:5001/myapp]a3d2f891b7c4: Pushede1f4a9c23d85: Pushedv1: digest: sha256:4b2f8a1c9d3e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a size: 740The image is now stored in kind-registry and is available for the cluster to pull.
Step 5: Deploy to the cluster
Section titled “Step 5: Deploy to the cluster”Create a Deployment and a Service that reference the local registry image:
apiVersion: apps/v1kind: Deploymentmetadata: name: myappspec: replicas: 1 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: localhost:5001/myapp:v1 ports: - containerPort: 8080---apiVersion: v1kind: Servicemetadata: name: myappspec: selector: app: myapp ports: - port: 80 targetPort: 8080Save this as deployment.yaml and apply it:
kubectl apply -f deployment.yamlWait for the pod to be ready:
kubectl get podsExpected output:
NAME READY STATUS RESTARTS AGEmyapp-6d4f8b9c7-xk2pq 1/1 Running 0 20sStep 6: Access the application
Section titled “Step 6: Access the application”Forward a local port to the myapp service:
kubectl port-forward svc/myapp 8080:80In a separate terminal, make a request:
curl http://localhost:8080Expected output:
Hello from v1!Step 7: Make a change and iterate
Section titled “Step 7: Make a change and iterate”Stop the port-forward with Ctrl+C, then edit main.go to change "Hello from v1!" to "Hello from v2!".
After saving the file, run the full iteration loop:
docker build -t localhost:5001/myapp:v2 .docker push localhost:5001/myapp:v2kubectl set image deployment/myapp myapp=localhost:5001/myapp:v2kubectl rollout status deployment/myappExpected output from kubectl rollout status:
Waiting for deployment "myapp" rollout to finish: 1 old replicas are pending termination...deployment "myapp" successfully rolled outStep 8: Verify the new version
Section titled “Step 8: Verify the new version”Start port-forward again:
kubectl port-forward svc/myapp 8080:80In a separate terminal:
curl http://localhost:8080Expected output:
Hello from v2!The rolling update replaced the old pod with one running the new image, with zero downtime.
Alternative: skip the registry and use kinder load images
Section titled “Alternative: skip the registry and use kinder load images”If you don’t want to run a registry at all — or you’re working in an air-gapped environment — kinder ships a kinder load images subcommand that bypasses the registry entirely. Instead of pushing to localhost:5001, you build the image locally and stream it directly into every node in the cluster.
Create the cluster with the local registry disabled if you prefer a leaner footprint:
apiVersion: kind.x-k8s.io/v1alpha4kind: Clusteraddons: localRegistry: falsekinder create cluster --config cluster.yamlThen the iteration loop becomes:
docker build -t myapp:dev .kinder load images myapp:devkubectl rollout restart deployment/myappSet imagePullPolicy: IfNotPresent on your deployment so Kubernetes uses the image already loaded into the node instead of trying to pull it from a remote registry:
containers: - name: myapp image: myapp:dev imagePullPolicy: IfNotPresentkinder load images also handles Docker Desktop 27+ containerd image store compatibility automatically via a two-attempt ctr import fallback. See the Load Images CLI reference for details on flags, provider behavior, and smart-load semantics.
Clean up
Section titled “Clean up”kinder delete clusterOptionally remove the project directory:
cd .. && rm -rf myappThis removes the cluster and the local registry container. The localhost:5001/myapp images in the registry are gone with it.