Skip to content
OpsAtlas

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:

FieldValue
App namenotes-api
Namespacedefault
Container imagenotes-api:1.0.0
Replicas2
Container port3000
Service port80
Readiness and liveness probeHTTP
Probe path/healthz
Hardened security contexton

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.
  • selector and the template labels must match. They tell the Deployment which Pods are its own.
  • template is the Pod to create: the image, the port, and how much CPU and memory it asks for (requests) and may use at most (limits).
  • readinessProbe and livenessProbe call /healthz. A Pod gets traffic only while the readiness probe passes, and is restarted if the liveness probe keeps failing.
  • securityContext runs 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 numeric USER 1000 that runAsNonRoot needs.
  • The Service’s selector uses 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 seeLikely cause
ErrImagePull / ImagePullBackOffThe 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/1The readiness probe fails. Check the path and port, and see kubectl describe pod.
port-forward works but the page is emptyThe 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.

    Kubernetes
  • Kubernetes 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