Guidebeginner

Kubernetes from Zero: Build and Operate Your First Local Cluster

Learn why Kubernetes exists. Then build a local cluster and practice deploying, scaling, fixing, updating, and rolling back an application.

Kubernetes becomes easier when you first understand the problem it solves. The YAML files make more sense after that.

Imagine that you have one containerized application on one server. You can start it with Docker, point traffic at the server, and restart it when it fails. That is a perfectly reasonable beginning.

Then the application becomes important. You need several copies. You need updates without downtime, automatic recovery, and one stable address while containers come and go. You may also have background workers, scheduled tasks, and several environments. Starting one container is no longer the hard part. The hard part is keeping all of these containers working together across machines.

Kubernetes is the system that performs that coordination.

In this guide, you will:

  • understand what Kubernetes solves and what it deliberately does not solve
  • learn the control plane, node, Pod, Deployment, and Service mental models
  • create a real Kubernetes cluster on your laptop with kind
  • deploy a small web application from declarative YAML
  • reach it through a stable Service
  • watch Kubernetes replace a deleted Pod
  • scale from two replicas to four
  • perform a rolling image update and a rollback
  • run a one-time Job and a scheduled CronJob
  • inspect common failures using a repeatable debugging path
  • remove the entire lab cleanly

The lab runs on macOS, Windows, or Linux. It requires no cloud account and does not push an image to a registry.

1. What problem does Kubernetes solve?

Containers package an application and its runtime dependencies. They make an application portable, but a container engine by itself does not answer the operational questions that appear when the application runs continuously:

  • Which machine should run each container?
  • What replaces a container after it crashes?
  • How do clients find healthy copies when their IP addresses change?
  • How do we add or remove replicas?
  • How do we roll out a new version gradually?
  • Where do configuration and credentials live?
  • How do multiple services share a pool of machines safely?

A manually managed application becomes a Kubernetes cluster that schedules, replaces, scales, updates, and exposes container replicas.

Kubernetes automates the coordination work that grows around containerized applications.

Kubernetes is an open-source container orchestration system. You describe the state you want, and a collection of control loops works to make the cluster match that description.

For example, you can declare:

replicas: 3

You are not writing a script that starts container one, then container two, then container three. You are declaring that three replicas should exist. If one disappears later, Kubernetes notices that the observed state has fallen below the desired state and creates a replacement.

This distinction—desired state rather than a sequence of commands—is the foundation of Kubernetes.

What Kubernetes gives you

Kubernetes provides building blocks for:

  • scheduling containers onto a pool of machines
  • maintaining a requested number of replicas
  • restarting or replacing failed workloads
  • stable service discovery and internal load balancing
  • controlled application rollouts and rollbacks
  • configuration and secret distribution
  • batch and scheduled work
  • resource requests, limits, and placement policy
  • extension through custom APIs and controllers

The Kubernetes overview describes these capabilities as a portable, extensible platform for managing containerized workloads and services.

What Kubernetes does not give you automatically

Kubernetes does not make an application correct, observable, secure, or highly available merely because it runs in a cluster.

You still need to design:

  • useful health checks
  • data durability and backup
  • application-level retries and idempotency
  • metrics, logs, traces, and alerts
  • network and identity policy
  • safe resource limits
  • multi-zone or multi-region topology when the availability target requires it

Kubernetes is an automation substrate. It continuously enforces the contracts you provide, including poorly designed contracts.

2. The mental model: an API plus control loops

Most Kubernetes interactions follow the same pattern:

  1. You submit a desired-state document to the Kubernetes API.
  2. The API validates and stores it.
  3. Controllers observe the stored desired state and the current world.
  4. Controllers create, update, or delete resources to reduce the difference.
  5. The loop repeats because the world can change at any time.

A controller compares three desired replicas with two observed replicas, creates one Pod, and observes again.

Reconciliation is continuous. A healthy cluster is not one that never changes; it is one that keeps converging after change.

This model explains several behaviors that otherwise feel surprising:

  • Deleting a managed Pod does not reduce capacity permanently; its controller replaces it.
  • Editing a live Pod is usually the wrong level of abstraction; its owning Deployment may overwrite or replace it.
  • A command can succeed before the application is ready; the API accepted the desired state, but reconciliation is still happening.
  • Events can be duplicated or missed by your terminal without breaking the control loop; current state remains authoritative.

3. Cluster anatomy

A Kubernetes cluster has a control plane and one or more worker nodes.

Kubernetes cluster anatomy showing kubectl, the API server, etcd, scheduler, controllers, and worker nodes.

The control plane decides

The main control-plane components are:

  • API server: the front door for reads and writes. kubectl, controllers, and other clients communicate through it.
  • etcd: the consistent key-value store holding Kubernetes API state.
  • scheduler: chooses a suitable node for each unscheduled Pod.
  • controller manager: runs control loops that reconcile Deployments, ReplicaSets, Nodes, Jobs, and other resources.

The Kubernetes components reference explains the responsibility of each component in more detail.

Worker nodes run workloads

Each worker node normally includes:

  • kubelet: ensures the Pod definitions assigned to the node are running
  • container runtime: starts and stops containers
  • networking components: implement Service routing and Pod connectivity

In a production cluster, nodes are usually virtual or physical machines. In this guide, kind runs each Kubernetes node as a Docker container. The Kubernetes behavior is real; only the underlying machines are local containers.

4. The five objects to learn first

Kubernetes exposes many API kinds. Begin with these five.

Pod: the smallest scheduled unit

A Pod contains one or more tightly coupled containers that share networking and selected storage. Most application Pods contain one main container, sometimes with sidecars or helper containers.

Pods are replaceable. Do not design around a particular Pod name or IP address surviving forever.

Deployment: a controller for stateless replicas

A Deployment describes an application template, replica count, and rollout strategy. It creates and manages ReplicaSets, which in turn maintain Pods.

Deployment
  └── ReplicaSet
        ├── Pod
        ├── Pod
        └── Pod

You normally update the Deployment, not its Pods directly.

Service: a stable network identity

A Service selects Pods by label and gives clients a stable virtual address. The selected Pods can be replaced without requiring clients to discover their new IP addresses.

ConfigMap and Secret: externalized configuration

A ConfigMap stores non-sensitive configuration. A Secret stores sensitive values in a Kubernetes object designed for controlled distribution. A Secret is not automatically encrypted merely because the object type is named Secret; production clusters need appropriate encryption-at-rest and access-control configuration.

Job and CronJob: finite work

A Job runs work to completion. A CronJob creates Jobs on a schedule. These are better fits than Deployments for migrations, reports, cleanup, and other finite tasks.

5. Prerequisites

You need a 64-bit macOS, Windows, or Linux laptop with:

  • 8 GB of memory; 12 GB is more comfortable
  • 8 GB of free disk space
  • Docker
  • kubectl
  • kind
  • curl or a browser

The lab uses a single Kubernetes node to keep resource usage low.

Check what is already installed

Run:

docker version
kubectl version --client
kind version
curl --version

If a command prints a version, keep that installation and move on.

Install Docker

macOS: install Docker Desktop for Mac, open it, and wait for the engine to start.

Windows: install Docker Desktop for Windows and use the WSL 2 backend.

Linux: install Docker Engine for your distribution and start the Docker service.

Verify the running engine:

docker run --rm hello-world

Install kubectl

macOS with Homebrew:

brew install kubectl

Windows with WinGet:

winget install -e --id Kubernetes.kubectl

Linux: use the current binary instructions from the official kubectl installation guide.

Verify:

kubectl version --client

Install kind

macOS with Homebrew:

brew install kind

Windows with WinGet:

winget install Kubernetes.kind

Linux:

KIND_VERSION=v0.32.0
KIND_ARCH=amd64
[ "$(uname -m)" = "aarch64" ] && KIND_ARCH=arm64
curl -Lo kind "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-${KIND_ARCH}"
chmod +x kind
sudo install -o root -g root -m 0755 kind /usr/local/bin/kind

The kind quick start lists current releases and alternative installation methods.

6. Create the local cluster

Create a directory for the lab:

mkdir kubernetes-intro
cd kubernetes-intro

Create kind-cluster.yaml:

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    image: kindest/node:v1.35.0
    extraPortMappings:
      - containerPort: 30080
        hostPort: 8080
        protocol: TCP

Create the cluster:

kind create cluster \
  --name kubernetes-intro \
  --config kind-cluster.yaml \
  --wait 5m

On PowerShell, place the command on one line:

kind create cluster --name kubernetes-intro --config kind-cluster.yaml --wait 5m

kind creates a kubectl context named kind-kubernetes-intro. Verify it before applying anything:

kubectl config current-context
kubectl cluster-info --context kind-kubernetes-intro
kubectl get nodes -o wide

The node should reach Ready.

7. Deploy the first application

Create hello.yaml:

apiVersion: v1
kind: Namespace
metadata:
  name: intro
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: hello-content
  namespace: intro
data:
  index.html: |
    <!doctype html>
    <html>
      <head><title>Kubernetes intro</title></head>
      <body>
        <h1>Hello from Kubernetes</h1>
        <p>A Service routed this request to a replaceable Pod.</p>
      </body>
    </html>
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello
  namespace: intro
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
        - name: web
          image: nginx:1.27.5-alpine
          ports:
            - name: http
              containerPort: 80
          readinessProbe:
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 2
            periodSeconds: 3
          livenessProbe:
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
          resources:
            requests:
              cpu: 20m
              memory: 24Mi
            limits:
              cpu: 200m
              memory: 96Mi
          volumeMounts:
            - name: content
              mountPath: /usr/share/nginx/html
      volumes:
        - name: content
          configMap:
            name: hello-content
---
apiVersion: v1
kind: Service
metadata:
  name: hello
  namespace: intro
spec:
  type: NodePort
  selector:
    app: hello
  ports:
    - name: http
      port: 80
      targetPort: http
      nodePort: 30080

Apply the desired state:

kubectl apply -f hello.yaml
kubectl rollout status deployment/hello -n intro --timeout=2m

Inspect what Kubernetes created:

kubectl get all -n intro
kubectl get deployment,replicaset,pods,service -n intro

The ownership chain should now be visible: the Deployment owns a ReplicaSet, and the ReplicaSet owns two Pods.

Read what each part of the manifest promises

The manifest says:

  • keep two Pods whose template contains NGINX
  • label each Pod app: hello
  • consider a Pod ready only when an HTTP request to / succeeds
  • restart the container when the liveness check repeatedly fails
  • request a small amount of CPU and memory from the scheduler
  • mount the HTML from a ConfigMap
  • expose ready Pods with app: hello through a Service
  • reserve NodePort 30080, which the kind configuration maps to laptop port 8080

The values are intentionally small for a laptop. Production requests and limits should come from measurement, not from copying this example.

8. Follow a request through the cluster

Open http://localhost:8080 or run:

curl http://localhost:8080

A request enters the mapped kind node port, reaches a stable Service, and routes to a healthy Pod selected by its label.

The Service is stable even though its backing Pods are replaceable.

Inspect the Service and its current endpoints:

kubectl describe service hello -n intro
kubectl get endpointslices -n intro -l kubernetes.io/service-name=hello

The selector app: hello connects the Service to the Pods. If the labels do not match, the Service exists but has no endpoints—a common source of “the Pod is running, but the application is unreachable.”

9. Practice self-healing

List the Pods and keep the command running:

kubectl get pods -n intro --watch

In a second terminal, select one Pod and delete it:

POD_NAME=$(kubectl get pods -n intro -l app=hello \
  -o jsonpath='{.items[0].metadata.name}')
kubectl delete pod -n intro "$POD_NAME"

On PowerShell, copy one Pod name from kubectl get pods -n intro and run:

kubectl delete pod -n intro POD_NAME

Watch what happens:

  1. The old Pod enters termination.
  2. The ReplicaSet observes fewer than two replicas.
  3. It creates a new Pod.
  4. The scheduler assigns the new Pod to the node.
  5. The readiness probe succeeds.
  6. The Service includes the new endpoint.

The replacement Pod has a different name and IP. The Deployment and Service remain stable.

Stop the watch with Ctrl+C, then confirm:

kubectl get pods -n intro -o wide
curl http://localhost:8080

10. Practice scaling

Scale the Deployment from two replicas to four:

kubectl scale deployment hello -n intro --replicas=4
kubectl rollout status deployment/hello -n intro --timeout=2m
kubectl get pods -n intro

The imperative command changes the Deployment's desired replica count in the API. For durable configuration, make the same change in hello.yaml:

spec:
  replicas: 4

Then reapply it:

kubectl apply -f hello.yaml

This prevents a future apply from silently returning the count to two.

Scale back down for the rest of the lab:

kubectl scale deployment hello -n intro --replicas=2

Kubernetes also supports automatic scaling, but an autoscaler needs metrics and a scaling policy. The important first lesson is that scaling changes desired state; controllers perform the actual creation and removal.

11. Practice a rolling update and rollback

Display the current image:

kubectl get deployment hello -n intro \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

On PowerShell, this simpler form avoids shell quoting differences:

kubectl get deployment hello -n intro -o wide

Update NGINX:

kubectl set image deployment/hello -n intro web=nginx:1.28.0-alpine
kubectl rollout status deployment/hello -n intro --timeout=2m
kubectl rollout history deployment/hello -n intro

The Deployment creates a new ReplicaSet. Because maxUnavailable is zero and maxSurge is one, it can create one extra Pod before removing an old one. Readiness gates traffic: a new Pod does not become a Service endpoint until its probe succeeds.

Inspect the result:

kubectl get replicasets,pods -n intro
kubectl get deployment hello -n intro \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
curl http://localhost:8080

Now roll back:

kubectl rollout undo deployment/hello -n intro
kubectl rollout status deployment/hello -n intro --timeout=2m
kubectl get deployment hello -n intro \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

Update hello.yaml back to the image you intend to keep. The file should remain the source of truth rather than becoming an outdated snapshot of the cluster.

12. Run finite and scheduled work

A Deployment is for a continuously running service. Use a Job when the work should finish.

Create jobs.yaml:

apiVersion: batch/v1
kind: Job
metadata:
  name: hello-once
  namespace: intro
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: task
          image: busybox:1.37
          command: ["sh", "-c", "echo one-time task completed; date"]
  backoffLimit: 2
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: hello-schedule
  namespace: intro
spec:
  schedule: "*/5 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: task
              image: busybox:1.37
              command: ["sh", "-c", "echo scheduled task ran; date"]

Apply and inspect the one-time Job:

kubectl apply -f jobs.yaml
kubectl wait -n intro --for=condition=complete job/hello-once --timeout=2m
kubectl logs -n intro job/hello-once
kubectl get jobs,cronjobs -n intro

You do not need to wait five minutes to test the CronJob template. Create an immediate Job from it:

kubectl create job -n intro \
  --from=cronjob/hello-schedule \
  hello-schedule-manual
kubectl wait -n intro \
  --for=condition=complete \
  job/hello-schedule-manual \
  --timeout=2m
kubectl logs -n intro job/hello-schedule-manual

This object choice communicates intent: continuously running service, finite task, or scheduled task.

13. Common Kubernetes use cases

The APIs you just used combine into several common patterns.

Stateless web API

Use a Deployment for interchangeable API replicas, a Service for stable discovery, ConfigMaps and Secrets for configuration, readiness probes for traffic admission, and an autoscaler when load varies.

Background worker

Use a Deployment for workers that continuously consume a queue. Scale replicas based on queue depth or processing latency. Make message handling idempotent because a worker can fail after performing work but before acknowledging a message.

Database migration or report

Use a Job for finite work. Set a bounded retry policy and make the operation safe to repeat. Do not hide migrations inside every application Pod startup; concurrent replicas can race.

Scheduled cleanup or backup trigger

Use a CronJob. Choose a concurrency policy deliberately and monitor missed or failed runs. A schedule expresses when Kubernetes should create a Job, not whether the business operation succeeded.

Stateful system

Kubernetes can run databases and other stateful systems using StatefulSets, persistent volumes, topology rules, and operators. The operational burden does not disappear. Backups, restores, replication semantics, and storage failure modes remain application-specific.

Platform for many teams

Namespaces, quotas, policy, workload identity, standard controllers, and reusable deployment templates can give teams a consistent application platform. This is where Kubernetes often provides more value than any single feature: it creates one extensible operating contract across workloads.

14. A debugging path that scales

When something fails, avoid random commands. Move from desired state to observed state and then to evidence.

1. Confirm context and namespace

kubectl config current-context
kubectl get namespace intro

2. Get a compact workload view

kubectl get deployment,replicaset,pods,service,endpointslices -n intro

3. Describe the failing object

kubectl describe deployment hello -n intro
kubectl describe pod POD_NAME -n intro

The Events section often reveals scheduling failures, image-pull errors, and failed probes.

4. Read bounded logs

kubectl logs -n intro POD_NAME --tail=100
kubectl logs -n intro POD_NAME --previous --tail=100

--previous is useful after a container restarts.

5. Verify selectors and endpoints

kubectl get pods -n intro --show-labels
kubectl get service hello -n intro -o yaml
kubectl get endpointslices -n intro -l kubernetes.io/service-name=hello

6. Check rollout state

kubectl rollout status deployment/hello -n intro
kubectl rollout history deployment/hello -n intro

This sequence works because it follows the system: API selection, controller state, Pod state, container evidence, and network selection.

15. Useful kubectl habits

These commands are worth learning early:

# Discover API resources
kubectl api-resources

# Explain a field from the server's API schema
kubectl explain deployment.spec.strategy

# Preview the server-side result without persisting it
kubectl apply --server-side --dry-run=server -f hello.yaml

# Show differences before an apply
kubectl diff -f hello.yaml

# Watch a resource change
kubectl get pods -n intro --watch

# Select objects by label
kubectl get pods -n intro -l app=hello

# Render structured output for scripts
kubectl get deployment hello -n intro -o json

Prefer declarative files in version control for lasting changes. Imperative commands are useful for learning, inspection, and incident response, but a cluster changed only from a terminal is hard to reproduce or review.

16. What this laptop lab teaches about production

The local cluster is small, but its principles transfer.

A Pod is replaceable

Store durable data outside a stateless Pod. Make shutdown graceful and startup repeatable.

Readiness controls traffic

A process can be running before it is ready. Readiness should reflect whether the instance can serve useful requests now.

Liveness is a restart policy

A bad liveness probe can create an outage by repeatedly killing slow but recoverable instances. Use it only for conditions a restart can fix.

Resource requests affect placement

The scheduler uses requests when choosing a node. Limits constrain runtime usage. Missing or unrealistic values make capacity planning and noisy-neighbor control harder.

Labels are part of the API design

Services, policies, dashboards, and automation select workloads through labels. Treat label names and values as stable contracts.

Rollback is not data rollback

kubectl rollout undo restores a previous Pod template. It does not reverse a database migration or an external side effect. Application releases need compatible data and rollback plans.

One local node is not high availability

This lab demonstrates Kubernetes behavior, not infrastructure resilience. Production availability requires multiple failure domains, redundant control-plane components, durable storage, load balancing, backups, and tested recovery.

17. Clean up

Delete the cluster:

kind delete cluster --name kubernetes-intro

Confirm that the context is gone:

kind get clusters
kubectl config get-contexts

The Docker containers, Pods, Services, Jobs, and namespace inside the cluster are removed. Your kind-cluster.yaml, hello.yaml, and jobs.yaml files remain as a reproducible lab.

18. What to learn next

You now have the vocabulary and operational model needed for deeper Kubernetes topics.

A useful sequence is:

  1. ConfigMaps, Secrets, and environment-specific configuration
  2. requests, limits, probes, and graceful shutdown
  3. Ingress or Gateway API for HTTP entry points
  4. persistent volumes and StatefulSets
  5. RBAC, service accounts, and workload identity
  6. network policies
  7. metrics, logs, tracing, and autoscaling
  8. controllers, operators, and custom resources

The key mental model should remain stable through all of them:

declare desired state
        ↓
observe current state
        ↓
reconcile the difference
        ↓
repeat forever

Sources and further reading

Expanded image100%