Your First Kubernetes Deployment and Service
By OpsAtlas · · 4 min read
Table of contents
Docker runs a container on your machine. Kubernetes runs containers across a cluster of machines and keeps them running: it restarts them when they crash, replaces them when a machine fails and spreads traffic across copies. In this tutorial you run the notes-api image on a local cluster and see that happen.
You need: Docker, kind (Kubernetes in Docker) and kubectl. You also need the notes-api:1.0.0 image from Write Your First Dockerfile, or any image of your own that listens on a port and has a health endpoint.
Three objects to know
- Pod: one running copy of your container. Pods are disposable: when one dies, it is replaced, not repaired.
- Deployment: says “keep N Pods of this image running”. It creates the Pods, replaces failed ones and rolls out new versions.
- Service: a stable name and address in front of the Pods. Pods come and go with new IP addresses; the Service finds the current ones by their labels.
You describe these objects in YAML and give them to the cluster with kubectl apply. Kubernetes then works to make the cluster match what you described.
1. Create a cluster and load the image
kind create cluster --name notes
kubectl get nodes # one node, STATUS Ready
A kind cluster runs inside Docker and can’t see images on your machine by default. Load the image into it:
kind load docker-image notes-api:1.0.0 --name notes
2. Generate the manifests
Open the Kubernetes manifest generator and fill in:
| Field | Value |
|---|---|
| App name | notes-api |
| Namespace | default |
| Container image | notes-api:1.0.0 |
| Replicas | 2 |
| Container port | 3000 |
| Service port | 80 |
| Readiness and liveness probe | HTTP |
| Probe path | /healthz |
| Hardened security context | on |
Leave everything else at its default and save the output as notes-api.yaml. It contains two objects, separated by ---:
apiVersion: apps/v1
kind: Deployment
metadata:
name: notes-api
namespace: default
labels:
app.kubernetes.io/name: notes-api
spec:
replicas: 2
revisionHistoryLimit: 5
selector:
matchLabels:
app.kubernetes.io/name: notes-api
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 25%
template:
metadata:
labels:
app.kubernetes.io/name: notes-api
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: notes-api
image: notes-api:1.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 3000
protocol: TCP
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: notes-api
namespace: default
labels:
app.kubernetes.io/name: notes-api
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: notes-api
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
How to read it:
replicas: 2: keep two Pods running.selectorand thetemplatelabels must match. They tell the Deployment which Pods are its own.templateis the Pod to create: the image, the port, and how much CPU and memory it asks for (requests) and may use at most (limits).readinessProbeandlivenessProbecall/healthz. A Pod gets traffic only while the readiness probe passes, and is restarted if the liveness probe keeps failing.securityContextruns the app as non-root with a read-only filesystem. The generator warns that this needs an image that runs as a non-root user. The image from the Dockerfile tutorial does, with the numericUSER 1000thatrunAsNonRootneeds.- The Service’s
selectoruses the same label, so it sends traffic on port 80 to port 3000 of every ready Pod.
3. Check the file before applying it
Paste notes-api.yaml into the Kubernetes YAML validator. It scores 100/100 (A), and the Diagram tab draws an arrow from the Service to the Deployment: the selector matches. If you change one character of a label in the Service, the arrow disappears and the validator reports that the Service selects nothing. That typo is a common cause of 503 errors.
The only finding is informational: no PodDisruptionBudget. That matters for production clusters; From Dockerfile to Kubernetes in Four Steps shows how to add one.
4. Apply it and watch
kubectl apply -f notes-api.yaml
kubectl rollout status deployment/notes-api # waits until both Pods are ready
kubectl get pods -l app.kubernetes.io/name=notes-api
Both Pods show READY 1/1 and STATUS Running. To reach the Service from your machine, forward a local port to it:
kubectl port-forward service/notes-api 8080:80
Open http://localhost:8080: {"notes":[]}. Leave the port-forward running and use a second terminal for the rest.
5. See Kubernetes keep it running
Delete a Pod, as if it had crashed:
kubectl delete pod -l app.kubernetes.io/name=notes-api --wait=false
kubectl get pods -l app.kubernetes.io/name=notes-api
New Pods with new names are already starting. The Deployment saw fewer than two and created replacements. Then scale up and read the logs:
kubectl scale deployment/notes-api --replicas=3
kubectl get pods -l app.kubernetes.io/name=notes-api # three Pods
kubectl logs deployment/notes-api # "notes-api listening on 3000"
kubectl describe deployment notes-api # events: what Kubernetes did and why
kubectl scale is handy for experiments, but the file still says replicas: 2. The next kubectl apply puts it back. Treat the YAML file as the source of truth and change it there.
6. Clean up
kind delete cluster --name notes
If something goes wrong
| What you see | Likely cause |
|---|---|
ErrImagePull / ImagePullBackOff | The image isn’t in the cluster. Run kind load docker-image again with the exact name and tag. |
CreateContainerConfigError mentioning “non-numeric user” | The image uses USER node instead of a number, so runAsNonRoot can’t verify it. Use USER 1000. |
Pods running but READY 0/1 | The readiness probe fails. Check the path and port, and see kubectl describe pod. |
port-forward works but the page is empty | The Service’s targetPort doesn’t match the container port name or number. |
Next steps
Next, From Dockerfile to Kubernetes in Four Steps ties the Docker and Kubernetes tools together for a real deploy. If a manifest ever behaves differently from what you wrote, read Why Kubernetes rejects value: yes.
Try it yourself
Kubernetes manifest generator
Generate a Kubernetes Deployment and Service with resources, probes and a hardened securityContext, plus optional Ingress and HPA.
KubernetesKubernetes YAML validator and visualizer
Paste Kubernetes manifests to get a resource diagram and a security and best-practice review: privileged pods, root, probes, limits, deprecated APIs.
KubernetesBeta