Kubernetes turned ten years old in 2024, and it still runs the plumbing behind most of the internet’s container workloads. Pull up a checkout page, stream a video, or call an API today and there is a decent chance a Kubernetes cluster handled the request somewhere behind the scenes.
This Kubernetes tutorial is built for developers who know their way around Docker and a terminal but have not yet stood up a cluster themselves. By the end, you will have a working local cluster running a real application, complete with a Deployment, a Service, an Ingress route, persistent storage, and a Helm chart wrapping the whole thing. The final step points you toward the three managed options, Amazon EKS, Google GKE, and Azure AKS, so you can carry what you build here straight into production.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What Is Kubernetes and Why It Still Matters in 2026
Kubernetes is an open source system for running containers across a group of machines. You describe the state you want (three replicas of a web server, a load balancer in front of them, a persistent volume for logs) and Kubernetes’ control plane continuously works to make the live cluster match that description. Kill a pod and it comes back. Overload a node and the scheduler moves work elsewhere. That reconciliation loop is the entire idea, and almost everything else in the ecosystem builds on top of it.
The design traces back to Borg, Google’s internal cluster manager, which ran the company’s own services for more than a decade before Kubernetes rebuilt the same ideas as an open source, vendor-neutral project in 2014. Google donated it to the newly formed Cloud Native Computing Foundation shortly after, which is why CNCF, not any single vendor, still governs the project today. That neutrality is a big part of why AWS, Google Cloud, and Azure all ship their own managed flavor of the same open source core instead of competing with incompatible alternatives.
The platform matters in 2026 for a fairly simple reason: nothing else has matched its ecosystem. The Cloud Native Computing Foundation’s catalog of tools, the volume of Helm charts published by vendors, and the sheer number of engineers who already know basic kubectl commands make Kubernetes the default choice once a team outgrows a single Docker host. Even AI workloads lean on it now. GPU scheduling, batch inference jobs, and model-serving frameworks increasingly ship with Kubernetes manifests as the primary deployment path rather than an afterthought, which is part of why our guide to setting up vLLM ends with a Kubernetes deployment option.
None of that makes Kubernetes simple. The learning curve is real, and plenty of tutorials skip past the parts that actually trip people up: indentation errors in YAML, pods stuck in a Pending state, Services with zero endpoints. This guide covers those failure points directly, with the exact commands to diagnose and fix them, plus the pitfalls worth avoiding before they cost you an afternoon.
Prerequisites: Tools, Versions, and System Requirements
Before starting this Kubernetes tutorial, get the following installed. None of it is exotic, and most of it installs in under ten minutes on macOS, Linux, or Windows with WSL2.
- A machine with at least 4 CPU cores and 8GB of RAM free (16GB is more comfortable if you plan to run multiple pods with resource requests).
- Docker Desktop, Docker Engine, or Podman installed and running, since your local cluster tool needs a container runtime to sit on top of.
- A terminal you’re comfortable in. Every command below works on macOS, Linux, and Windows (via WSL2 or PowerShell with minor syntax tweaks).
- About 130 minutes if you follow every step end to end, including reading the explanations, not just pasting commands.
Kubernetes itself currently sits at version 1.36, with 1.36.2 as the latest patch on the official release page. The project maintains the three most recent minor branches at any time: 1.36 (supported through 2027-06-28), 1.35 (latest patch 1.35.6, supported through 2027-02-28), and 1.34 (latest patch 1.34.9, supported through 2026-10-27), according to the Kubernetes end-of-life tracker. If you are installing tools fresh, the table below lists the versions this tutorial was tested against.
| Tool | Version Used in This Guide | Purpose | Check Command |
|---|---|---|---|
| kubectl | 1.36.x | Command-line client that talks to the cluster’s API server | kubectl version –client |
| kind | v0.32.0 | Runs a Kubernetes cluster inside Docker containers | kind version |
| minikube | v1.38.1 | Alternative single-VM or container-based local cluster | minikube version |
| k3s | v1.36.3+k3s1 | Lightweight distribution for edge devices and small servers | k3s –version |
| Helm | v4.2.3 | Package manager for bundling Kubernetes manifests into charts | helm version |
| Docker | Latest stable | Container runtime the local cluster tools build on | docker version |
You do not need all three cluster tools. Pick one in Step 2 based on the comparison table there. Everything else on this list is required regardless of which one you choose.
Three optional tools make the rest of this tutorial noticeably easier, though none of them are strictly required:
- k9s, a terminal UI that turns kubectl get and kubectl describe into a scrollable, searchable dashboard instead of separate commands you re-run constantly.
- The Kubernetes extension for VS Code, which adds YAML autocomplete and inline schema validation, catching indentation mistakes before you run kubectl apply.
- Lens, a desktop GUI for browsing multiple clusters and contexts side by side, useful once you’re juggling a local cluster and a cloud one at the same time.
Step 1: Install kubectl
kubectl is the command-line tool that talks to a Kubernetes API server. Every interaction in this tutorial, creating objects, checking pod status, reading logs, goes through it, so get it installed and confirmed working before touching a cluster.
# macOS (Homebrew)
brew install kubectl
# Linux
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
# Windows (via winget)
winget install -e --id Kubernetes.kubectl
# Verify installation
kubectl version --client
Expected output looks like this, with the exact patch version depending on when you installed it:
Client Version: v1.36.1
Kustomize Version: v5.7.1
If the command isn’t found, double check your PATH includes the install directory. On Linux, /usr/local/bin is usually already there. On Windows, winget handles this automatically, but a fresh terminal window is sometimes needed before the change takes effect. The official install docs cover a handful of other package managers if Homebrew or winget aren’t your thing.
Step 2: Choose and Launch a Local Cluster (minikube, kind, or k3s)
You have three solid options for running Kubernetes on a laptop, and picking between them mostly comes down to how you plan to use the cluster afterward.
| Feature | minikube | kind | k3s |
|---|---|---|---|
| Best for | Beginners, GUI dashboard fans | CI pipelines, fast multi-node testing | Edge devices, low-resource servers |
| Runs on | VM or container driver | Docker containers only | Bare metal or VM, no Docker required |
| Multi-node support | Yes, via –nodes flag | Yes, defined in a YAML config | Yes, join additional agents manually |
| Startup time | 60 to 90 seconds | 20 to 40 seconds | Under 20 seconds |
| Resource footprint | Moderate to heavy | Light | Very light, built for constrained hardware |
| Built-in addons | Dashboard, ingress, metrics-server | Minimal, add your own | Traefik ingress, local-path storage |
This tutorial uses kind in the examples because it starts fast and mirrors what CI systems use, but every YAML manifest below works identically on minikube or k3s. If you’d rather use minikube, the official quickstart walks through the same start and get nodes flow.
# Install kind (requires Go, or download a prebuilt binary from GitHub releases)
go install sigs.k8s.io/[email protected]
# Create a cluster named "demo"
kind create cluster --name demo
# Confirm the cluster is up
kubectl cluster-info --context kind-demo
kubectl get nodes
Expected output after a successful create:
NAME STATUS ROLES AGE VERSION
demo-control-plane Ready control-plane 45s v1.36.1
If kind create cluster hangs for more than a couple of minutes, Docker is almost always the culprit. Confirm Docker Desktop is actually running (not just installed) and that it has at least 4GB of memory allocated in its settings panel.
Step 3: Learn the Core Objects (Pods, Deployments, and Services)
Three objects cover most of what you’ll touch as a beginner. A Pod is the smallest deployable unit, one or more containers that share networking and storage. You rarely create Pods directly, though, because a bare Pod that crashes just stays dead. Instead, you wrap Pods in higher-level controllers that keep them running.
A Deployment is that controller for stateless workloads. According to the official Kubernetes documentation, “a Deployment provides declarative update for Pods and ReplicationControllers.” In plain terms, you tell it how many replicas you want and what container image to run, and it handles creating, replacing, and rolling out changes to the underlying Pods for you. As Mirantis puts it in its introduction to Kubernetes YAML, “a Kubernetes Deployment is a resource in Kubernetes that manages the deployment and lifecycle of applications.” Kubernetes educator TechWorld with Nana frames the same idea from the user’s side: “Kubernetes Deployments allow you to manage your workloads in a declarative way,” meaning you describe the end state and let the controller figure out how to get there.
A Service solves a different problem. Pods get replaced constantly, and each replacement gets a new internal IP address. A Service gives a stable name and IP that automatically routes to whichever Pods currently match its label selector, so the rest of your application never needs to track individual Pod addresses. Put together, Deployments keep your Pods alive and Services make them reachable. Everything else in this tutorial, Ingress, ConfigMaps, PersistentVolumeClaims, builds on top of that pair.
A quick reference for the objects you’ll create over the next nine steps:
| Object | What It Does | Where You’ll Use It |
|---|---|---|
| Namespace | Isolates a group of objects from the rest of the cluster | Step 4, wraps the entire demo app |
| Deployment | Keeps a set number of Pod replicas running and rolls out updates | Step 4 |
| Service | Gives a stable network address to a changing set of Pods | Step 5 |
| Ingress | Routes external HTTP traffic to a Service based on host and path | Step 7 |
| ConfigMap / Secret | Injects configuration or sensitive values into a Pod | Step 4 and Step 8 |
| PersistentVolumeClaim | Requests durable storage that survives a Pod restart | Step 9 |
Step 4: Write and Apply Your First Deployment Manifest
Kubernetes objects are usually defined in YAML and applied declaratively, meaning you describe the desired state and let the cluster reconcile toward it rather than issuing imperative “create this, then that” commands. Kelsey Hightower, the engineer whose Kubernetes workshops shaped how a generation of developers learned the tool, put it directly: “Deployments are the preferred way to manage application deployments.” He also noted the mechanics underneath: “Deployments sit on top of ReplicaSets and add the ability to define how updates to Pods should be rolled out,” which is what lets you change an image tag and get a controlled rollout instead of an all-at-once restart.
Start with a namespace and a ConfigMap holding a small HTML file, then a Deployment that mounts it into an nginx container.
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: hello-html
namespace: demo
data:
index.html: |
<html>
<body>
<h1>Hello from Kubernetes</h1>
<p>Served by an nginx pod running in a kind cluster.</p>
</body>
</html>
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-k8s
namespace: demo
labels:
app: hello-k8s
spec:
replicas: 3
selector:
matchLabels:
app: hello-k8s
template:
metadata:
labels:
app: hello-k8s
spec:
containers:
- name: web
image: nginx:stable-alpine
ports:
- containerPort: 80
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
volumes:
- name: html
configMap:
name: hello-html
kubectl create namespace demo
kubectl apply -f configmap.yaml
kubectl apply -f deployment.yaml
kubectl get pods -n demo
kubectl rollout status deployment/hello-k8s -n demo
A successful rollout prints this once all three replicas are ready:
NAME READY STATUS RESTARTS AGE
hello-k8s-7d9f8c6b45-2kxqz 1/1 Running 0 12s
hello-k8s-7d9f8c6b45-9mnbv 1/1 Running 0 12s
hello-k8s-7d9f8c6b45-plw2t 1/1 Running 0 12s
deployment "hello-k8s" successfully rolled out
Notice the resource requests and limits on the container. Skipping those is one of the most common mistakes newcomers make, and it’s the first entry in the pitfalls section further down.
Change the image tag in deployment.yaml and re-run kubectl apply, and the Deployment controller rolls out the new version one Pod at a time by default, waiting for each replacement to pass its readiness probe before moving to the next. If the new version turns out to be broken, you don’t need to hand-edit anything to get back to a working state:
kubectl rollout history deployment/hello-k8s -n demo
kubectl rollout undo deployment/hello-k8s -n demo
# Roll back to a specific revision instead of the previous one
kubectl rollout undo deployment/hello-k8s -n demo --to-revision=2
That history is what makes a Kubernetes deployment tutorial like this one worth doing properly instead of just poking at a cluster randomly. The rollout is recorded, reversible, and scriptable, which is the whole point of describing state declaratively instead of running one-off imperative commands against production.
Step 5: Expose the App with a Service
The Deployment is running, but nothing outside the cluster (or even other Pods inside it) can reach those three replicas yet without a stable address. A ClusterIP Service fixes that for internal traffic.
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: hello-k8s
namespace: demo
spec:
selector:
app: hello-k8s
ports:
- port: 80
targetPort: 80
type: ClusterIP
kubectl apply -f service.yaml
kubectl port-forward svc/hello-k8s 8080:80 -n demo
With the port-forward running, open http://localhost:8080 in a browser or run curl http://localhost:8080 in another terminal tab. You should see the “Hello from Kubernetes” page from the ConfigMap you applied in Step 4. If the connection refuses instead, check that the Service’s selector field matches the Deployment’s Pod labels exactly. A mismatched label is the single most common reason a Service shows zero endpoints, and it’s easy to typo when you’re copying YAML between files.
Step 6: Scale the Deployment Up and Down
Scaling is where the Deployment controller earns its keep. You can scale manually for a quick test, or hand the job to a HorizontalPodAutoscaler that reacts to real CPU load.
kubectl scale deployment/hello-k8s --replicas=6 -n demo
kubectl get pods -n demo -w
kubectl autoscale deployment hello-k8s --cpu-percent=70 --min=3 --max=10 -n demo
kubectl get hpa -n demo
The -w flag watches for changes live, so you’ll see new Pods flip from Pending to ContainerCreating to Running in real time. Scaling down works the same way, just with a lower number, and Kubernetes terminates the extra Pods gracefully rather than killing them abruptly, assuming you haven’t set an aggressively short terminationGracePeriodSeconds.
The HorizontalPodAutoscaler needs the metrics-server addon running to read CPU usage. kind and k3s don’t ship it by default, so if kubectl get hpa shows unknown under targets instead of a percentage, that’s almost always a missing metrics-server rather than a broken autoscaler.
Step 7: Route External Traffic with an Ingress Controller
Port-forwarding is fine for a quick check, but it isn’t how real traffic reaches a cluster. An Ingress controller (commonly ingress-nginx) watches Ingress objects and configures routing rules based on hostname and path, which is much closer to how a production load balancer behaves.
# Install ingress-nginx onto the kind cluster
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
# Wait for the controller pod to be ready
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=120s
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello-k8s
namespace: demo
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: hello.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello-k8s
port:
number: 80
kubectl apply -f ingress.yaml
echo "127.0.0.1 hello.local" | sudo tee -a /etc/hosts
curl http://hello.local
A 404 response here almost always means the Ingress class doesn’t match the controller you installed, or the controller pod isn’t in a Ready state yet. Run kubectl get pods -n ingress-nginx to confirm it’s actually running before troubleshooting the Ingress object itself.
Step 8: Manage Configuration with ConfigMaps and Secrets
You already used a ConfigMap in Step 4 for the HTML file. Secrets work almost identically at the API level but signal to Kubernetes (and to anyone reading the manifest) that the contents are sensitive. They’re base64-encoded, not encrypted, at rest by default in most local setups, so don’t treat them as a substitute for a real secrets manager in production. Our AWS Secrets Manager setup guide covers that upgrade path if you’re heading toward AWS.
kubectl create secret generic hello-api-key \
--from-literal=API_KEY=demo-key-change-me \
-n demo
# Confirm it exists and decode the value
kubectl get secret hello-api-key -n demo -o jsonpath="{.data.API_KEY}" | base64 -d
To use it, reference the Secret from a container’s env block with secretKeyRef, the same way you’d reference a ConfigMap with configMapKeyRef. Keep the actual YAML files with real secret values out of version control. A .gitignore entry for anything named secret.yaml is a cheap habit that prevents an expensive mistake.
Step 9: Add Persistent Storage with a PVC
Containers are ephemeral by design. Anything written inside one disappears the moment it restarts. For workloads that need to keep data around, like logs, uploaded files, or a database, you request storage through a PersistentVolumeClaim, and Kubernetes binds it to an available PersistentVolume based on the cluster’s StorageClass.
# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: hello-logs
namespace: demo
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
kubectl apply -f pvc.yaml
kubectl get pvc -n demo
kind and minikube both ship a default StorageClass, so the claim should move from Pending to Bound within a few seconds. On a fresh cluster with no default StorageClass configured, it stays Pending indefinitely, which is item eight in the troubleshooting table below. Mount the claim into the Deployment the same way you mounted the ConfigMap, just with a persistentVolumeClaim block instead of configMap under volumes.
Step 10: Package the Application with Helm
Applying five separate YAML files by hand works for a tutorial, but it doesn’t scale to a real application with multiple environments. Helm packages a set of manifests into a versioned chart with configurable values, so you can deploy the same chart to staging and production with different settings.
# Install Helm (see the official docs for other package managers)
brew install helm # macOS
choco install kubernetes-helm # Windows
helm version
# Scaffold a chart, then move deployment.yaml, service.yaml,
# and ingress.yaml into hello-k8s-chart/templates/
helm create hello-k8s-chart
helm install hello-k8s ./hello-k8s-chart --namespace demo
helm list -n demo
helm list confirms the release and its status:
NAME NAMESPACE REVISION STATUS CHART APP VERSION
hello-k8s demo 1 deployed hello-k8s-chart-0.1.0 1.0.0
To change the replica count without editing YAML directly, pass a value at upgrade time:
helm upgrade hello-k8s ./hello-k8s-chart --namespace demo --set replicaCount=5
See the Helm installation guide for the full list of supported package managers, including apt and a raw binary download for Linux distributions without Homebrew.
Step 11: Monitor, Log, and Debug the Cluster
Once something is deployed, you’ll spend more time reading its state than writing new YAML. These four kubectl commands cover most day-to-day debugging.
kubectl logs deploy/hello-k8s -n demo --tail=50
kubectl describe pod <pod-name> -n demo
kubectl top pods -n demo
kubectl get events -n demo --sort-by=.lastTimestamp
logs shows what the application itself printed. describe shows what Kubernetes did while scheduling and starting the Pod, including the exact error if it failed a probe or couldn’t pull an image, usually the fastest path to a root cause. top needs metrics-server, same as the autoscaler in Step 6. get events sorted by timestamp gives a chronological feed of everything that happened in the namespace, which is often the quickest way to spot a scheduling failure you didn’t even know to look for.
For anything beyond a single cluster, pair these kubectl commands with a proper log aggregator. Our AWS CloudTrail setup guide covers the audit-log side of that if your cluster runs on EKS.
Step 12: Graduate to a Managed Cloud Cluster (EKS, GKE, or AKS)
Everything above works the same way on a managed cluster, since kubectl and your YAML don’t know or care whether the API server is running on your laptop or in a cloud region. The only new decision is which managed control plane to pay for.
| Amazon EKS | Google GKE | Azure AKS | |
|---|---|---|---|
| Control plane price | $0.10/cluster/hour (standard support) | $0.10/cluster/hour (flat, all modes) | Free tier available; Standard is pay-as-you-go |
| Free tier | None for the control plane | $74.40/month credit, about one free cluster | Free tier: no charge for cluster management |
| Uptime SLA | Backed SLA on the API server | Backed SLA on Standard and Autopilot | 99.95% with availability zones, 99.9% without (Standard/Premium only) |
| Max nodes (typical) | Scales with node groups | Scales with node pools | 1,000 (Free), 5,000 (Standard/Premium) |
| Best for | Teams already on AWS | Teams that want the least ops overhead | Teams already on Azure or needing 24-month LTS |
Extended support on EKS, for clusters running an older Kubernetes minor version past the standard 14-month window, runs $0.60 per cluster per hour instead of $0.10, according to AWS’s EKS pricing page. That gap is a strong incentive to keep upgrading rather than let a cluster drift onto an unsupported version.
Amazon EKS
eksctl create cluster --name demo-eks --region us-east-1 --nodes 3
eksctl provisions the VPC, control plane, and a managed node group in one command, typically 15 to 20 minutes end to end. It’s the fastest path if your infrastructure already lives on AWS. Pairing this with Terraform for AWS is the more common production setup once you’re past the proof-of-concept stage, since it keeps the cluster definition in version control alongside everything else.
Google GKE
gcloud container clusters create-auto demo-gke --region us-central1
The create-auto flag provisions an Autopilot cluster, which bills per pod resource request ($0.0445 per vCPU-hour and $0.0049225 per GiB-hour on demand, per Google’s GKE pricing page) instead of per node, so you stop paying for idle capacity you provisioned but didn’t use. Standard mode is still available if you need direct node-level control.
Azure AKS
az aks create --resource-group demo-rg --name demo-aks --tier standard --generate-ssh-keys
AKS is the only one of the three with a genuinely free control plane tier, aimed at development and clusters under ten nodes with no financially backed SLA. Flip to –tier standard for production, which adds the uptime SLA and support for up to 5,000 nodes, per Microsoft’s AKS tier documentation. Premium adds 24-month long-term support on top of Standard, useful if your compliance team dictates a slower upgrade cadence than the community’s roughly one-year patch window.
Clean Up: Tearing Down the Cluster When You’re Done
A local cluster left running quietly eats CPU and memory in the background, and a cloud cluster left running quietly eats your budget. Both are worth tearing down the moment you’re done testing.
# Remove just the Helm release, keeping the cluster
helm uninstall hello-k8s --namespace demo
# Delete the local kind cluster entirely
kind delete cluster --name demo
# minikube equivalent
minikube delete
# Delete a cloud cluster (example for EKS)
eksctl delete cluster --name demo-eks --region us-east-1
Deleting a kind or minikube cluster removes every object inside it instantly since nothing was ever persisted outside your machine. Deleting a cloud cluster takes longer, usually a few minutes, because the provider also has to tear down the load balancers, node groups, and networking it provisioned on your behalf. Confirm the deletion finished in your provider’s console, since a half-deleted cluster can still generate charges.
The Complete Project, Start to Finish
Once you’ve worked through every step, the project folder looks like this, with raw manifests for reference and a Helm chart for actual deployment:
hello-k8s/
├── manifests/
│ ├── namespace.yaml
│ ├── configmap.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── secret.yaml
│ └── pvc.yaml
└── hello-k8s-chart/
├── Chart.yaml
├── values.yaml
└── templates/
├── deployment.yaml
├── service.yaml
└── ingress.yaml
That’s a working, if intentionally small, blueprint for a real deployment: three replicas behind a Service, routed through Ingress, configured via a ConfigMap and Secret, backed by persistent storage, and packaged for repeatable installs with Helm. Swap the nginx image for your own application container and the rest of the structure barely changes.
Common Pitfalls When Learning Kubernetes
Most of the friction people hit isn’t Kubernetes being obscure, it’s a handful of habits that work fine on a single Docker host and quietly break once a scheduler and multiple nodes enter the picture.
- Skipping resource requests and limits. Without them, the scheduler has no idea how much CPU or memory a Pod actually needs, which leads to overcrowded nodes and unpredictable evictions under pressure.
- Using the latest tag in production images. It defeats the entire point of a declarative rollout, since two Pods created minutes apart can silently run different code with the same manifest.
- No readiness or liveness probes. Without a readiness probe, Kubernetes sends traffic to a Pod the instant its container starts, even if the application inside needs another few seconds to boot.
- Editing live objects with kubectl edit instead of updating the source YAML. The change works until the next kubectl apply or a GitOps sync overwrites it, and now the fix you shipped last week is gone.
- Deploying everything into the default namespace. It works right up until two teams both create a Service named api and one of them silently overwrites the other’s traffic routing.
- Hardcoding secrets directly in manifests. A base64-encoded API key committed to a public or even private repository is still an exposed API key, since base64 is an encoding, not encryption.
- Letting kubectl drift too far from the cluster’s version. Kubernetes supports one minor version of skew between client and server. A kubectl that’s two or three versions behind the cluster will throw confusing errors on newer API fields instead of a clear version warning.
None of these show up in a “hello world” tutorial, which is exactly why they catch people a few weeks in, once a real application with real traffic is on the line.
Troubleshooting: Common Kubernetes Errors and Fixes
Almost every problem you’ll hit in your first month shows up as one of these nine symptoms. Start with kubectl describe pod, since it names the exact failure in most cases.
| Symptom | Likely Cause | Fix |
|---|---|---|
| ImagePullBackOff | Wrong image name, private registry with no credentials, or a typo in the tag | Run kubectl describe pod and check the exact image string; add an imagePullSecret if the registry is private |
| CrashLoopBackOff | The container starts and immediately exits, often a missing env var or a config the app can’t find | Check kubectl logs –previous to see the last crash’s output before the restart wiped it |
| Pod stuck in Pending | Cluster has no node with enough free CPU/memory, or an unbound PVC is blocking scheduling | Run kubectl describe pod and read the Events section; it names the exact scheduling constraint that failed |
| CreateContainerConfigError | Deployment references a ConfigMap or Secret key that doesn’t exist | Confirm the key names in kubectl get configmap/secret match exactly what the Deployment references |
| Service has zero endpoints | The Service’s label selector doesn’t match any running Pod’s labels | Compare kubectl get pods –show-labels against the Service’s selector field character for character |
| Ingress returns 404 or default backend | ingressClassName doesn’t match the installed controller, or the controller isn’t Ready yet | Run kubectl get pods -n ingress-nginx and confirm the controller pod is Running before debugging further |
| OOMKilled | Container exceeded its memory limit and the kernel terminated it | Raise the memory limit or fix a leak; check kubectl describe pod for the exact exit reason |
| PVC stuck in Pending | No StorageClass is set as default, so Kubernetes has nothing to bind the claim to | Run kubectl get storageclass and set one as default, or specify storageClassName explicitly in the PVC |
| kubectl: connection refused | Wrong context selected, or the cluster’s API server isn’t reachable from your network | Run kubectl config get-contexts and kubectl config use-context to confirm you’re pointed at the right cluster |
If none of those match, check CoreDNS next. A surprising number of “random” application errors trace back to DNS resolution failing inside the cluster, which you can confirm by running a throwaway debug pod: kubectl run -it –rm debug –image=busybox –restart=Never — nslookup kubernetes.default.
Advanced Tips for Running Kubernetes in Production
Finishing this Kubernetes tutorial gets you a working local cluster and a real mental model of the core objects. Production adds a handful of concerns that don’t matter much on a laptop but matter a great deal once real users depend on the cluster.
- Set a PodDisruptionBudget before you drain a node. Without one, a routine node upgrade can take down every replica of a Deployment at once if the scheduler happens to place them all on the same node.
- Adopt RBAC and Pod Security Standards early. The official Kubernetes security documentation covers namespace-scoped roles and pod-level restrictions that are far more painful to retrofit onto a cluster already running dozens of applications.
- Add NetworkPolicies once you have more than a couple of services. By default, every Pod in a cluster can talk to every other Pod, which is rarely what you actually want once multiple teams share the same cluster.
- Look at a cluster autoscaler alongside the HorizontalPodAutoscaler. The HPA adds Pods, while a cluster autoscaler adds the nodes those Pods actually need, and running one without the other just moves the bottleneck.
- Consider GitOps once more than one person touches the cluster. Tools like Argo CD or Flux apply manifests from a Git repository automatically, which closes the exact gap that the “kubectl edit in production” pitfall above describes.
None of this needs to happen on day one. Build the muscle memory from this tutorial first, then layer in these controls as the cluster starts carrying real traffic.
Frequently Asked Questions
What is Kubernetes used for?
Running and managing containerized applications across a group of machines, handling restarts, scaling, networking, and rollouts automatically instead of requiring manual intervention for each one.
Is this Kubernetes tutorial enough to get a job?
It covers the core objects and workflow that show up in almost every entry-level role, but hiring managers typically also want to see RBAC, networking policy, and some exposure to a managed cloud offering, all covered briefly in the advanced tips section above.
Do I need Docker to use Kubernetes?
You need a container runtime, and Docker is the easiest one to set up locally. The cluster itself typically runs containerd underneath, but Docker Desktop is what most of the local cluster tools in this guide build on top of.
What’s the difference between minikube, kind, and k3s?
minikube is the most beginner-friendly with built-in addons like a dashboard, kind is the fastest to start and closest to what CI pipelines use, and k3s is the lightest option, built for edge devices and low-resource servers. Full comparison is in Step 2 above.
How much does a production Kubernetes cluster cost?
The control plane itself runs about $0.10 per cluster per hour on AWS and Google Cloud, with Azure offering a free tier for small clusters. The real cost is almost always the worker nodes (compute, storage, and networking) sitting on top of that, which scales with how much your application actually needs.
Can I run Kubernetes without a cloud provider?
Yes. k3s in particular is built for exactly that, running on bare metal or small VMs without any cloud dependency, and it’s a common choice for home labs and edge deployments.
What is the difference between a Pod and a Deployment?
A Pod is one running instance of your container. A Deployment is the controller that creates, replaces, and scales Pods according to the replica count and image you specify, and it’s what you should be creating directly in almost every case.
Is Helm required to use Kubernetes?
No, plain YAML manifests applied with kubectl work fine, as this tutorial shows through Step 9. Helm becomes valuable once you’re deploying the same application to multiple environments with different configuration values, or installing third-party software that publishes its own chart.
What’s the difference between kubectl apply and kubectl create?
kubectl create makes a new object and fails if it already exists. kubectl apply is declarative: it creates the object if it’s missing or patches it to match the YAML if it’s already there, which is why every command in this tutorial uses apply instead of create.
Can Kubernetes run Windows containers?
Yes, with Windows node pools alongside Linux ones, though Windows nodes are heavier and slower to start. All three managed providers in Step 12 support mixed Linux and Windows node pools in the same cluster.
Related Coverage
- How to Set Up Terraform for AWS: 13 Steps, 75 Min [2026]
- ECR vs ACR vs Artifact Registry: $5/mo Floor Gap [2026]
- How to Set Up Google Cloud Run: 13 Steps, 80 Min [2026]
- AWS Fargate vs Lambda: 70% Spot Savings, 15-Min Cap [2026]
- How to Set Up vLLM: 12 Steps, 90 Min [2026]
- Step Functions vs Airflow: $0 vs $357/mo Floor [2026]
For more cloud computing tutorials and comparisons, browse the full cloud computing archive.


