Stackrig

Home Blog

tested run on a fresh Ubuntu 24.04, 6 October 2026 published

How to install Docker on Ubuntu 24.04

We earn commissions when you shop through the links below.

We installed Docker Engine on a fresh Ubuntu 24.04 and kept each output: a first container, “permission denied” on the socket, the uninstall, and what stayed behind.

Short answer: add Docker's own package repository (section 1), then sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin. On 6 October 2026 that gave us Docker 29.8.2, running, and sudo docker run hello-world started a first container.

As your own user, docker ps then fails with permission denied while trying to connect to the docker API at unix:///var/run/docker.sock. The socket belongs to root and to the group docker, and your user is neither. sudo usermod -aG docker "$USER" puts you in the group, but that doesn't count for the session you're in; a new login has it. Further down are the reason, what the group grants, two more errors, how to remove Docker again, and the list of what was still on the machine afterwards.

What this was run on, and what it does not cover

We ran every command on this page on a fresh virtual machine at a cloud provider, started from Ubuntu's official 24.04 server image, with nothing installed or changed on it beforehand. A script ran the commands and kept everything the machine printed. A block marked Output shows what the command above it printed, or the lines of it that matter, and a line is always shown whole. This is what the machine said about itself.

date -u +%Y-%m-%d
lsb_release -a
dpkg --print-architecture
nproc
free -m
2026-10-06
Distributor ID:	Ubuntu
Description:	Ubuntu 24.04.5 LTS
Release:	24.04
Codename:	noble
amd64
2
               total        used        free      shared  buff/cache   available
Mem:            3908         455        3267           0         404        3452

That is one machine, one architecture and one day. Docker's repository had version 29.8.2 that day and may have a newer one when you install, so the version numbers below may not be yours. The image's user is called ubuntu and may use sudo without a password, so where a line below says ubuntu, yours says your own user's name. We left out apt's lines with the address of Ubuntu's archive, which depends on where a machine is.

A script has no terminal, and apt wants one when it asks whether to continue. In those places the script ran the command under a pseudo-terminal and typed the answer, and the Output block shows the answer as the terminal echoed it. The script also ran every command in a shell of its own, but all of them in one login session. Section 5 is about exactly that difference.

This page doesn't cover Docker Desktop, reaching the daemon from another machine, registries and logins, Compose files, or hardening for production. It doesn't run rootless mode, which is Docker's own answer to what the docker group grants (section 5). Ubuntu's archive has a Docker package of its own, docker.io. Docker's install page lists it among the "unofficial packages" that "may conflict" with Docker's own, and we did not run it. The run pulled one image, hello-world, from Docker Hub without logging in. Whether that works from your network, and within Docker Hub's limits for such pulls, one run can't tell you.

Some things about the fresh machine matter later, so here they are before anything is installed. Its firewall has no rules and accepts everything, the kernel does not forward packets between networks, and these are the groups of the user.

sudo iptables -S
sysctl net.ipv4.ip_forward
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
net.ipv4.ip_forward = 0
id -nG
ubuntu adm cdrom sudo dip lxd

And the folder that section 1 puts Docker's key into is on the image already, with nothing in it.

ls -la /etc/apt/keyrings
total 8
drwxr-xr-x 2 root root 4096 Mar 31  2024 .
drwxr-xr-x 8 root root 4096 Sep 18 09:09 ..

1. Add Docker's repository

Docker's install page for Ubuntu starts by removing packages that would conflict with its own. On a fresh 24.04 there were none to remove.

sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc | cut -f1)
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

Docker's packages are signed, so the next step fetches the key, and for that it wants two small tools. Both were on the image already, but a newer curl was waiting in Ubuntu's archive, and so apt asked before it went on. The script typed y.

sudo apt update
sudo apt install ca-certificates curl
The following packages will be upgraded:
  curl libcurl3t64-gnutls libcurl4t64
Do you want to continue? [Y/n] y

The key goes into /etc/apt/keyrings, the folder that apt's manual recommends for keys the machine's operator adds (sources.list(5)), and is made readable by everyone. The folder was there already, as the check above showed. These three commands print nothing.

sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

The repository itself is one file. Docker's page writes it with a here-document, which is one command over eight lines, and the two parts in $( ) fill in the name of your Ubuntu release and your machine's architecture. tee prints the file as it wrote it.

sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: noble
Components: stable
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.asc

After a second sudo apt update, apt knows Docker's packages. The candidate is the version that the install in the next section brings.

sudo apt update
apt-cache policy docker-ce | head -n 4
docker-ce:
  Installed: (none)
  Candidate: 5:29.8.2-1~ubuntu.24.04~noble

2. Install Docker Engine

This is the one command of Docker's page that does the install. Without -y apt shows what it is about to add and asks, and the script typed y again.

sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
The following NEW packages will be installed:
  containerd.io docker-buildx-plugin docker-ce docker-ce-cli
  docker-ce-rootless-extras docker-compose-plugin pigz
Do you want to continue? [Y/n] y

Two of those seven packages you didn't name: docker-ce-rootless-extras and pigz. Both come back in section 7, because an undo has to take them away too.

3. Check what you got

docker --version
Docker version 29.8.2, build 7fc2dff

There's nothing to switch on. The install enabled and started three units: the Docker service, the socket unit that belongs to it, and containerd.

systemctl is-enabled docker.service docker.socket containerd.service
systemctl is-active docker.service docker.socket containerd.service
enabled
enabled
enabled
active
active
active

The service's own state has a line worth remembering for section 6. TriggeredBy says that the socket unit can start this service.

sudo systemctl status docker --no-pager --lines=0
● docker.service - Docker Application Container Engine
     Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-10-06 10:01:13 UTC; 34s ago
TriggeredBy: ● docker.socket

The install also made a group called docker, with nobody in it, and the socket through which every docker command talks to the daemon. Section 4 is about these two lines.

getent group docker
ls -l /var/run/docker.sock
docker:x:988:
srw-rw---- 1 root docker 0 Oct  6 10:01 /var/run/docker.sock

And it changed the machine's network. There is a new interface, docker0. The firewall that was empty has Docker's chains now, in the table iptables shows by default and in its nat table, with the policy for forwarded packets set to DROP. And the kernel forwards packets, which it didn't before: Docker's page on packet filtering and firewalls says that the daemon switches this setting on when it starts and finds it off. All three are still there after the uninstall, which is why section 7 comes back to them.

ip -brief link show docker0
sudo iptables -S
sudo iptables -t nat -S
sysctl net.ipv4.ip_forward
docker0          DOWN           d2:3e:fd:9d:17:85 <NO-CARRIER,BROADCAST,MULTICAST,UP>
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-BRIDGE
-N DOCKER-CT
-N DOCKER-FORWARD
-N DOCKER-INTERNAL
-N DOCKER-USER
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
net.ipv4.ip_forward = 1

Then the first container. Docker's page uses the image hello-world for this, and it needs sudo, for the reason the next section shows.

sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

4. The error: permission denied while trying to connect to the docker API at unix:///var/run/docker.sock

Not from our run: if the words you typed into a search were slightly different, this is why. Two older questions, Stack Overflow question 47854463 (2017) and Stack Overflow question 48957195 (2018), quote the error as "Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock". We ran one version of Docker, 29.8.2, and it prints the words in the heading above.

The same client, without sudo, is refused.

docker ps
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock

The message names a socket, and who may use a socket is written on its file.

ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Oct  6 10:01 /var/run/docker.sock

The s at the start says that this is a socket. After it come three sets of three characters: the owner, root, may read and write; the group, docker, may read and write; everyone else may do nothing. So the question is who you are.

id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),105(lxd)

Our user is ubuntu, which is not root, and docker is not among its groups. That's the whole error. Docker's post-installation page says the same from the daemon's side: the daemon listens on a Unix socket that root owns, and other users reach it only with sudo or through the docker group.

5. The fix: the docker group, and why it needs a new login

Know what this command gives away before you run it. Docker's post-installation page says it in one sentence: "The docker group grants root-level privileges to the user" (Manage Docker as a non-root user). If that is more than you want a user to have, keep typing sudo, or look at rootless mode.

The -a matters. Without it, usermod -G replaces the list of the user's groups instead of adding to it (usermod(8)).

sudo usermod -aG docker "$USER"
getent group docker
docker:x:988:ubuntu

The group has a member now. And the next command fails exactly as before.

docker ps
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock

Nothing is wrong with what you typed. A session gets its groups when you log in, and this one logged in before the change. id shows both sides of it: without a name it prints the groups of the current process, and with a name it prints what the account has (id(1)).

id -nG
id -nG "$USER"
ubuntu adm cdrom sudo dip lxd
ubuntu adm cdrom sudo dip lxd docker

The account is in docker, the session isn't. What you do about it is log out and back in, in Docker's words "so that your group membership is re-evaluated". For Linux in a virtual machine the same page adds that it may be necessary to restart the virtual machine.

Our script couldn't log out, because it would have ended with its session. It did two other things instead, and neither of them is what we just told you to do.

One command with the group: sg

sg runs a single command with another group as its group (sg(1)). It's the form for one command of the newgrp docker that Docker's page offers "to activate the changes to groups".

sg docker -c "docker ps"
sg docker -c "id"
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
uid=1000(ubuntu) gid=988(docker) groups=988(docker),4(adm),24(cdrom),27(sudo),30(dip),105(lxd),1000(ubuntu)

The empty table is docker ps working: no container is running. For that one command the group was docker. The session around it stayed as it was.

A new login shell, started through sudo

sudo -i -u "$USER" starts your own login shell afresh, and sudo gives it "the list of groups the target user is a member of" (sudo(8)). It's the closest our script could get to logging in again.

sudo -i -u "$USER" id -nG
sudo -i -u "$USER" docker ps
sudo -i -u "$USER" docker run hello-world
ubuntu adm cdrom sudo dip lxd docker
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
Hello from Docker!
This message shows that your installation appears to be working correctly.

That last one is a container started by the ordinary user: the sudo in front made the new shell, and docker itself ran as ubuntu. The session the script had been in all along was a different matter. After all of this it was still refused.

docker ps
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock

6. If something else goes wrong

We caused both of these ourselves, and each is followed by what ended it.

docker-compose: command not found

docker-compose version
bash: line 1: docker-compose: command not found

The start of that line, bash: line 1:, comes from the way our script runs a command. Compose is a plugin of the docker command now, installed in section 2 as docker-compose-plugin, and it's called with a space.

docker compose version
Docker Compose version v5.6.0

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

This one means what it asks: the daemon isn't running. To show it, the script stops the service alone first, on purpose, because its socket unit is still there, and then stops both.

sudo systemctl stop docker.service
systemctl is-active docker.service docker.socket
Stopping 'docker.service', but its triggering units are still active:
docker.socket
inactive
active

The service is stopped, as asked. Now any docker command, and then the same check.

sudo docker ps
systemctl is-active docker.service docker.socket
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
active
active

The service was stopped and its socket unit wasn't. The next docker command reached the socket, the socket unit started the service again, and the command simply worked. Only with both units stopped does the error come.

sudo systemctl stop docker.socket docker.service
systemctl is-active docker.service docker.socket
sudo docker ps
inactive
inactive
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

Starting the service ends it, and the socket unit comes up with it.

sudo systemctl start docker.service
systemctl is-active docker.service docker.socket
active
active

7. How to undo it

The two rm -rf commands in this section delete every image, container and volume on the machine, and nothing asks first. The purge before them does not: it removes the packages and leaves that data where it is.

The commands are those of the uninstall section of Docker's install page. The purge names docker-ce-rootless-extras, the package that came along in section 2.

sudo apt purge docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras
The following packages will be REMOVED:
  containerd.io* docker-buildx-plugin* docker-ce* docker-ce-cli*
  docker-ce-rootless-extras* docker-compose-plugin*
Do you want to continue? [Y/n] y

The programs are gone, and Docker's data folder is as it was.

sudo ls /var/lib/docker
buildkit
containers
engine-id
image
network
plugins
rootfs
runtimes
swarm
tmp
volumes

These are the commands the warning above is about, followed by the two that take away the repository and its key. All four print nothing.

sudo rm -rf /var/lib/docker
sudo rm -rf /var/lib/containerd
sudo rm /etc/apt/sources.list.d/docker.sources
sudo rm /etc/apt/keyrings/docker.asc

What is still there after Docker's own steps

That is where Docker's page ends, and the machine is not yet as we found it. The group is still there, with our user in it.

getent group docker
id -nG "$USER"
docker:x:988:ubuntu
ubuntu adm cdrom sudo dip lxd docker

The folders are gone, but the socket's file isn't.

sudo ls -ld /var/run/docker.sock /var/lib/docker /var/lib/containerd /etc/docker
ls: cannot access '/var/lib/docker': No such file or directory
ls: cannot access '/var/lib/containerd': No such file or directory
ls: cannot access '/etc/docker': No such file or directory
srw-rw---- 1 root docker 0 Oct  6 10:01 /var/run/docker.sock

The interface and the firewall rules of section 3 are unchanged, in both iptables tables, and the kernel still forwards packets.

ip -brief link show docker0
sudo iptables -S
sudo iptables -t nat -S
sysctl net.ipv4.ip_forward
docker0          DOWN           d2:3e:fd:9d:17:85 <NO-CARRIER,BROADCAST,MULTICAST,UP>
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-BRIDGE
-N DOCKER-CT
-N DOCKER-FORWARD
-N DOCKER-INTERNAL
-N DOCKER-USER
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
net.ipv4.ip_forward = 1

And pigz, the seventh package of section 2, is still installed. apt offers to remove it, and the script typed y.

sudo apt autoremove
The following packages will be REMOVED:
  pigz
Do you want to continue? [Y/n] y

apt autoremove removes every package apt considers no longer needed, not only Docker's. On our fresh machine that was this one package. On a machine with a history, read its list before you answer.

The group, the interface and the socket's file we removed by hand. On a machine where something else uses a group called docker, leave the group.

sudo groupdel docker
sudo ip link delete docker0
sudo rm /var/run/docker.sock

After these, the same checks found no group, no interface and no socket.

What was still on the machine when the script ended

The script's last step ran its checks once more. None of the following was on the fresh machine, and all of it was still there at the end:

  • Docker's firewall rules for IPv4. sudo iptables -S and sudo iptables -t nat -S printed the same rules as above, and the policy for forwarded packets was still DROP where it had been ACCEPT before the install.
  • Docker's firewall rules for IPv6. sudo ip6tables -S and sudo ip6tables -t nat -S printed chains and rules of Docker's, among them -N DOCKER-USER and -A FORWARD -j DOCKER-FORWARD, where on the fresh machine they had printed the policies and nothing else.
  • The kernel's setting for forwarding. The last check printed net.ipv4.ip_forward = 1 where the fresh machine had printed net.ipv4.ip_forward = 0. The setting for IPv6 printed net.ipv6.conf.all.forwarding = 0 in every check.
  • The folder /opt/containerd, with the folders bin and lib in it.
  • The folders /run/containerd and /run/docker, each with folders in it. Right after the install the first of them also held containerd's two sockets, and at the end those were gone.
  • The AppArmor profile docker-default. The kernel's list of loaded profiles still had the line docker-default (enforce).
  • Kernel modules that lsmod did not list on the fresh machine: bridge, overlay, veth, nf_nat, xt_MASQUERADE, xt_addrtype, xt_conntrack and xt_set.

The other way round, the folder /etc/apparmor.d/disable was on the fresh machine and was missing at the end. It was still there after the install, and gone in the check right after the purge at the start of this section. /etc/apt/keyrings was still there, with nothing in it, as on the image.

Docker's page on packet filtering and firewalls names two of these: the daemon switches forwarding on, and it sets the policy to drop, which stops the host from acting as a router. For a machine that forwards traffic for others, the policy is a change that outlives the uninstall. We ran no command that removes anything on this list, so this page gives none, and we did not restart the machine, so it can't say what a restart changes. A firewall isn't something to empty with a line copied from a page, and another program on your machine may need forwarding as it is.

How much a small server can take

Installing Docker is the easy part. What the machine under it can take is a different question, and we measured it for one common setup: how many users a $12 server with Nginx, Node.js and Postgres carries.

The playground is invite-only during the private preview: join the waitlist to get an invite.

Sources

Stackrig