CertGrid CertGrid
Installation·Docker

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

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.

One Docker host, captured before Docker was present on it. The package versions below come from a real install on a clean Ubuntu 26.04 machine, which is why they are specific rather than generic.
Server NameIP AddressOSRolesCPURAMHDD
DOCKER01192.168.0.21Ubuntu 26.04 LTSDocker Host2 Core4 GB50 GB

Before you start

  1. Remove the distro Docker packages

    Ubuntu ships its own docker.io package, and unofficial ones like podman-docker put a conflicting docker binary 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.

  2. Add Docker's GPG key

    apt refuses a repository it cannot verify, so the signing key goes in first. /etc/apt/keyrings is 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.asc

    Expected 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.asc

    Success 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.

  3. 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 stable

    Success conditionresolute is 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:.

  4. Install the engine, CLI and plugins

    Five packages, and all five matter. docker-ce is the daemon, docker-ce-cli is the docker command, containerd.io is the runtime underneath, and the two plugin packages are what make docker buildx and docker compose exist 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.

  5. 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.2

    Expected resultactive, enabled, and a version number. Anything else means the daemon did not start.

    Verify it worked

    bash
    sudo systemctl status docker --no-pager -l

    Success conditionis-active returns active. If it returns failed, the reason is in the last twenty lines of the status output.

  6. 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.

  7. 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 - alpine on 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 complete

    Expected 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.8MB

    Success 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.

  8. Drop the sudo prefix

    The daemon listens on a Unix socket owned by root:docker, so membership of the docker group 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.4

    Success 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

Official sources