Sign in
← Kubernetes: workloads
5 min · easy · Lesson 6 of 11

Job: a one-off task, start to finish

The controller for migrations and backups: completions, backoffLimit, restartPolicy, and why you can't clean up a Job by deleting its Pod.

Why

A Deployment assumes "the app runs forever". If the container exits, it gets restarted. A Job works the other way: "run the task to completion and stop." That's for database migrations, backups, a one-off data conversion, generating a report.

What a Job is

A Job is a batch/v1 controller. It creates a Pod from spec.template, waits for exit 0, and marks itself Complete. If the Pod fails with a non-zero code, the Job creates a new Pod and tries again, until it hits backoffLimit.

A Job computes its status from the Pods it created itself. A Job's Pods usually stay in the cluster after finishing, so you can look at their logs. Cleanup happens through ttlSecondsAfterFinished (if set) or by hand. Delete the Job, and its Pods go with it.

kubectl delete pod on a running Job is pointless: the controller immediately creates a replacement. To stop a Job from spawning more Pods, delete the Job itself.

Minimal YAML

apiVersion: batch/v1
kind: Job
metadata:
  name: hello-job
spec:
  completions: 1
  backoffLimit: 2
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: hello
          image: busybox:1.36.1
          command: ["sh", "-c", "echo done; exit 0"]

Fields explained:

You can look up the description of any field straight from the cluster:

kubectl explain job.spec
kubectl explain job.spec.backoffLimit
kubectl explain job.spec.template.spec.restartPolicy

restartPolicy: Never vs OnFailure

With OnFailure, kubelet restarts the container itself on the same Pod. With Never, the Job creates a new Pod for every retry. The behavior looks different: with OnFailure, kubectl get pods shows one Pod with several restarts; with Never, several Pods. Never is easier to debug with: every attempt has its own Pod and its own logs.

restartPolicyWho restartsWhat you see in kubectl get pods
NeverThe Job creates a new Podseveral Pods, one per retry
OnFailurekubelet restarts the containerone Pod with a growing restart count
Alwaysnobody, the manifest is rejectedapiserver rejects the Job

Trying to create a Job with restartPolicy: Always ends in a validation error:

The Job "always" is invalid: spec.template.spec.restartPolicy: Required value: valid values: "OnFailure", "Never"

How a Job counts attempts

The backoffLimit counter isn't reset by a success and doesn't decay over time. It counts failed attempts across the whole Job: failed Pods with Never, and container restarts with OnFailure. Between attempts the Job waits longer each time: 10 seconds, 20, 40, and so on, up to 6 minutes.

Here's a Job with restartPolicy: Never and backoffLimit: 2, whose script exits with code 1 right away:

NAME              READY   STATUS   RESTARTS   AGE
failnever-4kq7d   0/1     Error    0          75s
failnever-9xw2m   0/1     Error    0          64s
failnever-tz8rb   0/1     Error    0          43s

Three Pods: the first attempt and two retries. After the third failure the Job gives up:

NAME        STATUS   COMPLETIONS   DURATION   AGE
failnever   Failed   0/1           75s        75s

The reason is visible in the Job's conditions (kubectl describe job <name>, the Conditions block) and in events: BackoffLimitExceeded means the attempts ran out, DeadlineExceeded means activeDeadlineSeconds kicked in.

Job failed: what's next

Logs first. The Pods of the failed attempts stick around, so you can read the output of every attempt:

kubectl logs -l job-name=hello-job --prefix

The Job itself sets the job-name label (and batch.kubernetes.io/job-name) on its Pods. The --prefix flag tags each line with its Pod's name.

Then comes the trap. A Job in state Failed doesn't start anything else, even after you've fixed the cause: patched the ConfigMap, created the Secret, brought the database up. There won't be new Pods.

Editing the Job to "restart" it doesn't work either: a Job's spec.template is immutable.

The Job "hello-job" is invalid: spec.template: Invalid value: ...: field is immutable

So a Job is restarted by recreating it: delete it and create it again from the same manifest.

kubectl delete job hello-job
kubectl apply -f hello-job.yaml

Same thing in one command: kubectl replace --force -f hello-job.yaml. Keep the manifest in a file. If you dump a live Job with kubectl get job -o yaml and apply it back, the apiserver will refuse: the dump still has the old Job's selector and labels with controller-uid.

What kubectl get jobs shows

kubectl create job hello --image=busybox:1.36.1 -- sh -c 'echo done'
kubectl get jobs
NAME    STATUS     COMPLETIONS   DURATION   AGE
hello   Complete   1/1           5s         10s

STATUS can be Running, Complete, Failed. COMPLETIONS shows successful Pods out of the needed count. Other useful commands:

kubectl get pods -l job-name=hello
kubectl logs -l job-name=hello
kubectl wait --for=condition=complete job/hello --timeout=60s
kubectl delete job hello

Common mistakes

What you'll need for the tasks

The tasks are in the labs/ folder: each lab brings up its own kind cluster on your computer, instructions in its README.md.

Homework

Practice the theory on your own cluster: 2 steps with a check. Set up the cluster with the Kubernetes locally track, the tasks live in the k8s-practice repository.

./run.sh start k8s-first-job     # set up the starting state
./run.sh verify k8s-first-job    # check the current step

In short

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson k8s-workloads-06-job