Skip to content
Commitpedia
Log in Join & edit

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.

Edit history of Deploy your first app on Kubernetes: 1 recorded edit
Edit history · 1 edit
Type
Article
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Reading
4 min

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

  • kubectl installed and pointing at a cluster where you may create a namespace. Check with kubectl config current-context, so you are not about to deploy into production by mistake.
  • kubectl get nodes shows at least one node in the Ready state.

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.matchLabels and template.metadata.labels must match. The Service uses the same app: hello label to find the Pods.
  • resources.requests is what the scheduler reserves on a node (100m means a tenth of a CPU core). limits.memory is a hard ceiling; a container that goes over it can be OOM-killed.
  • targetPort: http refers 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 failureThreshold consecutive 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 * periodSeconds long 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.

  1. commitpedia-admin +5,876 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?