도커 permission denied /var/run/docker.sock 오류 해결: docker 그룹 추가와 재로그인, chmod 666을 쓰면 안 되는 이유

이 글의 핵심

docker ps를 sudo 없이 실행하면 /var/run/docker.sock에 permission denied가 나는 것은 고장이 아니라 소켓 권한이 root와 docker 그룹에만 열려 있기 때문입니다. 이 글은 오류 문구로 원인을 가르는 법, usermod로 그룹을 추가해도 바로 적용되지 않는 이유와 확인법, chmod 666이 위험한 이유, docker 그룹이 사실상 root 권한이라는 점과 rootless Docker라는 대안, snap·Docker Desktop·Jenkins·GitHub Actions 러너·컨테이너 안 소켓 마운트에서 생기는 변형까지 다룹니다.

증상: sudo를 붙이면 되고 빼면 안 되는 docker 명령

리눅스에 Docker Engine을 설치하고 나서 docker ps나 docker run hello-world를 sudo 없이 실행하면 다음과 같은 오류를 만나는 경우가 많습니다.

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.47/containers/json": dial unix /var/run/docker.sock: connect: permission denied

예전 버전의 Docker CLI는 앞에 Got이 붙은 형태로 출력합니다.

Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.40/containers/json: dial unix /var/run/docker.sock: connect: permission denied

URL 중간의 v1.40, v1.47 같은 숫자는 CLI가 사용하는 API 버전이고, 뒤의 containers/json은 실행한 명령에 따라 images/json, info, containers/create 등으로 바뀝니다. 최신 CLI에서는 문구가 조금 달라질 수 있지만, 핵심은 끝부분의 dial unix /var/run/docker.sock: connect: permission denied입니다. 이 부분이 있으면 이 글의 문제가 맞습니다.

같은 명령에 sudo를 붙이면 정상 동작합니다. Docker가 고장 난 것이 아니라, 일반 사용자 계정에 데몬과 대화할 권한이 아직 없는 상태라는 뜻입니다.

기준 환경: 우분투 24.04 LTS, Docker 공식 apt 저장소에서 설치한 Docker Engine(docker-ce) 루트 모드. 우분투 저장소의 docker.io 패키지, 데비안, 페도라 계열도 소켓과 그룹 구조는 같아서 대부분의 명령이 그대로 통합니다. snap 패키지와 Docker Desktop for Linux는 동작이 달라서 뒤에서 따로 다룹니다.

비슷해 보이지만 다른 오류: 데몬이 꺼져 있는 경우

검색하다 보면 아래 오류와 섞여서 해결법이 엇갈리는 일이 많습니다.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

이 오류는 권한 문제가 아니라 소켓 파일이 없거나, 소켓 뒤에서 응답할 데몬이 없는 상태입니다. 권한이 없는 경우에는 permission denied, 상대가 없는 경우에는 Cannot connect ... Is the docker daemon running?이 나온다고 구분하면 됩니다. 이 경우 docker 그룹에 사용자를 아무리 추가해도 해결되지 않습니다. 데몬 쪽 진단은 글 뒷부분의 “데몬이 실행 중이 아닐 때” 절에서 설명합니다.

두 가지를 한 번에 가르는 가장 빠른 방법은 sudo를 붙여 보는 것입니다.

docker pssudo docker ps원인
permission denied정상현재 셸에 docker 그룹 권한이 없음 (이 글의 본론)
Cannot connect …Cannot connect …데몬이 꺼져 있거나 설치가 불완전함
Cannot connect …정상CLI가 다른 소켓을 보고 있음 (DOCKER_HOST, 컨텍스트 확인)

세 번째 경우는 흔하지 않지만, Docker Desktop이나 rootless Docker를 한 번 설치했다가 지운 시스템에서 종종 나옵니다. 일반 사용자 환경에는 DOCKER_HOST 변수나 컨텍스트 설정이 남아 있고, sudo로 실행하면 root 환경에는 그런 설정이 없어 기본 소켓으로 가기 때문입니다.

docker.sock은 무엇이고 왜 아무나 쓸 수 없는가

docker 명령은 컨테이너를 직접 만들지 않습니다. 실제 작업은 root 권한으로 실행되는 데몬 dockerd가 하고, docker CLI는 데몬에게 HTTP 요청을 보내는 클라이언트일 뿐입니다. 둘이 같은 컴퓨터에 있을 때 이 HTTP 요청이 오가는 통로가 유닉스 도메인 소켓 /var/run/docker.sock입니다. 오류 메시지의 http://%2Fvar%2Frun%2Fdocker.sock/...이라는 이상한 URL은 소켓 경로를 호스트 이름 자리에 인코딩해 넣은 것입니다.

유닉스 소켓도 파일이므로 일반 파일처럼 소유자, 그룹, 권한 비트가 있습니다. 확인해 보면 다음과 같습니다. /var/run은 /run을 가리키는 심볼릭 링크라서 둘 중 어느 경로로 봐도 같습니다.

ls -l /var/run/docker.sock
# srw-rw---- 1 root docker 0 10월 13 09:12 /var/run/docker.sock

맨 앞의 s는 소켓이라는 뜻이고, rw-rw----는 소유자(root)와 그룹(docker)만 읽고 쓸 수 있다는 뜻입니다. 소켓에 연결하려면 쓰기 권한이 필요하므로, root도 아니고 docker 그룹도 아닌 사용자가 연결을 시도하면 커널이 connect() 단계에서 거부합니다. 오류 끝의 connect: permission denied가 바로 이 거부입니다.

이 소켓은 데몬이 직접 만드는 것이 아니라 systemd의 docker.socket 유닛이 만듭니다. 유닛 파일을 보면 권한이 어떻게 정해지는지 알 수 있습니다.

systemctl cat docker.socket
# [Socket]
# ListenStream=/run/docker.sock
# SocketMode=0660
# SocketUser=root
# SocketGroup=docker

즉 소켓의 권한(0660)과 그룹(docker)은 소켓이 만들어질 때마다 이 설정으로 다시 정해집니다. 이 점이 뒤에서 chmod 666이 왜 임시방편인지 설명하는 근거가 됩니다.

권한을 이렇게 좁게 잡은 이유는 데몬이 root로 돌기 때문입니다. 소켓에 요청을 보낼 수 있다는 것은 root 프로세스에게 “이 디렉터리를 마운트해서 컨테이너를 띄워라”, “이 컨테이너에 모든 권한을 줘라”라고 시킬 수 있다는 뜻입니다. 소켓에 접근할 권한은 사실상 root 권한과 같다는 점은 뒤에서 다시 다룹니다.

해결: docker 그룹에 사용자 추가

Docker 공식 문서의 “Linux post-installation steps”에 나오는 방법이 표준 해결책입니다.

1단계: docker 그룹이 있는지 확인

getent group docker
# docker:x:988:

docker-ce나 docker.io 패키지를 설치하면 보통 그룹이 자동으로 만들어집니다. 아무것도 출력되지 않으면 직접 만들고, 소켓이 새 그룹으로 다시 만들어지도록 소켓 유닛을 재시작합니다.

sudo groupadd docker
sudo systemctl restart docker.socket docker.service

그룹 ID(위 예의 988)는 시스템마다 다릅니다. 이 숫자는 나중에 컨테이너 안에 소켓을 마운트할 때 중요해지므로 기억해 두면 좋습니다.

2단계: 사용자를 그룹에 추가

sudo usermod -aG docker $USER

-a 옵션을 빼먹으면 안 됩니다. usermod -G docker는 “보조 그룹 목록을 docker 하나로 바꿔라”라는 뜻이라, 기존에 속해 있던 sudo, adm 같은 그룹에서 모두 빠집니다. 우분투에서 sudo 그룹에서 빠지면 다음부터 sudo를 쓸 수 없게 되고, 다른 관리자 계정이 없다면 복구 모드로 부팅해서 고쳐야 합니다. -a(append)는 기존 목록에 추가하라는 뜻입니다. 같은 일을 하는 sudo gpasswd -a $USER docker를 쓰면 이런 실수를 할 여지가 없습니다.

3단계: 다시 로그인하고 확인

여기서 대부분 막힙니다. 명령을 실행한 직후 같은 터미널에서 docker ps를 다시 해 보면 여전히 permission denied가 납니다. 설정이 틀린 것이 아닙니다. 다음 절에서 이유를 설명합니다.

일단 로그아웃했다가 다시 로그인한 뒤 확인합니다.

id
# uid=1000(dev) gid=1000(dev) groups=1000(dev),4(adm),27(sudo),988(docker)

docker run --rm hello-world

id 출력의 groups=에 docker가 보이고 hello-world가 메시지를 출력하면 끝입니다.

그룹을 추가했는데 왜 바로 안 되는가

리눅스에서 프로세스가 어느 그룹에 속하는지는 로그인하는 순간 정해집니다. 로그인 과정에서 /etc/group을 읽어 보조 그룹 목록을 만들고, 그 목록을 셸 프로세스에 붙입니다. 이후 그 셸에서 실행하는 모든 프로그램은 부모의 그룹 목록을 그대로 물려받습니다. usermod는 /etc/group 파일만 고칠 뿐, 이미 실행 중인 프로세스의 그룹 목록은 건드리지 않습니다.

그래서 두 가지 확인 명령이 서로 다른 답을 내놓는 상황이 생깁니다.

groups $USER     # /etc/group 기준: ... sudo docker   ← 이미 반영됨
id -nG $USER     # 사용자 이름을 지정하면 역시 /etc/group 기준
id -nG           # 인자 없이: 현재 셸 프로세스 기준 ... sudo    ← docker 없음
groups           # 인자 없이: 현재 셸 프로세스 기준

사용자 이름을 인자로 주면 데이터베이스를 조회하고, 인자 없이 실행하면 현재 프로세스의 실제 자격 증명을 보여 줍니다. 소켓 접근을 판단할 때 커널이 보는 것은 후자입니다. “groups에는 docker가 있는데 왜 안 되지?”라는 혼란은 대부분 groups $USER로 확인했기 때문에 생깁니다. 판단은 항상 인자 없는 id로 하세요.

재로그인 없이 쓰는 방법: newgrp와 sg

지금 열려 있는 작업을 닫기 싫다면 현재 터미널에서만 임시로 그룹을 적용할 수 있습니다.

newgrp docker
docker ps        # 이 셸 안에서는 동작
exit             # 원래 셸로 돌아감

newgrp는 docker 그룹을 기본 그룹으로 가진 새 셸을 하나 더 띄우는 명령입니다. 그 셸과 그 안에서 실행한 프로그램에만 적용되고, 다른 터미널 탭이나 이미 실행 중인 IDE에는 영향이 없습니다. 또 새 셸의 기본 그룹이 docker가 되므로, 그 안에서 만든 파일의 그룹 소유자가 docker로 잡힌다는 부작용이 있습니다. 명령 하나만 실행할 때는 sg docker -c "docker ps"가 더 깔끔합니다.

다시 로그인해도 안 되는 경우

로그아웃했다 들어왔는데도 id에 docker가 없다면, 새 로그인을 거치지 않고 살아남은 프로세스에서 셸을 열고 있을 가능성이 높습니다.

  • tmux, screen: 터미널을 닫아도 서버 프로세스가 남아 있고, 그 안의 셸은 예전 그룹 목록을 그대로 씁니다. tmux kill-server로 서버를 완전히 내린 뒤 다시 시작해야 합니다.
  • SSH 연결 재사용: ~/.ssh/config에 ControlMaster/ControlPersist를 설정해 두었다면 새 ssh 명령이 기존 연결을 재사용하므로 실제로는 재로그인이 일어나지 않습니다. ssh -O exit <호스트>로 마스터 연결을 닫고 다시 접속합니다.
  • VS Code Remote-SSH: 원격 서버에 VS Code Server 프로세스가 계속 떠 있어서, 창을 다시 열어도 같은 그룹 목록을 씁니다. 명령 팔레트의 “Remote-SSH: Kill VS Code Server on Host”를 실행한 뒤 다시 연결합니다.
  • 데스크톱 세션: GNOME에서는 로그아웃 후 로그인으로 보통 충분하지만, 사용자 서비스나 터미널 서버 프로세스가 남아 바로 반영되지 않는 경우도 있습니다. 이럴 때 가장 확실한 방법은 재부팅입니다.

chmod 666 docker.sock이 나쁜 해결책인 이유

검색 결과에는 다음 한 줄짜리 해결책이 자주 나옵니다.

sudo chmod 666 /var/run/docker.sock   # 권장하지 않음

이 명령을 실행하면 당장은 오류가 사라집니다. 하지만 두 가지 이유로 쓰지 말아야 합니다.

첫째, 오래가지 않습니다. 앞에서 본 것처럼 소켓의 권한은 docker.socket 유닛의 SocketMode=0660으로 정해집니다. 재부팅하거나 Docker를 업데이트하거나 systemctl restart docker.socket으로 소켓이 다시 만들어지면 권한은 0660으로 돌아갑니다. 그래서 “어제는 됐는데 오늘 또 안 된다”는 증상이 반복되고, 결국 crontab이나 rc 스크립트에 chmod를 넣는 식으로 문제를 덮게 됩니다.

둘째, 이쪽이 더 심각한데, 이 시스템의 모든 로컬 사용자에게 root 권한을 주는 것과 같습니다. 666은 “누구나 읽고 쓰기 가능”이라는 뜻입니다. 웹 서버를 돌리는 www-data, 데이터베이스 계정, 다른 사람의 SSH 계정 모두 소켓에 접근할 수 있게 됩니다. 웹 애플리케이션 하나에 원격 코드 실행 취약점이 생기면, 공격자는 www-data 권한만 얻었는데도 Docker 소켓을 통해 바로 root가 됩니다. 원래 침해 범위를 웹 서버 계정 하나로 막아 주던 권한 분리가 통째로 사라지는 셈입니다.

혼자 쓰는 노트북이라 상관없다고 생각할 수 있지만, 그 경우에도 docker 그룹에 자기 계정을 추가하는 것으로 같은 효과를 얻을 수 있으므로 굳이 666을 쓸 이유가 없습니다.

docker 그룹은 root와 같다

Docker 공식 문서도 docker 그룹이 root 수준의 권한을 준다고 명시하고 있습니다. 이 말이 과장이 아니라는 것은 명령 한 줄로 확인할 수 있습니다.

docker run --rm -it -v /:/host alpine chroot /host /bin/sh
# 이 셸은 호스트의 루트 파일 시스템 위에서 root로 동작합니다

컨테이너 안의 root는 기본적으로 호스트의 root(UID 0)와 같은 사용자입니다. 호스트의 /를 통째로 마운트하면 /etc/shadow를 읽거나 /etc/sudoers를 고치거나 /root/.ssh/authorized_keys에 키를 넣을 수 있습니다. sudo 비밀번호를 한 번도 묻지 않습니다. --privileged, --pid=host, --network=host를 조합하면 할 수 있는 일은 더 많아집니다.

따라서 docker 그룹에 사용자를 추가하는 것은 “그 사용자에게 비밀번호 없는 sudo를 주는 것”과 같은 결정으로 다뤄야 합니다. 정리하면 이렇습니다.

  • 혼자 쓰는 개발 PC: docker 그룹 추가가 일반적이고 실용적입니다.
  • 여러 사람이 쓰는 서버: docker 그룹에 넣는 사람은 곧 root 권한자 목록입니다. 그렇게 할 수 없다면 다음 절의 rootless Docker를 검토합니다.
  • 서비스 계정(웹 서버, DB 등): docker 그룹에 넣지 않습니다.

대안: rootless Docker와 그 한계

rootless 모드는 데몬 자체를 일반 사용자 권한으로 실행합니다. 사용자 네임스페이스를 이용해 컨테이너 안의 root를 호스트의 일반 사용자(그리고 그 사용자에게 할당된 하위 UID 범위)에 대응시키므로, 앞의 -v /:/host 같은 탈출을 해도 호스트에서는 일반 사용자 권한밖에 없습니다. 소켓도 사용자 자신의 런타임 디렉터리에 만들어지므로 그룹 문제 자체가 생기지 않습니다.

우분투 24.04에서 Docker 공식 저장소를 쓰고 있다면 설치는 다음과 같습니다.

sudo apt install uidmap docker-ce-rootless-extras
# 루트 모드 데몬이 이미 돌고 있다면 먼저 끄는 것을 권장
sudo systemctl disable --now docker.service docker.socket

dockerd-rootless-setuptool.sh install

설치 스크립트는 ~/.config/systemd/user/docker.service를 만들고, rootless라는 이름의 Docker 컨텍스트를 등록합니다. 소켓 경로는 $XDG_RUNTIME_DIR/docker.sock, 보통 /run/user/1000/docker.sock입니다. 스크립트가 안내하는 대로 DOCKER_HOST=unix:///run/user/1000/docker.sock을 셸 설정에 넣거나 docker context use rootless를 실행하면 됩니다. 로그아웃해도 컨테이너가 계속 돌게 하려면 sudo loginctl enable-linger $USER가 필요합니다.

우분투 24.04는 AppArmor로 비특권 사용자 네임스페이스 생성을 제한하므로, 설치 스크립트가 rootlesskit용 AppArmor 프로필을 만들라는 안내를 출력하는 경우가 있습니다. 출력된 안내를 그대로 따라야 하며, 이 부분은 버전에 따라 바뀔 수 있으니 공식 문서의 rootless 페이지에서 최신 절차를 확인하세요.

rootless 모드에는 다음과 같은 제약이 있습니다.

  • 1024 미만 포트를 기본으로 열 수 없습니다. -p 80:80이 실패하므로 net.ipv4.ip_unprivileged_port_start sysctl을 낮추거나 8080 같은 높은 포트를 써야 합니다.
  • 네트워크 성능과 동작이 다릅니다. 포트 포워딩이 사용자 공간 프로세스를 거치므로 처리량이 루트 모드보다 낮을 수 있고, 기본 설정에서는 컨테이너가 보는 요청의 출발지 IP가 실제 클라이언트 IP가 아닐 수 있습니다. 브리지 네트워크 구조에 대한 배경은 Docker 네트워킹 가이드를 참고하세요.
  • 리소스 제한(--memory, --cpus)은 cgroup v2와 systemd 위임 설정이 갖춰져야 동작합니다.
  • AppArmor, 일부 스토리지 드라이버, --privileged로 할 수 있는 일이 제한됩니다. 장치 접근이 필요한 컨테이너(GPU, USB 장치 등)는 추가 설정이 필요하거나 동작하지 않을 수 있습니다.
  • 이미지 저장소가 분리됩니다. 루트 모드에서 받아 둔 이미지와 볼륨은 rootless 데몬에서 보이지 않습니다.

개발 PC 한 대에서 혼자 쓴다면 rootless의 이득보다 이런 제약이 더 거슬릴 수 있습니다. 반대로 여러 사람이 각자 Docker를 써야 하는 공유 서버라면 rootless가 docker 그룹보다 훨씬 안전한 선택입니다.

설치 방식에 따라 달라지는 경우

우분투에서 snap으로 설치한 Docker

우분투 설치 중이나 sudo snap install docker로 Docker를 넣었다면 snap 패키지일 수 있습니다. 먼저 어떤 방식으로 설치되어 있는지 확인합니다.

snap list docker 2>/dev/null
dpkg -l | grep -E 'docker-ce|docker.io'
which -a docker

snap 버전은 그룹을 만들어 주지 않는 경우가 있어서, 그룹을 만들고 사용자를 추가한 뒤 snap 서비스를 재시작해야 소켓이 docker 그룹으로 다시 만들어집니다. snap의 데몬은 시작할 때 docker 그룹이 있는지 확인하기 때문입니다.

sudo groupadd --system docker      # 이미 있으면 건너뜀
sudo usermod -aG docker $USER
sudo snap disable docker
sudo snap enable docker
# 이후 재로그인

snap 버전은 엄격한 격리(confinement) 때문에 홈 디렉터리 밖의 경로를 바인드 마운트하지 못하는 등 apt 버전과 다르게 동작하는 부분이 있습니다. 더 큰 문제는 snap 버전과 apt 버전이 둘 다 설치된 경우입니다. 두 데몬이 같은 /var/run/docker.sock을 차지하려고 다투면서 어떤 때는 되고 어떤 때는 안 되는 증상이 생깁니다. 위 확인 명령에서 둘 다 나온다면 한쪽을 지우는 것이 먼저입니다. 개발용이라면 보통 sudo snap remove docker로 snap 쪽을 지우고 공식 apt 저장소 버전을 쓰는 편이 문서와 맞춰 쓰기 쉽습니다. apt 설치 절차는 우분투 설치 후 개발자가 처음 할 일에 정리해 두었습니다.

Docker Desktop for Linux

Docker Desktop for Linux는 Docker Engine을 호스트에서 직접 돌리지 않고 작은 가상 머신 안에서 실행합니다. CLI는 desktop-linux라는 컨텍스트를 통해 사용자 홈 아래의 소켓으로 접속합니다.

docker context ls
# NAME              DESCRIPTION                               DOCKER ENDPOINT
# default           Current DOCKER_HOST based configuration   unix:///var/run/docker.sock
# desktop-linux *   Docker Desktop                            unix:///home/dev/.docker/desktop/docker.sock

이 상태에서는 /var/run/docker.sock의 권한과 상관없이 Docker Desktop의 소켓을 씁니다. 그래서 Docker Desktop을 쓰는 중에 오류가 난다면 오류 메시지 속 소켓 경로를 먼저 봐야 합니다. ~/.docker/desktop/docker.sock에 대한 Cannot connect 오류라면 Docker Desktop 앱이 꺼져 있는 것이고, docker 그룹과는 관계가 없습니다.

반대로 Docker Engine과 Docker Desktop을 함께 설치했는데 Engine 쪽을 쓰고 싶다면 docker context use default로 바꾸면 됩니다. 이때부터는 /var/run/docker.sock을 쓰므로 docker 그룹 설정이 다시 필요해집니다. 환경 변수 DOCKER_HOST가 설정되어 있으면 컨텍스트 선택보다 우선하므로, 컨텍스트를 바꿨는데도 엉뚱한 소켓으로 가면 echo $DOCKER_HOST를 확인하세요.

CI 러너와 서비스 계정에서 나는 경우

Jenkins

Jenkins 파이프라인에서 sh 'docker build ...'가 permission denied로 실패하는 경우, Jenkins가 jenkins 사용자로 실행되고 있고 이 사용자가 docker 그룹에 없기 때문입니다.

sudo usermod -aG docker jenkins
sudo systemctl restart jenkins

여기서 재시작이 앞의 재로그인과 같은 역할을 합니다. 서비스 프로세스의 그룹 목록도 프로세스가 시작될 때 정해지므로, 그룹만 추가하고 Jenkins를 재시작하지 않으면 계속 실패합니다. Jenkins 웹 화면의 “안전한 재시작”은 Java 프로세스 내부에서만 다시 시작하는 경우가 있으므로 systemd로 서비스를 재시작하는 편이 확실합니다.

GitHub Actions 셀프 호스티드 러너, GitLab Runner

GitHub Actions 셀프 호스티드 러너는 ./svc.sh install을 실행한 사용자(또는 인자로 지정한 사용자)로 동작합니다. 그 사용자를 docker 그룹에 넣은 뒤 러너 서비스를 재시작합니다.

sudo usermod -aG docker <러너_실행_사용자>
cd ~/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh start

GitLab Runner의 shell executor라면 gitlab-runner 사용자를 docker 그룹에 넣고 sudo systemctl restart gitlab-runner를 실행합니다.

CI에서는 보안 의미를 한 번 더 생각해야 합니다. 러너 계정이 docker 그룹에 있으면, 그 저장소에 워크플로 파일을 푸시할 수 있는 모든 사람이 러너 머신의 root 권한을 얻습니다. 공개 저장소에 셀프 호스티드 러너를 붙이면 외부인의 풀 리퀘스트로 이 권한이 넘어갈 수 있다는 점은 GitHub 문서에서도 경고하는 내용입니다. 러너 머신은 다른 용도와 분리된 전용 머신이나 VM으로 두는 것이 좋습니다.

컨테이너 안에서 docker.sock을 쓸 때: GID 불일치

CI 에이전트 컨테이너, Portainer, Traefik처럼 컨테이너 안에서 호스트의 Docker를 조작해야 하는 경우 소켓을 바인드 마운트합니다.

docker run -v /var/run/docker.sock:/var/run/docker.sock my-ci-agent

이른바 “Docker outside of Docker” 방식입니다. 컨테이너가 root로 돌면 문제가 없지만, jenkins/jenkins처럼 이미지 안에서 일반 사용자로 실행되는 이미지라면 컨테이너 안에서 다시 permission denied를 만납니다.

원인은 그룹 이름이 아니라 그룹 ID 숫자로 권한을 판단하기 때문입니다. 바인드 마운트된 소켓은 호스트의 파일 그대로이므로 그룹 ID도 호스트의 docker 그룹 ID(예: 988)입니다. 그런데 컨테이너 안에는 자기만의 /etc/group이 있어서, 988이 아무 그룹도 아니거나 전혀 다른 그룹 이름으로 보이고, 컨테이너의 사용자는 그 GID에 속해 있지 않습니다.

# 컨테이너 안에서 확인
ls -ln /var/run/docker.sock
# srw-rw---- 1 0 988 0 ... /var/run/docker.sock
id
# uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins)

가장 깔끔한 해결은 컨테이너를 실행할 때 호스트 소켓의 GID를 보조 그룹으로 추가하는 것입니다.

docker run \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --group-add "$(stat -c '%g' /var/run/docker.sock)" \
  my-ci-agent

Compose에서는 group_add를 씁니다. Compose 파일 작성 전반은 Docker Compose 가이드를 참고하세요.

services:
  agent:
    image: my-ci-agent
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    group_add:
      - "988"   # 호스트에서 stat -c '%g' /var/run/docker.sock 결과

Dockerfile 안에서 groupadd -g 988 docker로 그룹을 만들어 두는 방법도 쓰이지만, 호스트마다 GID가 다르면 다른 서버에서 다시 깨집니다. 이미지에 숫자를 박아 넣기보다 실행 시점에 --group-add로 넘기는 편이 이식성이 좋습니다.

컨테이너 안에서 chmod 666 /var/run/docker.sock을 실행하는 해결책도 돌아다니는데, 바인드 마운트는 같은 파일을 가리키므로 호스트의 소켓 권한이 바뀝니다. 컨테이너 안에서 한 일이라 안전해 보이지만 효과는 호스트에서 chmod 666을 한 것과 같습니다.

소켓을 마운트한 컨테이너는 호스트의 root 권한을 가진 것과 같다는 점도 기억해야 합니다. 앞에서 본 -v /:/host 방법을 컨테이너 안에서도 그대로 쓸 수 있기 때문입니다. 그 컨테이너가 신뢰할 수 있는 이미지인지 확인하는 것이 중요합니다.

진짜 “Docker in Docker”, 즉 docker:dind 이미지로 컨테이너 안에 별도 데몬을 띄우는 방식은 호스트 소켓을 공유하지 않으므로 이 GID 문제는 없습니다. 대신 --privileged가 필요하고, 보통 TCP(2376 포트, TLS)로 접속합니다. 격리 수준이 더 높아지는 것은 아니므로 상황에 맞게 고르면 됩니다.

VS Code Dev Containers

리눅스에서 VS Code Dev Containers 확장이 “Docker returned an error”나 소켓 권한 오류를 내는 경우, 원인은 대부분 앞의 재로그인 문제입니다. VS Code를 그룹 추가 전에 실행해 두었다면 VS Code와 그 확장이 예전 그룹 목록을 그대로 씁니다. VS Code를 모두 닫는 것만으로 부족하면 로그아웃이나 재부팅을 합니다. 개발 컨테이너 안에서 docker 명령을 써야 한다면 공식 “docker-outside-of-docker” 기능을 쓰는 것이 좋습니다. 이 기능이 바로 위의 GID 맞추기를 자동으로 처리합니다.

데몬이 실행 중이 아닐 때

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?가 sudo로도 똑같이 나온다면 권한이 아니라 데몬 쪽을 봐야 합니다.

systemctl status docker.service docker.socket

우분투의 Docker는 소켓 활성화(socket activation) 구조를 씁니다. docker.socket이 소켓 파일을 먼저 열어 두고, 첫 연결이 들어오면 systemd가 docker.service(실제 데몬)를 시작합니다. 그래서 docker.service가 inactive로 보여도 docker.socket이 active라면 정상일 수 있습니다. sudo systemctl stop docker만 실행하면 다음과 같은 경고가 나오고, 다음 docker 명령이 데몬을 다시 깨웁니다.

Stopping 'docker.service', but its triggering units are still active:
docker.socket

데몬을 완전히 멈추려면 두 유닛을 함께 멈춰야 하고, 부팅 시 자동 시작하게 하려면 함께 켭니다.

sudo systemctl enable --now docker.service docker.socket

시작 자체가 실패한다면 로그에 이유가 나옵니다.

journalctl -u docker.service -e --no-pager

자주 보이는 원인은 /etc/docker/daemon.json의 JSON 문법 오류(마지막 항목 뒤의 쉼표 같은 것), 디스크 공간 부족, 이전 데몬 프로세스나 snap 버전 데몬이 소켓을 이미 차지하고 있는 경우입니다. daemon.json을 고친 직후 실패했다면 sudo dockerd --validate --config-file /etc/docker/daemon.json으로 설정 파일만 검사할 수 있습니다(최신 버전에서 지원).

WSL2에서는 systemd가 꺼져 있으면 systemctl 자체가 동작하지 않습니다. /etc/wsl.conf에 [boot] 아래 systemd=true를 넣고 wsl --shutdown으로 재시작하거나, systemd 없이 sudo service docker start로 띄웁니다. WSL2 설정 방법은 WSL2 설치 가이드에 있습니다.

같이 생기는 문제: ~/.docker/config.json permission denied

docker 그룹 설정 전에 sudo docker login 같은 명령을 실행했다면, 그룹 문제를 해결한 뒤 다음 경고를 보게 될 수 있습니다.

WARNING: Error loading config file: /home/dev/.docker/config.json: open /home/dev/.docker/config.json: permission denied

sudo로 실행한 CLI가 ~/.docker 디렉터리를 root 소유로 만들어 버렸기 때문입니다. 소유권을 돌려 놓으면 됩니다.

sudo chown "$USER":"$USER" "$HOME/.docker" -R
sudo chmod g+rwx "$HOME/.docker" -R

또는 디렉터리를 지우고(rm -rf ~/.docker, 로그인 정보도 사라지므로 다시 로그인해야 함) 새로 만들게 해도 됩니다. 기본 명령을 다시 익히려면 Docker 필수 명령어 가이드를 참고하세요.

되돌리기: 그룹에서 빼거나 rootless를 지우기

나중에 docker 그룹 권한을 회수하려면 사용자를 그룹에서 제거하고, 마찬가지로 다시 로그인해야 적용됩니다. 이미 열려 있는 셸은 계속 권한을 가진다는 점에 유의하세요.

sudo gpasswd -d $USER docker

chmod 666을 이미 적용했다면 소켓을 다시 만들어 원래 권한으로 되돌립니다. crontab이나 시작 스크립트에 chmod를 넣어 두었다면 그것도 지워야 합니다.

sudo systemctl restart docker.socket docker.service
ls -l /var/run/docker.sock   # srw-rw---- root docker 로 돌아왔는지 확인

rootless Docker를 시험해 보고 루트 모드로 돌아가려면 설치 스크립트로 사용자 서비스를 제거하고 컨텍스트를 기본값으로 돌립니다. 셸 설정 파일에 넣은 DOCKER_HOST 줄도 함께 지워야 앞에서 본 “sudo로는 되는데 일반 사용자로는 Cannot connect” 상황을 피할 수 있습니다.

dockerd-rootless-setuptool.sh uninstall
docker context use default
sudo systemctl enable --now docker.service docker.socket

uninstall은 데몬과 서비스만 제거하고 ~/.local/share/docker의 이미지와 볼륨 데이터는 남겨 둡니다. 디스크 공간을 돌려받으려면 스크립트가 안내하는 rootlesskit rm -rf ~/.local/share/docker 명령으로 직접 지워야 합니다. 일반 rm으로 지우면 하위 UID로 만들어진 파일 때문에 권한 오류가 나기 때문입니다.