BlueByte
CrashLoopBackOffWorkaround

Kubernetes 파드가 CrashLoopBackOff에 멈춤

작성 Haneul Seo2026년 8월 27일 업데이트5 min

안녕하세요, BlueByte입니다. CrashLoopBackOff는 무섭게 보이지만, 컨테이너가 시작 직후 계속 종료된다고 Kubernetes가 알려주는 것뿐입니다. 원인이 아니라 증상이죠. 오늘은 원인을 실제로 짚어주는 두 가지 — 이전 컨테이너의 로그와 종료 코드 — 를 읽고, 추측 대신 진짜 문제를 고치는 순서로 가보겠습니다.

CrashLoopBackOff가 (아닌) 것

파드가 Running에 못 이릅니다:

NAME        READY   STATUS             RESTARTS   AGE
api-7d9f    0/1     CrashLoopBackOff   5          3m

컨테이너가 시작하고, 종료되고, kubelet이 재시작합니다 — 매번 더 길게 물러서면서(10s, 20s, 40s, 최대 5분). 상태는 그 반복이지 이유가 아닙니다. 이유는 컨테이너를 종료시키는 무언가이고, 그건 로그와 종료 코드에 있습니다.

몇 안 되는 원인, 각각의 지문

주 프로세스가 시작 직후 종료되는 것입니다. 흔한 이유는 각각 알아볼 수 있습니다:

  • 시작 시 애플리케이션 오류 — 누락된 환경변수, 닿지 않는 의존성(아직 안 뜬 DB), 잘못된 설정. 종료 코드는 대개 1이나 2, 로그에 스택 트레이스.
  • 메모리 초과 — 메모리 한도를 넘어 kill됨. 종료 코드 137(SIGKILL), 이유 OOMKilled.
  • 앱이 느리게 떠서 liveness 프로브가 실패.
  • 잘못된 command나 entrypoint — 프로세스가 즉시 종료되거나 바이너리를 못 찾음, 로그가 비어 있는 경우가 많음.
  • 볼륨·시크릿 누락 — 앱이 설정을 못 읽고 죽음.

이전 컨테이너의 로그와 종료 코드 읽기

이게 일의 전부입니다. 지금 뜨는 인스턴스가 아니라 이미 죽은 인스턴스를 읽으세요:

kubectl logs api-7d9f --previous

그다음 종료 코드와 이유:

kubectl describe pod api-7d9f

Last State: Terminated를 보세요. 137 + OOMKilled는 메모리, 1/2는 로그가 설명하는 앱 오류, Liveness probe failed 이벤트는 프로브, Events 목록은 누락된 ConfigMap·Secret·마운트를 짚어 줍니다.

구체적 원인을 고치기

  • 앱 오류: 누락된 환경변수를 넣거나, 설정을 고치거나, 의존성이 닿게 한 뒤 파드가 재시작되게 둡니다.
  • OOMKilled (137): 메모리 한도를 올리거나 사용량을 줄입니다:
resources:
  limits:
    memory: "512Mi"
  • liveness 프로브가 너무 빡셈: 앱이 뜰 시간을 줍니다:
livenessProbe:
  httpGet: { path: /healthz, port: 8080 }
  initialDelaySeconds: 30

실제 사례: 누락된 환경변수

kubectl get pods가 api-7d9f를 재시작 6회의 CrashLoopBackOff로 보여줍니다. kubectl logs api-7d9f --previous는 Error: DATABASE_URL is not set만 찍고 끝이며, kubectl describe pod는 Exit Code: 1을 확인해 줍니다. 원인이 분명합니다 — 메모리도 프로브도 아닙니다. 배포의 env(또는 참조된 Secret)에 DATABASE_URL을 추가해 적용하면, 다음 재시작이 Running에 이르고 카운트가 멈춥니다. 이전 로그를 먼저 읽은 덕에, 원인도 아닌 메모리 한도를 올리는 헛수고를 피했습니다.

안정됐는지 확인

파드를 지켜보세요:

kubectl get pod api-7d9f -w

READY 1/1의 Running으로 옮겨가고 RESTARTS가 더 오르지 않아야 합니다. 재시작이 계속 오르면, 로그가 아직 해결 안 된 원인을 짚고 있는 것입니다.

ImagePullBackOff·Pending과의 구분

ImagePullBackOff는 kubectl get pods에서 비슷해 보이지만 이미지가 아예 시작되지 못한 것입니다 — 이름이나 레지스트리 자격 증명이 틀린 것이죠. Pending은 파드가 스케줄되지 못한 것(자리 있는 노드가 없음)입니다. CrashLoopBackOff는 컨테이너가 실제로 돌다가 종료된 것이라, 로그와 종료 코드가 지도가 되는 유일한 경우입니다. 메모리 한도를 신중히 잡고, 프로브를 넉넉히 두고, 의존성은 init 컨테이너로 기다리게 해서 파드가 반복하지 않고 대기하게 하세요.

관련 질문

종료 코드 137은 무슨 뜻인가요?

컨테이너가 SIGKILL로 종료된 것으로, 거의 항상 메모리 초과 kill입니다. 메모리 한도를 올리거나 사용량을 줄이세요.

kubectl logs --previous가 이전 컨테이너가 없다고 합니다.

컨테이너가 아무것도 쓰기 전에 죽는 것입니다 — 앱 로깅보다 먼저 도는 command/args와 이미지 entrypoint를 확인하세요.

재시작 횟수가 계속 오릅니다. 해로운가요?

백오프가 재시작 간 최대 5분까지 늘어나므로 노드를 몰아붙이진 않지만, 근본 원인이 고쳐질 때까지 파드는 트래픽을 처리하지 못합니다.

ImagePullBackOff와 어떻게 다른가요?

ImagePullBackOff는 이미지를 못 가져온 것(이름·자격 증명 오류)입니다. CrashLoopBackOff는 이미지가 돌았고 프로세스가 종료된 것입니다.

앱이 나중에 뜨는 DB를 필요로 합니다. 크래시 루프를 어떻게 막죠?

앱이 DB에서 크래시하게 두지 말고 init 컨테이너나 readiness 프로브로 의존성을 기다리게 하세요 — 그러면 DB가 뜰 때까지 파드가 반복하지 않고 대기합니다.

참고 자료

Haneul Seo

Infrastructure engineer · 10+ years running Linux fleets

같은 카테고리 다른 글

detected dubious ownershipFixed

Git: fatal: detected dubious ownership in repository

Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.

Git
failed calling webhookFixed

Kubernetes: Internal error occurred: failed calling webhook

admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.

Kubernetes
1205Fixed

MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.

MySQL
MISCONFFixed

Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk

읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.

Redis
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryFixed

Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)

V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.

Node.js
exec /docker-entrypoint.sh: exec format errorFixed

Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트

컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.

Docker