Mert Özen Further With Every Line
DevOps Docker — #02

Installing Docker: Docker Desktop, Docker Engine on Linux and the Alternatives

Mert Özen Nov 14, 2026 12 min 12 views
Installing Docker: Docker Desktop, Docker Engine on Linux and the Alternatives

On Windows you install Docker Desktop with WSL 2; on a Linux server you install Docker Engine from Docker's official apt repository, not the distribution's package. With real output we look at the docker.io and Compose v1 trap on Ubuntu 24.04, prove that the docker group really is root, try rootless mode, use docker context to see which engine you talk to, and cover Desktop licensing, alternatives such as Podman and WSL containers, and the amd64/arm64 emulation trap.

Version note: The examples in this post were run with Docker Engine 29.9.3 installed from Docker's official apt repository on Ubuntu 24.04 (containerd 2.4, runc 1.5, Buildx 0.39, Compose 5.7) and with Docker Desktop 4.64.

Guess First

You create a user called Ada on an Ubuntu server. She has no sudo rights; she was only added to the docker group so she can use Docker without a password. Can Ada read /etc/shadow, which only root can read? Can she write a file into /root? Second question: if you run sudo apt install docker.io docker-compose on the same server, which Docker and which Compose version do you get? The answers come halfway through the post, from the output of a real installation.

Docker Desktop and WSL 2 on Windows

In the previous post we saw that Linux containers cannot run directly on Windows and that Docker Desktop runs a Linux environment in the background. On Windows that environment is WSL 2 by default. In November 2026 Docker's Windows installation page lists these requirements: 64-bit Windows 10 22H2, build 19045, or Windows 11 23H2, build 22631, or later; WSL 2.1.5 or newer; 8 GB of RAM; and hardware virtualization enabled in the machine's firmware settings.

The installation flow is short. First you install or update WSL in PowerShell and check its version; then you run the Docker Desktop installer and pick WSL 2 as the backend:

wsl --install
wsl --update
wsl --version
wsl -l -v

Docker Desktop runs in its own docker-desktop WSL distribution, and that is where dockerd lives. If you work in your own Ubuntu distribution, you give it access to the Docker CLI under Settings → Resources → WSL Integration; then PowerShell and your Ubuntu terminal talk to the same engine. The installer does a per-user installation by default and does not need administrator rights. The Hyper-V backend can only be chosen in an all-users installation; WSL 2 is the path this series assumes. Because I prepared this post on a machine that is not running Windows, I did not run the Windows steps myself; the commands and requirements above come from Docker's and Microsoft's documentation.

Licensing: Docker Desktop Is Not Free for Everyone

Docker Engine is open source under Apache 2.0 and free on Linux. Docker Desktop is a separate product with subscription terms. According to Docker's licensing page, Desktop is free for personal use, education, non-commercial open source projects and companies with fewer than 250 employees and less than 10 million dollars in annual revenue; both conditions must hold. Professional use in other companies and in government entities requires a paid subscription: Pro, Team or Business. If the team developing Bookstore is part of a large company, talk to purchasing before installing Docker Desktop; that is why the alternatives below matter.

Docker Engine on Linux: The Official apt Repository

A production server runs Docker Engine directly, not Docker Desktop. On Ubuntu there are two ways to get it: the distribution's own package, docker.io, and Docker's official repository, docker-ce. Let's look at both on Ubuntu 24.04:

apt-cache policy docker.io docker-ce docker-compose | grep -E '^[a-z]|Candidate'
docker.io:
  Candidate: 29.1.3-0ubuntu3~24.04.2
docker-ce:
  Candidate: 5:29.9.3-1~ubuntu.24.04~noble
docker-compose:
  Candidate: 1.29.2-6ubuntu1

Here is the answer to the second question. Ubuntu's docker.io package is not terribly old, but it is eight minor versions behind Docker's own release and its updates follow Ubuntu's schedule. The real trap is docker-compose: that package installs Compose v1, 1.29.2, which is written in Python and went out of support years ago. Anyone who copies apt install docker.io docker-compose from an old blog post works with the Compose of 2021 today. That is why Docker's Ubuntu installation guide says to remove conflicting packages first and then add its own repository. The script below follows the guide's steps exactly; run it on a clean Ubuntu 24.04 as a user with sudo rights:

#!/usr/bin/env bash
set -euo pipefail

# 1. Remove distro/unofficial packages that conflict with Docker's packages
sudo apt-get remove -y $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc 2>/dev/null | cut -f1) || true

# 2. Docker's GPG key and repository (deb822 format)
sudo apt-get update
sudo apt-get install -y ca-certificates curl
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
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<SOURCES
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
SOURCES
sudo apt-get update

# 3. Engine, CLI, containerd, Buildx and Compose plugins
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 4. Check
sudo docker version
sudo docker run --rm hello-world

I ran the script in a clean Ubuntu 24.04 container standing in for a server; there is no systemd there, so I started dockerd by hand, whereas on a real server the package installation starts the service itself. The installed packages:

containerd.io 2.4.2-4~ubuntu.24.04~noble
docker-buildx-plugin 0.39.0-1~ubuntu.24.04~noble
docker-ce 5:29.9.3-1~ubuntu.24.04~noble
docker-ce-cli 5:29.9.3-1~ubuntu.24.04~noble
docker-ce-rootless-extras 5:29.9.3-1~ubuntu.24.04~noble
docker-compose-plugin 5.7.1-1~ubuntu.24.04~noble

Compose is no longer a separate Python program but a CLI plugin you call as docker compose, without the dash; docker compose version reports v5.7.1 here. docker-ce-rootless-extras brings the tooling for rootless mode, which we will use shortly. To pin a version, list them with apt list --all-versions docker-ce and install with docker-ce=5:29.9.3-1~ubuntu.24.04~noble; on production servers, upgrading deliberately through a pipeline is better than having the version change silently on every apt upgrade.

Docker's guide also makes one warning explicit: if you use ufw or firewalld, container ports published by Docker bypass those firewall rules. We will see why, and what a safe default looks like, together with networking in post 12; for now remember that a port you open with -p on a server is reachable from outside even if ufw says it is closed.

The docker Group and Rootless Mode

The docker Group Is Equivalent to Root

To avoid typing sudo before every command after installation, users are usually added to the docker group. Docker's post-install documentation states the cost in one sentence: "The docker group grants root-level privileges to the user." Let's test that with the first question. Ada has no sudo rights; she is only in the docker group:

sudo useradd -m -s /bin/bash ada
sudo usermod -aG docker ada
su - ada
id
cat /etc/shadow
docker run --rm -v /:/host alpine:3 sh -c "echo ada-was-here > /host/root/proof.txt; head -1 /host/etc/shadow"
uid=1002(ada) gid=1002(ada) groups=1002(ada),995(docker)
cat: /etc/shadow: Permission denied
root:*:20767:0:99999:7:::

Ada cannot read /etc/shadow directly, but when she asks Docker to mount the host's root directory into a container, she reads root's file and writes under /root:

-rw-r--r-- 1 root root 13 Nov 14 10:22 /root/proof.txt

We know why from the previous post: the dockerd that starts the container runs as root, and root in the container is uid 0 on the host unless a user namespace is in use. Anyone who can write to /var/run/docker.sock can tell dockerd to mount the host's disk or run a privileged container. The conclusion: only add people to the docker group whom you are already willing to make root on that server. On Windows, Docker gives the same warning for Docker Desktop's docker-users group: it grants access to the Docker socket, which is equivalent to administrator rights.

An Introduction to Rootless Mode

Rootless mode tackles the problem at its root: dockerd and the containers run without root, inside the user's own user namespace. Root in the container maps on the host to that user and to the range of subordinate uids assigned to it in /etc/subuid. The prerequisites are the uidmap package and at least 65536 subordinate uids for the user; the installation is done by the non-root user with the script that ships in the docker-ce-rootless-extras package:

sudo apt-get install -y uidmap
grep ^grace: /etc/subuid
dockerd-rootless-setuptool.sh install
grace:296608:65536

The script sets up a systemd user service and tells you to set DOCKER_HOST=unix:///run/user/<uid>/docker.sock. Since the lab container has no systemd, I started the rootless daemon by hand with dockerd-rootless.sh and tried the same attack against Grace's engine:

docker info | grep -A3 "Security Options"
docker run --rm -v /:/host alpine:3 sh -c "id; touch /host/root/x"
 Security Options:
  seccomp
   Profile: builtin
  rootless
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
touch: /host/root/x: Permission denied

Inside the container it still says uid=0(root), but because that root maps to Grace on the host, it cannot write to /root. Rootless mode has its own costs: opening ports below 1024 needs extra configuration, some networking and storage features are limited, and every user has their own daemon and their own images. We will go into the details and the user namespace remap option together with security in post 13.

Which Engine Are You Talking To? version, info and context

Since the docker command is a client, the first question should always be "which daemon am I connected to?" docker version shows both sides separately:

docker version
Client: Docker Engine - Community
 Version:           29.9.3
 API version:       1.56
 Go version:        go1.26.10
 Git commit:        5457199
 Built:             Tue Nov  3 21:41:21 2026
 OS/Arch:           linux/arm64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          29.9.3
  API version:      1.56 (minimum version 1.40)
  Go version:       go1.26.10
  Git commit:       70efc0e
  Built:            Tue Nov  3 21:41:21 2026
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          v2.4.2
  GitCommit:        b7a25261e029d68978bbf290df0a62fc819b3c51
 runc:
  Version:          1.5.3
  GitCommit:        v1.5.3-0-g46365c62
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

The OS/Arch lines depend on the machine's architecture: this output comes from an arm64 machine; on an x64 server you see linux/amd64, and with Docker Desktop on Windows the client shows windows/amd64. The minimum version on the server side is the oldest client API the daemon accepts; a very old CI tool or library below that limit gets a "client version is too old" error. docker info shows the daemon's settings: storage driver, cgroup version, default runtime, security options. Sharing the output of these two commands is a good habit when debugging; before you share it, remove the lines that identify your machine, such as the host name, kernel build number and user paths.

If you work with several engines, such as Docker Desktop, a remote test server or a rootless daemon, docker context lets you choose explicitly which one you talk to. In the lab I added a context for Docker Desktop's socket next to the Linux engine:

docker context create desktop --description "Docker Desktop engine" --docker host=unix:///var/run/desktop.sock
docker context ls
docker --context default version --format "default: {{.Server.Platform.Name}} {{.Server.Version}}"
docker --context desktop version --format "desktop: {{.Server.Platform.Name}} {{.Server.Version}}"
Successfully created context "desktop"
NAME        DESCRIPTION                               DOCKER ENDPOINT                ERROR
default *   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock
desktop     Docker Desktop engine                     unix:///var/run/desktop.sock
default: Docker Engine - Community 29.9.3
desktop: Docker Desktop 4.64.0 (221278) 29.2.1

The asterisk marks the active context; docker context use desktop changes it permanently, while the --context flag or the DOCKER_CONTEXT environment variable applies to a single command. The endpoint can also be SSH, for example host=ssh://deploy@test-server, so you can manage a remote daemon without opening a port. If the DOCKER_HOST environment variable is set, it overrides the context; the answer to "why am I seeing containers from the wrong machine?" is often a forgotten DOCKER_HOST.

Alternatives: They All Run OCI Images

As we saw in the previous post, the image format is a standard; tools that replace Docker Desktop run the same images. The main options in November 2026:

ToolPlatformHighlightsWhen
Podman / Podman DesktopLinux, Windows, macOSNo daemon, rootless by default, docker-compatible CLILicense constraints, rootless requirement, Red Hat ecosystem
Rancher DesktopWindows, macOS, LinuxChoice of dockerd or containerd, optional k3sA free desktop experience and local Kubernetes
WSL containers (wslc)WindowsCLI and API built into WSL, GA since 29 September 2026Containers on Windows without extra tools; no Compose yet
Colima, OrbStackmacOSDocker engine in a lightweight Linux VMAn alternative to Desktop on macOS; OrbStack is paid for commercial use
Apple containermacOS 26, Apple siliconEach container in its own lightweight VMmacOS workflows that do not need Docker compatibility

For readers on Windows the newest option is WSL containers. Microsoft announced the feature as a public preview in June 2026 and declared it generally available on 29 September 2026: wslc.exe, delivered through wsl --update and aliased as container.exe, offers Docker-like commands such as wslc run, wslc network and wslc events, and Visual Studio Code's dev containers can use it as their driver. According to Microsoft's own announcement, Compose support is the next goal; so for the Docker Compose series, where several containers are managed together, you still need Docker Desktop or Podman today. It has an effect on the .NET side too: we will look at the change in how the .NET 11 SDK chooses a local container runtime in post 15. This series explains the examples with Docker; most commands work unchanged in Podman by typing podman instead of docker.

The Architecture Trap: amd64 and arm64

Images are specific to a processor architecture. Most official images on Docker Hub and MCR are published for several architectures and Docker picks the one that fits your machine; you can ask for another architecture with --platform:

docker run --rm --platform linux/amd64 alpine:3 uname -m
docker run --rm --platform linux/arm64 alpine:3 uname -m
x86_64
aarch64

Both commands worked because Docker Desktop can run an image built for a different architecture than the machine's through emulation. On an x64 Windows machine, a linux/arm64 container runs by being translated into x86 instructions. With a single-architecture image Docker reports this with a warning. The output below is from an arm64 machine running a .NET SDK tag published only for amd64; on an x64 machine you see the same warning the other way round with an arm64-only image:

docker run --rm mcr.microsoft.com/dotnet/sdk:10.0-noble-amd64 sh -c 'uname -m; dotnet --version'
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
x86_64
10.0.404

On a developer machine emulation looks like it works, but it has two problems. The first is speed: every CPU instruction is translated, so work such as compilation, compression and JIT compilation slows down noticeably. The second is support: the .NET team's dotnet-docker documentation states plainly ".NET doesn't support being run in QEMU", so odd failures of .NET under QEMU are to be expected. That is why we run .NET images on their target architecture and, when building for another architecture, keep the SDK on the native one; we will see the Dockerfile side of this in posts 6 and 9.

In Production

A production server runs Docker Engine, not Docker Desktop, and you install it from Docker's repository rather than the distribution's package. Pin the version, upgrade deliberately and read the release notes for changes such as Engine 29 rejecting old API clients; as of November 2026 the current patch is 29.9.3.

Treat access to the server through the docker group as root access, and manage it that way. On a server where everyone on the team is in the docker group, everyone is effectively root. For remote management use an SSH context instead of exposing the daemon over TCP; a port 2375 opened without TLS is a root shell open to the network.

Settle licensing before installation. In a company where Docker Desktop is paid, Desktop installations that developers set up without telling anyone can later become an audit issue; Podman Desktop, Rancher Desktop or WSL containers are real alternatives in that case.

Common Pitfalls

Copying apt install docker.io docker-compose from an old guide. On Ubuntu 24.04 that means an Engine behind Docker's own release and Compose v1, 1.29.2, which has long been out of support. Use the official repository and call Compose as docker compose.

Running everything with sudo docker. After a while root-owned files appear under ~/.docker and commands run as your normal user fail with permission errors. Either use the docker group deliberately or move to rootless mode.

Publishing a container port and assuming ufw protects it. Docker's own rules run ahead of ufw; in post 12 we will test this and set up the right default.

Running an image of the wrong architecture and calling it "slow but working". A test that passes under emulation does not guarantee the same behaviour on the target server; check the architecture of the image you run with docker image inspect --format '{{.Architecture}}'.

A Small Experiment

★☆☆ Run docker version and docker context ls on your machine. Note the Client and Server versions, the Server's minimum version and the endpoint of the active context.

★★☆ On an Ubuntu 24.04 VM, or the Ubuntu distribution in WSL, first look at the output of apt-cache policy docker.io docker-compose docker-ce, then install Docker Engine with the script in this post. What do docker compose version and docker buildx version say?

★★★ On the same VM, set up rootless mode for a non-root user. Create a context for the rootful and one for the rootless engine, try docker run --rm -v /:/host alpine:3 touch /host/root/x on both and explain the difference.

Docker is installed and we know which engine we are talking to. In the next post we move on to the container lifecycle: docker run flags, ps, logs, exec, the difference between stop and kill, restart policies, and why a .NET application sometimes waits 10 seconds on docker stop before being killed.