Deploy your first app on Kubernetes
A step-by-step tutorial: write a Deployment and a Service in YAML, apply them, reach the app, add health probes, roll out a new version and roll it back.
This tutorial deploys a small web server (nginx) to a Kubernetes cluster, makes it reachable, and then walks through an update and a rollback. It works on any conformant cluster: a local one such as kind, minikube or k3s, or a test namespace on a shared cluster.
Before you start
kubectlinstalled and pointing at a cluster where you may create a namespace. Check withkubectl config current-context, so you are not about to deploy into production by mistake.kubectl get nodesshows at least one node in theReadystate.
Step 1: write the manifest
Save the following as hello.yaml. It contains three objects separated by ---: a namespace, a Deployment with two replicas and a Service in front of them.
apiVersion: v1
kind: Namespace
metadata:
name: hello
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
namespace: hello
labels:
app: hello
spec:
replicas: 2
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: web
image: nginx:1.27
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
memory: 128Mi
readinessProbe:
httpGet:
path: /
port: http
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: http
periodSeconds: 10
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: hello
namespace: hello
spec:
selector:
app: hello
ports:
- name: http
port: 80
targetPort: http
What the important fields do:
selector.matchLabelsandtemplate.metadata.labelsmust match. The Service uses the sameapp: hellolabel to find the Pods.resources.requestsis what the scheduler reserves on a node (100m means a tenth of a CPU core).limits.memoryis a hard ceiling; a container that goes over it can be OOM-killed.targetPort: httprefers to the named container port, so you can change the number in one place.
Step 2: apply it
kubectl apply -f hello.yaml
kubectl get all -n hello
kubectl rollout status deployment/hello -n hello
kubectl apply is declarative: run it again after editing the file and Kubernetes changes only what differs. Wait until rollout status reports that the deployment was successfully rolled out and both Pods show READY 1/1.
Step 3: reach the app
The Service is of type ClusterIP, so it is only reachable inside the cluster. For testing, forward a local port to it:
kubectl port-forward -n hello service/hello 8080:80
curl http://localhost:8080/
You should see the nginx welcome page. Press Ctrl+C to stop the forward. To expose the app permanently, change the Service type to NodePort or LoadBalancer, or add an Ingress.
Step 4: understand the probes
The kubelet runs the probes you define and acts on the result:
- Readiness probe: while it fails, the Pod is removed from the Service’s endpoints and gets no traffic, but it is not restarted. Use it for “I am up but not ready yet” (warming caches, waiting for a dependency).
- Liveness probe: after
failureThresholdconsecutive failures, the kubelet restarts the container. Use it only to detect a stuck process, and keep it cheap: it should not check databases or other services. - Startup probe: for slow-starting applications. Liveness and readiness checks are held back until it succeeds once. Give it a
failureThreshold * periodSecondslong enough to cover the worst-case start time; for example 30 x 10 seconds allows five minutes.
startupProbe:
httpGet:
path: /
port: http
failureThreshold: 30
periodSeconds: 10
Probes can use httpGet, tcpSocket, grpc or exec (run a command in the container). A liveness probe that is too strict is a common cause of endless restarts, so start with generous values.
Step 5: roll out a new version
Change the image tag. You can edit hello.yaml and apply it again (preferred, because the file stays the source of truth), or use kubectl set image for a quick test:
kubectl set image deployment/hello -n hello web=nginx:1.28
kubectl annotate deployment/hello -n hello kubernetes.io/change-cause="nginx 1.28"
kubectl rollout status deployment/hello -n hello
kubectl get replicasets -n hello
The Deployment creates a new ReplicaSet and replaces Pods a few at a time. Thanks to the readiness probe, new Pods only receive traffic once they answer. The old ReplicaSet stays with zero replicas as history.
Step 6: roll back
Simulate a bad release by setting an image tag that does not exist, then undo it:
kubectl set image deployment/hello -n hello web=nginx:does-not-exist
kubectl get pods -n hello
kubectl rollout history deployment/hello -n hello
kubectl rollout undo deployment/hello -n hello
kubectl rollout status deployment/hello -n hello
The new Pod gets stuck in ErrImagePull or ImagePullBackOff, but the old Pods keep serving because the rolling update never removes more than the allowed number of ready Pods. rollout undo returns to the previous revision; add --to-revision=N to pick a specific one from the history.
Step 7: look around
kubectl describe deployment hello -n hello
kubectl logs -n hello deployment/hello
kubectl get events -n hello --sort-by=.metadata.creationTimestamp
kubectl scale deployment/hello -n hello --replicas=4
Step 8: clean up
Warning: deleting a namespace deletes every object inside it, including data in volumes that use a Delete reclaim policy. First check that it contains only this tutorial with kubectl get all -n hello, then run:
kubectl delete namespace hello
Official documentation
See also
- 0132 Pods, ReplicaSets and Deployments explained A Pod runs one or more containers together, a ReplicaSet keeps a fixed number of identical Pods alive, and a Deployment manages… Beginner · Concept · Linux Article Concepts · Workloads & services 15 Oct 2026
- 0133 Services and Ingress: exposing an app in Kubernetes A Service gives a changing set of Pods one stable address. ClusterIP is internal, NodePort and LoadBalancer reach outside, and an Ingress… Intermediate · Concept · Linux Article Ingress & networking 15 Oct 2026
- 0136 Persistent storage in Kubernetes: PersistentVolumes, claims and StorageClasses How to give a Pod storage that survives restarts: request it with a PersistentVolumeClaim, let a StorageClass provision it, and mount it.… Intermediate · How-to · Linux Article Config, secrets & storage · Workloads & services 15 Oct 2026
- 0144 Fix: Pod stuck in Pending with “0/3 nodes are available: Insufficient cpu” The scheduler cannot find a node with enough unreserved CPU for the pod's request. Compare the request with node allocatable resources, then… Advanced · Fix · Linux Article Troubleshooting · Workloads & services 15 Oct 2026
- 0145 Fix: OOMKilled (exit code 137) in Kubernetes A container used more memory than its limit and the kernel killed it. Confirm the reason, measure real usage, then raise the… Advanced · Fix · Linux Article Troubleshooting · Workloads & services 15 Oct 2026
- 0153 Expose apps with OpenShift Routes: edge, passthrough and re-encrypt TLS A Route publishes a service on a host name through the OpenShift router. Learn when to use plain HTTP, edge, passthrough or… Intermediate · How-to · Any system Article Deploying applications · Ingress & networking 15 Oct 2026
History
Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.
- commitpedia-admin +5,876 chars