컨테이너 안에서 /var/run/docker.sock: connect: permission denied가 발생하면 먼저 호스트 소켓의 숫자 GID를 컨테이너의 보조 그룹으로 전달해야 한다. 그러나 모든 서비스에 소켓을 직접 마운트하는 방식은 호스트 Docker를 사실상 관리자 권한으로 조작하게 만든다. 조회만 필요하다면 socket proxy, 호스트가 rootless Docker라면 실제 사용자 소켓 경로를 쓰는 편이 안전하다. chmod 666, 무분별한 privileged: true, 인증 없는 TCP 2375 공개는 해결책에서 제외해야 한다.
핵심 요약
- Linux rootful Docker:
stat -c %g /var/run/docker.sock으로 GID를 확인하고 Composegroup_add에 같은 숫자를 넣는다. - Rootless Docker: 보통
/run/user/$UID/docker.sock을 사용한다. rootful 경로를 계속 마운트하면 권한을 고쳐도 다른 daemon을 가리킨다. - 보안: Docker 공식 문서는 소켓과 Docker 그룹이 root 수준 권한을 준다고 경고한다.
:ro만 붙여 API 쓰기 권한이 제한된다고 가정하면 안 된다. - 최소 권한: 컨테이너 목록·이벤트 조회만 필요하면 허용 API를 줄인 socket proxy를 별도 내부 네트워크에 둔다.
오류 원인은 파일 권한보다 숫자 GID 불일치인 경우가 많다
Unix socket 접근은 경로 문자열이나 그룹 이름이 아니라 커널이 보는 숫자 UID·GID와 권한 비트로 결정된다. 호스트의 docker 그룹이 GID 998인데 컨테이너 이미지 안의 같은 이름 그룹이 GID 999라면 이름이 같아도 권한이 맞지 않는다. 반대로 이미지에 docker라는 그룹이 없어도 실행 프로세스가 보조 그룹 998을 가지면 소켓의 그룹 읽기·쓰기 권한을 사용할 수 있다.
| 증상 | 우선 확인 | 가능성이 큰 원인 |
|---|---|---|
| 소켓 파일이 보이지만 permission denied | 호스트 소켓 GID와 컨테이너 프로세스 그룹 | 숫자 GID 불일치 |
| no such file or directory | 호스트와 컨테이너의 실제 소켓 경로 | rootless 경로 또는 Docker Desktop 차이 |
| 호스트에서는 되지만 컨테이너에서 실패 | 컨테이너의 id, 실행 사용자 | 비루트 사용자의 보조 그룹 누락 |
| 연결은 되지만 특정 작업 실패 | Docker API 응답과 proxy 정책 | 허용 endpoint·HTTP method 제한 |
| 재부팅·재배포 뒤 다시 실패 | 소켓 GID와 Compose 보간값 | 배포 시 하드코딩한 GID가 현재 호스트와 다름 |

출처: Docker 공식 문서 기반 구성
1단계: 경로·소유권·프로세스 그룹을 숫자로 확인한다
권한을 바꾸기 전에 아래 세 결과를 같은 시점에 수집한다. 첫 블록은 Docker 호스트에서, 두 번째 블록은 문제가 난 컨테이너 안에서 실행한다. ls -l의 그룹 이름만 보지 말고 ls -ln 또는 stat의 숫자를 비교해야 한다.
stat -c 'mode=%a uid=%u gid=%g path=%n' /var/run/docker.sock
ls -ln /var/run/docker.sock
id
docker inspect --format '{{.Config.User}} {{json .HostConfig.GroupAdd}}' SERVICE_CONTAINER
docker exec SERVICE_CONTAINER sh -c '
id; ls -ln /var/run/docker.sock; test -S /var/run/docker.sock && echo socket-ok
'
일반적인 rootful Docker 소켓은 root 소유이며 Docker 그룹에 접근 권한이 있다. 다만 배포판과 설치 방식에 따라 GID는 달라질 수 있으므로 글이나 다른 서버의 숫자를 복사하면 안 된다. SELinux·AppArmor가 적용된 환경에서는 Unix 권한이 맞아도 보안 정책이 막을 수 있다. 이 경우 감사 로그와 Docker 공식 보안 프로필 문서를 별도로 확인해야 한다.
2단계: Compose group_add로 호스트 GID를 전달한다
호스트가 Linux rootful Docker이고 서비스가 소켓을 직접 사용해야 한다면 다음 구성이 가장 단순하다. Docker Compose 공식 명세의 group_add는 컨테이너 사용자가 속할 추가 그룹을 지정한다. 숫자 GID를 넘기면 이미지 내부 그룹 이름에 의존하지 않는다.
export DOCKER_GID="$(stat -c '%g' /var/run/docker.sock)"
docker compose up -d --force-recreate
services:
manager:
image: example/manager:latest
user: "1000:1000"
group_add:
- "${DOCKER_GID}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
security_opt:
- no-new-privileges:true
- 현재 호스트에서
DOCKER_GID를 계산한다..env에 오래된 숫자를 영구 저장하지 않는다. - 서비스를 재생성한다. 실행 중 컨테이너의 그룹 목록은 Compose 파일만 수정해도 자동으로 바뀌지 않는다.
docker exec ... id로 보조 그룹을 확인한다. 소켓 GID와 같은 숫자가 보여야 한다.- 최소 API 호출을 검증한다. Docker CLI가 있으면
docker version, 없으면 애플리케이션의 health check나 Unix socket/_ping을 쓴다. - 애플리케이션이 정말 소켓 전체 권한을 필요로 하는지 재검토한다. 조회 목적이면 다음 단계의 proxy가 더 적합하다.
이 구성은 permission denied를 해결하지만 권한을 축소하지는 않는다. Docker 공식 docker run 문서는 소켓과 Docker 바이너리를 함께 마운트하면 컨테이너가 호스트 daemon의 컨테이너를 생성·조작할 수 있는 전체 접근권을 얻는다고 명시한다.
3단계: rootless Docker라면 소켓 경로부터 바꾼다
Rootless mode는 daemon과 컨테이너를 비루트 사용자로 실행한다. Docker 공식 설치 출력은 사용자별 소켓과 DOCKER_HOST 설정을 안내한다. 흔한 형태는 unix:///run/user/1000/docker.sock이지만 UID를 하드코딩하지 말고 현재 context와 환경변수로 확인해야 한다.
docker context show
docker context inspect "$(docker context show)"
echo "$DOCKER_HOST"
ls -ln "/run/user/$(id -u)/docker.sock"
services:
manager:
environment:
DOCKER_HOST: unix:///run/docker-user.sock
volumes:
- /run/user/${ROOTLESS_UID}/docker.sock:/run/docker-user.sock
Rootless socket은 사용자 runtime 디렉터리 아래에 있으므로 서비스 부팅 순서와 로그인 세션 유지 설정도 영향을 준다. rootless daemon이 시작되지 않았거나 사용자 runtime 디렉터리가 사라졌다면 권한 수정이 아니라 daemon·systemd user service 상태를 고쳐야 한다. rootful /var/run/docker.sock과 rootless 사용자 소켓을 동시에 두고 잘못된 쪽을 가리키는 구성도 피해야 한다.
4단계: 조회 목적이면 socket proxy로 권한을 줄인다
대시보드·모니터링·자동 업데이트 도구가 Docker API의 일부만 필요한다면 소켓을 각 컨테이너에 직접 배포하지 않는 편이 낫다. Tecnativa/docker-socket-proxy는 Docker 공식 구성요소가 아닌 공개 소스 프로젝트지만 endpoint 종류와 POST 허용 여부를 환경변수로 제한하는 대표적인 선택지다.
services:
docker-proxy:
image: tecnativa/docker-socket-proxy:latest
environment:
CONTAINERS: 1
IMAGES: 0
SERVICES: 0
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks: [docker-api]
restart: unless-stopped
dashboard:
environment:
DOCKER_HOST: tcp://docker-proxy:2375
networks: [docker-api]
networks:
docker-api:
internal: true
- proxy 포트를 호스트의
0.0.0.0:2375에 공개하지 않는다. 필요한 서비스만 참여하는 내부 Docker 네트워크에 둔다. POST=0부터 시작한다. 컨테이너 시작·중지·삭제가 필요하다고 확인된 경우에만 관련 endpoint와 쓰기를 추가한다.- 허용 목록을 버전 관리한다. 애플리케이션 업데이트가 새 endpoint를 요구하면 오류를 보고 검토한 뒤 열어야 한다.
- proxy 자체도 고가치 구성요소로 취급한다. 이미지 버전을 고정하고 로그·네트워크·업데이트 정책을 관리한다.
:ro bind 옵션만으로 Docker API를 읽기 전용으로 만들었다고 판단하면 안 된다. 이 옵션은 마운트된 경로의 파일시스템 쓰기를 제한하는 설정이지 daemon API의 HTTP method를 인가하는 정책이 아니다. 실제 API 작업을 제한하려면 proxy의 method·endpoint 정책이나 별도 권한 계층이 필요하다.
환경별 선택 매트릭스
| 환경·목적 | 권장 시작점 | 피해야 할 선택 | 검증 |
|---|---|---|---|
| Linux rootful, 관리 작업 필요 | 현재 소켓 GID + group_add | GID 하드코딩, chmod 666 | 컨테이너 id와 docker version |
| Linux rootless | 사용자 socket 경로 + 올바른 DOCKER_HOST | rootful 경로를 관성적으로 마운트 | context, user service, socket 존재 |
| 모니터링·대시보드 조회 | 내부망 socket proxy + POST 차단 | 각 서비스에 socket 직접 마운트 | 허용 GET 성공, POST 거부 |
| Docker Desktop | Desktop가 제공하는 Linux socket 연동 확인 | 호스트 OS 경로를 Linux와 동일시 | 컨테이너 유형·Desktop 설정 확인 |
| CI에서 원격 daemon 사용 | SSH 또는 인증된 TLS 연결 검토 | 인증 없는 TCP 2375 | 인증·네트워크 범위·감사 로그 |
Windows Docker Desktop에서 Windows 컨테이너와 Linux 컨테이너는 경로 의미가 다를 수 있다. 최근 공개 이슈에서도 /var/run/docker.sock 하드코딩 때문에 Windows named pipe 대체 경로가 없어 실패한 사례가 확인됐다. 이미지가 특정 플랫폼만 지원하는지 먼저 확인하고, 경로 치환만으로 해결되지 않으면 제품의 공식 지원 범위를 따라야 한다.
보안을 악화시키는 빠른 해결책 세 가지
chmod 666 /var/run/docker.sock: 모든 로컬 사용자가 daemon에 명령을 보낼 수 있게 한다. 재시작 뒤 사라지는 임시 처방이면서 공격 범위를 넓힌다.privileged: true추가: 단순 GID 문제보다 훨씬 큰 권한을 컨테이너에 준다. 소켓 Unix 권한을 해결하려고 사용할 이유가 없다.- TCP 2375를 외부에 공개: 인증·TLS 없는 Docker API는 원격 호스트 제어 경로가 될 수 있다. Docker 공식 보안 문서는 신뢰된 네트워크·VPN, SSH 또는 TLS 보호를 요구한다.
소켓이 필요한 관리 도구는 공급망 위험도 함께 가진다. 이미지가 침해되면 공격자는 마운트된 socket을 사용해 호스트 파일시스템을 마운트한 새 컨테이너를 만들 수 있다. 따라서 이미지 digest 또는 명시적 버전 고정, 자동 업데이트 범위 제한, 내부 네트워크, 최소 endpoint 허용, 로그 보존을 함께 적용해야 한다.
배포 전 체크리스트
| 확인 항목 | 통과 기준 |
|---|---|
| 실제 daemon | rootful·rootless·Desktop 중 어느 daemon인지 명확하다. |
| 소켓 경로 | 호스트에 존재하며 컨테이너에도 의도한 경로로 보인다. |
| 숫자 GID | 호스트 socket GID가 컨테이너 프로세스의 보조 그룹에 있다. |
| 권한 범위 | 직접 socket 전체 접근이 꼭 필요한 이유가 문서화됐다. |
| 네트워크 | proxy 또는 TCP endpoint가 공개 인터넷에 노출되지 않는다. |
| 실패 검증 | 허용 작업은 성공하고 금지한 POST·endpoint는 실제로 거부된다. |
| 재배포 | 호스트 교체·재부팅 뒤 GID와 socket 경로를 다시 계산한다. |
개인 서버의 전체 메모리와 장애 반경도 함께 점검하려면 Docker Compose 메모리 제한과 OOM 가이드를 연결해 보면 된다. 서버 사양 자체가 빠듯하다면 1GB·2GB·4GB VPS 사양 선택법에서 컨테이너별 메모리 예산을 먼저 잡는 편이 낫다.
FAQ
docker 그룹에 사용자를 추가하면 안전한가
일반 사용자보다 편리하지만 일반 권한은 아니다. Docker 공식 문서는 docker 그룹이 root 수준 권한을 부여한다고 경고한다. 신뢰된 운영 사용자로 제한하고, 서비스 컨테이너에는 필요한 경우에만 숫자 GID를 전달해야 한다.
소켓을 :ro로 마운트하면 컨테이너 생성이 막히나
그렇게 가정하면 안 된다. bind mount의 읽기 전용과 Docker API의 읽기 전용 인가는 다른 문제다. 쓰기 요청을 차단하려면 socket proxy에서 POST=0과 endpoint 허용 목록을 적용하고 실제 거부 응답을 테스트해야 한다.
group_add를 넣었는데도 permission denied가 계속되면 무엇을 보나
컨테이너가 재생성됐는지, 실행 프로세스의 id에 해당 숫자 그룹이 있는지, 마운트된 socket GID가 같은지 확인한다. 그다음 rootless 경로 혼동과 SELinux·AppArmor 감사 로그를 확인한다.
Docker-in-Docker가 더 안전한 대안인가
항상 그렇지 않다. 별도 daemon으로 격리할 수 있지만 저장소·캐시·네트워크·privileged 요구와 운영 복잡성이 생긴다. 호스트 daemon 제어가 필요 없는 CI 빌드라면 rootless builder나 전용 빌드 서비스를 포함해 요구사항별로 비교해야 한다.
참고 자료
- Docker Engine Linux post-installation
- Docker Engine security
- Docker Rootless mode
- Docker Compose group_add 명세
- docker run 및 socket 전체 접근 설명
- Tecnativa docker-socket-proxy
- Arcane docker.sock 경로·GID 관련 공개 이슈
- Windows named pipe 대체 경로 관련 공개 이슈
문서와 공개 이슈 확인 기준일은 2026년 8월 27일이다. 배포판, Docker Engine·Desktop 실행 방식, 보안 모듈과 사용하는 관리 도구에 따라 socket 경로와 필요한 API 범위가 달라질 수 있다.