The daemon socket is owned by root, and your user isn't allowed to touch it. Here's the fix, and why the obvious one is a security smell.
You run a plain docker ps on a fresh box and get slapped:
$ docker ps
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.45/containers/json":
dial unix /var/run/docker.sock: connect: permission denied
Nothing is broken. Docker is running fine. Your user account just isn't allowed to talk to it.
The Docker CLI is a thin client. It doesn't do much on its own; it sends API calls over a Unix socket to the dockerd daemon, which does the real work of pulling images and starting containers. That socket lives at /var/run/docker.sock, and the daemon runs as root.
Look at who owns the socket:
$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Jul 6 09:14 /var/run/docker.sock
Read that carefully. The owner is root, the group is docker, and the permission bits are srw-rw----. That means: root can read/write, members of the docker group can read/write, and everyone else gets nothing. You are "everyone else." Hence permission denied.
Confirm you're not in the group:
$ groups
kiril adm sudo users
No docker in that list. That's the whole problem.
Add your user to the docker group:
$ sudo usermod -aG docker $USER
The -aG matters. -a appends; leave it off and usermod will replace your entire group membership with just docker, quietly kicking you out of sudo, adm, and everything else. People have locked themselves out of a box this way.
Group membership is read at login, so the change won't apply to your current shell. Either start a fresh session for the group:
$ newgrp docker
or log out and back in (for SSH, drop the connection and reconnect). Then verify:
$ id
uid=1000(kiril) gid=1000(kiril) groups=1000(kiril),4(adm),27(sudo),100(users),998(docker)
There it is, 998(docker). Now docker ps works without sudo.
It does, and that's the trap. Running sudo docker ps sidesteps the socket permission check because root can read any socket. Plenty of people stop here, alias docker to sudo docker, and move on.
Two reasons that bites you. First, everything the daemon writes now lands as root. Bind-mount your project directory into a container, have it write a file, and you'll be chown-ing things back to yourself for the rest of the day. Second, it papers over the real permission model, so the next person on the box has no idea how access is actually granted.
Sudo working is a signal that your user account isn't wired up, not a solution.
Here's the uncomfortable truth about the "standard fix." Adding yourself to the docker group is, for practical purposes, granting yourself passwordless root on the machine.
The daemon runs as root, and anyone who can send it API calls can ask it to do root things. One line proves it:
$ docker run -v /:/host -it alpine chroot /host sh
That mounts the host's entire filesystem into a container and drops you into a shell with full write access to it. No password prompt. From there you can edit /etc/shadow, add a sudoer, read anyone's SSH keys. The Docker docs say this outright: the docker group grants privileges equivalent to root.
So on your own laptop, fine. On a shared build server or a box where other people have accounts, handing out docker-group membership like it's read access is how you end up with a privilege-escalation finding in the next audit.
Rootless mode runs the entire daemon as your unprivileged user, using user namespaces so that "root" inside a container maps to your normal UID outside it. A container breakout gets your permissions, not the host's.
Setup is a one-liner on a modern distro:
$ dockerd-rootless-setuptool.sh install
That sets up a per-user daemon under systemd. Point the CLI at your own socket:
$ export DOCKER_HOST=unix:///run/user/1000/docker.sock
$ systemctl --user start docker
$ docker run hello-world
Now the socket lives under your own /run/user/$UID, owned by you, and no group edit is needed. The tradeoffs are real: binding ports below 1024 needs an extra capability, some storage drivers behave differently, and a few network setups need slirp4netns tuning. For most build and dev workloads none of that matters, and you've removed a root-equivalent group from the machine. That's a trade worth making.
If rootless doesn't fit, the next-best move is scoped access, not blanket group membership: hand specific service accounts the socket through a controlled path, or front the daemon with something that enforces policy, rather than making every developer effectively root.
Docker Desktop (Mac and Windows) doesn't use a raw root socket the way native Linux does; it runs the engine inside its own VM and already isolates it from your host user. If you hit permission denied there, it's usually the Desktop app not being started, or a stale DOCKER_HOST pointing at a socket that no longer exists. Unset it and let Desktop manage the context:
$ unset DOCKER_HOST
$ docker context use desktop-linux
CI runners. In a pipeline there's no interactive login, so newgrp and "log back in" don't apply. The runner's service user has to be in the docker group at the time the agent process starts, or you mount the socket into the job explicitly. If you added the group but the running agent predates the change, restart the agent; it's still holding its old group set.
systemd services. Same shape. A unit that shells out to docker runs as whatever User= you set, and that user needs group access resolved when the service starts. After changing the group, sudo systemctl restart your-service so it picks up the new membership. Changing the group won't retroactively affect a process that's already running.
For a wider map of daemon and connection failures, the Docker troubleshooting guide covers the neighboring errors this one tends to travel with.
On your personal machine, add yourself to the docker group and get on with your day; the blast radius is you. On anything shared, build infrastructure, or anywhere an auditor will eventually look, don't. Stand up rootless Docker so a compromised container can't become a compromised host, or hand out scoped socket access to the specific accounts that need it. The docker group is a convenience that quietly means root, and treating it as anything less is a bet you don't want to be holding when it goes wrong.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Our best engineer quit citing on-call. We rebuilt the whole thing: saner rotations, runbooks that actually help at 3am, and escalation that doesn't punish asking for help.
We ripped every client secret out of our CI pipelines by pointing Azure federated credentials at GitHub's OIDC issuer. Here's the exact setup and the claims that trip people up.
Explore more articles in this category
The IaC landscape fractured after the Terraform license change. This is the map to what each tool is actually best at, and how to choose without regret.
Terragrunt keeps large Terraform setups DRY and orchestrated, but small teams often pay its learning curve for little return.
A practical comparison of Kubernetes-native continuous reconciliation against the classic CLI-driven, state-file IaC model for platform teams.
Evergreen posts worth revisiting.