컨테이너 환경에서 처음 만나는 문제들
신규 입사자가 첫 배포를 시도하자마자 port is already allocated, permission denied, Exited (137)이 연달아 발생합니다.
슬랙에는 같은 질문이 반복되고, 멘토는 매번 처음부터 로그 읽는 법을 다시 설명합니다.
문제는 Docker를 "명령어 모음"으로만 배워서 에러를 패턴으로 해석하지 못하는 데 있습니다.
이 모듈은 첫 주에 실제로 마주치는 실패를 재현하고, 진단 흐름을 몸에 익히도록 설계되었습니다.
팀 슬랙 #docker-help 채널에 같은 에러가 반복해서 올라옵니다. "포트 이미 사용 중인데 어떻게 하나요?", "볼륨 마운트했는데 permission denied가 납니다", "컨테이너가 실행하자마자 바로 꺼져요". 에러 메시지는 똑같지만 보내는 사람은 매번 다릅니다. Docker를 처음 쓰는 사람이라면 누구나 한 번씩은 부딪히는 문제들입니다. 이 챕터는 도구 사용법이 아닌 에러 패턴 → 원인 → 해결 흐름으로, 신규 팀원 온보딩 첫 주에 마주치는 대표적인 5가지 에러를 체계적으로 다룹니다. 에러 메시지를 읽는 법과 docker inspect, docker logs, docker events로 원인을 추적하는 습관을 함께 익힙니다.
컨테이너 입문자가 첫 주에 반드시 마주치는 에러 패턴들을 직접 재현하고 진단 → 원인 → 해결 흐름으로 풀어냅니다. 에러 메시지를 두려워하지 않고, 진단 명령으로 원인을 좁혀가는 사고 방식을 익히는 것이 목표입니다.
- 1포트 충돌 — `port is already allocated` 진단과 해결 (ss, lsof, docker ps)
- 2볼륨 UID/GID 불일치 — `permission denied` 원인 추적과 chown/chmod 해결
- 3컨테이너 즉시 종료 — CMD vs ENTRYPOINT 포그라운드 실행 원칙
- 4Alpine vs Debian 셸 차이 — `exec /bin/sh: no such file or directory` 해결
- 5OOMKilled — 메모리 제한 초과 감지와 limits 조정
- 6빌드 캐시 미적용 — COPY 순서 최적화와 .dockerignore 누락 문제
실습은 순서대로 진행합니다. 각 실습은 에러를 의도적으로 재현한 뒤 진단 → 해결 순서로 따라갑니다. 실습 환경은 Linux 호스트 또는 Docker Desktop(Mac/Windows) 모두 동작합니다.
docker --versionmkdir -p ~/docker-debug-lab && cd ~/docker-debug-labdocker run -d --name port-occupier -p 8080:80 nginx:alpinemkdir -p ~/docker-debug-lab/data && echo 'hello' > ~/docker-debug-lab/data/test.txtdocker rm -f port-occupier crash-test oom-test && rm -rf ~/docker-debug-lab
포트 충돌 — port is already allocated 진단과 해결
nginx 컨테이너를 8080 포트로 띄우려는데 이런 에러가 납니다:
docker: Error response from daemon: driver failed programming external
connectivity on endpoint myapp (...):
Bind for 0.0.0.0:8080 failed: port is already allocated.
에러 메시지는 명확합니다. 호스트의 8080 포트가 이미 점유되어 있습니다. 그런데 무엇이 쓰고 있는지 모릅니다. 이 시점에서 docker inspect나 docker events보다 먼저 쓸 도구는 포트 점유 프로세스를 보여주는 명령입니다.

진단 흐름
1단계: 호스트 포트 점유 프로세스 확인
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part4/exam_15 && cd /tmp/docker/part4/exam_15
# 방법 1: ss (modern, 대부분 Linux 기본 탑재)
ss -tlnp | grep 8080
# State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
# LISTEN 0 511 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=12345,fd=4))
# 방법 2: lsof (macOS/Linux 공통)
lsof -i :8080
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
# docker-pr 12345 root 4u IPv4 ... 0t0 TCP *:http-alt (LISTEN)
# 방법 3: netstat (구형 시스템)
netstat -tlnp | grep 8080
docker-proxy가 보이면 다른 컨테이너가 8080을 이미 바인딩하고 있는 것입니다.
2단계: 점유 중인 컨테이너 찾기
# 8080을 바인딩 중인 컨테이너 확인
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep 8080
# NAMES PORTS
# port-occupier 0.0.0.0:8080->80/tcp
# 또는 더 상세하게 (컨테이너 ID까지)
docker ps --filter "publish=8080"
3단계: 해결 선택지
# 선택 1: 기존 컨테이너 중지 후 새로 실행
docker stop port-occupier
docker run -d --name myapp -p 8080:80 nginx:alpine
# 선택 2: 다른 호스트 포트 사용 (기존 컨테이너 유지)
docker run -d --name myapp -p 8081:80 nginx:alpine
# ^^^^ 충돌 없는 포트로 변경
# 선택 3: docker events로 실시간 충돌 모니터링
docker events --filter 'type=container' --filter 'event=die' &
docker run -d --name myapp -p 8080:80 nginx:alpine
# 재시도 중에 어떤 이벤트가 발생하는지 확인
docker inspect로 바인딩 정보 확인
컨테이너가 실행 중이라면 inspect로 포트 바인딩 상세 정보를 확인할 수 있습니다:
docker inspect port-occupier --format \
'{{range $port, $bindings := .NetworkSettings.Ports}}{{$port}} -> {{range $bindings}}{{.HostIp}}:{{.HostPort}}{{end}}{{"\n"}}{{end}}'
# 80/tcp -> 0.0.0.0:8080
핵심 원칙
포트 충돌은 에러 메시지가 명확하게 원인을 알려줍니다. ss -tlnp | grep <포트> → 점유 프로세스 확인 → 종료하거나 다른 포트 사용. 이 3단계 패턴을 습관으로 만들면 됩니다.
실습 1: 볼륨 마운트 UID/GID 불일치 — permission denied 해결
볼륨 마운트 후 컨테이너 내부에서 파일을 읽거나 쓸 때 permission denied가 나는 가장 흔한 원인은 UID/GID 불일치입니다. 같은 숫자여야 같은 사람입니다 — 컨테이너 내부와 호스트가 이름이 아닌 숫자(UID)로 파일 소유권을 판단하기 때문입니다.
문제 재현
# 호스트에서 root 소유 디렉토리 생성 (권한 700)
sudo mkdir -p /tmp/strict-data
sudo chmod 700 /tmp/strict-data
sudo bash -c 'echo "secret" > /tmp/strict-data/config.txt'
# nginx는 내부적으로 UID 101(nginx 사용자)로 실행됩니다
docker run --rm \
-v /tmp/strict-data:/data \
nginx:alpine \
cat /data/config.txt
# cat: can't open '/data/config.txt': Permission denied
진단: 누가 누구인지 확인
# 1. 호스트 디렉토리의 소유자 확인
ls -la /tmp/strict-data/
# drwx------ 2 root root 4096 ... ← UID 0 (root)만 접근 가능
# 2. 컨테이너 내부 실행 프로세스의 UID 확인
docker run --rm nginx:alpine id
# uid=0(root) gid=0(root) groups=0(root) ← nginx 마스터는 root
docker run --rm nginx:alpine \
sh -c "ps aux | grep nginx"
# 1 root nginx: master process ... ← PID 1은 root
# 6 nginx nginx: worker process ← 워커는 UID 101(nginx)
# 3. docker inspect로 마운트 확인
docker inspect <중지된-컨테이너-ID> --format '{{range .Mounts}}{{.Source}} -> {{.Destination}} ({{.Mode}}){{"\n"}}{{end}}'
해결 방법
# 방법 1: 호스트 디렉토리 권한을 컨테이너 UID에 맞게 변경
# nginx 워커의 UID는 101
sudo chown -R 101:101 /tmp/strict-data
sudo chmod 755 /tmp/strict-data
docker run --rm \
-v /tmp/strict-data:/data \
nginx:alpine \
cat /data/config.txt
# secret ← 정상 출력
# 방법 2: 컨테이너 실행 시 --user로 UID 맞추기
# 호스트 현재 사용자 UID로 컨테이너 실행
docker run --rm \
--user $(id -u):$(id -g) \
-v ~/docker-debug-lab/data:/data \
alpine \
cat /data/test.txt
# hello ← 정상 출력
# 방법 3: Dockerfile에서 소유권 설정 (이미지 빌드 단계)
# COPY --chown=nginx:nginx ./config /etc/nginx/conf.d/
# RUN chown -R nginx:nginx /var/cache/nginx
docker events로 실패 이벤트 추적
# 터미널 1: 이벤트 스트림 켜기
docker events --filter 'type=container' &
# 터미널 2: 실패하는 컨테이너 실행
docker run --rm -v /tmp/strict-data:/data alpine cat /data/config.txt
# events 출력에서 start → die (exit code 1) 패턴 확인
# 컨테이너가 정상 종료인지 오류 종료인지 구분할 수 있습니다
컨테이너 즉시 종료 — CMD vs ENTRYPOINT 포그라운드 실행 원칙
컨테이너를 띄웠는데 docker ps에 보이지 않습니다. docker ps -a로 보면 Exited(0) 또는 Exited(1) 상태입니다. docker logs를 봐도 아무 출력이 없거나, 출력은 있는데 곧바로 꺼집니다.
이 패턴의 핵심은 하나입니다: 컨테이너는 PID 1 프로세스가 종료되면 함께 종료됩니다.

즉시 종료가 발생하는 대표 케이스
케이스 1: 백그라운드 데몬 실행 명령
# 잘못된 예시 — service, systemctl은 데몬을 백그라운드로 띄우고 즉시 반환
CMD ["service", "nginx", "start"]
# 또는
CMD ["systemctl", "start", "nginx"]
# 또는
CMD ["/etc/init.d/nginx", "start"]
# 재현
docker run --name crash-test nginx:alpine sh -c "nginx && echo done"
# done 출력 후 즉시 종료
docker ps -a | grep crash-test
# crash-test nginx:alpine "sh -c 'nginx && ..." 3 sec ago Exited (0)
docker logs crash-test
# done ← 정상 출력처럼 보이지만 컨테이너는 이미 종료됨
# 올바른 예시 — 포그라운드 실행
CMD ["nginx", "-g", "daemon off;"]
케이스 2: 셸 형식 vs exec 형식 ENTRYPOINT
# 셸 형식 (shell form) — /bin/sh -c 를 통해 실행
ENTRYPOINT "nginx -g 'daemon off;'"
# exec 형식 (exec form, 권장) — 직접 실행, PID 1이 됨
ENTRYPOINT ["nginx", "-g", "daemon off;"]
셸 형식은 /bin/sh -c가 PID 1이 됩니다. Alpine처럼 /bin/sh가 없는 환경에서는 즉시 실패합니다. exec 형식은 지정한 바이너리가 PID 1이 됩니다.
docker inspect로 종료 원인 추적
# 컨테이너 종료 후 상세 상태 확인
docker inspect crash-test --format '
ExitCode: {{.State.ExitCode}}
OOMKilled: {{.State.OOMKilled}}
Error: {{.State.Error}}
StartedAt: {{.State.StartedAt}}
FinishedAt: {{.State.FinishedAt}}'
# ExitCode: 0 → 정상 종료 (CMD가 끝남)
# ExitCode: 1 → 오류 종료 (CMD 실행 실패)
# ExitCode: 137 → SIGKILL (OOM 또는 강제 종료)
# ExitCode: 139 → Segmentation fault
docker events로 라이프사이클 이벤트 확인
# 컨테이너 시작과 종료 이벤트를 시간순으로 확인
docker events \
--since $(date -d '5 minutes ago' -u +%Y-%m-%dT%H:%M:%SZ) \
--filter 'type=container' \
--filter 'event=start' \
--filter 'event=die' \
--format '{{.Time}} {{.Action}} {{.Actor.Attributes.name}} exitCode={{.Actor.Attributes.exitCode}}'
# 1748540000 start crash-test exitCode=
# 1748540001 die crash-test exitCode=0 ← 1초 만에 종료
포그라운드 실행 확인 방법
# 올바르게 실행된 컨테이너 확인
docker run -d --name fg-test nginx:alpine
docker inspect fg-test --format '{{.State.Status}}'
# running ← 계속 실행 중
# PID 1 확인
docker exec fg-test cat /proc/1/cmdline | tr '\0' ' '
# nginx: master process nginx -g daemon off; ← PID 1이 nginx 자체
실습 2: Alpine vs Debian 셸 차이 — exec /bin/sh: no such file or directory
이미지를 바꿨더니 갑자기 컨테이너가 뜨지 않습니다. Dockerfile에서 Alpine 이미지를 사용하는데 셸 스크립트 기반 ENTRYPOINT가 실패합니다.
문제 재현
# 셸 형식 CMD — /bin/sh 가 없으면 실패
docker run --rm alpine:3.18 sh -c "echo hello"
# hello ← Alpine에는 /bin/sh 있음 (busybox ash가 /bin/sh로 링크됨)
# 그런데 특정 최소화 이미지나 scratch 기반은 다릅니다
# 다음 예시: 비표준 이미지에서 셸 경로 문제 재현
cat > /tmp/test-entrypoint.sh << 'EOF'
#!/bin/bash
echo "Starting application..."
exec "$@"
EOF
chmod +x /tmp/test-entrypoint.sh
# Alpine에서 bash shebang 스크립트 실행 시 실패
docker run --rm \
-v /tmp/test-entrypoint.sh:/entrypoint.sh \
alpine:3.18 \
/entrypoint.sh echo "app started"
# /entrypoint.sh: line 1: bash: not found
진단
# Alpine과 Debian의 셸 차이 비교
docker run --rm alpine:3.18 ls -la /bin/sh /bin/bash 2>&1
# lrwxrwxrwx 1 root root 12 ... /bin/sh -> /bin/busybox ← sh만 있음
# ls: /bin/bash: No such file or directory ← bash 없음
docker run --rm debian:bookworm-slim ls -la /bin/sh /bin/bash 2>&1
# lrwxrwxrwx 1 root root 4 ... /bin/sh -> dash
# -rwxr-xr-x 1 root root ... /bin/bash ← bash 있음
# 컨테이너가 실패했다면 docker inspect로 에러 확인
docker run --name shell-test \
alpine:3.18 /entrypoint.sh echo "test" 2>/dev/null; true
docker inspect shell-test --format '{{.State.ExitCode}} {{.State.Error}}'
docker rm shell-test
해결 방법
# 방법 1: shebang을 #!/bin/sh 로 변경 (POSIX sh, Alpine 호환)
cat > /tmp/test-entrypoint-fixed.sh << 'EOF'
#!/bin/sh
echo "Starting application..."
exec "$@"
EOF
chmod +x /tmp/test-entrypoint-fixed.sh
docker run --rm \
-v /tmp/test-entrypoint-fixed.sh:/entrypoint.sh \
alpine:3.18 \
/entrypoint.sh echo "app started"
# Starting application...
# app started
# 방법 2: Alpine에 bash 설치 (Dockerfile)
# RUN apk add --no-cache bash
# 방법 3: Debian 계열 이미지 사용 (bash가 필요하다면)
# FROM debian:bookworm-slim 또는 ubuntu:22.04
exec /bin/sh: no such file or directory 가 나는 경우
이 에러는 이미지 자체에 /bin/sh가 없는 경우입니다 (scratch, distroless 기반):
# docker inspect로 이미지 정보 확인
docker inspect nginx:alpine --format '{{.Config.Cmd}}'
# [nginx -g daemon off;] ← exec 형식, /bin/sh 불필요
# docker events로 즉시 종료 이벤트 확인
docker events --filter 'event=die' --format '{{.Actor.Attributes.name}} {{.Actor.Attributes.exitCode}}' &
docker run --name no-shell gcr.io/distroless/static:latest /bin/sh 2>/dev/null; true
# no-shell 127 ← exitCode 127 = command not found
OOMKilled — 메모리 제한 초과 감지와 limits 조정
컨테이너가 갑자기 종료되고 docker ps -a에 Exited(137)이 보입니다. 137은 128 + SIGKILL(9)입니다. 커널이 메모리 부족으로 프로세스에 SIGKILL을 보낸 것입니다. OOMKilled입니다.

진단 흐름
1단계: OOMKilled 여부 확인
# 가장 먼저: docker inspect로 OOMKilled 플래그 확인
docker inspect <컨테이너명> --format \
'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}'
# OOMKilled=true ExitCode=137 ← 메모리 부족으로 커널이 종료함
# 전체 컨테이너 중 OOMKilled된 것 찾기
docker ps -a --format '{{.Names}} {{.Status}}' | while read name status; do
if docker inspect "$name" --format '{{.State.OOMKilled}}' 2>/dev/null | grep -q true; then
echo "OOMKilled: $name ($status)"
fi
done
2단계: 메모리 설정과 사용량 확인
# 현재 메모리 제한 확인
docker inspect <컨테이너명> --format \
'MemoryLimit={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
# MemoryLimit=104857600 MemorySwap=209715200 ← 100MB 제한
# 실행 중인 컨테이너의 실시간 메모리 사용량
docker stats --no-stream <컨테이너명>
# CONTAINER CPU % MEM USAGE / LIMIT MEM %
# myapp 45.2% 98MiB / 100MiB 98.0% ← 제한에 근접
# 호스트 커널 OOM 로그 확인
dmesg | grep -i "oom\|killed" | tail -20
# [12345.678] oom-kill:constraint=CONSTRAINT_MEMCG,...
# [12345.679] oom_reaper: reaped process ... (myapp), now anon-rss:0kB
3단계: 메모리 제한 조정
# 실행 중인 컨테이너 메모리 제한 즉시 변경 (임시 조치)
docker update --memory 512m --memory-swap 768m <컨테이너명>
# 변경 확인
docker inspect <컨테이너명> --format '{{.HostConfig.Memory}}'
# 536870912 ← 512MB (512 * 1024 * 1024)
4단계: 근본 원인 파악
OOMKilled는 증상이지 원인이 아닙니다. 메모리 누수인지, 제한이 너무 낮은지 파악해야 합니다:
# 컨테이너 시작 직후부터 메모리 추이 관찰
docker stats <컨테이너명>
# 메모리가 계속 증가 → 누수 가능성
# 메모리가 일정 수준 유지 → 제한이 너무 낮은 것
# docker logs로 OOM 직전 로그 확인
docker logs --tail 100 <컨테이너명> 2>&1 | grep -iE "memory|heap|gc|error"
docker-compose에서 메모리 제한 설정
services:
myapp:
image: myapp:latest
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
# 또는 (Compose v2 단독 실행 시)
mem_limit: 512m
memswap_limit: 768m
실습 3: 빌드 캐시 미적용 — COPY 순서와 .dockerignore 누락
코드를 한 줄 수정했는데 docker build가 처음부터 전부 다시 빌드됩니다. 의존성 설치에만 2분이 걸립니다. 반대로 .dockerignore를 빠뜨려서 빌드 컨텍스트에 node_modules가 통째로 올라가 빌드가 10분씩 걸리기도 합니다.
캐시 미적용 문제 재현과 진단
mkdir -p ~/docker-debug-lab/cache-test
cd ~/docker-debug-lab/cache-test
# 잘못된 Dockerfile — COPY 순서 문제
cat > Dockerfile.bad << 'EOF'
FROM node:18-alpine
WORKDIR /app
# 소스 코드를 먼저 복사 — 코드 한 줄 바꾸면 아래 npm install도 재실행
COPY . .
RUN npm install # ← 캐시 미적용: 코드 변경 시마다 2분 소요
CMD ["node", "index.js"]
EOF
# package.json 생성
cat > package.json << 'EOF'
{
"name": "cache-test",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.0"
}
}
EOF
echo "console.log('hello')" > index.js
# 첫 빌드 (캐시 없음)
docker build -f Dockerfile.bad -t cache-test:bad . --progress=plain 2>&1 | grep -E "^#[0-9]|CACHED|RUN"
올바른 Dockerfile — 의존성 레이어 분리
cat > Dockerfile.good << 'EOF'
FROM node:18-alpine
WORKDIR /app
# 1. 의존성 파일만 먼저 복사 — package.json이 안 바뀌면 npm install 캐시 사용
COPY package*.json ./
RUN npm install # ← package.json 변경 시에만 재실행
# 2. 소스 코드는 의존성 설치 후 복사
COPY . .
CMD ["node", "index.js"]
EOF
# 첫 빌드
docker build -f Dockerfile.good -t cache-test:good . --progress=plain
# index.js 수정 후 재빌드 — npm install은 CACHED 처리됨
echo "console.log('world')" > index.js
docker build -f Dockerfile.good -t cache-test:good . --progress=plain 2>&1 | grep -E "CACHED|RUN npm"
# #5 CACHED ← npm install 캐시 적용!
.dockerignore 누락 문제
# node_modules 없는 상태에서 빌드 컨텍스트 크기 확인
du -sh .
# 4.0K ← 작음
# node_modules 설치 후 크기 변화
npm install 2>/dev/null
du -sh .
# 28M ← node_modules 포함
# .dockerignore 없이 빌드 시 컨텍스트 전송 크기 확인
docker build -f Dockerfile.good -t cache-test:context . 2>&1 | grep "Sending build context"
# Sending build context to Docker daemon 28.5MB ← 느림!
# .dockerignore 작성
cat > .dockerignore << 'EOF'
node_modules
.git
*.log
.env
.DS_Store
coverage
.nyc_output
EOF
# 다시 빌드 — 컨텍스트 크기 비교
docker build -f Dockerfile.good -t cache-test:context . 2>&1 | grep "Sending build context"
# Sending build context to Docker daemon 3.584kB ← 1000배 작아짐
캐시 적용 여부 확인
# docker build history로 각 레이어 빌드 시간 확인
docker history cache-test:good --no-trunc --format "table {{.CreatedBy}}\t{{.Size}}"
# BuildKit 상세 로그로 CACHED/RUN 구분
DOCKER_BUILDKIT=1 docker build -f Dockerfile.good -t cache-test:good . \
--progress=plain 2>&1 | grep -E "^#[0-9]+.*CACHED|^#[0-9]+.*RUN"
- port 충돌/권한/OOM 사례를 에러 문구만 보고도 어떤 진단 명령부터 칠지 판단할 수 있는가?
- `docker inspect`에서 상태(State), 메모리 제한(HostConfig.Memory), 마운트(Mounts) 값을 직접 추출해 원인을 좁혔는가?
- 동일 에러를 만났을 때 "재시작"보다 "원인 확인"을 먼저 하는 흐름을 설명할 수 있는가?
문제 상황
$ docker ps -a
CONTAINER ID IMAGE STATUS NAMES
a1b2c3d4e5f6 myapp:1.0 Exited (137) 3 minutes ago myapp
$ docker logs myapp
... (정상 로그들)
... (갑자기 끊김, 종료 메시지 없음)
원인 확인
# OOMKilled 플래그와 종료 코드 확인
docker inspect myapp --format \
'Status={{.State.Status}} OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}'
# Status=exited OOMKilled=true ExitCode=137
# 설정된 메모리 제한 확인
docker inspect myapp --format \
'MemLimit={{.HostConfig.Memory}}'
# MemLimit=104857600 (= 100MB)
# 호스트 커널 OOM 이벤트 확인
sudo dmesg | grep -i oom | tail -5
해결 방법
# 1. 즉시 조치: 메모리 제한 증가
docker update --memory 512m --memory-swap 1g myapp
docker start myapp
# 2. 메모리 사용 추이 모니터링
docker stats myapp
# 3. 근본 원인 파악: 메모리가 계속 증가하면 누수, 일정하면 제한 부족
# 메모리 누수 의심 시 → 애플리케이션 코드 점검 (캐시 무제한 성장, 커넥션 풀 누수 등)
# 제한만 낮은 경우 → docker-compose.yml의 mem_limit 또는 deploy.resources.limits.memory 조정
# 4. 재발 방지: docker-compose.yml 업데이트
# deploy:
# resources:
# limits:
# memory: 512M
문제 상황
$ docker ps
Cannot connect to the Docker daemon at unix:///var/run/docker.sock.
Is the docker daemon running?
# 또는
$ docker ps
permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json":
dial unix /var/run/docker.sock: connect: permission denied
원인 분류와 해결
# 원인 1: Docker 데몬이 실행 중이지 않음
sudo systemctl status docker
# ● docker.service - Docker Application Container Engine
# Loaded: loaded (/lib/systemd/system/docker.service; enabled)
# Active: inactive (dead) ← 꺼져 있음
sudo systemctl start docker
sudo systemctl enable docker # 부팅 시 자동 시작
# 원인 2: 현재 사용자가 docker 그룹에 없음 (sudo 없이 docker 실행 시)
groups
# user sudo ← docker 그룹 없음
sudo usermod -aG docker $USER
newgrp docker # 현재 세션에 즉시 적용 (재로그인 없이)
# 또는 재로그인 후
groups
# user sudo docker ← docker 그룹 추가됨
# 원인 3: 소켓 파일 권한 문제
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker ... ← docker 그룹에 rw 권한
# docker 그룹 확인
getent group docker
# docker:x:998:user1,user2
# 원인 4: DOCKER_HOST 환경 변수가 잘못 설정된 경우
echo $DOCKER_HOST
# tcp://wrong-host:2375 ← 잘못된 원격 호스트
unset DOCKER_HOST # 초기화하여 로컬 소켓 사용
docker events로 데몬 연결 확인
# 데몬 연결 후 이벤트 스트림이 정상적으로 흐르는지 확인
docker events --since 1m --until now
# 정상이면 최근 1분 이벤트 출력 (또는 빈 출력)
# 연결 실패면 에러 메시지
배경
스타트업 C사에 DevOps 팀원이 합류했습니다. Docker는 개념만 알고 실무 경험은 없습니다. 온보딩 첫 주, 슬랙 #docker-help 채널에 올라온 질문들과 팀에서 공유한 해결 패턴입니다.
Day 1 — 포트 충돌
# 팀원 질문: "docker run 했는데 이런 에러가..."
$ docker run -d -p 8080:80 nginx:alpine
Error: port is already allocated
# 시니어 답변: 이렇게 찾아요
$ ss -tlnp | grep 8080
LISTEN 0 128 0.0.0.0:8080 users:(("docker-proxy",pid=4521))
$ docker ps --format "{{.Names}}\t{{.Ports}}" | grep 8080
old-nginx 0.0.0.0:8080->80/tcp
$ docker stop old-nginx && docker run -d --name nginx -p 8080:80 nginx:alpine
# 또는
$ docker run -d --name nginx -p 8081:80 nginx:alpine # 포트 변경
Day 2 — 볼륨 권한 문제
# 팀원 질문: "볼륨 연결했는데 permission denied..."
$ docker run -v /home/user/data:/app/data myapp:latest
Error: open /app/data/config.json: permission denied
# 시니어 답변: UID 먼저 확인해요
$ docker inspect myapp:latest --format '{{.Config.User}}'
# 1000 (또는 appuser)
$ ls -la /home/user/data/
drwx------ 2 root root ... ← root 소유, 1000번 사용자 접근 불가
$ sudo chown -R 1000:1000 /home/user/data/
$ docker run -v /home/user/data:/app/data myapp:latest # 정상 실행
Day 3 — 컨테이너 즉시 종료
# 팀원 질문: "docker run 하면 바로 꺼져요..."
$ docker run myapp:latest
$ docker ps
# (아무것도 없음)
$ docker ps -a
myapp Exited(0) 2 sec ago
# 시니어 답변: logs와 inspect 보세요
$ docker logs $(docker ps -lq)
# (로그 있으면 뭔가 실행은 됨, 없으면 CMD 자체가 즉시 종료)
$ docker inspect $(docker ps -lq) --format '{{.Config.Cmd}} {{.Config.Entrypoint}}'
# [service nginx start] [] ← 백그라운드 데몬 실행이 원인
# Dockerfile 수정: CMD ["nginx", "-g", "daemon off;"]
Day 4 — 빌드 캐시 미적용
# 팀원 질문: "코드 한 줄 바꿨는데 왜 2분씩 걸리죠?"
# 원인: COPY . . 가 RUN npm install 위에 있음
# 시니어 답변: Dockerfile 순서 바꾸세요
# Before:
# COPY . .
# RUN npm install
#
# After:
# COPY package*.json ./
# RUN npm install
# COPY . .
# 그리고 .dockerignore 꼭 만들어요
$ cat .dockerignore
node_modules
.git
*.log
Day 5 — OOMKilled
# 팀원 질문: "컨테이너가 계속 죽어요, 로그도 없이..."
$ docker inspect crashed-app --format \
'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}'
# OOMKilled=true ExitCode=137
# 시니어 답변: 메모리 제한 먼저 확인해요
$ docker inspect crashed-app --format '{{.HostConfig.Memory}}'
# 67108864 (= 64MB, 너무 낮음)
$ docker update --memory 256m crashed-app
$ docker start crashed-app
# docker-compose.yml도 업데이트해두세요:
# deploy:
# resources:
# limits:
# memory: 256M
팀 온보딩 가이드 — 에러 발생 시 첫 3분 루틴
1. docker logs <컨테이너> → 무슨 에러인지 파악
2. docker inspect <컨테이너> 확인 항목:
- .State.ExitCode (종료 코드)
- .State.OOMKilled (메모리 종료 여부)
- .State.Error (에러 메시지)
- .HostConfig.Memory (메모리 제한)
- .Mounts (볼륨 마운트 확인)
3. docker events --since 5m → 최근 이벤트 흐름 확인
4. ss -tlnp | grep <포트> → 포트 충돌 확인
핵심 요약
이번 챕터에서 다룬 5가지 에러 패턴과 진단 명령 정리입니다.
| 에러 패턴 | 첫 번째 진단 명령 | 핵심 원인 |
|---|---|---|
| port is already allocated | ss -tlnp | grep <포트> | 호스트 포트 이중 바인딩 |
| permission denied (볼륨) | docker exec <id> id | 컨테이너 UID ≠ 호스트 소유자 |
| 컨테이너 즉시 종료 | docker inspect --format '{{.State.ExitCode}}' | PID 1이 백그라운드로 실행됨 |
| exec /bin/sh 실패 | docker run --rm <이미지> ls /bin/sh | Alpine은 bash 없음, sh만 있음 |
| OOMKilled (137) | docker inspect --format '{{.State.OOMKilled}}' | 메모리 제한 초과 |
에러 발생 시 기본 3단계: docker logs → docker inspect → docker events. 이 순서를 습관으로 만드는 것이 이번 챕터의 가장 중요한 목표입니다.