본문으로 건너뛰기
무한 루프에 갇힌 컨테이너가 계속 재시작되는 모습을 표현한 일러스트
백엔드 개발 / · PT4M read

Kubernetes CrashLoopBackOff: 파드가 계속 재시작되는 원인과 해결법 완벽 가이드

“저 파드 왜 자꾸 Restart 횟수가 올라가요?”

월요일 아침, 출근하자마자 슬랙 알람이 쏟아집니다. kubectl get pods를 치니 눈앞이 빨개집니다.

NAME                        READY   STATUS             RESTARTS   AGE
my-app-7d4b5c8f9-xk2mv      0/1     CrashLoopBackOff   15         12m

CrashLoopBackOff. 쿠버네티스를 운영하다 보면 OOMKilled만큼이나 자주 마주치는 골치 아픈 상태입니다.

파드가 시작되고, 곧바로 죽고, 쿠버네티스가 다시 살리고, 또 죽고… 이 무한 루프가 바로 CrashLoopBackOff입니다. 더 짜증나는 건, 쿠버네티스가 “야, 너 자꾸 죽어서 이제 천천히 다시 시작시킬게”라며 재시작 간격을 점점 늘린다는 것입니다.

10초 → 20초 → 40초 → 80초 → … → 최대 5분

디버깅하려고 로그를 보려는데, 파드가 5분마다 떴다 죽었다 하니 답답해서 미칠 지경이죠.

CrashLoopBackOff의 정체: 쿠버네티스의 자기 보호 본능

CrashLoopBackOff는 에러 그 자체가 아니라, “에러가 계속 발생하고 있다”는 상태 표시입니다.

쿠버네티스 입장에서 생각해 봅시다. 컨테이너가 시작되자마자 죽으면 당연히 재시작시킵니다. 그런데 재시작해도 또 죽으면? 또 재시작합니다. 이게 10번, 20번 반복되면 어떻게 될까요?

시스템 리소스가 낭비되고, CPU가 무의미하게 소모됩니다. 그래서 쿠버네티스는 지수 백오프(Exponential Backoff) 전략을 사용해서 재시작 간격을 점점 늘립니다. 이게 바로 ‘BackOff’의 의미입니다.

핵심 포인트: 진짜 에러는 다른 곳에 있다

CrashLoopBackOff를 해결하려면 **“왜 컨테이너가 죽는가?”**를 찾아야 합니다. CrashLoopBackOff 자체를 고치는 게 아니라, 그 원인이 되는 진짜 문제를 해결해야 합니다.

Exit Code로 범인 추적하기

컨테이너가 죽을 때 남기는 Exit Code는 결정적인 단서입니다.

Exit Code의미일반적인 원인
0정상 종료프로세스가 의도대로 종료됨 (Job에서는 정상, Deployment에서는 문제)
1일반 에러애플리케이션 레벨 에러, 코드 버그, 설정 오류
126실행 불가명령어를 찾았지만 실행 권한이 없음
127명령어 없음지정된 명령어나 바이너리를 찾을 수 없음
137SIGKILL (9)OOMKilled, 강제 종료 (128 + 9)
139SIGSEGV (11)Segmentation Fault, 잘못된 메모리 접근
143SIGTERM (15)정상적인 종료 요청 (128 + 15)

Exit Code 확인하는 법

kubectl describe pod <pod-name> | grep -A 5 "Last State"

출력 예시:

Last State:     Terminated
  Reason:       Error
  Exit Code:    1
  Started:      Mon, 20 Jan 2026 09:15:23 +0900
  Finished:     Mon, 20 Jan 2026 09:15:25 +0900

Exit Code가 1이면 애플리케이션 에러, 137이면 메모리 문제(OOMKilled)를 의심하세요.

7가지 흔한 원인과 해결책

1. 애플리케이션 코드 에러 (Exit Code 1)

증상: 컨테이너가 시작되자마자 즉시 종료됩니다. 로그에 Python traceback이나 Java stack trace가 출력됩니다.

원인: 코드 버그, 초기화 실패, 의존성 누락 등

디버깅:

# 이전 컨테이너의 로그 확인 (중요!)
kubectl logs <pod-name> --previous

# 더 자세한 정보
kubectl describe pod <pod-name>

해결책: 로그를 분석해서 코드를 수정합니다. 로컬에서 동일한 이미지로 테스트해 보세요.

# 로컬에서 동일한 이미지 테스트
docker run -it <image-name> /bin/sh

2. 잘못된 명령어/엔트리포인트 (Exit Code 126, 127)

증상: command not found, permission denied 에러가 로그에 출력됩니다.

원인: Dockerfile의 ENTRYPOINT나 Kubernetes의 command 설정 오류

디버깅:

# 컨테이너 내부에서 직접 확인
kubectl run debug --image=<your-image> --rm -it -- /bin/sh

# 명령어 경로 확인
which node
which python

해결책:

# 잘못된 예
spec:
  containers:
  - name: app
    command: ["node"]  # 경로가 없음
    args: ["app.js"]

# 올바른 예
spec:
  containers:
  - name: app
    command: ["/usr/local/bin/node"]  # 전체 경로 명시
    args: ["/app/app.js"]

3. 설정 파일/환경 변수 누락 (Exit Code 1)

증상: Config file not found, Environment variable X is required 같은 에러

원인: ConfigMap, Secret이 마운트되지 않았거나, 필수 환경변수가 설정되지 않음

디버깅:

# 환경변수 확인
kubectl exec <pod-name> -- env | grep DATABASE

# ConfigMap 상태 확인
kubectl get configmap <configmap-name> -o yaml

# Secret 상태 확인 (base64 디코딩 필요)
kubectl get secret <secret-name> -o jsonpath='{.data.password}' | base64 -d

해결책:

spec:
  containers:
  - name: app
    env:
    - name: DATABASE_URL
      valueFrom:
        secretKeyRef:
          name: db-secret
          key: url
    envFrom:
    - configMapRef:
        name: app-config

4. Liveness Probe 실패

증상: 컨테이너는 시작되지만, 일정 시간 후 계속 재시작됩니다. Events에 Liveness probe failed 메시지가 있습니다.

원인: Liveness Probe가 너무 공격적으로 설정되었거나, 애플리케이션 시작 시간이 오래 걸림

디버깅:

kubectl describe pod <pod-name> | grep -A 10 "Liveness"

해결책:

# 너무 공격적인 설정 (문제)
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 5   # 시작 5초 후 바로 체크
  periodSeconds: 5         # 5초마다 체크
  failureThreshold: 1      # 1번 실패하면 바로 재시작

# 적절한 설정 (해결)
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 30  # 충분한 시작 시간 확보
  periodSeconds: 10        # 10초마다 체크
  failureThreshold: 3      # 3번 연속 실패해야 재시작

# 시작 시간이 오래 걸리는 앱이라면 startupProbe 사용
startupProbe:
  httpGet:
    path: /health
    port: 8080
  failureThreshold: 30
  periodSeconds: 10        # 최대 5분까지 기다림

:::tip Startup Probe vs Liveness Probe

  • startupProbe: 앱이 완전히 시작될 때까지만 사용. 성공하면 비활성화됨
  • livenessProbe: 앱 실행 중 건강 상태 체크. 실패하면 재시작
  • Java나 .NET 같이 시작 시간이 오래 걸리는 앱에는 startupProbe를 꼭 설정하세요! :::

5. OOMKilled (Exit Code 137)

증상: Exit Code가 137이고, kubectl describe pod에서 Reason: OOMKilled가 보입니다.

원인: 컨테이너가 설정된 메모리 limit을 초과함

디버깅:

kubectl describe pod <pod-name> | grep -A 3 "Last State"

해결책:

resources:
  requests:
    memory: "256Mi"
  limits:
    memory: "512Mi"  # request의 1.5~2배 정도로 설정

:::note OOMKilled에 대한 더 자세한 내용은 Kubernetes OOMKilled: 내 파드가 자꾸 죽는 진짜 이유 (Exit Code 137) 포스트를 참고하세요. :::

6. 이미지 Pull 실패 후 CrashLoopBackOff

증상: 처음엔 ImagePullBackOff였다가 나중에 CrashLoopBackOff로 바뀜

원인: 이미지가 손상되었거나, 플랫폼(amd64/arm64) 불일치

디버깅:

kubectl describe pod <pod-name> | grep -A 5 "Events"

# 노드의 아키텍처 확인
kubectl get nodes -o wide

해결책:

# 멀티 아키텍처 이미지 빌드
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .

7. 볼륨 마운트 실패

증상: Unable to mount volumes, subPath 관련 에러

원인: PVC가 아직 Bound되지 않았거나, subPath가 존재하지 않음

디버깅:

# PVC 상태 확인
kubectl get pvc

# PV 상태 확인
kubectl get pv

해결책:

# PVC가 Bound될 때까지 기다리는 initContainer 사용
initContainers:
- name: wait-for-volume
  image: busybox
  command: ['sh', '-c', 'until [ -d /data ]; do sleep 1; done']
  volumeMounts:
  - name: data-volume
    mountPath: /data

실전 디버깅 플로우차트

CrashLoopBackOff를 만나면 이 순서로 디버깅하세요:

1. kubectl describe pod <pod-name>
   → Events 섹션에서 에러 메시지 확인
   → Last State에서 Exit Code 확인

2. kubectl logs <pod-name> --previous
   → 이전 컨테이너의 로그 확인 (매우 중요!)

3. Exit Code에 따른 조치:
   - 1: 애플리케이션 로그 분석, 코드/설정 확인
   - 126/127: 명령어 경로, 실행 권한 확인
   - 137: 메모리 limit 확인, OOMKilled 대응
   - 139: 코드의 메모리 접근 버그 확인

4. 그래도 안 되면 디버그 컨테이너 사용:
   kubectl debug <pod-name> -it --image=busybox -- /bin/sh

디버깅 꿀팁: --previous 플래그

CrashLoopBackOff 상황에서 가장 많이 하는 실수가 kubectl logs <pod-name>만 치는 것입니다. 문제는 이 명령어가 현재 실행 중인 컨테이너의 로그를 보여준다는 점입니다.

CrashLoopBackOff 상태에서는 컨테이너가 시작되자마자 죽기 때문에, 로그를 보는 시점에 이미 새 컨테이너가 떴을 수 있습니다.

반드시 --previous 플래그를 사용하세요!

# 이전(죽은) 컨테이너의 로그 확인
kubectl logs <pod-name> --previous

# 여러 컨테이너가 있는 경우
kubectl logs <pod-name> -c <container-name> --previous

예방이 최고의 치료

1. 로컬에서 먼저 테스트

# 프로덕션과 동일한 환경으로 로컬 테스트
docker run -it \
  -e DATABASE_URL=xxx \
  -e API_KEY=xxx \
  <your-image>

2. 적절한 Probe 설정

# 모든 프로덕션 워크로드에 기본으로 설정
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 3

3. 리소스 제한 설정

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "256Mi"

4. 모니터링 및 알람 설정

Prometheus + Grafana로 파드 재시작 횟수를 모니터링하세요:

# 최근 1시간 동안 재시작 횟수가 5번 이상인 파드
sum(increase(kube_pod_container_status_restarts_total[1h])) by (pod, namespace) > 5

마치며

CrashLoopBackOff는 쿠버네티스가 “뭔가 잘못됐어!”라고 외치는 신호입니다. 당황하지 말고 체계적으로 접근하세요:

  1. Exit Code 확인 → 범인의 윤곽을 잡고
  2. --previous 로그 확인 → 결정적 증거를 찾고
  3. 원인별 해결 → 근본 원인을 제거

특히 kubectl logs --previous는 정말 중요합니다. 이것만 기억해도 디버깅 시간을 절반으로 줄일 수 있습니다.

오늘도 여러분의 파드가 무사히 Running 상태로 남아있길 바랍니다!


관련 글:

My avatar

글을 마치며

이 글이 도움이 되었기를 바랍니다. 궁금한 점이나 의견이 있다면 댓글로 남겨주세요.

더 많은 기술 인사이트와 개발 경험을 공유하고 있으니, 다른 포스트도 확인해보세요.

유럽살며 여행하며 코딩하는 노마드의 여정을 함께 나누며, 함께 성장하는 개발자 커뮤니티를 만들어가요! 🚀


관련 포스트