문제 해결: 일반적인 Docker 컨테이너 오류 빠르게 진단하기

이 필수 가이드로 신속한 Docker 컨테이너 문제 해결 기술을 마스터하세요. 핵심 Docker 명령어를 사용하여 시작 실패를 진단하는 구조화된 프로세스를 배웁니다. `docker ps -a`를 활용한 충돌 식별, `docker logs`를 사용한 중요 정보 추출, `docker inspect`를 통한 고급 구성 분석 방법을 자세히 설명합니다. 이 문서는 종료 코드 127 오류, 포트 충돌, OOMKilled 이벤트를 포함한 빈번한 문제에 대한 실용적인 예제와 해결 방안을 제공하여 근본 원인을 신속히 파악하고 서비스를 복원할 수 있도록 합니다.

문제 해결: 일반적인 Docker 컨테이너 오류 빠르게 진단하기

Docker 컨테이너가 즉시 종료될 때, 이미지를 다시 빌드하거나 임의의 플래그를 변경하는 것으로 시작하지 마세요. Docker가 알고 있는 정보(컨테이너 상태, 종료 코드, 로그, Docker가 실행하려고 시도한 정확한 명령어)를 찾는 것부터 시작하세요. 이 네 가지 정보는 일반적으로 문제를 빠르게 좁혀줍니다.

컨테이너는 격리된 프로세스에 불과합니다. 메인 프로세스가 종료되면 컨테이너도 종료됩니다. 이는 충돌, 실행 파일 누락, 완료된 배치 작업, 실패한 상태 의존성, 또는 커널이 너무 많은 메모리를 사용하여 프로세스를 종료하는 경우일 수 있습니다. 아래 명령어는 이러한 경우를 구분하는 데 도움이 됩니다.

중지된 컨테이너를 먼저 찾으세요

docker ps는 실행 중인 컨테이너만 표시합니다. 실패한 시작 컨테이너는 일반적으로 모든 컨테이너를 요청하지 않으면 숨겨져 있습니다:

docker ps -a

STATUS, COMMAND, NAMES를 확인하세요:

CONTAINER ID   IMAGE          COMMAND              STATUS                      NAMES
2d3f4b5c6e7a   my-app:latest  "/usr/bin/start"     Exited (127) 2 minutes ago  web-service
91aa34c0db22   worker:latest  "python worker.py"   Exited (0) 10 minutes ago   nightly-worker

Exited (0)은 종종 프로세스가 성공적으로 완료되었음을 의미합니다. 이는 일회성 작업의 경우 정상입니다. 웹 서비스의 경우 명령어가 포그라운드에서 유지되지 않고 실행되어 완료되었음을 의미할 수 있습니다.

0이 아닌 종료 코드는 실패를 가리키지만 최종 답변이 아닌 단서로 취급하세요. 종료 코드 127은 일반적으로 명령어를 찾을 수 없음을 의미합니다. 126은 일반적으로 찾았지만 실행할 수 없음을 의미합니다. 137은 종종 프로세스가 SIGKILL을 수신했음을 의미하며, 컨테이너에서는 메모리 압박과 관련이 있는 경우가 많지만 항상 그런 것은 아닙니다. 항상 로그와 검사 출력으로 확인하세요.

변경하기 전에 로그를 읽으세요

Docker는 기본 로깅 드라이버에 대해 컨테이너의 메인 프로세스에서 stdout과 stderr를 캡처합니다. 다음을 사용하세요:

docker logs web-service

유용한 옵션:

docker logs --tail 100 web-service
docker logs --since 15m web-service
docker logs -t web-service
docker logs -f web-service

로그에 config file not found라고 표시되면 마운트와 환경을 확인하세요. 애플리케이션 스택 추적이 표시되면 애플리케이션을 디버깅하세요. 로그가 비어 있으면 프로세스가 출력을 생성하기 전에 실패했거나 이미지 진입점이 잘못되었을 수 있습니다.

충돌 루프의 경우 docker logs -f만 사용하지 마세요. 이는 상태를 제공하지 않고 실패가 활성 상태인 것처럼 느끼게 할 수 있습니다. 로그를 docker inspect와 함께 사용하세요.

컨테이너 상태 검사

docker inspect는 큰 JSON 문서를 반환합니다. 모든 것이 필요한 경우는 드뭅니다. 형식이 지정된 필드부터 시작하세요:

docker inspect -f 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}' web-service

그런 다음 명령어와 이미지 구성을 검사하세요:

docker inspect -f 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} user={{.Config.User}}' web-service

오류에 파일이 포함된 경우 마운트를 확인하세요:

docker inspect -f '{{json .Mounts}}' web-service

컨테이너가 메모리로 인해 종료된 경우 .State.OOMKilled가 중요한 필드입니다. true이면 메모리를 늘리는 것이 도움이 될 수 있지만, 더 나은 다음 질문은 메모리가 왜 증가했는지입니다. 더 큰 제한은 나중에 실패할 때까지 누출을 숨길 수 있습니다.

가능하면 대화형 셸로 재현하세요

이미지에 셸이 포함된 경우 진입점을 재정의하고 파일 시스템을 검사하세요:

docker run --rm -it --entrypoint /bin/sh my-app:latest

일부 이미지에는 Bash가 있습니다:

docker run --rm -it --entrypoint /bin/bash my-app:latest

내부에서 파일과 명령어 경로를 확인하세요:

ls -l /usr/bin/start
id
env

최소 이미지에는 셸이 포함되지 않을 수 있습니다. 이 경우 이미지의 사용 가능한 도구를 사용하거나, 임시 디버그 변형을 다시 빌드하거나, Dockerfile 및 빌드 출력을 검사하세요. 문제 해결이 한 번 불편했다고 해서 프로덕션 이미지에 디버그 패키지를 영구적으로 추가하지 마세요.

명령어를 찾을 수 없음: 종료 127

종료 127은 일반적으로 Docker가 ENTRYPOINT 또는 CMD로 지정된 실행 파일을 찾을 수 없거나 시작 스크립트가 누락된 명령어를 실행하려고 시도했음을 의미합니다.

일반적인 원인:

  • 실행 파일이 이미지에 복사되지 않았습니다.
  • 경로는 호스트에서는 올바르지만 이미지 내부에서는 올바르지 않습니다.
  • 스크립트가 /bin/bash를 사용하지만 이미지에는 /bin/sh만 있습니다.
  • 명령어가 PATH에 의존하며 PATH가 예상과 다릅니다.

이미지 명령어를 확인하세요:

docker inspect -f '{{json .Config.Entrypoint}} {{json .Config.Cmd}}' web-service

진입점이 스크립트인 경우 shebang과 줄 끝을 확인하세요. Windows CRLF 줄 끝이 있는 스크립트는 인터프리터 경로에 캐리지 리턴이 포함되어 있기 때문에 혼란스러운 "찾을 수 없음" 메시지와 함께 실패할 수 있습니다.

권한 거부됨: 종료 126 또는 파일 오류

종료 126은 종종 Docker가 명령어를 찾았지만 실행할 수 없음을 의미합니다. 스크립트의 경우 파일에 실행 비트가 없을 수 있습니다:

COPY start.sh /usr/local/bin/start.sh
RUN chmod 0755 /usr/local/bin/start.sh
ENTRYPOINT ["/usr/local/bin/start.sh"]

볼륨 마운트 파일의 경우 호스트 권한이 적용된다는 점을 기억하세요. 컨테이너가 UID 1000으로 실행되고 호스트 디렉터리가 루트 소유이고 쓰기 권한이 없는 경우 "Docker 내부"에 있더라도 컨테이너는 해당 디렉터리에 쓸 수 없습니다.

런타임 사용자를 확인하세요:

docker inspect -f 'user={{.Config.User}}' web-service

비어 있으면 많은 이미지가 기본적으로 루트로 실행되지만 모든 이미지가 그런 것은 아닙니다. 공식 및 보안 강화 이미지는 종종 루트가 아닌 사용자를 사용합니다.

포트가 이미 할당됨

바인드 오류는 일반적으로 이미 사용 중인 호스트 포트를 게시할 때 나타납니다:

docker run -p 8080:80 nginx

Docker는 bind: address already in use와 같은 메시지를 보고할 수 있습니다. 충돌을 찾으세요:

docker ps --format 'table {{.Names}}\t{{.Ports}}'
lsof -iTCP:8080 -sTCP:LISTEN

그런 다음 충돌하는 프로세스를 중지하거나 다른 호스트 포트를 선택하세요:

docker run -p 8081:80 nginx

컨테이너 포트는 동일하게 유지될 수 있습니다. 호스트 포트는 콜론 앞의 부분입니다.

누락된 파일 및 잘못된 마운트

로그에 구성 파일이 누락되었다고 표시되면 애플리케이션이 예상하는 것과 Docker가 마운트한 것을 비교하세요:

docker inspect -f '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' web-service

일반적인 실수는 이미지에 이미 파일이 있는 경로에 호스트 디렉터리를 마운트하는 것입니다. 마운트는 해당 대상의 이미지 내용을 숨깁니다. 이미지에 /app/config/default.yml이 포함되어 있고 빈 호스트 디렉터리를 /app/config에 마운트하면 컨테이너 보기에서 기본 파일이 사라집니다.

또한 상대 경로를 확인하세요. -v ./config:/app/config는 Dockerfile이 있는 디렉터리가 아니라 docker run을 실행한 디렉터리에 따라 다릅니다.

상태 확인 실패가 항상 컨테이너 충돌은 아닙니다

컨테이너가 실행 중이지만 비정상일 수 있습니다:

docker ps

Up 2 minutes (unhealthy)가 표시될 수 있습니다. 상태 출력을 검사하세요:

docker inspect -f '{{json .State.Health}}' web-service

상태 확인은 종종 앱이 다른 포트에서 수신 대기 중이거나, 127.0.0.1에만 바인딩되거나, 상태 확인이 허용하는 것보다 시작하는 데 시간이 더 걸리거나, 아직 준비되지 않은 데이터베이스가 필요하기 때문에 실패합니다. 비정상 컨테이너와 종료된 컨테이너를 혼동하지 마세요. 진단 경로가 다릅니다.

빠른 문제 해결 순서

빠르게 답이 필요할 때 이 순서를 사용하세요:

  1. docker ps -a로 컨테이너와 종료 코드를 찾습니다.
  2. docker logs --tail 100 <name>으로 애플리케이션 오류를 읽습니다.
  3. docker inspect -f ...로 상태, 명령어, 사용자 및 마운트를 확인합니다.
  4. 명령어나 파일 시스템이 의심스러운 경우 이미지에서 임시 셸을 실행합니다.
  5. 포트 및 마운트된 디렉터리 권한에 대한 호스트 충돌을 확인합니다.
  6. 문제가 이미지 내용, 런타임 플래그 또는 애플리케이션 구성인지 확인한 후에만 다시 빌드합니다.

이 순서는 조사를 근거 있게 유지합니다. Docker는 일반적으로 충분한 증거를 가지고 있습니다. 요령은 장면을 변경하기 전에 읽는 것입니다.

신뢰하기 전에 다시 시작 정책 확인

다시 시작 정책은 컨테이너가 지속적으로 실패하거나 지속적으로 복구되는 것처럼 보이게 할 수 있습니다. 확인하세요:

docker inspect -f 'restart={{json .HostConfig.RestartPolicy}}' web-service

정책이 always 또는 unless-stopped인 경우 Docker는 각 충돌 후 컨테이너를 다시 시작할 수 있습니다. docker ps는 몇 초 동안 실행된 후 다시 시작되는 것으로 표시할 수 있습니다. 이 경우 타임스탬프가 있는 로그를 사용하고 다시 시작 횟수를 검사하세요:

docker inspect -f 'restarts={{.RestartCount}} started={{.State.StartedAt}} finished={{.State.FinishedAt}}' web-service

높은 다시 시작 횟수는 일반적으로 메인 프로세스가 빠르게 종료됨을 의미합니다. 해결책은 "다시 시작 정책 변경"이 거의 아닙니다. 정책은 기본 실패를 드러낼 뿐입니다.

빌드 시간 문제와 런타임 문제 구분

컨테이너 내부에 파일이 누락된 경우 언제 나타났어야 하는지 물어보세요. Dockerfile에서 복사된 파일은 빌드 시간 문제입니다. -v 또는 Compose 볼륨으로 마운트된 파일은 런타임 문제입니다.

빌드 시간 확인:

docker image inspect my-app:latest
docker run --rm --entrypoint /bin/sh my-app:latest -c 'ls -la /app'

런타임 확인:

docker inspect -f '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' web-service

이 구분은 시간을 절약합니다. 이미지를 다시 빌드해도 잘못된 호스트 마운트가 수정되지 않습니다. 볼륨 플래그를 변경해도 바이너리를 복사하지 않은 Dockerfile이 수정되지 않습니다.

환경 변수 및 비밀은 조용히 실패할 수 있습니다

많은 애플리케이션은 필수 환경 변수가 누락되어 종료되지만 Docker 오류는 프로세스가 코드 1로 종료되었다고만 말합니다. 구성된 환경을 주의 깊게 검사하세요:

docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' web-service

해당 명령어를 실행하는 위치에 주의하세요. 비밀을 인쇄할 수 있습니다. 공유 로그에서는 변수 이름만 인쇄하세요:

docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' web-service | sed 's/=.*//'

--env-file을 사용하는 경우 CRLF 줄 끝, 따옴표 없는 공백 및 누락된 파일을 확인하세요. Docker env 파일은 완전한 셸 스크립트가 아닙니다. 간단하게 유지하세요: KEY=value 줄, Docker 버전에서 지원되는 경우 주석, 파일 내에서 셸 확장이 발생할 것이라는 가정은 하지 마세요.

출시 전 실제 검토

스크립트나 컨테이너 설정을 완료했다고 부르기 전에, 다음에 오전 2시에 디버깅해야 할 사람인 것처럼 한 번 읽어보세요. 그러면 눈에 띄는 것이 달라집니다. 스크립트를 작성하는 동안 이해가 되었던 프롬프트가 CI 로그에 나타나면 모호할 수 있습니다. 명백해 보였던 Docker 서비스 이름이 애플리케이션의 변수 이름과 일치하지 않을 수 있습니다. 개발에는 안전하고 프로덕션에는 위험한 Bash 기본값이 있을 수 있습니다.

의도적으로 어색한 값으로 짧은 드라이 런을 하는 것을 좋아합니다. 공백이 있는 경로를 사용하세요. 빈 선택적 값을 사용하세요. 대시로 시작하는 파일 이름을 시도해보세요. 다른 작업 디렉터리에서 스크립트를 실행하세요. 하나의 예상 환경 변수 없이 컨테이너를 시작하세요. 이러한 테스트는 화려하지 않지만 일반적으로 먼저 깨지는 가정을 잡아냅니다.

또한 실패 메시지를 확인하세요. 유일한 출력이 failed인 경우 기사의 조언이 구현에 반영되지 않은 것입니다. 유용한 실패는 어떤 값이 사용되었는지, 어떤 검사가 실패했는지, 운영자가 무엇을 변경할 수 있는지 알려줍니다. 이는 모든 환경 변수를 덤프하거나 비밀을 인쇄하는 것을 의미하지 않습니다. 특정성이 도움이 되는 곳에서 구체적으로 설명하는 것을 의미합니다: 구성 경로, 누락된 명령어 이름, 네트워크 이름, 서비스 호스트 이름 또는 프로세스가 바인딩하려고 시도한 포트.

마지막 습관은 예제를 시스템이 실제로 실행되는 방식에 가깝게 유지하는 것입니다. 프로덕션에서 Compose를 사용하는 경우 Compose로 테스트하세요. 스크립트가 systemd에 의해 시작되는 경우 systemd 또는 유사하게 최소 환경으로 테스트하세요. 명령어가 복사하여 붙여넣기에 안전해야 하는 경우 예제 자체에 인용, -- 구분 기호 및 유효성 검사를 포함하세요. 독자는 경고보다 작동하는 패턴을 더 자주 복사합니다.

그 검토 과정은 관료주의가 아닙니다. 작은 자동화를 지루하게 유지하는 방법입니다. 셸 프롬프트, 구성 로더, 변수 확장, 컨테이너 진단 및 Docker 네트워킹에서 원하는 것은 지루함입니다. 동작이 덜 놀라울수록 다음 운영자가 신뢰하기 쉽습니다.

Docker 실패의 경우 가능하면 컨테이너를 생성한 정확한 명령어를 저장하세요. docker inspect는 현재 구성을 표시할 수 있지만 원래 docker run 명령어, Compose 서비스, 이미지 태그 및 env 파일 이름을 포함하는 인시던트 노트는 나중에 재현하기 훨씬 쉽습니다. 심각한 디버깅 중에는 latest 사용을 피하세요. 이동하는 태그는 로그를 읽는 동안 이미지가 변경되기 때문에 하나의 실패를 두 개의 다른 조사로 바꿀 수 있습니다.

프로덕션과 유사한 문제를 로컬에서 진단하는 경우 동일한 이미지 다이제스트 또는 태그를 가져오고, 동일한 볼륨 레이아웃을 사용하고, 동일한 비밀 환경 형태를 전달하세요. 재현은 추측을 능가합니다.