When you deploy an application to Kubernetes, you work with three objects almost every time: Pods, Deployments, and Services.

  • A Pod is the unit that runs your containers
  • A Deployment manages a set of Pods and handles updates
  • A Service gives your Pods a stable network address

Understanding these three is the foundation for everything else in Kubernetes.

Prerequisites: A running Kubernetes cluster. See Kubernetes Tutorial #2: Installing Kubernetes Locally.

Pods — The Smallest Unit

A Pod is the smallest deployable unit in Kubernetes. It wraps one or more containers that share the same network and storage.

Most Pods run a single container. But some use multiple containers that need to work closely together — for example, an app container and a logging sidecar.

Here is a simple Pod YAML:

# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
    - name: myapp
      image: nginx:alpine
      ports:
        - containerPort: 80

Apply it:

kubectl apply -f pod.yaml
kubectl get pods
NAME         READY   STATUS    RESTARTS   AGE
myapp-pod    1/1     Running   0          5s

Native Sidecar Containers (Stable since K8s 1.33)

Native sidecar containers graduated to GA in Kubernetes 1.33 and are enabled by default. A sidecar is a helper container that runs alongside your main container — for logging, proxying, or metrics.

Before K8s 1.29, sidecars were a workaround. Now they are a first-class feature using initContainers with restartPolicy: Always:

apiVersion: v1
kind: Pod
metadata:
  name: myapp-with-sidecar
spec:
  initContainers:
    - name: log-collector
      image: fluent/fluent-bit:latest
      restartPolicy: Always    # This makes it a native sidecar
      volumeMounts:
        - name: logs
          mountPath: /logs
  containers:
    - name: myapp
      image: myapp:1.0
      volumeMounts:
        - name: logs
          mountPath: /app/logs
  volumes:
    - name: logs
      emptyDir: {}

With native sidecars:

  • The sidecar starts before the main container
  • The sidecar stops after the main container
  • If the sidecar crashes, Kubernetes restarts it automatically

Why You Rarely Create Pods Directly

The kubelet will restart containers inside a Pod if they crash (the default restartPolicy: Always). But if the node running the Pod fails, a standalone Pod is gone — there is no controller to recreate it on another node.

In production, you almost never create Pods directly. Instead, you use Deployments — which have a ReplicaSet controller that recreates Pods on healthy nodes if the original node fails.

# Clean up the pod we created
kubectl delete pod myapp-pod

Deployments — Managing Pods at Scale

A Deployment is a higher-level object that manages a set of identical Pods. It:

  • Ensures the desired number of Pods are always running
  • Handles rolling updates with zero downtime
  • Supports rollback to previous versions

Here is a Deployment that runs 3 replicas of nginx:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
  labels:
    app: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp          # This Deployment manages Pods with this label
  template:
    metadata:
      labels:
        app: myapp        # Pods created from this template get this label
    spec:
      containers:
        - name: myapp
          image: nginx:alpine
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "100m"
              memory: "64Mi"
            limits:
              cpu: "250m"
              memory: "128Mi"

Apply it:

kubectl apply -f deployment.yaml
kubectl get deployments
kubectl get pods
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
myapp-deployment    3/3     3            3           10s

NAME                             READY   STATUS    RESTARTS   AGE
myapp-deployment-7d6b4b5c8-4xkzp  1/1   Running   0          10s
myapp-deployment-7d6b4b5c8-9pvfq  1/1   Running   0          10s
myapp-deployment-7d6b4b5c8-wqmnr  1/1   Running   0          10s

The Deployment creates 3 Pods. If you delete one, the Deployment creates a replacement immediately.

# Delete one pod — Kubernetes will recreate it
kubectl delete pod myapp-deployment-7d6b4b5c8-4xkzp
kubectl get pods   # A new pod appears within seconds

Scaling a Deployment

Scale up with kubectl scale:

kubectl scale deployment myapp-deployment --replicas=5
kubectl get pods

Or update the YAML and re-apply:

# Edit deployment.yaml: change replicas to 5
kubectl apply -f deployment.yaml

Rolling Updates

Update the container image with zero downtime:

kubectl set image deployment/myapp-deployment myapp=nginx:1.27-alpine

Watch the rolling update happen:

kubectl rollout status deployment/myapp-deployment
Waiting for deployment "myapp-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "myapp-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "myapp-deployment" rollout to finish: 1 old replicas are pending termination...
deployment "myapp-deployment" successfully rolled out

Kubernetes replaces old Pods one by one. Your app stays available throughout.

Rollback

If the new version has a bug, roll back instantly:

# Roll back to the previous version
kubectl rollout undo deployment/myapp-deployment

# Roll back to a specific version
kubectl rollout history deployment/myapp-deployment
kubectl rollout undo deployment/myapp-deployment --to-revision=1

Services — Stable Network Access to Pods

Pods are ephemeral. They come and go. Each Pod gets a new IP address when it is created.

This creates a problem: how does one service find another if their IPs keep changing?

The answer is a Service. A Service provides a stable, permanent network endpoint in front of a group of Pods. It routes traffic to healthy Pods automatically.

A Service uses labels to find its Pods. Any Pod with a matching label receives traffic.

Service Types

Kubernetes has three main Service types:

TypeWhen to use
ClusterIPInternal traffic only — default type, accessible within the cluster
NodePortExpose on a static port on every node — for local testing
LoadBalancerCloud load balancer — for production on AWS, GCP, Azure

ClusterIP Service (Internal)

This is the most common type. It creates an internal IP that other services in the cluster can reach.

# service-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp          # Routes to Pods with label app: myapp
  ports:
    - protocol: TCP
      port: 80          # Port on the Service
      targetPort: 80    # Port on the Pod
  type: ClusterIP
kubectl apply -f service-clusterip.yaml
kubectl get services
NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes       ClusterIP   10.96.0.1       <none>        443/TCP   2d
myapp-service    ClusterIP   10.96.45.123    <none>        80/TCP    5s

Now any Pod in the cluster can reach myapp-service on port 80. Kubernetes DNS resolves myapp-service to the ClusterIP automatically.

NodePort Service (Local Testing)

Exposes the service on a static port on every node. Use this for local testing.

# service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: myapp-nodeport
spec:
  selector:
    app: myapp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      nodePort: 30080   # Must be 30000-32767
  type: NodePort

With minikube, access it via:

minikube service myapp-nodeport --url

LoadBalancer Service (Cloud)

On cloud providers, this creates a real load balancer. On minikube, use minikube tunnel to simulate it:

apiVersion: v1
kind: Service
metadata:
  name: myapp-loadbalancer
spec:
  selector:
    app: myapp
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer

Port-Forward for Quick Testing

For testing a service locally without exposing it:

kubectl port-forward service/myapp-service 8080:80
# Open http://localhost:8080

Debugging Pods

Here are the commands you will use most for debugging:

# View pod logs
kubectl logs myapp-deployment-7d6b4b5c8-4xkzp

# Follow logs in real-time
kubectl logs -f myapp-deployment-7d6b4b5c8-4xkzp

# Open a shell inside a running pod
kubectl exec -it myapp-deployment-7d6b4b5c8-4xkzp -- /bin/sh

# Get detailed info about a pod (events, conditions, etc.)
kubectl describe pod myapp-deployment-7d6b4b5c8-4xkzp

# See resource usage
kubectl top pods

Putting It All Together

Here is a complete example: a Deployment + a Service in one YAML file.

# myapp-full.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: nginx:alpine
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "100m"
              memory: "64Mi"
            limits:
              cpu: "250m"
              memory: "128Mi"
---
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
  ports:
    - port: 80
      targetPort: 80
  type: ClusterIP
kubectl apply -f myapp-full.yaml
kubectl get all

Common Mistakes

Creating Pods directly

Never create Pods directly in production. Use Deployments. If a Pod dies, a Deployment recreates it. A standalone Pod stays dead.

Inconsistent labels

The Deployment’s selector.matchLabels must exactly match the Pod’s template.metadata.labels. If they do not match, the Deployment cannot manage its Pods.

# This will fail — labels do not match
selector:
  matchLabels:
    app: myapp
template:
  metadata:
    labels:
      app: my-app    # Different from "myapp"

Confusion between Service types

  • ClusterIP — only accessible inside the cluster
  • NodePort — accessible from outside on a high port (30000+)
  • LoadBalancer — creates a cloud load balancer (costs money on cloud providers)

For production traffic routing, use the Gateway API instead of NodePort or LoadBalancer directly. We cover that in Kubernetes Tutorial #6.

What’s Next?

Your app is running with multiple replicas and has a stable network address. The next step is managing configuration — how to pass settings and secrets to your containers without hardcoding them.

Next: Kubernetes Tutorial #4: ConfigMaps and Secrets