Docker Engine Installation on Ubuntu 26.04
Install Docker Engine from the official apt repository on Ubuntu 26.04, confirm the daemon is running, watch a real multi-layer image pull, and drop the sudo prefix. Every command is paired with the output you should expect.
Getting Started Guide 1 of 46 Beginner
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.x
- Shellbash
- Architectureamd64 / arm64
- TimeAbout 15 min
- Reviewed20 August 2026
Written against the versions above. Layer IDs, digests, download speeds, timings and package versions differ on every run and every mirror. Match the shape of the output, not the exact strings.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| DOCKER01 | 192.168.0.21 | Ubuntu 26.04 LTS | Docker Host | 2 Core | 4 GB | 50 GB |
Before you start
- A 64-bit Ubuntu 26.04 machine, local or over SSH, with an account in the sudo group.
- Outbound HTTPS to download.docker.com and registry-1.docker.io. These are separate hosts, and a proxy may allow one but not the other.
- The distro packages docker.io or podman-docker not in use for anything you care about. Step 1 removes them.
- About 500 MB free on / for the engine, the CLI and the plugins.
-
Remove the distro Docker packages
Ubuntu ships its own
docker.iopackage, and unofficial ones likepodman-dockerput a conflictingdockerbinary on PATH. Installing the official packages on top produces a working CLI wired to the wrong daemon, which is a confusing failure. Clear them first.bash for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove -y $pkgdoneReading package lists...Building dependency tree...Reading state information...Package 'docker.io' is not installed, so not removedSolving dependencies...0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded....Solving dependencies...0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.Expected resultThe same three lines per package, and for each one either a removal or a 'not installed, so not removed' notice. On a clean server every package is absent and nothing is removed.
Success conditionNothing failed. This loop is defensive, not corrective - it exists so a distro package cannot silently shadow the one you are about to install.
-
Add Docker's GPG key
apt refuses a repository it cannot verify, so the signing key goes in first.
/etc/apt/keyringsis the modern location, and the key must be world-readable: apt drops privileges before fetching, so a root-only key produces a signature error later that looks nothing like a permissions problem.bash sudo apt-get updatesudo apt-get install -y ca-certificates curlsudo install -m 0755 -d /etc/apt/keyringssudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.ascsudo chmod a+r /etc/apt/keyrings/docker.ascExpected resultcurl is silent on success. Nothing printed is the good outcome here.
Verify it worked
bash Example session ls -l /etc/apt/keyrings/docker.asc-rw-r--r-- 1 root root 3639 Aug 19 09:12 /etc/apt/keyrings/docker.ascSuccess conditionThe permission block reads -rw-r--r--. If it reads -rw-------, the chmod did not run and step 4 will fail to verify the repository.
-
Add the Docker apt repository
The repository line is built from two facts the machine already knows: its architecture, and its Ubuntu codename. Hardcoding either is the most common reason this step appears to work and then step 4 cannot find the packages. On 26.04 the codename is
resolute.bash echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/nullcat /etc/apt/sources.list.d/docker.listdeb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu resolute stablesudo apt-get updateGet:1 https://download.docker.com/linux/ubuntu resolute InRelease [32.5 kB]Get:2 https://download.docker.com/linux/ubuntu resolute/stable amd64 Packages [27.1 kB]Hit:3 http://archive.ubuntu.com/ubuntu resolute InReleaseHit:4 http://security.ubuntu.com/ubuntu resolute-security InReleaseHit:5 http://archive.ubuntu.com/ubuntu resolute-updates InReleaseHit:6 http://archive.ubuntu.com/ubuntu resolute-backports InReleaseFetched 59.6 kB in 1s (59.4 kB/s)Reading package lists...Expected resultThe repository line echoed back with your architecture and release codename filled in, then apt fetching Docker's index for the first time.
Verify it worked
bash Example session cat /etc/apt/sources.list.d/docker.listdeb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu resolute stableSuccess condition
resoluteis Ubuntu 26.04's codename - the command substitution filled it in, which is why this line is copy-pasteable across releases. The two Get: lines from download.docker.com are the repository being seen for the first time; on later runs they become Hit:. -
Install the engine, CLI and plugins
Five packages, and all five matter.
docker-ceis the daemon,docker-ce-cliis thedockercommand,containerd.iois the runtime underneath, and the two plugin packages are what makedocker buildxanddocker composeexist as subcommands.bash sudo apt-get install -y docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-pluginThe following NEW packages will be installed: containerd.io docker-buildx-plugin docker-ce docker-ce-cli docker-ce-rootless-extras docker-compose-pluginGet:2 https://download.docker.com/linux/ubuntu resolute/stable amd64 containerd.io amd64 2.3.3-1~ubuntu.26.04~resolute [22.7 MB]Fetched 129 MB in 10s (13.2 MB/s)Setting up containerd.io (2.3.3-1~ubuntu.26.04~resolute) ...Created symlink '/etc/systemd/system/multi-user.target.wants/containerd.service' -> '/usr/lib/systemd/system/containerd.service'.Setting up docker-ce (5:29.7.2-1~ubuntu.26.04~resolute) ...# the exact package list and download size depend on what the machine# already has - a minimal server pulls in more dependencies than a full one.Expected resultThe five packages you named plus docker-ce-rootless-extras, then each one being set up and its systemd unit enabled.
Success conditionThe versions are visible in the package names: containerd 2.3.3 and Docker Engine 29.7.2 for Ubuntu 26.04. The symlink lines are the installer enabling the services, which is why the next step finds the daemon already running.
-
Confirm the daemon is running
The package installing cleanly and the daemon running are two different claims. Check the second one directly before trying to run anything.
bash Example session systemctl is-active dockeractivesystemctl is-enabled dockerenabledsudo docker version --format '{{.Server.Version}}'29.7.2Expected resultactive, enabled, and a version number. Anything else means the daemon did not start.
Verify it worked
bash sudo systemctl status docker --no-pager -lSuccess conditionis-active returns active. If it returns failed, the reason is in the last twenty lines of the status output.
-
Verify end to end with hello-world
This is the real test: it proves the CLI reaches the daemon, the daemon reaches Docker Hub, and a container can start and exit. The image is not present locally, so a pull happens first.
bash sudo docker run --rm hello-world# on a machine that has never run it, the image is pulled first -# "Unable to find image locally", then a layer, then a digest. Hello from Docker!This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon.Expected resultThe greeting, preceded by a pull the first time you run it.
Success conditionFour things just worked at once: the client reached the daemon, the daemon pulled from Docker Hub, it created a container, and it streamed the output back. A failure at any of those stages would have stopped here.
-
Watch a real multi-layer pull
hello-world is one tiny layer, so it teaches nothing about how images arrive. Pull a pinned nginx tag instead and watch the layers come down in parallel. Pinning the minor version is also the habit you want -
alpineon its own moves under you.bash docker pull nginx:1.29-alpine1.29-alpine: Pulling from library/nginx612c0c1df4c5: Pulling fs layeraee4e54b3865: Pulling fs layer6a0ac1617861: Pulling fs layer82736a35d0e7: Pulling fs layer583599bb7d38: Pulling fs layer453da7dbc73e: Pulling fs layer781ff50d2644: Pulling fs layer4a8b0b2a5b19: Pulling fs layer4a8b0b2a5b19: Download complete6a0ac1617861: Download completeaee4e54b3865: Download complete583599bb7d38: Download complete612c0c1df4c5: Download complete82736a35d0e7: Download complete453da7dbc73e: Download completeExpected resultEight layers announced together, then completing out of order as they finish downloading in parallel.
Verify it worked
bash Example session docker image ls --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}'REPOSITORY TAG IMAGE ID SIZEnginx alpine db35bfc6b295 94.2MBpostgres 17-alpine 18cfe3ef5e68 424MBredis 7-alpine e7723ff73d96 57.8MBSuccess conditionThe completion order does not match the announcement order - the layers download concurrently. Those hashes are content addresses: any other image built on the same base reuses these exact layers instead of downloading them again. guide 17 builds on this.
-
Drop the sudo prefix
The daemon listens on a Unix socket owned by
root:docker, so membership of thedockergroup is what lets you talk to it unprivileged. Group membership is established at login, so a brand new group does not apply to the shell you are already in. That is the single most common stumble here, not a broken install.bash sudo usermod -aG docker $USERnewgrp docker# newgrp starts a new shell with the group applied. Log out and back in# instead if you want every future shell to have it.docker run --rm hello-worldHello from Docker!Expected resultThe same banner as step 6, with no sudo and no permission error.
Verify it worked
bash Example session id -nG | tr ' ' '\n' | grep -x dockerdockerdocker compose version --short5.4.0docker run --rm nginx:alpine nginx -vnginx version: nginx/1.31.4Success conditionNo sudo, and the group is listed. Be clear-eyed about what you just did: the docker group is root-equivalent, because anyone in it can mount the host filesystem into a container. See guide 39.
Troubleshooting
permission denied while trying to connect to the Docker daemon socket
Why: Your shell's group list predates the docker group being added. The install is fine; the session is stale.
Fix:Re-establish the group for this shell, or log out and back in.
bash id -nGnewgrp dockerE: Unable to locate package docker-ce
Why: The repository was never added successfully, or the codename in docker.list is wrong or still a literal string.
Fix:Check the file resolved to a real codename, then update again.
bash cat /etc/apt/sources.list.d/docker.listsudo apt-get updateThe following signatures couldn't be verified ... NO_PUBKEY
Why: The keyring file is not world-readable, so apt cannot use it after dropping privileges.
Fix:Re-apply read permission and update. The file should show -rw-r--r--.
bash sudo chmod a+r /etc/apt/keyrings/docker.ascls -l /etc/apt/keyrings/docker.ascsudo apt-get updatedocker: 'compose' is not a docker command
Why: docker-compose-plugin was not installed. The engine is healthy; only the subcommand is absent.
Fix:Install the plugin package, then confirm.
bash sudo apt-get install -y docker-compose-plugindocker compose versionCannot connect to the Docker daemon at unix:///var/run/docker.sock
Why: The daemon is not running. Common in containers, WSL without systemd, and VMs where the service failed to start.
Fix:Check the service state and read the journal. The reason is almost always in the last twenty lines.
bash systemctl is-active dockersudo systemctl start dockersudo journalctl -u docker -n 20 --no-pagerA pull stalls at Waiting, or retries with TLS handshake timeout
Why: Outbound HTTPS to registry-1.docker.io is blocked or proxied. Docker's apt repo and Docker Hub are different hosts, so the install can succeed while pulls fail.
Fix:Confirm reachability, then configure a proxy for the daemon if your network needs one.
bash curl -sS -o /dev/null -w '%{http_code}\n' https://registry-1.docker.io/v2/sudo systemctl show docker --property=Environment