Kubernetes deployments become much easier to reason about when we separate the process into three questions: what should run, how should Kubernetes keep it healthy, and how should traffic reach it? In this walkthrough, we will deploy a small NGINX web application using a Deployment and a Service.
The same structure applies to most stateless applications. Replace the image, ports, health checks, and resource values with those required by your service.
Before you begin
You need a running Kubernetes cluster and kubectl configured to communicate with it. A local cluster such as kind or minikube is enough for this example.
Confirm that the connection works:
kubectl cluster-info
kubectl get nodes
Create a dedicated namespace so the example stays isolated:
kubectl create namespace miel-demo
Define the Deployment
A Deployment describes the desired state of our application. Kubernetes creates the Pods, replaces unhealthy instances, and coordinates rolling updates when the specification changes.
Create a file named deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: miel-web
namespace: miel-demo
labels:
app: miel-web
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: miel-web
template:
metadata:
labels:
app: miel-web
spec:
containers:
- name: web
image: nginx:1.27-alpine
ports:
- name: http
containerPort: 80
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
Several details here matter:
- The selector and Pod labels match, allowing the Deployment to manage the correct Pods.
- Three replicas give the application more than one running instance.
- The rolling update settings keep all existing replicas available while a replacement starts.
- The readiness probe prevents traffic from reaching a Pod before it is ready.
- Resource requests help the scheduler place Pods, while limits prevent one container from consuming unbounded resources.
Expose the application
Pods are replaceable and their IP addresses can change. A Service gives the application a stable network identity and distributes traffic across ready Pods.
Create service.yaml:
apiVersion: v1
kind: Service
metadata:
name: miel-web
namespace: miel-demo
spec:
type: ClusterIP
selector:
app: miel-web
ports:
- name: http
port: 80
targetPort: http
ClusterIP exposes the service inside the cluster. In a real environment, an Ingress or Gateway would usually handle external traffic, TLS certificates, and routing.
Apply and watch the rollout
Apply both manifests declaratively:
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
Do not stop at a successful apply message. Wait for Kubernetes to confirm that the rollout completed:
kubectl rollout status deployment/miel-web \
--namespace miel-demo \
--timeout 120s
Then inspect the resources:
kubectl get deployment,pods,service --namespace miel-demo
You should see three ready Pods, an available Deployment, and a Service with a cluster IP.
Verify the application
For a local check, forward a port from your workstation to the Service:
kubectl port-forward service/miel-web 8080:80 \
--namespace miel-demo
Open http://localhost:8080 or test it from another terminal:
curl --fail http://localhost:8080
A successful NGINX response proves that the Service can route traffic to a ready Pod.
Update and roll back safely
Change the container image through the Deployment rather than editing individual Pods:
kubectl set image deployment/miel-web \
web=nginx:1.27.1-alpine \
--namespace miel-demo
kubectl rollout status deployment/miel-web \
--namespace miel-demo
If the new version fails its health checks or behaves incorrectly, inspect the history and roll back:
kubectl rollout history deployment/miel-web --namespace miel-demo
kubectl rollout undo deployment/miel-web --namespace miel-demo
When the rollout fails
Start with the state Kubernetes already exposes:
kubectl get pods --namespace miel-demo
kubectl describe deployment miel-web --namespace miel-demo
kubectl describe pod <pod-name> --namespace miel-demo
kubectl logs <pod-name> --namespace miel-demo
kubectl get events --namespace miel-demo --sort-by=.lastTimestamp
Common causes include an image that cannot be pulled, an incorrect container port, failing probes, insufficient cluster capacity, and missing configuration or secrets. Read the Pod events before changing the manifest; they usually identify which layer is failing.
Production considerations
This example establishes a sound workload baseline, but a production service usually needs more:
- Use immutable image digests instead of mutable tags.
- Add a PodDisruptionBudget and topology spread constraints for availability.
- Store application configuration in ConfigMaps and sensitive values in an appropriate secrets system.
- Define a HorizontalPodAutoscaler only after measuring realistic CPU, memory, or application metrics.
- Add an Ingress or Gateway with TLS and explicit network policies.
- Send metrics, logs, and traces to an observability platform.
- Validate manifests in CI and deploy them through GitOps or another audited delivery workflow.
The important habit is to treat the manifest as the beginning of the deployment—not proof that the application is working. Apply, observe, verify, and keep a tested recovery path.
More practical notes from Mohamed Lembarki and Ihsane Elkhaldi are coming soon.