Installing Docker Engine and running your first container
Docker Engine from Docker's apt repository, the docker group, and a first run.
On a freshly installed Linux host, the first docker version usually prints a full Client block and then a single line instead of the Server block: permission denied while trying to connect to the docker API at unix:///var/run/docker.sock. The installation worked. The client simply is not allowed to talk to the daemon yet, and how you allow it is the first security decision you make with Docker. This lesson looks at what the lab kit installed and how to install the same thing on a real host, then makes that decision explicit before running the smoke test.
Which Docker to install
On a Linux server or a Linux workstation, install Docker Engine. Docker publishes it as the docker-ce packages in its own apt and dnf repositories, together with containerd.io and the Buildx and Compose plugins. Ubuntu and Debian also ship a docker.io package in their own archives. It works, but it is built and versioned by the distribution and usually trails Docker's releases, and it conflicts with Docker's packages; this track uses Docker's repository so that versions match the release notes.
On macOS and Windows, containers need a Linux kernel, so every option runs Docker Engine inside a Linux VM. Docker Desktop is the official one; it is free for personal use, education and small businesses and needs a paid subscription in larger companies (check the current terms). OrbStack, Colima, Lima, Rancher Desktop and Podman Desktop are alternatives. For this track, use the lab VM from "What Docker is, and why" whichever host you have.
You will also see curl -fsSL https://get.docker.com | sh. That convenience script installs the newest release with no version choice and runs as root on your machine. Docker documents it for test and development machines, not production; if you use it, download it and read it first.
Installing from Docker's apt repository
These steps follow Docker's Ubuntu install guide. They remove conflicting distribution packages, add Docker's signing key and repository, and install the Engine with its plugins. On a host that already runs containers, check first what depends on Ubuntu's containerd and runc (apt-cache rdepends --installed containerd runc), because removing them stops those workloads. Images, containers and volumes under /var/lib/docker are not removed with the packages.
# remove distribution packages that conflict with Docker's (older docker.io, the legacy docker-compose v1, ...)# no -y: read the list apt prints, including anything it would remove along with themsudo apt-get remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc | cut -f1)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.ascsudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOFTypes: debURIs: https://download.docker.com/linux/ubuntuSuites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")Components: stableArchitectures: $(dpkg --print-architecture)Signed-By: /etc/apt/keyrings/docker.ascEOFsudo apt-get updatesudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
The lab kit's setup/install-docker.sh runs the same steps with two additions: it checks the key's fingerprint against the one Docker publishes, and it installs exact versions and holds them. On the lab VM you can see the result:
The repository file names the Ubuntu release (resolute is 26.04), the CPU architecture and the key that must sign the packages. apt-cache policy shows the installed and candidate versions, both 29.8.2. apt-mark showhold lists the six packages that apt upgrade, run by hand or by configuration management, will not move. Holding is a reasonable habit on servers too: upgrading the Engine restarts the daemon, and with it every container, so you want to choose when that happens. Live restore ("Configuring the daemon safely" in Docker in depth) keeps containers running across a daemon restart, but Docker supports it only for patch upgrades such as 29.8.1 to 29.8.2, not for a move to a new minor release. The three systemd units are the daemon (docker.service), containerd, and docker.socket, which owns /var/run/docker.sock and starts the daemon on demand.
A hold also blocks Docker's security releases, so a held host needs a planned upgrade: release the hold, install the new versions you chose, check the daemon and your containers, and hold again.
# docker-compose-plugin is the current `docker compose` plugin, not the legacy docker-compose v1pkgs="docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-ce-rootless-extras docker-compose-plugin"sudo apt-mark unhold $pkgssudo apt-get updateapt-cache madison docker-ce | head -n 3 # pick the version string to installsudo apt-get install docker-ce=<VERSION> docker-ce-cli=<VERSION> containerd.io=<CONTAINERD_VERSION> \docker-buildx-plugin=<BUILDX_VERSION> docker-ce-rootless-extras=<VERSION> docker-compose-plugin=<COMPOSE_VERSION>docker version && docker ps # daemon back, containers runningsudo apt-mark hold $pkgs
Client and daemon
Two blocks mean the client reached the daemon. The Client block is the docker binary you ran; the Server block is dockerd and the components under it: containerd 2.3.6, runc 1.5.1, and docker-init, the tiny init process --init uses. API version 1.56 is what both speak; the daemon still accepts clients down to API 1.40, so older CLIs and SDKs keep working. OS/Arch is linux/arm64 on this VM and linux/amd64 on Intel and AMD machines. If the Server block is replaced by an error saying the client cannot connect, the daemon is not running: check sudo systemctl status docker and sudo journalctl -u docker.
docker info describes the daemon. The storage lines matter most. overlayfs with driver-type: io.containerd.snapshotter.v1 means Docker uses the containerd image store: containerd keeps image content and unpacked layers under /var/lib/containerd, and /var/lib/docker holds containers, volumes, networks and build cache, with no overlay2 directory. That is the default for fresh installs since Docker Engine 29. A host upgraded from an older Engine keeps the classic overlay2 storage driver and its data under /var/lib/docker until someone migrates it, so you will meet both on real servers. "Where Docker keeps data" in Docker in depth explains the difference. The cgroup lines say Docker uses cgroup v2 through systemd, which is what the resource-limit lessons assume.
Who may talk to the daemon
Anyone who can open /var/run/docker.sock can use the full Docker API. Look at its permissions, at your groups, and at what a user outside the docker group gets:
The socket belongs to root and the docker group with mode rw-rw----. ubuntu is in docker (the kit added it), so its commands work. The nobody account is not, and the kernel refused its connect() on the socket file, which is why the client still printed its own version and then stopped. That refusal tells you the socket exists and the user lacks permission on it. It does not prove that the daemon is healthy: with socket activation, systemd holds the socket even while dockerd is down.
There are three ways to give someone access, and they are not equally safe.
With sudo docker, each command is an explicit, logged privilege step, which suits shared servers where several administrators work. The docker group removes the sudo and is the usual choice on a personal machine; you join it with sudo usermod -aG docker $USER and it takes effect in new login sessions (log out and in, or start a shell with newgrp docker). Rootless Docker runs a separate daemon as your own user, so the API is no longer a path to root, at the cost of some features and extra setup; "Rootless Docker" in Advanced container security covers it.
sudo usermod -aG docker $USERnewgrp docker # or log out and back indocker version # the Server block appears
The lab check and the smoke test
The kit includes a checker that compares the VM against the versions the lessons used. The relevant lines:
The full run prints one line per check (operating system, memory, disk, required tools, AppArmor, registry access) and ends with a count; a FAIL names what to fix. The docker group line is always a WARN on the main VM, deliberately, for the reason above. Then the classic smoke test:
The output narrates what happened. Docker did not find hello-world:latest locally (you named no tag, so Docker used latest), pulled it from Docker Hub, and printed the digest of what it downloaded. The pull lines are shown as Docker prints them when its output is not a terminal; in your terminal the same lines redraw in place as progress bars. Line 2 of the message names the variant it pulled, (arm64v8) on this VM and (amd64) on an Intel or AMD machine. The daemon then created a container, the program in it printed the text and exited, and --rm removed the container. When docker version shows both blocks, docker info shows the storage you expect and hello-world runs, the installation is done; anything that fails after that is about your containers, not the install.
docker ps and gets permission denied while trying to connect to the docker API at unix:///var/run/docker.sock. Which conclusion is justified?dockerd is up.docker group.docker group.sudo usermod -aG docker $USER and immediately retry docker ps in the same terminal. It still fails with permission denied. Why?newgrp docker, is enough.usermod -aG updated /etc/group correctly; id $USER would already list docker, while id without an argument still shows the old session groups.docker group on a shared server "just to run tests". What are you giving them?Try this
Work through “The lab check and the smoke test” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
If you keep one thing from installing docker engine and running your first container, keep “The lab check and the smoke test”. Decide now which check you will run when this shows up on a live system, and write it somewhere your team will find it.