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:
| Type | When to use |
|---|---|
| ClusterIP | Internal traffic only — default type, accessible within the cluster |
| NodePort | Expose on a static port on every node — for local testing |
| LoadBalancer | Cloud 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