Kubernetes Pods and Services Deep Dive
A deep dive into Kubernetes Pods and Services: pod lifecycle, multi-container patterns, init containers, probes, ClusterIP, NodePort, LoadBalancer, and service discovery.
What you'll learn
- ✓Pod lifecycle phases and what each means
- ✓Multi-container pod patterns: sidecar, ambassador, adapter
- ✓Init containers for setup and dependency checks
- ✓Health probes: liveness, readiness, startup
- ✓Service types: ClusterIP, NodePort, LoadBalancer, and when to use each
Prerequisites
- •Basic Kubernetes concepts (clusters, nodes)
- •Familiarity with YAML and kubectl
- •Understanding of containers and Docker basics
Pods and Services are the two most fundamental resources in Kubernetes. A Pod runs your containers. A Service makes them accessible. Everything else in Kubernetes — Deployments, StatefulSets, Ingress — is built on top of these two primitives. Understanding them deeply is the foundation for working effectively with Kubernetes.
What a Pod actually is
A Pod is the smallest deployable unit in Kubernetes. It is not a container — it is a group of one or more containers that share the same network namespace, storage volumes, and lifecycle. Containers in a Pod share localhost and can communicate via IPC.
# pod.yaml — the simplest possible pod
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
kubectl apply -f pod.yaml
kubectl get pods
kubectl describe pod nginx-pod
kubectl logs nginx-pod
kubectl delete pod nginx-pod
In practice, you almost never create Pods directly. You use a Deployment or StatefulSet, which creates Pods for you and handles replication, rolling updates, and self-healing. But understanding Pods is essential because every higher-level resource ultimately manages Pods.
Pod lifecycle
A Pod moves through several phases during its lifetime:
Pending → Running → Succeeded/Failed
↘ Unknown (node lost)
Pending — The Pod has been accepted by the cluster but one or more containers are not yet running. This includes time spent waiting for scheduling, pulling images, or starting init containers.
Running — At least one container is running. The Pod has been bound to a node.
Succeeded — All containers in the Pod have terminated successfully (exit code 0) and will not be restarted. Typical for batch jobs.
Failed — At least one container terminated with a non-zero exit code.
Unknown — The Pod’s state cannot be determined, usually because the node is unreachable.
# Watch pod phase transitions
kubectl get pods -w
# See detailed status including container states
kubectl describe pod my-pod
Container restart policies
The restartPolicy determines what happens when a container exits.
apiVersion: v1
kind: Pod
metadata:
name: restart-demo
spec:
restartPolicy: Always # Always | OnFailure | Never
containers:
- name: app
image: my-app:latest
- Always (default) — Restart the container regardless of exit code. Used by long-running services.
- OnFailure — Restart only if the container exits with a non-zero code. Used by batch jobs.
- Never — Never restart. Used for one-shot tasks.
Kubernetes uses exponential backoff for restarts: 10s, 20s, 40s, up to 5 minutes. A container that keeps crashing enters CrashLoopBackOff state.
Multi-container Pods
Multiple containers in a Pod share the same network and storage. The three main multi-container patterns are:
Sidecar pattern
A helper container that extends the main container’s functionality without modifying it.
apiVersion: v1
kind: Pod
metadata:
name: web-with-logging
spec:
volumes:
- name: shared-logs
emptyDir: {}
containers:
- name: web-server
image: nginx:1.27
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
- name: log-shipper
image: fluentd:latest
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
readOnly: true
env:
- name: FLUENTD_CONF
value: "fluent.conf"
The web server writes logs to a shared volume. The log-shipper sidecar reads those logs and forwards them to a centralized logging system. Neither container needs to know about the other’s implementation.
Ambassador pattern
A proxy container that handles external communication on behalf of the main container.
apiVersion: v1
kind: Pod
metadata:
name: app-with-proxy
spec:
containers:
- name: app
image: my-app:latest
env:
- name: DB_HOST
value: "localhost" # Connects to ambassador on localhost
- name: DB_PORT
value: "5432"
- name: cloud-sql-proxy
image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:latest
args:
- "--port=5432"
- "my-project:us-central1:my-db"
The app connects to localhost:5432. The Cloud SQL Proxy ambassador handles authentication, encryption, and connection pooling to the actual database.
Adapter pattern
A container that transforms the main container’s output into a standardized format.
apiVersion: v1
kind: Pod
metadata:
name: app-with-metrics
spec:
containers:
- name: app
image: legacy-app:latest
ports:
- containerPort: 8080
- name: prometheus-adapter
image: prom-adapter:latest
ports:
- containerPort: 9090
args:
- "--source=http://localhost:8080/status"
- "--format=prometheus"
Init containers
Init containers run to completion before the main containers start. They are used for setup tasks, dependency checks, and initialization.
apiVersion: v1
kind: Pod
metadata:
name: app-with-init
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command:
- sh
- -c
- |
until nc -z postgres-service 5432; do
echo "Waiting for database..."
sleep 2
done
echo "Database is ready!"
- name: run-migrations
image: my-app:latest
command: ["python", "manage.py", "migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
containers:
- name: app
image: my-app:latest
ports:
- containerPort: 8000
Init containers run in order, one at a time. If any init container fails, the Pod restarts (subject to restartPolicy). The main containers do not start until all init containers succeed.
Health probes
Kubernetes uses probes to determine if a container is healthy, ready to serve traffic, or still starting up.
Liveness probe
Determines if the container is running. If the liveness probe fails, the container is killed and restarted.
containers:
- name: app
image: my-app:latest
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
Readiness probe
Determines if the container is ready to accept traffic. If the readiness probe fails, the Pod’s IP is removed from Service endpoints. The container is not killed.
containers:
- name: app
image: my-app:latest
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
Startup probe
Used for slow-starting containers. Until the startup probe succeeds, liveness and readiness probes are disabled.
containers:
- name: legacy-app
image: legacy-app:latest
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10
# Allows up to 5 minutes for startup (30 * 10s)
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
Probe types include httpGet, exec (run a command), and tcpSocket (check if port is open).
# Exec probe example
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
periodSeconds: 5
# TCP socket probe example
readinessProbe:
tcpSocket:
port: 3306
periodSeconds: 10
Services
A Service provides a stable network endpoint for a set of Pods. Pods are ephemeral — they get new IPs when they restart. Services provide a consistent DNS name and IP that routes to the current set of matching Pods.
ClusterIP (default)
Accessible only within the cluster. This is the default and most common type.
apiVersion: v1
kind: Service
metadata:
name: backend-service
spec:
type: ClusterIP # Default, can be omitted
selector:
app: backend
ports:
- protocol: TCP
port: 80 # Service port
targetPort: 8080 # Container port
# Other pods can access this service at:
# backend-service (same namespace)
# backend-service.default.svc.cluster.local (fully qualified)
NodePort
Exposes the service on a static port on every node in the cluster. Accessible from outside the cluster via <NodeIP>:<NodePort>.
apiVersion: v1
kind: Service
metadata:
name: frontend-service
spec:
type: NodePort
selector:
app: frontend
ports:
- protocol: TCP
port: 80
targetPort: 3000
nodePort: 30080 # Optional: auto-assigned from 30000-32767
# Accessible at any node's IP on port 30080
curl http://<node-ip>:30080
LoadBalancer
Provisions an external load balancer (on cloud providers). Builds on NodePort.
apiVersion: v1
kind: Service
metadata:
name: public-api
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: nlb
spec:
type: LoadBalancer
selector:
app: api
ports:
- protocol: TCP
port: 443
targetPort: 8443
On AWS, this creates an NLB or ALB. On GCP, a Google Cloud Load Balancer. On bare metal, you need a solution like MetalLB.
Headless Service
A Service with clusterIP: None. No load balancing or proxying. DNS returns the individual Pod IPs. Used for StatefulSets where clients need to connect to specific Pods.
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
spec:
clusterIP: None
selector:
app: postgres
ports:
- port: 5432
# DNS returns individual Pod IPs
nslookup postgres-headless
# postgres-0.postgres-headless.default.svc.cluster.local → 10.244.1.5
# postgres-1.postgres-headless.default.svc.cluster.local → 10.244.2.8
Service discovery
Kubernetes provides two mechanisms for service discovery:
DNS (recommended)
Every Service gets a DNS entry automatically: <service-name>.<namespace>.svc.cluster.local.
# In application code, just use the service name
import requests
# Same namespace
response = requests.get("http://backend-service/api/data")
# Different namespace
response = requests.get("http://backend-service.production.svc.cluster.local/api/data")
Environment variables
Kubernetes injects environment variables for each Service into every Pod.
# Automatically available in any pod (same namespace)
echo $BACKEND_SERVICE_SERVICE_HOST # 10.96.0.15
echo $BACKEND_SERVICE_SERVICE_PORT # 80
DNS is preferred because environment variables are only set at Pod creation time and do not update when Services change.
Resource requests and limits
Always set resource requests and limits on your containers. Without them, a single Pod can starve others of CPU and memory.
containers:
- name: app
image: my-app:latest
resources:
requests:
memory: "128Mi"
cpu: "250m" # 0.25 CPU cores
limits:
memory: "256Mi"
cpu: "500m"
Requests are what the scheduler uses to decide where to place the Pod. A node must have enough unrequested resources to accept the Pod.
Limits are the maximum the container can use. If a container exceeds its memory limit, it is killed (OOMKilled). If it exceeds CPU, it is throttled.
Set requests based on typical usage and limits based on peak usage. Avoid setting limits too low (causes throttling and OOMKills) or too high (wastes cluster resources).
Pods and Services are where Kubernetes starts making sense. Everything else — Deployments, HPA, Ingress — is built on these two concepts. Master the Pod lifecycle, understand how Services route traffic, and the rest of Kubernetes becomes much more approachable.
Related articles
- Kubernetes Kubernetes DaemonSets Explained with Practical Examples
Learn how DaemonSets run exactly one pod per node for logging, monitoring, and networking agents. Includes tolerations and update strategies.
- Kubernetes Kubernetes Persistent Volumes and Storage Classes
Understand PersistentVolumes, PersistentVolumeClaims, and StorageClasses to give your Kubernetes workloads reliable, portable storage.
- Kubernetes Kubernetes Network Policies: Pod-to-Pod Security Guide
Lock down pod communication with Kubernetes NetworkPolicies. Learn ingress and egress rules, label selectors, and namespace isolation.
- Kubernetes Kubernetes Resource Limits, Requests, and QoS Classes
Set CPU and memory requests and limits correctly. Understand QoS classes, OOMKilled errors, and CPU throttling in Kubernetes.