Exit code 0 means the container completed successfully, but Deployments restart completed containers causing CrashLoopBackOff. Use Jobs for one-time tasks or ensure your container runs a long-lived process.
Fixes Exit Code 0
apiVersion: batch/v1
kind: Job
metadata:
name: my-batch-job
spec:
template:
spec:
containers:
- name: worker
image: my-image:latest
command: ["./run-migration.sh"]
restartPolicy: OnFailure # or Never
backoffLimit: 3apiVersion: batch/v1kind: Jobmetadata:name: my-batch-jobspec:template:spec:containers:- name: workerimage: my-image:latestcommand: ["./run-migration.sh"]restartPolicy: OnFailure # or NeverbackoffLimit: 3Exit Code 0
Exit code 0 indicates successful termination—the container completed its task without errors. However, when running in a Deployment or StatefulSet with the default restartPolicy: Always, Kubernetes restarts completed containers immediately, creating a CrashLoopBackOff loop. This is a semantic mismatch: exit code 0 signals success to the container runtime, but Kubernetes controllers treat any container exit as a condition requiring correction. The container runs, completes successfully, exits, restarts, completes, exits—and eventually enters CrashLoopBackOff as Kubernetes applies exponential backoff delays.
Ask: Should this container run continuously or complete and exit?
Long-running services (web servers, APIs, workers):
- Should use Deployment, StatefulSet, or DaemonSet
- Container must run a persistent foreground process
- Process should handle SIGTERM for graceful shutdown
Batch tasks (scripts, migrations, one-time jobs):
- Should use Job or CronJob resources
- Expected to complete and exit with code 0
- Kubernetes won't restart completed Jobs
If your container should run once and complete, use a Job:
apiVersion: batch/v1
kind: Job
metadata:
name: my-batch-job
spec:
template:
spec:
containers:
- name: worker
image: my-image:latest
command: ["./run-migration.sh"]
restartPolicy: OnFailure # or Never
backoffLimit: 3Jobs expect containers to exit. Exit code 0 = success, no restart.
For setup steps that run before your main application:
spec:
initContainers:
- name: setup
image: my-setup-image
command: ["./init.sh"] # Runs once, exits with 0
containers:
- name: app
image: my-app-image
command: ["./start-server.sh"] # Long-runningInit containers run to completion before the main container starts. Exit code 0 from init containers is expected and doesn't cause restarts.
For Deployments, the container must run continuously:
# Wrong - script completes and exits
CMD ["./setup.sh"]
# Correct - exec into long-running process
CMD ["./setup.sh", "&&", "exec", "python", "server.py"]Or in your shell script:
#!/bin/bash
./setup.sh
exec python server.py # exec replaces shell with server processThe last command must be a process that runs until terminated.
If you need the container to stay running without a real service (for debugging):
command: ["tail", "-f", "/dev/null"]
# or
command: ["sleep", "infinity"]This keeps the container alive so you can exec into it. Not recommended for production—use proper service processes instead.
Deployment defaults to restartPolicy: Always, which cannot be changed. For different policies:
# Jobs allow OnFailure or Never
restartPolicy: OnFailure # Restart only if exit code != 0
restartPolicy: Never # Never restart, even on failureIf you need restartPolicy other than Always for a pod, you cannot use Deployment—use a bare Pod or Job instead.
The CrashLoopBackOff state for exit code 0 is technically correct behavior—Kubernetes is doing what it's designed to do. The "fix" is using the right resource type for your workload.
Common patterns that cause this issue:
- Database migrations that should run once before the app
- Configuration scripts that generate files for the main container
- Health check scripts mistakenly deployed as the main container
- CI/CD pipelines deploying test containers as Deployments
For sidecar containers that should exit after completing their task, consider the new Kubernetes sidecar container feature (beta in 1.28+) or use Jobs with multiple containers.
In GitOps workflows, this often appears when a Job manifest is accidentally converted to a Deployment, or when a Dockerfile CMD is changed from a service to a script.