Containers vs virtual machines
One shared kernel versus one kernel per guest, and what that costs.
Run uname -sr inside an Alpine Linux container on the lab VM and it prints Linux 7.0.0-38-generic. That isn't an Alpine kernel. It's the Ubuntu VM's own kernel, the same one uname reports outside the container. That one line explains most of the difference between containers and virtual machines. It's why containers start so fast, and it's why their isolation needs care.
Two ways to isolate a workload
A virtual machine (VM) is built by a hypervisor such as KVM, Hyper-V or Apple's Virtualization framework. The hypervisor presents virtual CPUs, memory, disks and network cards. A complete guest operating system boots on them, with its own kernel. The kernel is the core of the operating system. It schedules processes, manages memory and talks to hardware. Every VM pays for its own kernel and its own init system before your application starts.
A container boots nothing. It's an ordinary process on the host, started by the container runtime with two kinds of kernel features around it. Namespaces limit what the process can see (its own process list, network interfaces, mount table, hostname). Control groups (cgroups) limit what it can use (CPU, memory, number of processes). The files it sees come from the image. The kernel underneath is the host's.
One kernel, different userlands
Compare the VM and a container side by side, and you'll see the difference at once:
The kernel lines are identical. The /etc/os-release lines aren't, because that file comes from the image. A "Debian" or "Alpine" container is that distribution's userland (its shell, libraries and package manager). It runs on whatever kernel the host has. Two consequences follow. A Linux container can't run Windows programs, since there's no Windows kernel to call. And software that needs a particular kernel feature depends on the host, not on the image. An image can't bring a newer kernel with it. Windows containers do exist, but they are a separate world that runs on Windows hosts with the Windows kernel.
systemd-detect-virt answers qemu, so the lab "host" is itself a virtual machine. That stack is normal. Cloud instances are VMs, and Docker Desktop on a Mac runs a Linux VM, so most production containers run inside a VM. The containers share the VM's kernel, not the laptop's or the hypervisor's.
A container is a process you can see from the host
Start a container that does nothing for ten minutes, then look at it from both sides. The docker inspect -f template prints one field of the container's state. Here it's the process ID on the host ("Inspect, logs and exec" covers templates). docker exec runs an extra command inside a running container, which gives you the view from inside:
From the VM, the container is process 76588, a plain sleep 600 owned by root. Inside, the same process is PID 1, and it can see only itself and the ps you just ran. That's the PID namespace. The container gets its own numbering and its own, much shorter, process list. The PID on your VM will differ.
Each process has a set of namespace links under /proc/self/ns. You can compare two of them:
The PID namespace numbers differ, so the container really has its own. The user namespace number is the same on both sides. Docker doesn't create a user namespace by default, which means UID 0 in the container is UID 0 on the host kernel. By default you get isolation for process IDs, network, mounts, hostname and IPC, plus a cgroup namespace. User IDs are shared unless you opt into rootless Docker or user-namespace remapping. Advanced container security spends several lessons on what root in a container can and can't do. For now, do not read "it runs in a container" as "it is not root".
What a container costs
The image was already on disk. Creating a container, running true in it and deleting it took well under half a second of wall-clock time (real, 0.20 s in this run). There's no boot. The runtime sets up namespaces and cgroups and executes one program. The idle container uses 944 KiB of memory and one process (PIDS 1). docker stats shows the limit as the VM's whole memory because no limit was set. The image is 4.28 MB of compressed content and 13.6 MB on disk once unpacked; "Inspecting, exporting and cleaning up images" in Docker in depth explains those two columns. A VM, by comparison, holds memory for its guest kernel and services before your application starts, and it boots before it can do anything. Your timings and memory figures will differ a little.
The trade-off
Sharing a kernel is the price you pay for that speed. A hypervisor puts a hardware-enforced boundary between guests. A container boundary is made of kernel features, and every container makes system calls into the same kernel. So a kernel vulnerability can cross from one container to the host and to its neighbours. That's why container hardening exists: dropping capabilities, seccomp filters, running as a non-root user, keeping the host kernel patched. It's also why some platforms put each container in a small VM or a user-space kernel, as with Kata Containers or gVisor.
Choose a VM when your workload needs a different kernel or operating system. Choose one too when you must separate tenants that don't trust each other, or when the software expects to own the whole machine (a kernel module, a network appliance). Choose containers for many Linux services that can share a kernel, start in under a second and are rebuilt from an image on every release. Most teams run both, with containers inside VMs, and that combination is where this track's lab lives too.
Your turn
Start lab-idle again and compare one more namespace: run readlink /proc/self/ns/net on the VM, then the same command through docker exec lab-idle. Predict whether the two numbers match, check, and compare with the pid and user results above. Finish with docker rm -f lab-idle.
Bottom line
Nothing boots in a container, which is why docker run --rm alpine:3.24 true finished in 0.20 s and the idle container used 944 KiB. The cost is one shared kernel and, by default, a shared user namespace: the matching user link means root in lab-idle is root to the host kernel. When you decide what may run side by side, treat a container as a process with a narrower view, not as a separate machine.
uname -r. What does it print?/etc/os-release and the other userland files come from the Alpine image.uname is a normal program in Alpine's BusyBox. It asks the kernel, and the kernel answers with its own version.readlink /proc/self/ns/user gives the same number on the host and inside a container started with plain docker run. What does that tell you?/proc/self in the container is its own process's view. The pid link it printed was different, so it was not reading the host's.ps showed root). Sharing the user namespace means the UIDs are the same numbers, not the same account as your shell.