Kubernetes Pods, Deployments, and Services Explained for Beginners

A clear, beginner friendly explanation of the three Kubernetes objects you will use most: pods, deployments, and services, with simple analogies, manifests, and real world advice.

Kubernetesbeginner
12 min read

Once you understand what Kubernetes is, the next thing every beginner needs to learn is the small handful of objects you actually create. Three of them — Pod, Deployment, and Service — do almost all the work in any real workload. Master those three and you can read 90% of the Kubernetes manifests you will encounter in any tutorial, blog post, or production cluster.

This guide explains each one with a simple analogy, shows you the YAML, demonstrates how they fit together to run a small web app, and points out the gotchas. By the end you will be able to deploy and expose a containerised app on any Kubernetes cluster from scratch.

The 30-Second Mental Model

Three sentences:

  1. A Pod wraps one or more containers and is the thing that actually runs.
  2. A Deployment says "keep N copies of this Pod alive and updated" — and rolls out new versions safely.
  3. A Service gives those Pods a stable network name so other apps can talk to them.

You almost never create Pods directly. You create a Deployment, which creates Pods. You then create a Service so the rest of the world (or other in-cluster apps) can reach them. That is the entire workflow.

Pods: The Smallest Deployable Unit

A Pod is a wrapper around one or more containers that share network and storage. Most pods have exactly one container. Multi-container pods exist for "sidecar" patterns (an Envoy proxy alongside an app, a log shipper, a service mesh sidecar) but you do not need them on day one.

Why a wrapper instead of just a container? Because Kubernetes adds things every container needs in a cluster: a unique IP address, mounted secrets and config, resource requests/limits, liveness/readiness probes, and lifecycle hooks. The pod is the unit those things attach to.

yamlyaml
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
    - name: web
      image: ghcr.io/me/web:1.0
      ports: [{ containerPort: 3000 }]
      resources:
        requests: { cpu: "100m", memory: "128Mi" }
        limits:   { cpu: "500m", memory: "256Mi" }

Apply with kubectl apply -f pod.yaml. View with kubectl get pods. Logs with kubectl logs web. Shell inside with kubectl exec -it web -- sh. Done — but again, you would not normally create a bare pod. You would let a Deployment manage it.

Deployments: Pods With Superpowers

A Deployment declares the desired state for a set of identical pods: "run 3 replicas of this image with this config, and when I change the image, roll out the new version one or two at a time without downtime."

yamlyaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector: { matchLabels: { app: web } }
  template:
    metadata: { labels: { app: web } }
    spec:
      containers:
        - name: web
          image: ghcr.io/me/web:1.0
          ports: [{ containerPort: 3000 }]
          readinessProbe:
            httpGet: { path: /health, port: 3000 }
            initialDelaySeconds: 5

Three things to notice. The replicas: 3 is your desired count — Kubernetes maintains it forever. The selector + labels is how the Deployment knows which pods belong to it (any pod with app: web label). And the readinessProbe tells the cluster when a new pod is actually ready to receive traffic — without one, rolling updates can serve traffic to half-started containers.

Day-to-day operations:

  • Update the image: edit the YAML and kubectl apply -f. Or kubectl set image deploy/web web=ghcr.io/me/web:1.1.
  • Watch the rollout: kubectl rollout status deploy/web.
  • Roll back if it broke: kubectl rollout undo deploy/web.
  • Scale up: kubectl scale deploy/web --replicas=10.

This is the workflow modern teams use thousands of times a day.

Services: A Stable Address for Moving Targets

Pods are ephemeral. They get rescheduled, restarted, replaced. Their IP addresses change. So how does another app find "the web service"? You create a Service — a stable virtual IP and DNS name that load-balances across whichever pods currently match its selector.

yamlyaml
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector: { app: web }
  ports:
    - port: 80
      targetPort: 3000
  type: ClusterIP

Now any pod in the cluster can curl http://web/ and traffic will round-robin to whichever app: web pods are alive. The IP and DNS name are stable forever, even as the underlying pods come and go.

There are three Service types you will see:

  • ClusterIP (default) — only reachable from inside the cluster. Use for internal-only services (database, internal API).
  • NodePort — exposes the service on a port on every node. Mostly used for local clusters; rarely in production.
  • LoadBalancer — provisions a cloud load balancer (an AWS NLB, a Google L4 LB) with a public IP. Use when you want to expose a service to the internet directly.

For HTTP traffic from the outside world in production, you typically use a ClusterIP service plus an Ingress (or modern Gateway API) which terminates TLS and routes by hostname/path.

Putting It All Together: A Tiny Web App

A complete, deployable app in two manifests:

yamlyaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector: { matchLabels: { app: web } }
  template:
    metadata: { labels: { app: web } }
    spec:
      containers:
        - name: web
          image: ghcr.io/me/web:1.0
          ports: [{ containerPort: 3000 }]
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector: { app: web }
  ports: [{ port: 80, targetPort: 3000 }]

kubectl apply -f web.yaml and you have three replicas of your container running, load-balanced behind a stable cluster-internal address called web. Add an Ingress to expose web.example.com to the internet, and you have a real production setup.

How They Connect: Labels and Selectors

The glue between these objects is the label/selector system. The Deployment's template.metadata.labels paint each pod with app: web. The Service's selector: { app: web } tells it "send traffic to any pod with that label."

This loose coupling is intentional and powerful. You can run two Deployments (web-v1 and web-v2, both labelled app: web) and the Service load-balances across both — perfect for canary deployments. Get the labels wrong and your Service has zero endpoints (kubectl get endpoints web will be empty), the most common "why is nothing working" cause.

Common Mistakes Beginners Make

  • Mismatching labels and selectors. A typo in app: wbe and your Service has no endpoints. Always kubectl get endpoints after deploying.
  • No readiness probe. Rolling updates send traffic to half-started pods and users see errors. Add one for any HTTP service.
  • No resource requests. The scheduler cannot place pods sensibly without requests. The whole cluster runs worse.
  • Creating bare Pods. A bare Pod is not restarted by anything if its node dies. Always use a Deployment (or StatefulSet for stateful workloads).
  • Using LoadBalancer for every service. Each cloud LB costs money and takes minutes to provision. Use one Ingress in front of many internal ClusterIP services.

Quick Reference

  • See pods: kubectl get pods -o wide. Watch live: kubectl get pods -w.
  • Describe (events + status): kubectl describe pod <name>.
  • Logs: kubectl logs -f deploy/web. Previous container's logs: kubectl logs deploy/web --previous.
  • Apply: kubectl apply -f file.yaml. Diff before apply: kubectl diff -f file.yaml.
  • Rollout: kubectl rollout status deploy/web. Undo: kubectl rollout undo deploy/web.
  • Endpoints (debug Service): kubectl get endpoints <svc>.
  • Port-forward to test locally: kubectl port-forward svc/web 8080:80.
  • Useful aliases: alias k=kubectl, alias kgp='kubectl get pods'.
Rune AI

Rune AI

Key Insights

  • A Pod runs one or more containers; a Deployment manages many identical Pods.
  • A Service gives those Pods a stable virtual IP and DNS name with load balancing.
  • The label/selector system is the loose-coupled glue between all three.
  • Always use a Deployment (not bare Pods), add readiness probes, and set resource requests.
  • Use one Ingress (or Gateway) in front of many internal ClusterIP services in production.
RunePowered by Rune AI

Frequently Asked Questions

Pod vs Container?

container is one running process from one image. A pod is a Kubernetes wrapper around one or more containers that share network and storage.

Deployment vs StatefulSet vs DaemonSet?

Deployment for stateless apps (web, API). StatefulSet for stateful apps that need stable identity and storage (databases). DaemonSet for one pod per node (log collectors, monitoring agents).

Service vs Ingress?

Service load-balances inside the cluster (L4, TCP/UDP). An Ingress (or Gateway) does HTTP routing from the outside world (L7, hostnames, paths, TLS).

Why are my pods stuck in `Pending`?

Usually the cluster has no node with enough resources, or your pod requests more CPU/memory than any node has. `kubectl describe pod <name>` shows the reason in the Events section.

What is the Gateway API?

The successor to Ingress, GA since Kubernetes 1.30. More expressive, role-oriented, and supported by most modern controllers (Istio, Envoy Gateway, NGINX). Worth using on new clusters in 2026.

Conclusion

Pods, Deployments, and Services are the three Kubernetes objects you will create most often, and the three you need to truly understand. Spin up a local cluster with k3d, apply the two manifests above, and play with rollouts, scaling, and label changes. Once the dance between Deployment-creates-Pods and Service-finds-Pods-by-label clicks, the rest of Kubernetes becomes much less intimidating.