Docker 컨테이너 문제 해결: 일반적인 시작 문제와 해결 방법
종료되거나 포트 바인딩에 실패하거나 파일이 누락되거나 권한 문제가 발생하거나 메모리 부족으로 종료되는 Docker 컨테이너를 진단합니다.
Docker 컨테이너 문제 해결: 일반적인 시작 문제와 해결 방법
Docker 컨테이너가 시작되지 않을 때, 가장 빠른 해결책은 추측을 자제하는 데서 나옵니다. 컨테이너는 파일 시스템, 환경, 네트워크 설정, 제한 사항으로 둘러싸인 하나의 프로세스에 불과합니다. 해당 프로세스가 종료되면 Docker는 그 이유를 기록합니다. 여러분의 역할은 올바른 순서로 증거를 수집하는 것입니다.
저는 보통 세 가지 질문으로 시작합니다: Docker가 컨테이너를 생성했는가, 메인 프로세스가 시작되었는가, 프로세스 외부에서 무언가가 이를 종료하거나 차단했는가? 이 질문들은 잘못된 이미지 이름과 깨진 명령어, 포트 충돌과 애플리케이션 충돌, 권한 문제와 메모리 제한을 구분해 줍니다.
진실을 알려주는 기본 명령어로 시작하세요:
docker ps -a
STATUS, PORTS, NAMES를 확인하세요. Created는 Docker가 컨테이너를 만들었지만 실제로 실행하지는 않았음을 의미합니다. Exited (1)은 종종 애플리케이션이 일반적인 오류를 반환했음을 의미합니다. Exited (127)은 일반적으로 명령어가 없음을 나타냅니다. Exited (137)은 종종 프로세스가 외부에서 종료되었으며, 주로 메모리 압박 때문임을 의미합니다. 이 코드들은 단서이지 최종 답변이 아니지만, 잘못된 레이어를 디버깅하는 것을 방지해 줍니다.
그런 다음 로그를 읽으세요:
docker logs --tail 100 <container>
docker logs -f <container>
컨테이너가 즉시 종료된다면, docker logs가 동일한 docker run 명령어를 다시 실행하는 것보다 보통 더 유용합니다. 애플리케이션 프레임워크는 종료되기 전에 정확한 누락된 환경 변수, 마이그레이션 실패, 잘못된 설정 파일, 또는 바인딩 오류를 출력하는 경우가 많습니다.
저수준 상태를 확인하려면 컨테이너를 검사하세요:
docker inspect <container> --format '{{json .State}}'
docker inspect <container> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}'
두 번째 명령어는 외워둘 가치가 있습니다. Docker가 OOM 종료를 감지했는지, 기록된 종료 코드가 무엇인지, 런타임 자체에 오류가 있었는지 알려줍니다.
컨테이너가 즉시 종료되는 경우
컨테이너는 메인 프로세스가 살아있는 동안만 유지됩니다. 명령어가 완료되면 Docker는 컨테이너를 중지합니다. 이는 백그라운드에서 데몬을 시작한 후 반환되는 스크립트를 실행할 때 사람들을 놀라게 합니다.
예를 들어, 다음 패턴은 종종 종료됩니다:
CMD service nginx start
service 명령어는 nginx를 시작한 후 완료될 수 있습니다. Docker는 메인 프로세스가 종료된 것을 보고 컨테이너를 중지합니다. 컨테이너 친화적인 패턴은 서버를 포그라운드에서 실행하는 것입니다:
CMD ["nginx", "-g", "daemon off;"]
동일한 아이디어가 Node, Python, Java 및 워커 프로세스에도 적용됩니다. CMD 또는 ENTRYPOINT의 명령어는 실제 작업을 백그라운드로 보내고 종료되는 실행기가 아닌, 장기 실행 프로세스여야 합니다.
로그에 command not found, no such file or directory, 또는 exec format error가 표시되면 대화형으로 이미지를 테스트하세요:
docker run --rm -it --entrypoint sh <image>
일부 이미지, 특히 Alpine 및 distroless 스타일 이미지에는 bash가 포함되어 있지 않습니다. bash가 존재한다는 것을 알지 못하면 먼저 sh를 사용하세요. 내부에 들어가면 파일 경로, 권한 및 인터프리터를 확인하세요:
ls -l /app
which python || true
head -1 /app/start.sh
스크립트가 존재하더라도 shebang이 누락된 인터프리터(예: /bin/sh만 있는 이미지에서 #!/bin/bash)를 가리키면 no such file or directory 오류가 발생할 수 있습니다. 또 다른 일반적인 원인은 Windows 줄 바꿈입니다. 셸 스크립트가 Windows에서 편집된 경우 보이지 않는 \r 문자로 인해 Linux가 /bin/sh\r을 찾게 될 수 있습니다.
Docker가 포트가 이미 할당되었다고 말하는 경우
포트 충돌은 호스트 측에서 발생합니다. -p 8080:80에서 8080은 호스트 포트이고 80은 컨테이너 포트입니다. 호스트 포트 8080에서 이미 수신 중인 것이 있으면 Docker는 바인딩할 수 없습니다.
bind: address already in use 또는 port is already allocated와 같은 오류가 표시될 수 있습니다. 수신자를 찾으세요:
sudo lsof -i :8080
# 또는
sudo ss -ltnp 'sport = :8080'
macOS에서는 lsof가 일반적으로 가장 쉽습니다. Linux 서버에서는 ss가 기본적으로 사용 가능한 경우가 많습니다. Windows PowerShell에서는 다음을 사용하세요:
Get-NetTCPConnection -LocalPort 8080
그런 다음 다른 호스트 포트를 선택하거나 해당 포트를 소유한 서비스를 중지하세요:
docker run -d -p 8081:80 nginx
컨테이너 내부의 애플리케이션이 실제로 새 포트에서 수신하지 않는 한 컨테이너 포트를 변경하지 마세요. nginx가 컨테이너 내부에서 80번 포트에서 수신하는 경우 -p 8081:80이 올바릅니다. -p 8081:8081은 컨테이너 내부에서 8081번 포트에서 수신하는 것이 없으면 브라우저에서 실패합니다.
앱이 시작되지만 구성을 찾을 수 없는 경우
많은 시작 실패는 누락된 환경 변수 때문입니다. 이미지는 괜찮고 명령어도 괜찮지만 앱이 DATABASE_URL, REDIS_URL, API 키 또는 설정 파일을 기대합니다.
Docker가 전달한 내용을 확인하세요:
docker inspect <container> --format '{{range .Config.Env}}{{println .}}{{end}}'
Compose 프로젝트의 경우 docker-compose.yml만 읽는 대신 해결된 구성을 검사하세요:
docker compose config
이렇게 하면 들여쓰기 오류, .env 파일의 예상치 못한 내용, 빈 문자열로 확장된 변수를 찾을 수 있습니다. 실제 예: DATABASE_URL=${DATABASE_URL}은 무해해 보이지만 셸이나 .env 파일이 이를 정의하지 않으면 애플리케이션이 빈 값을 받아 시작 중에 실패할 수 있습니다.
로그와 터미널 기록에서 비밀 정보를 조심하세요. 빠른 로컬 디버깅을 위해 -e NAME=value를 전달하는 것은 괜찮습니다. 공유 시스템의 경우 플랫폼의 비밀 메커니즘이나 제어된 권한이 있는 환경 파일을 사용하세요.
바인드 마운트 또는 볼륨이 권한 오류를 일으키는 경우
컨테이너는 설정 파일을 읽을 수 없거나, PID 파일을 쓰지 못하거나, 캐시 디렉토리를 생성하지 못하거나, 데이터베이스 디렉토리를 초기화하지 못해 시작 시 실패할 수 있습니다. 로그에는 일반적으로 permission denied, read-only file system, 또는 operation not permitted이 표시됩니다.
먼저 마운트를 검사하세요:
docker inspect <container> --format '{{json .Mounts}}'
그런 다음 컨테이너가 실행되는 사용자를 확인하세요:
docker inspect <container> --format 'user={{.Config.User}}'
user가 비어 있으면 이미지가 기본적으로 root로 실행될 수 있지만, 많은 프로덕션 이미지는 비-root 사용자를 설정합니다. 로컬 UID가 소유한 호스트 디렉토리는 컨테이너 내부의 UID 1000, 1001 또는 서비스별 사용자가 쓸 수 없을 수 있습니다.
실용적인 디버깅 순서는 다음과 같습니다:
ls -ld ./data
docker run --rm -it -v "$PWD/data:/data" --entrypoint sh <image>
id
ls -ld /data
touch /data/test
모든 권한 문제를 chmod 777로 해결하지 마세요. 즉각적인 문제를 숨기면서 더 나쁜 문제를 만들 수 있습니다. 소유권을 일치시키거나 애플리케이션 데이터에 명명된 볼륨을 사용하는 것이 좋습니다:
docker volume create app_data
docker run -d -v app_data:/var/lib/app <image>
명명된 볼륨은 Docker Desktop에서 특히 유용합니다. 바인드 마운트는 가상화 경계를 넘어 네이티브 Linux 파일 시스템과 다르게 동작할 수 있기 때문입니다.
컨테이너가 메모리 부족으로 종료된 경우
종료 코드 137은 프로세스가 SIGKILL을 수신했음을 강력히 암시합니다. Docker 작업에서 이는 종종 커널이나 Docker Desktop이 메모리 부족으로 인해 프로세스를 종료했음을 의미합니다. inspect로 확인하세요:
docker inspect <container> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}'
OOMKilled가 true이면 두 가지 작업이 있습니다: 프로세스가 시작할 수 있을 만큼 충분한 메모리를 제공하고, 왜 그렇게 많은 메모리가 필요한지 이해하는 것입니다. 데이터베이스나 JVM 서비스의 경우 제한을 높이는 것이 올바른 프로덕션 수정일 수 있습니다. 작은 웹 서비스의 경우 잘못된 기본값을 드러낼 수 있습니다.
Java 앱은 전형적인 예입니다. 이전 JVM 동작은 항상 컨테이너 제한에 잘 맞지 않았으며, 최신 JVM도 예측 가능한 동작을 위해 적절한 -Xmx 또는 백분율 기반 설정이 필요합니다. Node 서비스는 메모리가 제한된 환경에서 --max-old-space-size가 필요할 수 있습니다. 데이터베이스는 명시적인 캐시 설정이 필요할 수 있습니다.
일회성 테스트의 경우:
docker run --memory=1g <image>
Docker Desktop을 사용하는 경우 Docker VM에 할당된 메모리도 확인하세요. VM 자체가 부족하면 컨테이너 제한이 도움이 되지 않습니다.
이미지를 가져오지 못하거나 빌드가 이미지를 생성하지 못한 경우
때로는 사용 가능한 이미지가 없어 컨테이너 문제가 없는 경우도 있습니다. docker run이 컨테이너를 생성하기 전에 실패하면 이미지를 별도로 확인하세요:
docker image ls | grep my-app
docker pull my-registry/my-app:tag
프라이빗 레지스트리의 경우 인증을 확인하세요:
docker login <registry>
로컬 이미지의 경우 실행하는 태그가 빌드한 태그와 일치하는지 확인하세요:
docker build -t my-app:dev .
docker run --rm my-app:dev
일반적인 로컬 실수는 my-app:dev를 빌드하고 my-app:latest를 실행하는 것입니다. 이는 이전 이미지나 아무것도 가리키지 않을 수 있습니다.
네트워킹이 비난받지만 서비스가 수신하지 않는 경우
브라우저가 컨테이너에 도달할 수 없을 때 사람들은 종종 Docker 네트워킹으로 뛰어듭니다. 먼저 애플리케이션이 컨테이너 내부에서 수신 중인지 확인하세요.
docker exec -it <container> sh
ss -ltnp || netstat -ltnp
앱이 컨테이너 내부에서 127.0.0.1에 바인딩된 경우 Docker 포트 게시는 도움이 되지 않습니다. 앱은 0.0.0.0 또는 컨테이너의 인터페이스 주소에서 수신해야 합니다. 이는 개발 서버에서 흔히 발생합니다. 예를 들어, 많은 프레임워크가 localhost를 기본값으로 사용하며 --host 0.0.0.0과 같은 플래그가 필요합니다.
그런 다음 게시된 포트를 확인하세요:
docker port <container>
docker ps --format 'table {{.Names}} {{.Ports}}'
0.0.0.0:8080->3000/tcp와 같은 내용이 표시되어야 합니다. 게시된 포트가 없으면 서비스가 동일한 네트워크의 다른 컨테이너에서는 작동할 수 있지만 호스트 브라우저에서는 작동하지 않을 수 있습니다.
신뢰할 수 있는 시작 체크리스트
막혔을 때 이 순서를 사용하세요:
docker ps -a로 컨테이너가 존재하는지, 어떻게 종료되었는지 확인합니다.docker logs --tail 100 <container>로 애플리케이션의 자체 불만 사항을 읽습니다.docker inspect <container>로 종료 코드, OOM 상태, 명령어, 사용자, 마운트 및 포트를 확인합니다.docker run --rm -it --entrypoint sh <image>로 이미지를 수동으로 테스트합니다.- 한 번에 하나의 변수를 제거합니다: 먼저 마운트 없이 실행한 다음, 사용자 정의 네트워크 없이, 마지막으로 필수 환경 변수만 사용하여 실행합니다.
마지막 단계가 중요합니다. 포트, 볼륨, 환경 파일, 사용자 정의 DNS, 메모리 제한 및 사용자 정의 엔트리포인트가 포함된 긴 docker run 명령어는 너무 많은 용의자를 제공합니다. 이미지가 시작될 때까지 설정을 제거한 다음, 문제가 발생할 때까지 설정을 다시 추가하세요. 방금 추가한 설정이 일반적으로 실제 문제가 있는 곳입니다.