실행 중인 컨테이너 디버깅 기법
새벽 장애에서 로그만으로 원인이 안 보이고, 컨테이너는 계속 재시작됩니다. 이때 무작정 재배포하면 증거가 사라지고 동일 장애가 반복됩니다. 필요한 것은 실행 중 상태를 보존한 채 내부/메타데이터/리소스를 순서대로 확인하는 디버깅 루틴입니다. 이 모듈은 exec부터 inspect, OOM 판별까지 운영 대응 흐름을 실전 기준으로 익히게 합니다.
배포하고 나서 문제가 생겼습니다. 로그만으로는 원인을 알 수 없고, 새벽 2시에 프로덕션 컨테이너가 크래시 루프에 빠졌습니다. 이 챕터에서는 실행 중인 컨테이너를 안전하게 들여다보고, 문제를 진단하고, 필요하면 시스템 수준까지 파고드는 모든 기법을 다룹니다.
컨테이너가 프로덕션에서 문제를 일으킬 때 사용하는 진단 도구들을 체계적으로 익힙니다. 셸이 없는 distroless 컨테이너부터 OOMKilled 메모리 장애까지 실전 시나리오를 중심으로 학습합니다.
- 1docker exec -it로 실행 중인 컨테이너 내부 진입 — 다양한 활용 패턴
- 2docker inspect --format으로 JSON 메타데이터에서 원하는 정보 추출
- 3docker stats와 docker top으로 실시간 리소스와 프로세스 모니터링
- 4nsenter로 네임스페이스 직접 진입 — distroless 컨테이너 디버깅
- 5docker cp로 컨테이너 파일 추출 및 주입
- 6임시 디버그 사이드카 컨테이너 패턴 — 프로덕션 컨테이너를 수정하지 않고 진단
nginx와 python 컨테이너를 백그라운드로 실행한 상태에서 실습을 진행합니다. nsenter는 대부분의 Linux 배포판에 기본 포함되어 있습니다(util-linux 패키지).
docker --versiondocker run -d --name debug-nginx -p 8080:80 nginx:alpinedocker run -d --name debug-python python:3.11-slim sleep infinitywhich nsenter || echo 'nsenter 미설치: sudo apt-get install util-linux'docker rm -f debug-nginx debug-python
docker exec — 실행 중인 컨테이너 내부 접근의 모든 것
프로덕션 컨테이너에서 502 오류가 납니다. 로그에는 "upstream connect error"만 찍히고 원인이 안 보입니다. 컨테이너 안에 직접 들어가서 nginx 설정을 확인하거나, 실제로 포트가 열려 있는지 봐야 합니다. docker exec -it myapp sh로 컨테이너에 진입하면 마치 서버에 SSH로 접속한 것처럼 내부를 탐색할 수 있습니다. 재시작 없이, 이미지 수정 없이, 실행 중인 상태 그대로 들어갑니다. 이 ConceptBlock에서는 docker exec의 동작 원리와 디버깅에 쓰이는 주요 사용 패턴을 다룹니다.

docker exec 기본 원리
docker exec는 이미 실행 중인 컨테이너에 새로운 프로세스를 추가로 실행합니다. docker run이 새 컨테이너를 생성하는 것과 다릅니다. exec으로 실행한 프로세스는 컨테이너의 네임스페이스(파일시스템, 네트워크, PID)를 공유합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part5/exam_20 && cd /tmp/docker/part5/exam_20
# 기본 형식
docker exec [옵션] <컨테이너명 또는 ID> <명령어> [인수...]
# -i: stdin 연결 (interactive)
# -t: pseudo-TTY 할당 (터미널 에뮬레이션)
# -e: 환경 변수 설정
# -u: 실행할 사용자 지정
# -w: 작업 디렉토리 지정
주요 사용 패턴
셸 접속 (인터랙티브 디버깅)
# bash가 있는 컨테이너 (Ubuntu, Debian 기반)
docker exec -it debug-nginx bash
# sh만 있는 컨테이너 (Alpine 기반)
docker exec -it debug-nginx sh
# 컨테이너 내부에 진입하면:
# / # (Alpine) 또는 root@<id>:~# (Debian) 프롬프트 표시
# 나가기
exit # 또는 Ctrl+D
단발성 명령 실행 (셸 없이)
# 컨테이너 내부에서 파일 확인
docker exec debug-nginx cat /etc/nginx/nginx.conf
# 환경 변수 확인
docker exec debug-nginx env
# 프로세스 목록 확인 (ps가 있는 경우)
docker exec debug-nginx ps aux
# 네트워크 상태 확인
docker exec debug-nginx netstat -tlnp 2>/dev/null || \
docker exec debug-nginx ss -tlnp
# 파일시스템 사용량
docker exec debug-nginx df -h
# 특정 포트 리스닝 여부 확인
docker exec debug-nginx wget -qO- http://localhost:80 | head -20
특정 사용자로 명령 실행
# root 권한으로 실행 (앱이 비root 사용자로 실행 중이어도 진입 가능)
docker exec -u root -it myapp bash
# 특정 사용자 ID로 실행
docker exec -u 1000:1000 myapp id
# uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)
# 실행 중인 앱과 동일한 사용자로 실행 (권한 문제 재현 시)
APP_UID=$(docker exec myapp id -u)
docker exec -u $APP_UID myapp ls -la /app
특정 디렉토리에서 명령 실행
# 작업 디렉토리를 지정하여 상대 경로 명령 실행
docker exec -w /app myapp ls -la
# 앱 설정 파일 내용 확인
docker exec -w /etc/nginx debug-nginx cat nginx.conf
컨테이너 내부에서 디버그 도구 설치 후 사용
# 컨테이너 내부에서 네트워크 도구 임시 설치
docker exec -it debug-nginx sh -c "apk add --no-cache curl && curl -sv http://localhost"
# Debian 계열에서
docker exec -it debug-python bash -c "apt-get install -y -q strace && strace python app.py"
docker inspect — JSON 메타데이터 마스터하기
컨테이너가 종료됐는데 원인이 안 보입니다. docker logs는 아무것도 없고, 마지막으로 뭔가 잘못됐다는 힌트도 없습니다. docker inspect로 OOMKilled: true와 ExitCode: 137을 확인하는 순간 원인이 명확해집니다. 메모리 부족으로 커널이 죽인 겁니다. inspect가 출력하는 JSON은 150줄이 넘지만, --format으로 필요한 필드만 뽑으면 스크립트에서도 바로 활용할 수 있습니다. 이 ConceptBlock에서는 docker inspect 출력 구조와 Go 템플릿으로 원하는 정보를 추출하는 방법을 다룹니다.

docker inspect 출력 구조
docker inspect는 컨테이너(또는 이미지, 네트워크, 볼륨)의 상세 정보를 JSON 배열로 출력합니다. 출력이 방대하므로 --format으로 필요한 필드만 추출합니다.
# 전체 JSON 출력 (약 150줄 이상)
docker inspect debug-nginx
# 출력 구조:
# [
# {
# "Id": "...",
# "State": { "Status": "running", "Pid": 1234, ... },
# "NetworkSettings": { "IPAddress": "172.17.0.2", ... },
# "HostConfig": { "Memory": 0, "CpuShares": 0, ... },
# "Mounts": [...],
# "Config": { "Env": [...], "Cmd": [...], ... },
# ...
# }
# ]
--format으로 특정 필드 추출 (Go 템플릿)
# IP 주소 추출 (브릿지 네트워크)
docker inspect --format '{{.NetworkSettings.IPAddress}}' debug-nginx
# 172.17.0.2
# 컨테이너 상태 확인
docker inspect --format '{{.State.Status}}' debug-nginx
# running
# PID 추출 (nsenter에 활용)
docker inspect --format '{{.State.Pid}}' debug-nginx
# 12345
# 마운트된 볼륨 목록
docker inspect --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' debug-nginx
# /var/lib/docker/volumes/nginx-data/_data -> /var/cache/nginx
# 환경 변수 목록
docker inspect --format '{{range .Config.Env}}{{.}}{{"\n"}}{{end}}' debug-nginx
# PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# NGINX_VERSION=1.25.3
# ...
# 메모리 제한 확인 (0이면 제한 없음)
docker inspect --format '{{.HostConfig.Memory}}' debug-nginx
# 0
# OOMKilled 여부 확인 (장애 진단)
docker inspect --format '{{.State.OOMKilled}}' <crashed-container>
# true ← 메모리 부족으로 종료됨
# 재시작 횟수 확인
docker inspect --format '{{.RestartCount}}' myapp
# 7 ← 7번 재시작됨 (문제 징후)
# 종료 코드 확인
docker inspect --format '{{.State.ExitCode}}' myapp
# 137 ← SIGKILL (OOMKilled)
유용한 복합 포맷 예시
# 여러 필드를 한 번에 출력
docker inspect --format \
'ID: {{.Id}}
Status: {{.State.Status}}
Started: {{.State.StartedAt}}
IP: {{.NetworkSettings.IPAddress}}
OOMKilled: {{.State.OOMKilled}}
ExitCode: {{.State.ExitCode}}' myapp
# 사용자 정의 네트워크의 IP 추출 (브릿지 네트워크가 아닌 경우)
docker inspect --format \
'{{range $net, $cfg := .NetworkSettings.Networks}}{{$net}}: {{$cfg.IPAddress}}{{"\n"}}{{end}}' \
debug-nginx
# bridge: 172.17.0.2
# JSON 출력을 jq로 처리 (더 복잡한 쿼리)
docker inspect debug-nginx | jq '.[0].State'
docker inspect debug-nginx | jq '.[0].NetworkSettings.Networks'
여러 컨테이너 일괄 조회
# 모든 실행 중인 컨테이너의 이름과 IP 일괄 출력
docker ps -q | xargs docker inspect --format \
'{{.Name}} — {{.NetworkSettings.IPAddress}}'
# /debug-nginx — 172.17.0.2
# /debug-python — 172.17.0.3
# 메모리 제한이 없는 컨테이너 찾기 (보안/안정성 감사)
docker ps -q | xargs docker inspect --format \
'{{if eq .HostConfig.Memory 0}}{{.Name}}: 메모리 제한 없음{{end}}'
docker stats와 docker top — 실시간 리소스 모니터링
배포 후 API 응답이 갑자기 느려졌는데 로그에는 에러가 없습니다. docker stats를 켜는 순간 특정 컨테이너의 CPU가 95%에서 내려오지 않는 것이 보입니다. 어떤 프로세스가 CPU를 잡아먹는지는 docker top으로 확인합니다. 로그만 봐서는 절대 알 수 없는 문제를 리소스 모니터링으로 5초 만에 찾는 경우가 현장에서 자주 있습니다. 이 ConceptBlock에서는 docker stats와 docker top으로 컨테이너 리소스와 프로세스를 실시간으로 파악하는 방법을 다룹니다.

docker stats
# 모든 실행 중인 컨테이너의 리소스 사용량 실시간 모니터링 (htop처럼 갱신)
docker stats
# 출력 예시:
# CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
# a1b2c3d4e5f6 debug-nginx 0.00% 3.281MiB / 7.77GiB 0.04% 656B / 0B 0B / 0B 2
# f9e8d7c6b5a4 debug-python 0.00% 12.45MiB / 7.77GiB 0.16% 876B / 0B 0B / 0B 1
# 특정 컨테이너만 모니터링
docker stats debug-nginx debug-python
# 한 번만 스냅샷 출력 (스크립트에 활용)
docker stats --no-stream
# 커스텀 포맷 출력
docker stats --no-stream --format \
"table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
# NAME CPU % MEM USAGE / LIMIT MEM %
# debug-nginx 0.00% 3.281MiB / 7.77GiB 0.04%
컬럼 설명
CPU % — CPU 사용률 (호스트 전체 대비 %)
MEM USAGE — 현재 메모리 사용량 / --memory 제한값
MEM % — 메모리 사용률 (제한 대비)
NET I/O — 네트워크 수신 / 송신 누적 바이트
BLOCK I/O — 디스크 읽기 / 쓰기 누적 바이트
PIDS — 컨테이너 내 프로세스/스레드 수
docker top — 컨테이너 내 프로세스 목록
# 컨테이너 내 프로세스 목록 (ps 형식)
docker top debug-nginx
# UID PID PPID C STIME TTY TIME CMD
# root 12345 12300 0 09:00 ? 00:00:00 nginx: master process nginx -g daemon off;
# 101 12346 12345 0 09:00 ? 00:00:00 nginx: worker process
# 101 12347 12345 0 09:00 ? 00:00:00 nginx: worker process
# ps 옵션 전달 가능
docker top debug-nginx aux
# 특정 프로세스가 실행 중인지 확인
docker top myapp | grep "python\|node\|java"
# 좀비 프로세스 확인 (STAT에 Z가 있는 것)
docker top myapp aux | awk '$8 ~ /Z/ {print "좀비 프로세스:", $0}'
메모리 사용량 상세 분석
# /proc/meminfo로 컨테이너 내 메모리 세부 정보
docker exec myapp cat /proc/meminfo
# cgroup 메모리 통계 직접 확인 (호스트에서)
PID=$(docker inspect --format '{{.State.Pid}}' myapp)
cat /sys/fs/cgroup/memory/docker/$(docker inspect --format '{{.Id}}' myapp)/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/docker/$(docker inspect --format '{{.Id}}' myapp)/memory.stat
nsenter — 셸 없는 컨테이너 디버깅

nsenter가 필요한 상황
distroless 이미지나 scratch 기반 이미지는 운영 환경에서 권장됩니다. 공격 표면을 줄이고 이미지를 경량화하기 때문입니다. 하지만 문제가 생기면 docker exec으로 진입할 수 없습니다.
# distroless 컨테이너에 exec 시도
docker exec -it distroless-app sh
# OCI runtime exec failed: exec failed: unable to start container process:
# exec: "sh": executable file not found in $PATH: unknown
docker exec -it distroless-app bash
# OCI runtime exec failed: exec failed: unable to start container process:
# exec: "bash": executable file not found in $PATH: unknown
nsenter 동작 원리
Linux 네임스페이스 종류:
mnt — 파일시스템 마운트 네임스페이스
pid — 프로세스 ID 네임스페이스
net — 네트워크 인터페이스/라우팅 네임스페이스
ipc — IPC(공유 메모리, 세마포어) 네임스페이스
uts — 호스트명/도메인명 네임스페이스
user — 사용자/그룹 ID 네임스페이스
nsenter --target <PID> --net --pid --mnt
↑ 대상 프로세스의 네임스페이스로 진입
실행할 명령은 호스트의 파일시스템에서 가져옴 (컨테이너에 없어도 됨)
nsenter 기본 사용법
# 1단계: 컨테이너의 호스트 PID 확인
PID=$(docker inspect --format '{{.State.Pid}}' distroless-app)
echo "컨테이너 PID: $PID"
# 컨테이너 PID: 23456
# 2단계: 네트워크 네임스페이스 진입 (네트워크만)
sudo nsenter --target $PID --net -- ip addr
# 1: lo: <LOOPBACK,UP,LOWER_UP>
# inet 127.0.0.1/8 scope host lo
# 2: eth0@if45: <BROADCAST,MULTICAST,UP,LOWER_UP>
# inet 172.17.0.5/16 brd 172.17.255.255 scope global eth0
# 3단계: 모든 네임스페이스에 진입 (완전한 컨테이너 뷰)
sudo nsenter --target $PID --mount --uts --ipc --net --pid -- bash
# 이제 distroless 컨테이너의 파일시스템, 네트워크, 프로세스가 보임
# bash는 호스트의 bash를 사용하므로 컨테이너에 없어도 됨
# 4단계: 컨테이너 파일시스템 탐색
sudo nsenter --target $PID --mount -- ls /
sudo nsenter --target $PID --mount -- cat /app/config.json
sudo nsenter --target $PID --mount -- find /app -name "*.log"
네트워크 진단 (nsenter + 호스트 도구)
PID=$(docker inspect --format '{{.State.Pid}}' myapp)
# 컨테이너 네트워크 네임스페이스에서 ss 실행
sudo nsenter --target $PID --net -- ss -tlnp
# Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
# tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*
# 컨테이너의 라우팅 테이블 확인
sudo nsenter --target $PID --net -- ip route
# default via 172.17.0.1 dev eth0
# 172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.5
# 컨테이너 내 DNS 설정
sudo nsenter --target $PID --mount -- cat /etc/resolv.conf
# 외부 연결 확인 (호스트의 curl 사용)
sudo nsenter --target $PID --net -- curl -sv https://api.example.com/health
strace로 시스템 콜 추적
PID=$(docker inspect --format '{{.State.Pid}}' myapp)
# 컨테이너 메인 프로세스의 시스템 콜 추적
sudo strace -p $PID -f -e trace=network,file 2>&1 | head -50
# 출력 예시:
# [pid 23456] openat(AT_FDCWD, "/app/config.json", O_RDONLY) = 3
# [pid 23456] read(3, '{"database": "postgres://..."}', 4096) = 98
# [pid 23456] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 4
# [pid 23456] connect(4, {sa_family=AF_INET, sin_port=htons(5432), ...}) = -1 ECONNREFUSED
# ECONNREFUSED: 데이터베이스 연결 실패 → 연결 설정 문제임을 확인
# 파일 접근만 추적 (권한 문제 진단)
sudo strace -p $PID -f -e trace=open,openat,read,write 2>&1 | grep -i "denied\|EPERM\|EACCES"
docker cp와 디버그 사이드카 컨테이너 패턴
Java 서비스가 OOMKilled로 종료됐습니다. 힙 덤프가 /tmp/heap.hprof에 생성됐는데 컨테이너가 이미 Exited 상태입니다. docker cp는 중지된 컨테이너에서도 파일을 꺼낼 수 있습니다. 반대로 distroless 컨테이너에서 exec이 안 될 때 같은 네트워크를 공유하는 디버그 컨테이너(사이드카)를 붙이면 curl, tcpdump 같은 도구를 컨테이너 수정 없이 사용할 수 있습니다. 이 ConceptBlock에서는 docker cp로 파일을 추출·주입하는 방법과 디버그 사이드카 컨테이너 패턴을 다룹니다.

docker cp — 파일 추출과 주입
# 컨테이너 → 호스트로 파일 복사 (실행 중이거나 중지된 컨테이너 모두 가능)
docker cp debug-nginx:/etc/nginx/nginx.conf ./nginx.conf.bak
docker cp debug-nginx:/var/log/nginx/error.log ./error.log
# 호스트 → 컨테이너로 파일 복사 (임시 설정 변경 시)
docker cp ./nginx-debug.conf debug-nginx:/etc/nginx/conf.d/debug.conf
# 디렉토리 전체 복사
docker cp debug-nginx:/etc/nginx ./nginx-config-backup/
# 크래시된 컨테이너(Exited 상태)에서도 파일 추출 가능
docker ps -a | grep myapp
# abc123 myapp "..." 5 min ago Exited (137)
docker cp abc123:/app/heap-dump.hprof ./heap-dump.hprof
임시 구성 변경으로 디버깅
# 1. 현재 nginx 설정 파일 추출
docker cp debug-nginx:/etc/nginx/nginx.conf ./nginx.conf
# 2. 로컬에서 디버그 설정 수정 (로그 레벨 변경 등)
sed -i 's/error_log .*/error_log \/var\/log\/nginx\/debug.log debug;/' ./nginx.conf
# 3. 수정된 설정 파일을 컨테이너에 다시 주입
docker cp ./nginx.conf debug-nginx:/etc/nginx/nginx.conf
# 4. nginx 설정 리로드 (컨테이너 재시작 없이)
docker exec debug-nginx nginx -s reload
디버그 사이드카 컨테이너 패턴
프로덕션 컨테이너를 전혀 수정하지 않고 같은 네트워크와 볼륨을 공유하는 디버그 전용 컨테이너를 임시로 실행하는 패턴입니다.
# 문제가 있는 프로덕션 컨테이너 (distroless, 수정 불가)
docker run -d --name prod-app \
--network my-network \
--volume app-data:/data \
myapp:distroless
# 디버그 사이드카: 같은 네트워크와 볼륨 공유, 풍부한 도구 포함
docker run -it --rm \
--network container:prod-app \
--volumes-from prod-app \
--pid container:prod-app \
ubuntu:22.04 bash
# 사이드카 컨테이너 내부에서:
# - prod-app과 같은 네트워크 인터페이스 공유 (localhost로 앱에 접근)
# - prod-app의 볼륨 데이터에 접근
# - prod-app의 프로세스를 pid 네임스페이스에서 확인
# 사이드카에서 네트워크 진단
apt-get install -y curl netcat-openbsd nmap
curl -v http://localhost:8080/health
nc -zv localhost 5432
# 사이드카에서 공유 볼륨 파일 확인
ls -la /data
cat /data/app.log
네트워크 공유 사이드카로 트래픽 캡처
# tcpdump가 있는 사이드카로 컨테이너 네트워크 트래픽 캡처
docker run -it --rm \
--network container:debug-nginx \
--cap-add NET_ADMIN \
nicolaka/netshoot tcpdump -i eth0 -w /tmp/capture.pcap port 80
# 캡처 파일을 호스트로 복사해서 Wireshark로 분석
NETSHOOT_ID=$(docker ps -q -f name=netshoot)
docker cp $NETSHOOT_ID:/tmp/capture.pcap ./nginx-traffic.pcap
# nicolaka/netshoot: 네트워크 디버깅 도구가 모두 포함된 전용 이미지
# (tcpdump, curl, dig, nmap, iperf3, netstat, ss 등)
기본 실습
목표
실행 중인 nginx 컨테이너에 진입하여 설정 파일 구조, 실행 중인 프로세스, 네트워크 상태를 직접 확인합니다.
실습
1단계: nginx 컨테이너 준비
실습 전 디렉토리와 예제 파일을 먼저 준비합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part4/exam_2 && cd /tmp/docker/part4/exam_2
# 디버깅 대상 컨테이너용 Dockerfile 생성
cat > Dockerfile << 'EOF'
FROM nginx:alpine
RUN echo "debug test" > /usr/share/nginx/html/debug.html
EOF
이제 실습을 진행합니다.
# 이미 실행 중이 아니라면 실행
docker run -d --name debug-nginx -p 8080:80 nginx:alpine
docker ps -f name=debug-nginx
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS
# a1b2c3d4e5f6 nginx:alpine "/docker-entrypoint.…" 5 seconds ago Up 4 seconds 0.0.0.0:8080->80/tcp
2단계: 컨테이너 내부 sh 접속 (Alpine이므로 sh 사용)
docker exec -it debug-nginx sh
# 이제 컨테이너 내부
# / #
3단계: 내부에서 탐색
# 컨테이너 OS 확인
cat /etc/os-release
# NAME="Alpine Linux"
# VERSION_ID=3.18.4
# nginx 버전 및 컴파일 옵션 확인
nginx -V 2>&1 | head -5
# nginx version: nginx/1.25.3
# built with OpenSSL 3.1.3
# nginx 설정 파일 구조 확인
find /etc/nginx -type f | sort
# /etc/nginx/conf.d/default.conf
# /etc/nginx/mime.types
# /etc/nginx/nginx.conf
# 메인 설정 파일 확인
cat /etc/nginx/nginx.conf
# 프로세스 확인
ps aux
# PID USER TIME COMMAND
# 1 root 0:00 nginx: master process nginx -g daemon off;
# 30 nginx 0:00 nginx: worker process
# 네트워크 소켓 확인
netstat -tlnp 2>/dev/null || ss -tlnp
# tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=30,...))
# 환경 변수 확인
env | sort
exit
4단계: 컨테이너 외부에서 단발성 명령
# nginx 설정 문법 검증
docker exec debug-nginx nginx -t
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful
# 현재 연결 수 확인
docker exec debug-nginx sh -c "ss -s"
# Total: 6
# TCP: 3 (estab 0, closed 0, orphaned 0, timewait 0)
# 설정 파일 추출
docker cp debug-nginx:/etc/nginx/conf.d/default.conf ./default.conf.bak
cat ./default.conf.bak
- `docker exec`, `docker inspect --format`, `docker stats`를 용도별로 구분해 사용했는가?
- OOMKilled/ExitCode를 통해 "증상"이 아닌 종료 원인을 확인했는가?
- 원인 확인 전 컨테이너를 삭제하지 않고 증거를 먼저 수집했는가?
목표
--format 플래그로 다양한 컨테이너 메타데이터를 스크립트에서 활용할 수 있는 형태로 추출합니다.
실습
1단계: 기본 정보 추출
# IP 주소 추출
docker inspect --format '{{.NetworkSettings.IPAddress}}' debug-nginx
# 172.17.0.2
# 컨테이너 상태 확인
docker inspect --format 'Status: {{.State.Status}} | Running: {{.State.Running}}' debug-nginx
# Status: running | Running: true
# 시작 시각 확인
docker inspect --format '{{.State.StartedAt}}' debug-nginx
# 2026-03-28T02:15:43.123456789Z
2단계: 환경 변수 전체 목록 출력
docker inspect --format '{{range .Config.Env}}{{.}}
{{end}}' debug-nginx
# PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# NGINX_VERSION=1.25.3
# PKG_RELEASE=1
# NJS_VERSION=0.8.2
3단계: 네트워크 정보 상세 추출
# 사용자 정의 네트워크까지 포함한 전체 IP 정보
docker inspect --format \
'{{range $net, $cfg := .NetworkSettings.Networks}}네트워크: {{$net}} | IP: {{$cfg.IPAddress}} | Gateway: {{$cfg.Gateway}}
{{end}}' debug-nginx
# 네트워크: bridge | IP: 172.17.0.2 | Gateway: 172.17.0.1
# 포트 바인딩 정보
docker inspect --format '{{range $port, $bindings := .NetworkSettings.Ports}}{{$port}} -> {{range $bindings}}{{.HostPort}}{{end}}
{{end}}' debug-nginx
# 80/tcp -> 8080
4단계: 장애 진단용 스크립트
# 컨테이너 장애 진단 스크립트
cat > /tmp/diagnose.sh << 'SCRIPT'
#!/bin/bash
CONTAINER=${1:-debug-nginx}
echo "=== 컨테이너 장애 진단: $CONTAINER ==="
echo ""
echo "[기본 상태]"
docker inspect --format \
'Status: {{.State.Status}}
ExitCode: {{.State.ExitCode}}
OOMKilled: {{.State.OOMKilled}}
RestartCount: {{.RestartCount}}' $CONTAINER
echo ""
echo "[리소스 정보]"
docker inspect --format \
'Memory Limit: {{.HostConfig.Memory}} bytes
CPU Shares: {{.HostConfig.CpuShares}}' $CONTAINER
echo ""
echo "[네트워크]"
docker inspect --format \
'IP: {{.NetworkSettings.IPAddress}}' $CONTAINER
echo ""
echo "[최근 로그 (마지막 20줄)]"
docker logs --tail 20 $CONTAINER 2>&1
SCRIPT
chmod +x /tmp/diagnose.sh
bash /tmp/diagnose.sh debug-nginx
목표
메모리 제한에 걸려 OOMKilled로 종료되는 컨테이너를 시뮬레이션하고, 사후 진단 방법을 익힙니다.
실습
1단계: 의도적으로 OOMKilled 되는 컨테이너 생성
# 메모리를 20MB로 제한하고, 100MB를 사용하는 파이썬 스크립트 실행
docker run -d --name oom-test \
--memory=20m \
python:3.11-slim \
python -c "
import time
data = []
for i in range(100):
data.append('x' * 1024 * 1024) # 매 반복 1MB 할당
print(f'Allocated {i+1}MB', flush=True)
time.sleep(0.1)
"
# 잠시 대기
sleep 5
# 상태 확인 — OOMKilled로 종료되었을 것
docker ps -a -f name=oom-test
# CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES
# b2c3d4e5f6a7 python:3.11-slim "python -c..." 10s ago Exited (137) 3 seconds ago oom-test
2단계: OOMKilled 원인 확인
# 종료 코드 확인 (137 = SIGKILL)
docker inspect --format '{{.State.ExitCode}}' oom-test
# 137
# OOMKilled 플래그 확인
docker inspect --format '{{.State.OOMKilled}}' oom-test
# true
# 종료 직전 로그 확인
docker logs oom-test
# Allocated 1MB
# Allocated 2MB
# ...
# Allocated 18MB
# (이후 로그 없음 — 갑자기 종료됨)
3단계: 종합 진단 정보 수집
# 전체 장애 정보 수집
echo "=== OOM 장애 진단 ==="
docker inspect oom-test | python3 -c "
import json, sys
data = json.load(sys.stdin)[0]
state = data['State']
hc = data['HostConfig']
print(f'상태: {state[\"Status\"]}')
print(f'종료코드: {state[\"ExitCode\"]}')
print(f'OOMKilled: {state[\"OOMKilled\"]}')
print(f'시작시각: {state[\"StartedAt\"]}')
print(f'종료시각: {state[\"FinishedAt\"]}')
print(f'메모리제한: {hc[\"Memory\"]} bytes ({hc[\"Memory\"] // 1024 // 1024}MB)')
print(f'재시작횟수: {data[\"RestartCount\"]}')
"
# 호스트 커널 로그에서 OOM 이벤트 확인
sudo dmesg | grep -i "oom\|killed" | tail -5
# [12345.678] oom_reaper: reaped process 99999 (python), now anon-rss:0kB, file-rss:0kB
4단계: 정리
docker rm oom-test
트러블슈팅
문제 상황
$ docker exec -it my-distroless-app bash
OCI runtime exec failed: exec failed: unable to start container process:
exec: "bash": executable file not found in $PATH: unknown
$ docker exec -it my-distroless-app sh
OCI runtime exec failed: exec failed: unable to start container process:
exec: "sh": executable file not found in $PATH: unknown
원인
gcr.io/distroless/*, scratch, 또는 매우 최소화된 이미지들은 셸을 포함하지 않습니다. docker exec은 컨테이너 내부에 실행 파일이 있어야 동작합니다.
해결 방법 1: nsenter (즉시 적용 가능, 권장)
# 컨테이너 PID 확인
PID=$(docker inspect --format '{{.State.Pid}}' my-distroless-app)
echo "컨테이너 메인 프로세스 PID: $PID"
# 모든 네임스페이스에 진입 (호스트의 bash 사용)
sudo nsenter --target $PID --mount --uts --ipc --net --pid -- bash
# 이제 distroless 컨테이너의 파일시스템에서 탐색 가능
ls /app
cat /app/config.json
ps aux # PID 네임스페이스의 프로세스 목록
해결 방법 2: 디버그 사이드카 컨테이너
# 같은 네트워크 네임스페이스를 공유하는 사이드카 실행
docker run -it --rm \
--network container:my-distroless-app \
--pid container:my-distroless-app \
--volumes-from my-distroless-app \
busybox sh
# 사이드카 내부에서:
# - 컨테이너와 같은 IP로 localhost 접근 가능
# - 컨테이너의 볼륨 파일 접근 가능
# - 컨테이너의 프로세스 목록 확인 가능
해결 방법 3: 디버그용 이미지 태그 별도 관리 (사전 예방)
# Dockerfile에 디버그 스테이지 추가
# syntax=docker/dockerfile:1.6
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o server .
# 프로덕션 이미지 (distroless)
FROM gcr.io/distroless/base-debian12 AS production
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
# 디버그 이미지 (셸 포함)
FROM debian:bookworm-slim AS debug
COPY --from=builder /app/server /server
RUN apt-get update && apt-get install -y \
bash curl strace procps net-tools \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/server"]
# 프로덕션: distroless 이미지 사용
docker build --target production -t myapp:latest .
# 디버그: 셸 포함 이미지 사용
docker build --target debug -t myapp:debug .
docker run -it --rm myapp:debug bash
오류 메시지 비교
# exec 시 보이는 메시지들과 의미:
"executable file not found in $PATH" → 해당 실행 파일이 컨테이너에 없음
"permission denied" → 실행 파일은 있으나 권한 없음
"no such file or directory" → 바이너리 또는 의존 라이브러리 누락
문제 상황
# 실행 중인 컨테이너에서 파일 쓰기 시도
$ docker exec debug-nginx sh -c "echo 'test' > /etc/nginx/test.txt"
sh: /etc/nginx/test.txt: Read-only file system
# 또는 일반 권한 문제
$ docker exec myapp touch /app/debug.flag
touch: cannot touch '/app/debug.flag': Permission denied
원인 분류
원인 1: --read-only 플래그로 읽기 전용 컨테이너 실행
# 현재 컨테이너가 read-only인지 확인
docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' myapp
# true ← read-only 루트 파일시스템
# 또는 docker ps 출력에서 확인
docker inspect myapp | grep -i readonly
원인 2: 비root 사용자로 실행 중
# 현재 실행 사용자 확인
docker exec myapp id
# uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)
# 파일 소유자 확인
docker exec myapp ls -la /app
# drwxr-xr-x 2 root root 4096 ... config/ ← root 소유, appuser 쓰기 불가
# drwxrwxr-x 2 appuser appuser 4096 ... logs/ ← appuser 소유, 쓰기 가능
해결 방법
읽기 전용 컨테이너에서 임시 쓰기 영역 활용
# read-only 컨테이너는 특정 디렉토리만 tmpfs로 마운트하여 쓰기 허용
docker run -d \
--read-only \
--tmpfs /tmp:rw,size=100m \
--tmpfs /var/log/nginx:rw \
--name nginx-readonly \
nginx:alpine
# 디버그 파일은 /tmp에 작성
docker exec nginx-readonly sh -c "nginx -T > /tmp/nginx-debug.conf 2>&1"
docker cp nginx-readonly:/tmp/nginx-debug.conf ./
root 사용자로 강제 진입 (보안 주의)
# -u root 옵션으로 root 사용자로 exec
docker exec -u root -it myapp bash
# root로 진입 후 소유권 임시 변경
chown appuser:appuser /app/config
chmod 755 /app/logs
파일 대신 stdout/stderr 활용 (디버그 로그)
# 파일에 쓰지 않고 표준 출력으로 디버그 정보 수집
docker exec myapp sh -c "cat /proc/net/tcp" # 네트워크 연결
docker exec myapp sh -c "cat /proc/meminfo" # 메모리 상태
docker exec myapp sh -c "ls -laR /app" > /tmp/app-files.txt # 로컬에 저장
docker cp를 활용한 우회
# 읽기 전용이라도 docker cp는 컨테이너 밖에서 파일을 변경
# 수정이 필요한 파일을 호스트로 가져와 수정 후 다시 넣기
docker cp nginx-readonly:/etc/nginx/conf.d/default.conf ./
# 로컬에서 수정
nano ./default.conf
# 다시 컨테이너에 주입
docker cp ./default.conf nginx-readonly:/etc/nginx/conf.d/default.conf
docker exec nginx-readonly nginx -s reload
실무 맥락
배경
스타트업 B사의 주문 처리 서비스. 새벽 2시, 모니터링 알림이 연속으로 발생합니다. 주문 서비스 컨테이너가 5분마다 재시작을 반복하며 --restart=always 정책으로 겨우 유지되고 있습니다. 온콜 엔지니어가 접속했을 때의 상황입니다.
사고 대응 순서
1단계: 현황 파악 (5분)
# 전체 컨테이너 상태 확인
docker ps -a
# CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES
# 9f8e7d6c5b4a order-service "java …" 2 min ago Up 1 minute (unhealthy) order-service
# 3a2b1c0d9e8f order-service "java …" 7 min ago Exited (137) 2 minutes ago order-service_old
# 재시작 횟수와 OOMKilled 여부 확인
docker inspect order-service | python3 -c "
import json, sys
d = json.load(sys.stdin)[0]
print('재시작 횟수:', d['RestartCount'])
print('OOMKilled:', d['State']['OOMKilled'])
print('현재 메모리 사용:', end=' ')
"
# 재시작 횟수: 12
# OOMKilled: false ← 현재 실행 중인 것은 아직 OOM 안 됨
# 방금 종료된 컨테이너에서 OOMKilled 확인
PREV_ID=$(docker ps -aq --filter "name=order-service_old" | head -1)
docker inspect $PREV_ID --format '{{.State.OOMKilled}} | ExitCode: {{.State.ExitCode}}'
# true | ExitCode: 137 ← 메모리 부족으로 종료됨
2단계: 메모리 사용 추이 모니터링 (10분)
# 실시간 메모리 모니터링
watch -n 2 'docker stats --no-stream order-service'
# CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
# order-service 85.2% 892MiB / 1GiB 89.2% ... ...
# (2초 후)
# order-service 92.1% 951MiB / 1GiB 95.1% ... ... ← 계속 증가 중!
3단계: 컨테이너 내부 상세 진단
# JVM 힙 메모리 상태 확인 (Java 서비스)
docker exec order-service sh -c "
PID=\$(pgrep java)
jstat -gcutil \$PID 5000 3
"
# S0 S1 E O M CCS YGC YGCT FGC FGCT
# 0.00 88.23 98.14 97.43 96.5 93.2 1234 12.345 89 89.234
# ← Old Gen(O) 97%, Full GC 89회 → 메모리 누수 징후
# 스레드 덤프 (데드락, 스레드 누수 확인)
docker exec order-service sh -c "kill -3 \$(pgrep java)" 2>&1 | head -50
# 힙 덤프 생성 (메모리 누수 원인 분석용)
docker exec order-service sh -c "
PID=\$(pgrep java)
jmap -dump:format=b,file=/tmp/heap.hprof \$PID
"
# 힙 덤프 파일 호스트로 추출
docker cp order-service:/tmp/heap.hprof ./heap-$(date +%Y%m%d-%H%M).hprof
echo "힙 덤프 추출 완료: heap-$(date +%Y%m%d-%H%M).hprof"
4단계: 임시 조치 (메모리 제한 완화)
# 임시: 메모리 제한을 2GB로 높여 서비스 안정화
docker update --memory 2g --memory-swap 3g order-service
echo "메모리 제한 임시 증가: 1GB → 2GB"
# 메모리 모니터링 재확인
sleep 30
docker stats --no-stream order-service
# order-service 45.2% 934MiB / 2GiB 46.7% ← 안정됨
5단계: 근본 원인 분석 (사후)
# 힙 덤프 분석 (Eclipse MAT 또는 jhat 사용)
jhat -port 7000 ./heap-20260328-0215.hprof &
# 브라우저에서 http://localhost:7000 접속
# 로그에서 메모리 관련 패턴 검색
docker logs order-service 2>&1 | grep -iE "outofmemory|leak|connection pool|queue size"
# 2026-03-28 02:10:23 WARN HikariPool - Connection pool exhausted, queueSize=500
# ← DB 커넥션 풀 고갈 → 스레드 대기 → 메모리 증가 → OOM
# 결론: DB 커넥션 풀 크기가 부족하여 대기 스레드가 누적된 메모리 누수
# 해결: HikariCP maximumPoolSize 10 → 30으로 증가, 커넥션 타임아웃 설정 추가
정리 — 온콜 대응 체크리스트
새벽 장애 발생 시 디버깅 순서:
1. docker ps -a — 전체 컨테이너 상태 파악
2. docker inspect — OOMKilled, ExitCode, RestartCount 확인
3. docker logs --tail — 종료 직전 로그 확인
4. docker stats — 실시간 리소스 추이 모니터링
5. docker exec (가능 시) — 내부 진단 도구 실행
6. nsenter (distroless) — 셸 없는 컨테이너 진입
7. docker cp — 힙 덤프, 로그 파일 추출
8. 임시 조치 (docker update 등)
9. 근본 원인 분석 및 코드/설정 수정
핵심 요약
| 개념 | 명령/설정 | 설명 |
|---|---|---|
| 셸 진입 | docker exec -it <id> bash 또는 sh | 실행 중인 컨테이너에 인터랙티브 세션 |
| 단발성 명령 | docker exec <id> cat /etc/config | 셸 없이 특정 명령만 실행 |
| 메타데이터 추출 | docker inspect --format '{{.NetworkSettings.IPAddress}}' | Go 템플릿으로 원하는 필드만 추출 |
| OOMKilled 확인 | docker inspect --format '{{.State.OOMKilled}}' | 메모리 부족 종료 여부 확인 |
| 실시간 리소스 | docker stats --no-stream | CPU/메모리/네트워크 스냅샷 |
| 프로세스 목록 | docker top <id> | 컨테이너 내 실행 중인 프로세스 |
| 파일 추출 | docker cp <id>:/path/file ./local | 실행 중/중지된 컨테이너 모두 가능 |
| distroless 진단 | sudo nsenter --target $PID --net --pid --mount -- bash | 셸 없는 컨테이너 네임스페이스 직접 진입 |
| 시스템 콜 추적 | sudo strace -p $PID -f | 프로세스의 시스템 콜 실시간 추적 |
| 사이드카 디버그 | docker run --network container:<id> nicolaka/netshoot | 프로덕션 컨테이너 수정 없이 네트워크 진단 |
| 트래픽 캡처 | docker run --network container:<id> tcpdump | 컨테이너 네트워크 패킷 분석 |