Skip to content

Kubernetes Deployment YAML Example: Copy-Paste Templates

Copy-paste Kubernetes Deployment YAML examples — Deployment, Service, and env-var patterns, validated before you apply them.

Try it now: Kubernetes YAML Validator Validate Kubernetes manifests locally and catch what the API server only hints at — bad selectors, memory in millibytes, a removed apiVersion.

A Minimal Single-Container Deployment

Every Kubernetes Deployment needs four things: an apiVersion, a kind, a metadata.name, and a spec whose selector.matchLabels matches the labels on the pod template it manages. Miss that last part and the API server accepts the manifest anyway — the mismatch only surfaces later as a Deployment stuck at 0/1ready with no obvious cause. Here's the smallest complete, valid k8s deployment template:

deployment.yaml — minimal
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  labels:
    app: web-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web-app
          image: nginx:1.27
          ports:
            - containerPort: 80

spec.selector.matchLabels and spec.template.metadata.labels both say app: web-app. That's not stylistic — it's the field Kubernetes uses to decide which pods belong to this Deployment, and it's immutable after creation. Change the pod template's labels later without also updating the selector and the apply is rejected outright.

Resource Requests, Limits, and a Health Probe

Without resources, a container can consume unbounded CPU and memory on its node, and the scheduler has no number to place it against. Without a readinessProbe, Kubernetes sends traffic to a pod the instant its container starts, whether or not the application inside is actually ready to serve it.

deployment.yaml — resources + probes
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  labels:
    app: api-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
        - name: api-server
          image: registry.example.com/api-server:1.4.2
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 20

250m means 0.25 of a CPU core — m here is a millicpu suffix, and it only means anything on the cpu line. It is not a memory unit, and memory: "256m" is not valid Kubernetes — the field expects a byte quantity like Mi (mebibytes, 1024² bytes) or M(megabytes, 10⁶ bytes), and the two aren't interchangeable at scale. This exact class of typo — a stray m where a byte suffix belongs, orMi swapped for M — is precisely what a kubernetes yaml validator catches before kubectl apply does, since kubectl will happily accept a syntactically valid but semantically wrong unit and let the scheduler figure out the fallout.

Exposing a Deployment with a Service

A Deployment on its own gives pods no stable network identity — pods are recreated with new IPs constantly. A Service gives them one, and it does so the same way a Deployment finds its pods: through a label selector. This is the single most common bug in a hand-written kubernetes manifest example — a Service whose selectordoesn't match the Deployment's pod labels, resulting in a Service with zero endpoints and no error at all.

deployment.yaml + service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  labels:
    app: web-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web-app
          image: nginx:1.27
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web-app-service
spec:
  selector:
    app: web-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP

Three fields have to line up for this to work: the Deployment's spec.template.metadata.labels, the Deployment's own spec.selector.matchLabels, and the Service's spec.selector. All three say app: web-app above. Run kubectl get endpoints web-app-service after applying — an empty ENDPOINTScolumn means the selector doesn't match anything, and that's almost always a label typo, not a networking problem.

Injecting Configuration with a ConfigMap

Hardcoding environment-specific values into a container image means rebuilding the image every time a config value changes. A ConfigMap separates that config from the image and lets the Deployment reference it — either as individual environment variables or as a whole file mounted into the container.

configmap.yaml + deployment.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: api-server-config
data:
  LOG_LEVEL: "info"
  API_TIMEOUT_SECONDS: "30"
  FEATURE_FLAGS: "beta-search=on,dark-mode=on"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  labels:
    app: api-server
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
        - name: api-server
          image: registry.example.com/api-server:1.4.2
          envFrom:
            - configMapRef:
                name: api-server-config
          ports:
            - containerPort: 8080

envFrom.configMapRef loads every key in data as an environment variable in one reference, rather than listing each one individually under env. All ConfigMap values are stored and injected as strings — API_TIMEOUT_SECONDS: "30" arrives in the container as the string "30", not a number, and code reading it needs to parse it accordingly. A ConfigMap update alone doesn't restart pods to pick up the new values; that requires a rollout, which is usually triggered by changing something in the pod template itself (an annotation with a config hash is the common pattern).

Validate Before You Apply

Every example above is a complete, applyable manifest, but kubectl apply -fis a slow feedback loop for catching mistakes — it means a real cluster round-trip to find out a selector doesn't match or a resource unit is wrong. A Kubernetes YAML validator checks structure, required fields, and label-selector consistency locally, before anything reaches an API server. If you're validating a full stack rather than one manifest, a Docker Compose validator is the equivalent check for the Compose file most projects start with before they ever need a cluster — and if you're still deciding whether that move to Kubernetes is worth it at all, that comparison is covered directly in Docker Compose vs. Kubernetes.

Frequently asked questions

What's the minimum valid Kubernetes Deployment YAML?

apiVersion: apps/v1, kind: Deployment, a metadata.name, and a spec with replicas, a selector.matchLabels, and a template whose pod labels match that selector exactly. Every other field — resources, probes, env vars — is optional on top of that base.

Why is my Service showing no endpoints even though the Deployment is running?

The Service's spec.selector doesn't match the labels on the running pods. Check spec.template.metadata.labels on the Deployment against spec.selector on the Service — they need to be identical, not just similar. Run kubectl get endpoints <service-name> to confirm; an empty result confirms a selector mismatch rather than a networking issue.

What does the 'm' suffix mean on cpu vs memory in a resources block?

On cpu, 'm' means millicpu — 250m is 0.25 of a CPU core. That suffix is CPU-only; it has no meaning on memory. Memory takes a byte-quantity suffix instead, most commonly Mi (mebibytes, 1024^2 bytes) or Gi (gibibytes), and memory: "256m" is simply invalid, not a small value.

What's the difference between Mi and M for memory limits?

Mi (mebibyte) is a binary unit equal to 1024^2 bytes. M (megabyte) is a decimal unit equal to 10^6 bytes. They're close but not equal — 256Mi is about 268.4 MB. Kubernetes accepts both suffixes, but mixing them inconsistently across a manifest makes resource sizing harder to reason about than picking one and using it everywhere.

Do I need a Service for every Deployment?

Only if something needs a stable way to reach the pods — another service inside the cluster, an Ingress, or a load balancer. A Deployment running a background worker with no inbound traffic doesn't need one at all.

Does updating a ConfigMap automatically restart the pods using it?

No. Pods that reference a ConfigMap via envFrom or env don't pick up changes until they're recreated. A common pattern is to hash the ConfigMap's contents into a pod template annotation, so a config change alters the template itself and triggers a normal rolling update.

Last updated