컨테이너 학습 노트 목차

트러블슈팅 — 증상에서 원인으로

마지막 편은 실제로 마주치는 증상에서 출발한다. 앞의 다섯 편에서 본 것들이 현장에서 어떤 모습으로 나타나는지의 목록이다.


1. 컨테이너가 자꾸 재시작된다

$ docker ps -a
STATUS: Restarting (137) 3 seconds ago
137 + OOMKilled=true메모리 제한 초과
137 + OOMKilled=false유예 시간 안에 안 죽어 강제 종료 (PID 1 문제)
1애플리케이션 예외 — 로그를 본다
127명령을 못 찾음 — 경로·libc 불일치
126실행 권한 없음
  • ② 마지막 로그를 본다

    • docker logs --tail 200 web
  • ③ 재시작 루프면 로그가 계속 밀린다

    • docker logs --since 10m web | head -50 ← 처음 부분을 본다

OOMKilled 인데 애플리케이션 로그가 없다

  • 커널이 SIGKILL 로 죽인 것이라 정리 코드가 돌지 않는다
  • 애플리케이션 입장에서는 아무 일도 안 일어난 것처럼 보인다
확인
  dmesg | grep -i "killed process"        호스트 커널 로그
  docker events --filter event=oom          실시간 이벤트

원인은 대개 둘 중 하나다

  • 제한이 실제 사용량보다 작다
  • 애플리케이션이 컨테이너 제한을 모르고 호스트 메모리 기준으로 잡는다

런타임이 제한을 인식하는지 확인한다

JVM 은 Java 10+ 에서 컨테이너 제한을 인식한다 (UseContainerSupport, 기본 켜짐)

  • -XX:MaxRAMPercentage=75 로 limit 대비 비율로 힙을 잡는다
  • -Xmx 를 직접 주면 limit 이 바뀌어도 따라가지 않는다

주의 — 힙 밖에도 메모리가 든다

  • 메타스페이스 · 코드 캐시 · 스레드 스택 · 다이렉트 버퍼 · GC 구조
  • 힙을 limit 의 100% 로 잡으면 반드시 OOMKilled 된다
  • 실무 감각은 limit 의 60~75%

2. CPU 는 여유로운데 느리다

docker stats 에서 CPU 40% 인데 응답이 튄다

CPU 스로틀링을 의심한다

--cpus=1.0 은 '평균 1코어' 가 아니라 '주기당 상한' 이다

  • 기본 주기 100ms 에 100ms 만 쓸 수 있다
  • 멀티스레드 애플리케이션이 순간적으로 4코어를 쓰려 하면
  • 25ms 만에 할당량을 다 쓰고 나머지 75ms 를 강제로 쉰다
  • 평균 사용률은 낮게 보이는데 지연은 튄다
확인 — cgroup 의 스로틀 통계
  cat /sys/fs/cgroup/cpu.stat
    nr_throttled     스로틀된 주기 수
    throttled_usec   스로틀된 총 시간

nr_throttled 가 계속 늘면 확정이다

대응

  • limit 을 올린다 (가장 단순)
  • 애플리케이션의 스레드 풀·병렬도를 limit 에 맞춘다
    • (코어 수를 호스트 기준으로 잡으면 과도한 병렬이 된다)
  • GC 스레드 수도 확인한다 — 자동 산정이 호스트 코어를 볼 수 있다

3. 빌드가 매번 느리다

  • 증상 — 소스 한 줄만 고쳤는데 의존성을 처음부터 다시 받는다

  • 원인 — COPY 순서 (02편)

    • COPY . . 이 의존성 설치보다 앞에 있다
  • 확인 — 빌드 로그에서 어느 단계부터 CACHED 가 사라지는지 본다

    • BuildKit 은 각 단계에 CACHED 를 표시해 준다

CI 에서만 캐시가 안 먹는 경우

  • 러너가 매번 새 머신이라 로컬 캐시가 없다
    • 레지스트리 캐시를 쓴다
      • docker buildx build \
      • --cache-from type=registry,ref=myrepo/cache \
      • --cache-to type=registry,ref=myrepo/cache,mode=max
  • 빌드 컨텍스트에 매번 바뀌는 파일이 들어간다 (타임스탬프 · 빌드 번호 파일)
    • .dockerignore 로 제외

빌드 캐시가 디스크를 먹는 것도 흔하다 docker system df -v 로 Build Cache 항목을 본다 docker builder prune --filter until=168h

4. 로컬에서는 되는데 서버에서 안 된다

  • ① 아키텍처 불일치

    • 증상: exec format error
    • 애플 실리콘에서 빌드 → x86 서버
    • 확인: docker image inspect myapp --format '{{.Os}}/{{.Architecture}}'
    • 대응: buildx 로 멀티 아키텍처 빌드
  • ② libc 불일치

    • 증상: "not found" 인데 파일은 있다
    • glibc 로 빌드한 바이너리를 alpine(musl)에 올렸다
    • 확인: ldd ./app
    • 대응: 정적 빌드 · 같은 계열 베이스
  • ③ 태그가 같은데 이미지가 다르다

    • latest 를 쓰면 노드마다 다른 이미지가 떠 있을 수 있다
    • 확인: docker inspect --format '{{index .RepoDigests 0}}'
    • 대응: 다이제스트·불변 태그로 고정
  • ④ 파일이 이미지에 안 들어갔다

    • .dockerignore 가 필요한 파일까지 제외했다
    • 확인: docker run --rm myapp ls -al /app

5. 디스크가 찼다

docker system df -v
  • TYPE — RECLAIMABLE
  • Images — ...
  • Containers — ...
  • Local Volumes — ...
  • Build Cache — ... ← 여기가 가장 큰 경우가 많다
정리 순서 (위험도 낮은 것부터)
  docker container prune          정지된 컨테이너
  docker builder prune            빌드 캐시
  docker image prune              dangling 이미지 (태그 없는 것)
  docker image prune -a           사용 중이 아닌 이미지 전부   ← 신중
  docker volume prune             참조되지 않는 볼륨          ← 데이터 손실 주의

한 번에 지우기 전에 --dry-run 이 없는 명령이므로 docker system df -v 로 무엇이 큰지 먼저 확인한다

로그가 채우는 경우

json-file 드라이버는 기본 무제한이다 (04편)

  • /var/lib/docker/containers/<id>/<id>-json.log

확인

  • du -sh /var/lib/docker/containers/* | sort -h | tail

대응

  • daemon.json 에 max-size · max-file 설정 후 데몬 재시작
  • 기존 컨테이너는 재생성해야 적용된다

6. 네트워크가 안 된다

  • 컨테이너끼리 이름으로 못 찾는다

    • 기본 bridge 에는 내장 DNS 가 없다
    • 사용자 정의 네트워크를 만들어 붙인다
  • ② localhost 로 연결이 안 된다

    • 컨테이너 안의 localhost 는 자기 자신이다
    • 다른 컨테이너는 이름, 호스트는 host.docker.internal
  • ③ 포트가 안 열린다

    • EXPOSE 는 문서일 뿐 (03편) — -p 로 매핑해야 한다
    • 확인: docker port web
      • ss -lnt | grep 8080 호스트에서 실제로 열렸는지
  • ④ 방화벽으로 막았는데 열려 있다

    • 도커가 iptables 를 직접 만져 ufw 규칙보다 앞선다
    • -p 127.0.0.1:8080:80 으로 바인딩 주소를 제한하거나
      • 도커의 iptables 관리를 끄고 직접 규칙을 쓴다
# 안에서 확인
docker exec -it web sh -c 'cat /etc/resolv.conf; nslookup db; wget -qO- http://db:5432'
# 네트워크 구성 확인
docker network inspect appnet --format '{{json .Containers}}' | python3 -m json.tool

7. 셸이 없는 이미지를 디버깅한다

# 실행 중인 컨테이너의 네임스페이스를 공유하는 임시 컨테이너
docker run --rm -it \
  --pid=container:web --network=container:web \
  --cap-add=SYS_PTRACE \
  nicolaka/netshoot sh

--pid=container:web 그 컨테이너의 프로세스를 본다 --network=container:web 그 컨테이너의 네트워크에서 진단한다

쿠버네티스에는 같은 목적의 임시 컨테이너 기능이 있다

  • kubectl debug -it pod/web --image=nicolaka/netshoot --target=web

운영 이미지에서 셸을 빼는 대신 이 경로를 갖추는 것이 정석이다

8. 진단 명령 정리

# 상태
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker inspect web --format '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}}'
docker stats --no-stream
docker events --since 30m

# 안쪽
docker top web
docker diff web                    # 이미지 대비 바뀐 파일
docker exec -it web sh

# 이미지
docker history --no-trunc myapp:1.0
docker image inspect myapp:1.0 --format '{{.Os}}/{{.Architecture}} {{.Config.User}}'

# 자원
docker system df -v
cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.stat    # 컨테이너 안에서

# 호스트
dmesg | grep -i 'killed process'
journalctl -u docker --since '30 min ago'

9. 배포 전 체크리스트

□ ENTRYPOINT/CMD 가 exec form(대괄호)인가 → 시그널 전달 □ USER 를 숫자 UID 로 지정했는가 → root 실행 방지 □ .dockerignore 에 .git · .env 가 있는가 → 비밀 유출 □ 태그가 불변인가 (latest 아님) → 재현·롤백 □ 메모리·CPU limit 을 걸었는가 → 호스트 보호 □ 런타임이 limit 을 인식하는가 (MaxRAMPercentage) → OOMKilled 예방 □ 로그 드라이버에 max-size 가 있는가 → 디스크 보호 □ 유예 시간이 최장 요청 처리 시간보다 긴가 → 무중단 □ 헬스체크가 정의돼 있는가 → 자동 복구 □ 이미지 스캔에서 Critical 이 남아 있지 않은가 → 보안 □ 멀티 아키텍처가 필요한 환경인가 → exec format error


한눈에 정리

  • 재시작 루프종료 코드부터. 137+OOMKilled 로 갈린다
  • OOMKilled — 로그가 안 남는다. dmesg · docker events 로 확인
    • 런타임이 limit 을 인식하는지, 힙 밖 메모리를 봤는지
  • CPU 스로틀 — 사용률은 낮은데 지연이 튄다. cpu.stat 의 nr_throttled
  • 빌드 느림 — COPY 순서. CI 는 레지스트리 캐시. builder prune
  • 로컬만 됨 — 아키텍처 · libc · 태그 불일치 · dockerignore 과다 제외
  • 디스크 — system df -v 로 먼저 확인. Build Cache 가 큰 경우가 많다
  • 네트워크 — 사용자 정의 네트워크 · localhost 오해 · EXPOSE 는 문서
  • 셸 없는 이미지 네임스페이스 공유 임시 컨테이너 · kubectl debug

출처 — Docker Docs: "Troubleshoot" · "Runtime options with Memory, CPUs, and GPUs" · "Configure logging drivers" · "Multi-platform builds" · Linux cgroups(7) · Kubernetes Docs "Debug Running Pods"

이미지 크기와 보안 — 같은 문제의 두 얼굴