Skip to content

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.

  • kinder installed
  • Docker (or Podman) installed and running
  • kubectl installed and on PATH
  • A text editor
Terminal window
kinder create cluster

Confirm that the registry container is up:

Terminal window
docker ps --filter name=kind-registry

Expected output:

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a3f7b2c91d4e registry:2 "/entrypoint.sh /etc…" 2 minutes ago Up 2 minutes 0.0.0.0:5001->5000/tcp kind-registry

The 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.

Create a new directory for the project and add two files:

Terminal window
mkdir myapp && cd myapp

main.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 builder
WORKDIR /app
COPY main.go .
RUN go build -o server main.go
FROM alpine:latest
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]

Build the image and tag it with the local registry address:

Terminal window
docker build -t localhost:5001/myapp:v1 .

Push it to the local registry:

Terminal window
docker push localhost:5001/myapp:v1

Expected output:

The push refers to repository [localhost:5001/myapp]
a3d2f891b7c4: Pushed
e1f4a9c23d85: Pushed
v1: digest: sha256:4b2f8a1c9d3e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a size: 740

The image is now stored in kind-registry and is available for the cluster to pull.

Create a Deployment and a Service that reference the local registry image:

apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: localhost:5001/myapp:v1
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 8080

Save this as deployment.yaml and apply it:

Terminal window
kubectl apply -f deployment.yaml

Wait for the pod to be ready:

Terminal window
kubectl get pods

Expected output:

NAME READY STATUS RESTARTS AGE
myapp-6d4f8b9c7-xk2pq 1/1 Running 0 20s

Forward a local port to the myapp service:

Terminal window
kubectl port-forward svc/myapp 8080:80

In a separate terminal, make a request:

Terminal window
curl http://localhost:8080

Expected output:

Hello from v1!

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:

Terminal window
docker build -t localhost:5001/myapp:v2 .
docker push localhost:5001/myapp:v2
kubectl set image deployment/myapp myapp=localhost:5001/myapp:v2
kubectl rollout status deployment/myapp

Expected output from kubectl rollout status:

Waiting for deployment "myapp" rollout to finish: 1 old replicas are pending termination...
deployment "myapp" successfully rolled out

Start port-forward again:

Terminal window
kubectl port-forward svc/myapp 8080:80

In a separate terminal:

Terminal window
curl http://localhost:8080

Expected 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:

cluster.yaml
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
addons:
localRegistry: false
Terminal window
kinder create cluster --config cluster.yaml

Then the iteration loop becomes:

Terminal window
docker build -t myapp:dev .
kinder load images myapp:dev
kubectl rollout restart deployment/myapp

Set 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: IfNotPresent

kinder 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.

Terminal window
kinder delete cluster

Optionally remove the project directory:

Terminal window
cd .. && rm -rf myapp

This removes the cluster and the local registry container. The localhost:5001/myapp images in the registry are gone with it.