Docker 컨테이너가 계속 재시작될 때는 로그를 보는 순서만 잡아도 원인 범위가 빠르게 좁혀집니다. docker ps -a로 종료 코드를 확인하고, docker logs로 마지막 출력을 읽고, 재시작 자체를 멈춰 컨테이너 안을 들여다보는 흐름을 명령어와 함께 정리합니다.

Docker 컨테이너가 계속 재시작되는 문제는 일부 환경에서는 "로그를 어느 시점에 읽었는가"에서 갈립니다. 컨테이너가 살아났다 죽기를 반복하면 로그도 계속 초기화되는 것처럼 보여서, 화면에 뜬 마지막 몇 줄만 보고 엉뚱한 곳을 고치기 쉽습니다.
이 글은 재시작 루프에 빠진 컨테이너에서 어떤 명령을 어떤 순서로 실행해 원인을 좁히는지를 다룹니다.
여기서 흔한 함정이 하나 있습니다. docker logs에 아무것도 안 나오면 "로그가 없다"고 판단하기 쉽지만, 실제로는 컨테이너가 시작되기도 전에 죽어서 애초에 출력할 게 없는 경우가 많습니다. 둘은 원인도, 해결법도 완전히 다릅니다.
참고로 이 글에서 말하는 컨테이너는 프로그램과 실행 환경을 통째로 묶어 격리 실행하는 단위이고, 터미널은 컴퓨터에 명령어를 직접 입력하는 창구입니다. 명령어는 모두 터미널에 그대로 붙여 넣으면 됩니다.
재시작 반복일 때 먼저 실행할 명령 세 개
재시작 루프 진단은 상태 확인 → 로그 읽기 → 종료 코드 확인, 이 세 단계 순서로 하면 일부 환경에서는 원인 범위가 잡힙니다.
먼저 컨테이너 목록과 상태를 봅니다.
docker ps -a
이 명령은 실행 중인 것뿐 아니라 죽은 컨테이너까지 모두 보여줍니다. STATUS 칸에 Restarting (1) 5 seconds ago 처럼 표시되면 재시작 루프가 맞습니다. 괄호 안 숫자가 종료 코드입니다.
다음으로 마지막 출력을 읽습니다.
docker logs --tail 100 컨테이너이름
--tail 100은 "마지막 100줄만 보여달라"는 뜻입니다. 재시작이 반복되면 로그가 길게 쌓이니, 처음부터 다 읽지 말고 끝부분부터 봅니다.
그리고 시간 정보를 붙여 다시 봅니다.
docker logs -t --tail 50 컨테이너이름
-t는 각 줄에 시간을 붙입니다. 같은 에러가 환경과 절차에 따라 간격으로 반복되는지 보이면 "루프의 주기"가 잡힙니다.
💡 여러 줄이 뒤섞여 헷갈리면 docker logs 컨테이너이름 2>&1 | tail -50 처럼 표준 출력과 에러 출력을 합쳐서 보는 편이 낫습니다. 프로그램에 따라 에러를 다른 통로로 내보내기 때문입니다.
여기까지 했을 때 로그 마지막에 명확한 에러 문장이 있으면 절반은 끝난 셈입니다. 문제는 로그가 비어 있거나, 있어도 원인이 애매한 경우입니다. 그럴 때는 종료 코드를 봅니다.
종료 코드로 원인 범위 좁히기
종료 코드는 컨테이너 안의 프로그램이 죽으면서 남긴 숫자 한 개이고, 로그가 비어 있을 때 가장 빠른 단서입니다.
정확한 값은 이렇게 확인합니다.
docker inspect 컨테이너이름 --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'
종료 코드, 메모리 초과로 강제 종료됐는지 여부, 도커가 기록한 에러 문구를 한 줄로 뽑아줍니다.
리눅스 계열에서 관례적으로 쓰이는 종료 코드의 의미는 대략 이렇게 나뉩니다.
| 종료 코드 | 흔한 의미 | 먼저 볼 곳 |
|---|---|---|
| 0 | 정상 종료했는데 재시작 정책 때문에 다시 뜸 | restart 정책, 실행 명령이 바로 끝나는 형태인지 |
| 1 | 애플리케이션이 스스로 에러로 종료 | 앱 로그, 환경 변수, 설정 파일 |
| 125 | 도커 자체가 실행을 거부 | 명령 옵션, 포트·볼륨 설정 |
| 126 | 실행 파일이 있지만 실행 권한 없음 | 엔트리포인트 스크립트 권한 |
| 127 | 명령이나 파일을 찾을 수 없음 | 경로 오타, 이미지 안에 해당 명령 존재 여부 |
| 137 | 외부에서 강제 종료(메모리 초과 포함) | 메모리 한도, OOMKilled 값 |
| 143 | 종료 신호를 받고 내려감 | 오케스트레이터·헬스체크 설정 |
숫자별 정확한 정의는 사용 중인 도커 버전의 공식 문서에서 확인하는 편이 안전합니다. 위 표는 원인을 좁히는 출발점 정도로 쓰면 충분합니다.
특히 OOMKilled가 true면 코드 문제가 아니라 메모리 한도 문제입니다. 예를 들어 이미지 변환 작업을 컨테이너에서 돌리면, 큰 파일이 들어올 때만 한도를 넘겨 죽는 패턴이 나오기도 합니다. 이때 로그에는 아무 에러도 안 남습니다. 갑자기 끊긴 것처럼 보일 뿐입니다.
흔한 원인을 빈도 순서로 점검
재시작 루프의 원인은 대체로 설정·환경 변수·의존 서비스 순으로 몰려 있습니다. 아래 순서대로 훑는 게 시간을 가장 아낍니다.
1. restart 정책 + 명령이 바로 끝나는 구조
↓ 아니면
2. 환경 변수 / 설정 파일 누락
↓ 아니면
3. 의존 서비스(DB 등) 미준비
↓ 아니면
4. 포트 충돌 · 볼륨 권한
↓ 아니면
5. 메모리 한도 초과
1. 정상 종료인데 계속 살아나는 경우. 종료 코드가 0인데 재시작이 반복되면 앱이 잘못된 게 아닙니다. 컨테이너 안 프로세스가 할 일을 마치고 끝났고, 재시작 정책이 always라서 도커가 계속 되살리는 겁니다. 이 경우 정책을 바꾸는 게 답입니다.
2. 환경 변수·설정 누락. 데이터베이스 주소나 비밀 키 같은 값이 비어 있으면 앱이 시작 직후 스스로 종료합니다. 로그 첫 줄에 설정 관련 문구가 있는지 봅니다.
docker inspect 컨테이너이름 --format '{{range .Config.Env}}{{println .}}{{end}}'
컨테이너에 실제로 들어간 환경 변수 목록을 출력합니다. .env 파일을 만들었는데 컨테이너까지 전달되지 않은 상황이 생각보다 흔합니다.
3. 의존 서비스 미준비. 앱이 DB보다 먼저 떠서 연결 실패로 죽고, 재시작하고, 또 죽는 패턴입니다. 로그에 connection refused 계열 문구가 반복되면 여기입니다. 앱 쪽에 재시도 로직을 넣거나 시작 순서를 조정해야 합니다.
4. 포트 충돌·볼륨 권한. port is already allocated, permission denied 같은 문구가 도커 쪽에서 나옵니다. 이건 앱 로그가 아니라 docker inspect의 .State.Error나 실행 시 출력에 찍힙니다.
로그가 아예 안 남을 때 확인 순서
docker logs가 비어 있다면, 로그를 못 보는 게 아니라 로그를 남길 만큼 오래 살지 못한 상태일 가능성이 큽니다.
이럴 때는 재시작을 먼저 멈춥니다. 계속 되살아나면 관찰 자체가 안 되기 때문입니다.
docker update --restart=no 컨테이너이름 docker stop 컨테이너이름
첫 줄은 재시작 정책을 끄고, 둘째 줄은 컨테이너를 멈춥니다. 이제 조용해진 상태에서 안을 들여다봅니다.
그다음 원래 실행 명령을 무시하고 셸(명령을 받아주는 프로그램)만 띄웁니다.
docker run -it --entrypoint sh 이미지이름
컨테이너 안으로 들어가면 파일이 제대로 있는지, 실행하려던 명령이 존재하는지 직접 확인합니다. 종료 코드 127(명령을 못 찾음)이나 126(권한 없음)일 때 특히 유용합니다.
컨테이너 밖에서 벌어진 일을 보고 싶으면 도커 자체 이벤트를 봅니다.
docker events --since 10m
최근 도커에서 일어난 시작·종료·재시작 기록을 시간순으로 보여줍니다. 누가 컨테이너를 죽였는지(도커인지, 오케스트레이터인지, 메모리 한도인지) 판단할 때 도움이 됩니다.
점검 체크리스트로 정리하면 이렇습니다.
- ☐
docker ps -a에서 STATUS와 괄호 안 종료 코드 확인 - ☐
docker logs --tail 100으로 마지막 출력 읽기 - ☐
docker inspect로 ExitCode·OOMKilled·Error 세 값 확인 - ☐환경 변수가 실제로 전달됐는지 확인
- ☐
--restart=no로 루프를 멈춘 뒤--entrypoint sh로 안에서 확인
왜 로그가 사라진 것처럼 보이는가
컨테이너 로그가 짧게만 남는 현상은 컨테이너가 삭제·재생성되는 구조일 때 자주 생깁니다.
docker restart처럼 같은 컨테이너를 다시 켜면 로그가 이어집니다. 반면 컨테이너를 지우고 새로 만드는 방식(도커 컴포즈로 다시 올리거나, 오케스트레이터가 새 컨테이너를 띄우는 경우)이면 로그도 새 컨테이너 기준으로 시작합니다. 이전 기록은 이전 컨테이너에 남아 있습니다.
| 상황 | 로그 유지 | 이전 로그 찾는 법 |
|---|---|---|
| 같은 컨테이너 재시작 | 이어짐 | docker logs 그대로 |
| 컨테이너 삭제 후 재생성 | 새로 시작 | docker ps -a에서 이전 컨테이너 ID로 조회 |
| 컨테이너까지 정리됨 | 사라짐 | 로그 드라이버·외부 수집 설정 필요 |
세 번째 줄이 중요합니다. 컨테이너가 정리되면 그 안의 로그도 함께 없어집니다. 재시작이 잦은 서비스라면 로그를 컨테이너 밖으로 보내는 설정을 미리 해두는 편이 낫습니다. 로그 드라이버 옵션과 지원 범위는 도커 공식 문서의 logging driver 항목에 나와 있습니다.
막히면 여기부터 다시
재시작 루프는 "무엇이 잘못됐나"보다 "누가 죽였나"를 먼저 정하면 훨씬 빨리 풀립니다.
앱이 스스로 죽었다면 로그와 환경 변수 쪽, 도커나 시스템이 죽였다면 종료 코드와 메모리 한도 쪽입니다. 이 갈림길을 docker inspect 한 줄로 판별하는 게 이 글의 핵심입니다.
진단이 잘 안 될 때는 환경에 따라 재시작 정책을 먼저 끄세요. 계속 되살아나는 컨테이너를 관찰하려는 시도가 시간을 가장 많이 잡아먹습니다.
개인 서버에 블로그나 작은 API를 올려둔 상태라면, 위 다섯 줄짜리 체크리스트만 순서대로 돌려도 원인이 정책 문제인지 설정 문제인지 갈립니다. 거기서부터 고치면 됩니다.
함께 보면 좋은 글
'개발 & 기술 > DevOps·인프라' 카테고리의 다른 글
| Docker no space left on device 해결: 뭐부터 지울까 (0) | 2026.08.28 |
|---|---|
| Cloudflare AI Search 사용법: 내 데이터에 검색 붙이기 (0) | 2026.08.12 |
| Cloudflare Internal DNS 정리: 사내망에 도입할 만한가 (1) | 2026.08.02 |
| Ollama 느릴 때 점검할 7가지: 모델·양자화·GPU 설정 (1) | 2026.08.01 |
| Cloudflare Tunnel 끊김, 원인 진단과 재연결 자동화 정리 (0) | 2026.07.30 |