What Docker is, and why

The problem containers solve, what Docker ships, and the lab VM for this track.

Beginner10 min · lesson 1 of 14

This lesson sets up the lab VM used for the whole track and uses it to show what Docker changes about running software and what it leaves to you. The change is easiest to see in a common failure. A service passes its tests on a developer laptop with Node.js 24 and crashes at startup on a build server that still has Node.js 20, installed by hand years ago from a wiki page. The code is identical; the machine around it is not.

Docker moves the runtime, the libraries and the files an application needs into one versioned artifact, the image, and starts it the same way everywhere a Docker engine runs. The server then needs Docker and nothing else from the application's world.

Images, containers and registries

Three words carry the rest of the track. An image is a read-only bundle of files (an operating system userland such as Alpine or Debian, a language runtime, your application) plus metadata saying which program to start. A container is a process started from an image, with its own view of the filesystem, network and process list, and a thin writable layer of its own on top of the image. A registry is a server that stores images by name; Docker Hub is the default one, and docker run downloads (pulls) an image from it when the machine does not have it yet. The lesson "Images, containers and the core commands" takes the image and container pair apart properly; here you only need the vocabulary.

What Docker actually ships

"Docker" names several pieces. On Linux you install Docker Engine: the dockerd daemon (a background service that owns images, containers, networks and volumes), containerd, which manages the container processes and on current installs also stores the images, and runc, the small program that asks the kernel to create each container. The docker command you type is only a client. It sends API requests to dockerd over the Unix socket /var/run/docker.sock. Two plugins ride along with the client: Buildx, which builds images, and Compose, which runs multi-container applications from one YAML file. On macOS and Windows, Docker Desktop (or an alternative such as OrbStack, Colima or Podman Desktop) runs that same Linux engine inside a small virtual machine, because Linux containers need a Linux kernel.

What one docker command passes through
1docker CLI
you type docker run ...
2dockerd
API on /var/run/docker.sock
3containerd
images, snapshots, tasks
4shim + runc
runc asks the kernel for the container
5your process
namespaces and cgroups around it
"Engine architecture on Docker 29" in Docker in depth covers each hop. For now it is enough to know that the CLI only asks and the daemon does the work.

The lab you will use

Every command and every output in this track was run in a disposable Ubuntu 26.04 virtual machine with Docker Engine 29.8.2, containerd 2.3.6, Buildx 0.37.1 and Compose 5.6.0. You can build the same VM. Download the lab kit from the course page (secopslog-docker-lab.tar.gz), unpack it, and run its setup script on your workstation. It uses Multipass if it is installed and Lima otherwise; Windows users run the multipass commands from the kit's README instead. The first run downloads an Ubuntu cloud image and installs packages, so allow 10 to 20 minutes.

workstation terminal
tar -xzf secopslog-docker-lab.tar.gz
cd secopslog-docker-lab
./setup/create-lab.sh # creates the VM secopslog-docker
multipass shell secopslog-docker # or: limactl shell secopslog-docker
/opt/secopslog-lab/setup/verify-lab.sh # inside the VM: PASS/WARN/FAIL per check

The script creates secopslog-docker, installs Docker from Docker's own package repository at pinned versions, and adds your login in the VM to the docker group so you can run docker without sudo. On Multipass that login is ubuntu; on Lima it is your Mac username, with its own home directory. The prompts and paths in this track come from the Multipass VM, so on Lima read /home/ubuntu as your home directory. "Installing Docker Engine and running your first container" shows what the script installed and why that group membership is a serious privilege. A second VM, secopslog-docker-sec, exists for lessons that change the daemon or the host; those lessons say so at the top, so do not create it yet.

Lessons that use files have a Lesson files box on the page with the command that downloads and unpacks them inside the VM, into ~/lab/<lesson>. Everything a lesson creates in Docker is named lab-... or carries the label secopslog.lab=true, and /opt/secopslog-lab/setup/reset-lab.sh uses that to clean up. A plain reset removes lab containers, networks, Compose projects and builders, and keeps volumes, images and your ~/lab/<lesson> folders so you can pick a lesson up again. reset-lab.sh --all also removes lab volumes and lab-built images, and --daemon puts the kit's daemon.json back.

Use the VM rather than your laptop's Docker. The outputs will match what you read here, and the experiments in later courses (root inside containers, the Docker socket, daemon settings) belong on a machine you can throw away.

A runtime the machine does not have

Back to the Node.js problem. The lab VM has no Node.js at all, so the shell answers with exit status 127, "command not found". In an interactive shell Ubuntu adds a hint to install the nodejs package; leave it alone, because the point is that the VM does not need it. The same VM runs Node 24 from the official node:24-alpine image without installing anything:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ node --version
-bash: line 1: node: command not found
$ docker run --rm node:24-alpine node --version
v24.21.0
$ docker run --rm node:24-alpine cat /etc/alpine-release
3.24.2

The second command printed the version from inside the image. --rm deletes the container as soon as the command ends, so nothing is left behind. The third command shows where that Node.js lives, an Alpine Linux 3.24 userland, while the VM itself is Ubuntu. If the image is not on your machine yet, docker run first prints Unable to find image ... locally and the pull progress, then runs the command; the install lesson shows that first pull. The version you see depends on when the 24-alpine tag was last updated, because a tag is a name the publisher can move to a newer build.

One image name, several CPU architectures

"Runs the same everywhere" has one important limit. An image contains compiled programs, and compiled programs are built for a CPU architecture. Popular images are published as an image index, a list that points to one variant per platform. docker image ls --tree shows the variants Docker knows about for an image on this machine:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker image ls --tree node:24-alpine
IMAGE ID DISK USAGE CONTENT SIZE EXTRA node:24-alpine ebfe2f904627 238MB 62.4MB ├─ linux/amd64 83f1c388c31f 0B 0B ├─ linux/arm64/v8 38a36422dc7d 237MB 62.1MB └─ linux/s390x fb2a6de21b05 0B 0B
$ uname -m
aarch64

The lab VM in this run is arm64 (aarch64, an Apple Silicon Mac), so Docker pulled only the linux/arm64/v8 variant and the other rows show 0B. On an Intel or AMD machine the linux/amd64 row carries the size instead, and the image IDs differ. When an image has no variant for your platform, docker run fails with no matching manifest for linux/arm64/v8 in the manifest list entries (or the amd64 equivalent). That is the error people on Apple Silicon or AWS Graviton hit with images built only for amd64. "Multi-platform images" in Docker in depth shows how to build both.

A long-running service

Most containers you run will be long-lived services. This one starts the nginx web server in the background and publishes its port 80 on port 8080 of the VM, bound to the loopback address so only the VM itself can reach it:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run -d --name lab-web -p 127.0.0.1:8080:80 nginx:1.30-alpine
164a6281f8818a1ff9d217df4c0d47d927fe8b74567bdc5b5e10726519736b41
$ docker ps --filter name=lab-web
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 164a6281f881 nginx:1.30-alpine "/docker-entrypoint.…" 1 second ago Up Less than a second 127.0.0.1:8080->80/tcp lab-web
$ curl -s http://127.0.0.1:8080/ | grep '<title>'
<title>Welcome to nginx!</title>
$ docker rm -f lab-web
lab-web

docker run -d printed the new container's full ID and returned at once. docker ps lists running containers, and its PORTS column shows the mapping 127.0.0.1:8080->80/tcp. The curl request reached nginx inside the container. docker rm -f stopped and deleted it. Each of those flags gets its own section in "docker run in depth"; the container IDs in your output will be different, since Docker generates a new random ID for every container.

What Docker does not do for you

A container is not a virtual machine. It shares the host's kernel, which is why it starts in a fraction of a second and why the next lesson, "Containers vs virtual machines", is about what that sharing costs. A container is also not a security boundary you can stop thinking about: by default the process inside runs as root, and the Docker daemon itself runs as root on the host. And an image freezes its contents. The Node.js and OpenSSL inside node:24-alpine do not patch themselves; you get fixes by pulling or building a newer image and replacing the container. Docker moves the install guide into a file you can version, test and rebuild. Secrets, upgrades, backups and monitoring are still your job, and later lessons show how to do each of them with containers.

Quick check
01The lab VM prints node: command not found, yet docker run --rm node:24-alpine node --version prints v24.21.0. Where did that Node.js binary come from?
Incorrect — Nothing was installed on the VM. Run node --version again afterwards and it still fails with exit status 127.
Correct — The image holds the runtime and its libraries; the container runs that binary from the image's filesystem, not from the VM's.
Incorrect — The kernel provides system calls, not language runtimes. Node.js is an ordinary program file inside the image.
Incorrect — A registry only stores images. The command ran locally, in a container on the VM.
02A colleague on an Apple Silicon Mac reports no matching manifest for linux/arm64/v8 in the manifest list entries for an internal image that works on every CI runner. What is the most likely cause?
Incorrect — Image indexes have been supported for many years; the message itself shows Docker read the index and searched it.
Incorrect — A corrupt image fails a digest check. This message says the list was read correctly and had no entry for the platform.
Incorrect — Docker Desktop already runs an arm64 Linux VM on Apple Silicon. Nothing is missing on the Mac side.
Correct — The CI runners are amd64 and the build never produced an arm64 variant. Building for both platforms, or running the amd64 variant under emulation, fixes it.
03Why does this track run every command in a disposable lab VM instead of on your workstation's Docker?
Correct — Membership of the docker group is root-equivalent, and later courses change daemon settings on purpose; a VM you can recreate keeps that away from your real machine, with the same versions the lessons used.
Incorrect — On Linux, containers run directly on the host kernel. Only macOS and Windows need a Linux VM underneath.
Incorrect — Docker Desktop runs those images fine. The reasons are safety and matching outputs, not capability.
Incorrect — Start time is about the same; the VM adds a layer, it does not speed anything up.

Try this

Run tar -xzf secopslog-docker-lab.tar.gz on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.

Takeaway

If you keep one thing from what docker is, and why, keep “What Docker does not do for you”. Decide now which check you will run when this shows up on a live system, and write it somewhere your team will find it.

Related