Kubernetes Pods, Deployments, and Services
A hands-on tour of the three Kubernetes objects you will use every day — pods, deployments, and services — with real YAML, the kubectl commands to apply them, and how they fit together.
What you'll learn
- ✓How to write and apply a Pod YAML — and why you rarely do
- ✓How a Deployment manages replicas and performs rolling updates
- ✓The three Service types: ClusterIP, NodePort, and LoadBalancer
- ✓The everyday kubectl commands: apply, get, describe, logs, exec
- ✓How namespaces partition a cluster
Prerequisites
- •A mental model of Kubernetes — see What Is Kubernetes?
- •A local cluster (Minikube, kind, k3d, or Docker Desktop with K8s enabled)
The previous post introduced the vocabulary. This one gets your hands dirty: real YAML, real kubectl commands, and a small app you can deploy, scale, update, and tear down. By the end you will have the muscle memory to read most production Kubernetes manifests.
Set the scene
You need a local cluster and kubectl configured to talk to it. Any of these will do:
# Minikube
minikube start
# kind
kind create cluster
# k3d
k3d cluster create demo
# Docker Desktop — enable Kubernetes in Settings
Then verify:
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# minikube Ready control-plane 1m v1.30.0
One node is enough for everything below.
A pod, the smallest unit
A pod wraps one or more containers that share a network and storage. In 95% of cases there is exactly one container per pod, so it is fine to think of a pod as “a running container with extra Kubernetes metadata.”
Create pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: hello-pod
labels:
app: hello
spec:
containers:
- name: web
image: nginx:1.27-alpine
ports:
- containerPort: 80
Apply it:
kubectl apply -f pod.yaml
# pod/hello-pod created
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# hello-pod 1/1 Running 0 10s
The pod is alive. Look at it more closely:
kubectl describe pod hello-pod
kubectl logs hello-pod
describe dumps everything Kubernetes knows about the pod — events, IP address, container state. logs shows stdout/stderr from the container, same as docker logs.
Now delete it:
kubectl delete pod hello-pod
# pod "hello-pod" deleted
And here is the catch: the pod is gone forever. Nothing brings it back. Bare pods are not self-healing. That is why you almost never deploy a bare pod — you deploy a Deployment that manages pods on your behalf.
A Deployment, the everyday object
A Deployment says “I want N copies of this pod, and I want updates to roll out gradually.” It owns a ReplicaSet which owns the pods.
Create deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
spec:
replicas: 3
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: web
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
Apply and inspect:
kubectl apply -f deployment.yaml
# deployment.apps/hello created
kubectl get deployments
# NAME READY UP-TO-DATE AVAILABLE AGE
# hello 3/3 3 3 20s
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# hello-6d9c7f8b8d-abcde 1/1 Running 0 20s
# hello-6d9c7f8b8d-fghij 1/1 Running 0 20s
# hello-6d9c7f8b8d-klmno 1/1 Running 0 20s
Three pods, with random suffixes appended to the deployment name. Now delete one by hand:
kubectl delete pod hello-6d9c7f8b8d-abcde
kubectl get pods
Within seconds, a new pod with a new suffix appears. The Deployment notices the missing replica and recreates it. That is self-healing.
Scaling
You can change the replica count without editing YAML:
kubectl scale deployment hello --replicas=5
kubectl get pods
# 5 pods running
Or edit the YAML, change replicas: 3 to replicas: 5, and re-apply. Either way works — the second is preferred because it keeps Git as the source of truth.
Rolling updates
Change the image tag in deployment.yaml:
image: nginx:1.27-alpine # change to nginx:1.28-alpine
Apply again:
kubectl apply -f deployment.yaml
kubectl rollout status deployment/hello
# deployment "hello" successfully rolled out
Kubernetes brings up new pods running 1.28, waits for them to be ready, then terminates old 1.27 pods — one or two at a time. There is no downtime. If something goes wrong:
kubectl rollout undo deployment/hello
instantly reverts to the previous version.
Try it yourself. With the deployment running, run watch kubectl get pods in one terminal. In another, change the image tag and re-apply. Watch new pods appear, become ready, and old pods disappear. Then run kubectl rollout undo and watch the reverse happen.
A Service, the stable address
Pods have IP addresses but those IPs change every time a pod is recreated. A Service is a stable virtual address that automatically load-balances across whichever pods match its selector.
There are three Service types you will use.
ClusterIP — inside the cluster only
The default. The service is reachable only from inside the cluster. Use it for internal communication (web → database, web → cache).
Create service-clusterip.yaml:
apiVersion: v1
kind: Service
metadata:
name: hello
spec:
type: ClusterIP
selector:
app: hello
ports:
- port: 80
targetPort: 80
Apply:
kubectl apply -f service-clusterip.yaml
kubectl get service hello
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# hello ClusterIP 10.96.123.45 <none> 80/TCP 5s
From any pod in the cluster, http://hello (yes, just the service name — Kubernetes provides DNS) routes to one of the three nginx pods.
NodePort — exposed on every node
Opens a high-numbered port (30000–32767) on every node in the cluster. Useful for local development and quick demos:
apiVersion: v1
kind: Service
metadata:
name: hello-np
spec:
type: NodePort
selector:
app: hello
ports:
- port: 80
targetPort: 80
nodePort: 30080
Apply and hit it from your laptop:
kubectl apply -f service-nodeport.yaml
# On Minikube
minikube service hello-np --url
# On Docker Desktop / kind / k3d
curl http://localhost:30080
NodePort is great for learning, less so for production — you typically do not want random high-numbered ports facing the internet.
LoadBalancer — a real public IP
Asks the cloud provider for a managed load balancer with a public IP. On AWS, this provisions an ELB; on GKE, a Google load balancer.
apiVersion: v1
kind: Service
metadata:
name: hello-lb
spec:
type: LoadBalancer
selector:
app: hello
ports:
- port: 80
targetPort: 80
On a real cloud cluster this would get a public IP within a minute or two. On a local cluster, EXTERNAL-IP stays <pending> because there is no cloud LB to provision.
In production, most teams use a single LoadBalancer in front of an Ingress controller, then route many services through it with Ingress rules — but that is a topic for another post.
How the pieces fit
Picture three layers stacked on top of each other:
Service (stable address: hello)
|
v (selector: app=hello)
Pods (3 replicas, ephemeral IPs)
|
v
Deployment (desired: 3 replicas, image: nginx:1.27)
The Deployment creates and replaces pods. The Service routes traffic to whichever pods happen to be alive and match its label selector. Pods themselves are disposable — you can delete them at will and the Deployment makes more.
This separation is why Kubernetes is so flexible: you can update the Deployment without touching the Service, scale pods up and down without changing addresses, and so on.
Namespaces, briefly
Everything above lives in a namespace — by default, the one called default. Namespaces partition a cluster so different teams or environments do not collide:
kubectl create namespace staging
kubectl apply -f deployment.yaml -n staging
kubectl get pods -n staging
Inside a namespace, names must be unique. Across namespaces, they can repeat. A service called hello in staging is reachable as hello.staging.svc.cluster.local from anywhere in the cluster.
For local learning, the default namespace is fine. For production, you typically separate by environment (prod, staging) or by team.
The everyday kubectl kit
You will use these commands constantly. Worth committing to memory:
# Apply YAML
kubectl apply -f file.yaml
kubectl apply -f . # all YAML in current directory
# List things
kubectl get pods
kubectl get pods -o wide # include node and IP columns
kubectl get all # everything in the namespace
# Inspect
kubectl describe pod <name>
kubectl describe deployment <name>
# Logs
kubectl logs <pod>
kubectl logs -f <pod> # follow, like tail -f
kubectl logs <pod> -c <container> # specific container in a multi-container pod
# Exec into a pod
kubectl exec -it <pod> -- sh
# Delete
kubectl delete -f file.yaml
kubectl delete pod <name>
# Rollouts
kubectl rollout status deployment/<name>
kubectl rollout undo deployment/<name>
kubectl rollout history deployment/<name>
The -f flag takes a file or a directory; -n selects a namespace; -o yaml or -o json returns full machine-readable output. Once those flags are reflex, navigating Kubernetes feels less alien.
Try it yourself. Apply the deployment and the ClusterIP service. Then kubectl exec into one of the pods and run wget -qO- http://hello. You should see the nginx welcome page — confirmation that the service is correctly load-balancing across pods inside the cluster.
Cleanup
When you are done experimenting:
kubectl delete -f .
# or delete everything labelled app=hello
kubectl delete all -l app=hello
And destroy the local cluster if you no longer need it:
minikube delete # or kind delete cluster / k3d cluster delete demo
Recap
You now know how to:
- Write and apply a Pod — and why you rarely do, because bare pods are not self-healing
- Use a Deployment for replica management, rolling updates, and
kubectl rollout undo - Choose between ClusterIP, NodePort, and LoadBalancer services
- Read state with
getanddescribe, debug withlogsandexec - Partition a cluster with namespaces
That is the working core of Kubernetes for an application developer.
Next steps
So far this series has been on-premises agnostic — everything runs on your laptop or any cluster. Real production usually lives in a cloud, and on the internet, “the cloud” overwhelmingly means AWS. The next post zooms out to introduce AWS as a mental model.
→ Next: What Is AWS? A Practical Introduction
Questions or feedback? Email codeloomdevv@gmail.com.
Related articles
- Kubernetes What Is Kubernetes? Container Orchestration Explained
A practical introduction to Kubernetes — what problem it solves, the vocabulary you need (cluster, node, pod, deployment, service), and when running a single Docker container is enough.
- Kubernetes Kubernetes cert-manager: Automate TLS Certificates
Set up cert-manager in Kubernetes to automate TLS certificate provisioning with Let's Encrypt. Covers Issuers, Certificates, Ingress integration.
- Kubernetes Kubernetes Cost Optimization: Right-Size Your Cluster
Reduce Kubernetes costs with right-sizing, autoscaling, spot instances, resource quotas, and monitoring. Practical strategies with real savings.
- Kubernetes Kubernetes Kustomize: Manage Configs Without Helm
Learn Kustomize for Kubernetes config management. Covers overlays, patches, generators, and multi-environment deployments without templating.