What Docker is, and why
The problem containers solve, what Docker ships, and the lab VM for this track.
Here you'll set up the lab VM for the whole track. Then you'll use it to see what Docker changes about running software, and what it still 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. On the build server it crashes at startup, because that server still has Node.js 20, installed by hand years ago from a wiki page. The code is identical. The machine around it isn't.
Docker moves the runtime, the libraries and the files an application needs into one versioned artifact, called the image. It starts that image the same way everywhere a Docker engine runs. Your 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 plus metadata saying which program to start. The files are an operating system userland such as Alpine or Debian, a language runtime and your application. A container is a process started from an image. It gets 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 doesn't 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, which has three parts. The dockerd daemon is a background service that owns images, containers, networks and volumes. Then containerd manages the container processes, and on current installs it also stores the images. Finally, runc is 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 runs that same Linux engine inside a small virtual machine, because Linux containers need a Linux kernel. Alternatives such as OrbStack, Colima or Podman Desktop work the same way.
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.9.2, containerd 2.4.2, Buildx 0.38.1 and Compose 5.6.1. 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's 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.
tar -xzf secopslog-docker-lab.tar.gzcd secopslog-docker-lab./setup/create-lab.sh # creates the VM secopslog-dockermultipass 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 and installs Docker from Docker's own package repository at pinned versions. It also 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 don't 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. Your 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: the point is that the VM doesn't need it. The same VM runs Node 26 from the official node:26-alpine image without installing anything:
The second command printed the version from inside the image. --rm deletes the container as soon as the command ends, so nothing's 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 isn't 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 26-alpine tag was last updated. 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:
The lab VM in this run is arm64 (aarch64, an Apple Silicon Mac). So Docker pulled only the linux/arm64/v8 variant, and the linux/amd64 row shows 0B. On an Intel or AMD machine that 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's 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. It publishes the container's port 80 on port 8080 of the VM, bound to the loopback address so only the VM itself can reach it:
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 isn't a virtual machine. It shares the host's kernel. That's why it starts in a fraction of a second, and it's why the next lesson, "Containers vs virtual machines", is about what that sharing costs. A container also isn't 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:26-alpine don't 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.
Try this
Start lab-web again, but publish it with -p 127.0.0.1:8081:80 instead. Run the same curl against port 8080 and then port 8081, and predict which one reaches nginx before you look. Remove it with docker rm -f lab-web when you're done.
Takeaway
An image carries its own runtime, which is how a VM with no Node.js at all printed v26.12.1 from node:26-alpine. In real work this breaks in two places: architecture and freshness. An amd64-only image fails on arm64 with no matching manifest, and a moving tag like 26-alpine can hand you a different build next month.
node: command not found, yet docker run --rm node:26-alpine node --version prints v26.12.1. Where did that Node.js binary come from?node --version again afterwards and it still fails with exit status 127.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?