Both let you run isolated workloads on shared hardware. The difference is what gets virtualised, and everything else follows from it.
<DockerVmDiagram />A virtual machine virtualises hardware. A hypervisor presents virtual CPUs, memory and disks, and each VM runs a complete guest operating system with its own kernel.
A container virtualises the operating system. It is a process on the host, isolated using kernel features, namespaces for what it can see, cgroups for what it can use, and it shares the host kernel.
Virtual machines Containers
âââââââââââ¬ââââââââââ âââââââââââ¬ââââââââââ
â App A â App B â â App A â App B â
â Libs â Libs â â Libs â Libs â
â Guest OSâ Guest OSâ â own kernel âââââââââââ´ââââââââââ
âââââââââââ´âââââââââ⤠âââââââââââââââââââââ¤
â Hypervisor â â Container runtimeâ
ââââââââââââââââââââ⤠âââââââââââââââââââââ¤
â Host OS â â Host OS â â one kernel
ââââââââââââââââââââ⤠âââââââââââââââââââââ¤
â Hardware â â Hardware â
âââââââââââââââââââââ âââââââââââââââââââââ
A container has no guest OS to boot, no kernel to initialise, and no duplicated system libraries. That single fact produces every practical difference below.
| Container | Virtual machine | |
|---|---|---|
| Startup | Milliseconds | Tens of seconds |
| Size | Megabytes | Gigabytes |
| Density per host | Hundreds | Tens |
| Memory overhead | Near zero | A full OS each |
| Kernel | Shared with host | Its own |
| Run a different OS kernel | No | Yes |
| Isolation strength | Good | Stronger |
| Snapshot and live migrate | Limited | Mature |
A container cannot run a different kernel. Linux containers need a Linux kernel. This is why Docker Desktop on macOS and Windows runs a hidden Linux VM: your containers are not native, and that is the reason bind-mounted file access is noticeably slow on those platforms. Windows containers exist but require a Windows host.
Isolation is weaker than a VM's. Containers share one kernel, so a kernel vulnerability is a shared attack surface, and a container escape reaches the host. A VM escape has to get through the hypervisor, which is a far smaller and more hardened boundary.
For your own code that distinction rarely matters. For genuinely untrusted multi-tenant workloads it does, which is why platforms running arbitrary customer code use VMs, or microVM runtimes like Firecracker and gVisor that combine container ergonomics with a stronger boundary.
Not inherently, but the boundary is thinner, and the defaults are more permissive than people assume.
The practical risks are usually configuration rather than architecture:
USER.--privileged effectively grants host-level access./var/run/docker.sock gives control of the daemon, which is equivalent to root on the host.Set a non-root USER, drop capabilities, use a read-only root filesystem, and scan images. With those, containers are secure enough for the overwhelming majority of workloads.
Containers for stateless applications and microservices, CI build and test isolation, anything needing fast scaling, packaging an application with its dependencies, and local development environments.
Virtual machines when you need a different operating system or kernel, when running genuinely untrusted third-party code, for legacy applications expecting a full OS, where compliance requires hardware-level isolation, and for kernel-level work such as drivers or custom modules.
The framing "containers versus VMs" is mostly a false choice. Nearly every cloud deployment runs containers inside VMs:
You get container density and packaging within a VM boundary between tenants. The hypervisor isolates customers from each other; containers pack workloads efficiently inside that boundary.
That is also why "containers replaced VMs" is wrong. Containers changed how applications are packaged and scheduled; VMs remain the unit of hardware isolation underneath.
| Question | Answer |
|---|---|
| Faster to start? | Container (milliseconds vs tens of seconds) |
| Smaller? | Container (MB vs GB) |
| Stronger isolation? | VM |
| Run a different kernel? | VM only |
| Higher density? | Container |
| Better for untrusted code? | VM, or a microVM runtime |
| What runs on cloud Kubernetes? | Containers, inside VMs |
--privileged, a mounted Docker socket.<MCQExercise exercise={{ id: 'docker-vm-kernel', title: 'Running Linux containers on a Mac', description: 'Docker Desktop on macOS runs Linux containers. How, given macOS does not have a Linux kernel?', options: [ { id: 'a', text: 'Docker Desktop runs a lightweight Linux VM and the containers run inside it, which is also why bind mounts are slow there' }, { id: 'b', text: 'Docker translates Linux system calls to macOS equivalents at runtime' }, { id: 'c', text: 'Linux containers include their own kernel, so the host kernel is irrelevant' }, { id: 'd', text: 'macOS includes a Linux compatibility layer that Docker uses directly' }, ], correctId: 'a', explanation: 'Containers share the host kernel, so a Linux container needs a Linux kernel. Docker Desktop provides one by running a small Linux VM and placing your containers inside it. Everything works transparently, but the boundary is real. File access from a bind mount has to cross between the macOS filesystem and the VM, which is why large projects with many small files feel noticeably slow.', wrongExplanations: { b: 'System call translation is enormously complex and is not what Docker Desktop does. It runs a genuine Linux kernel in a VM.', c: 'Not having their own kernel is the defining property of containers, and the source of their size and startup advantages.', d: 'macOS has no Linux compatibility layer. That approach exists on Windows as WSL, which Docker Desktop can use there.', }, }} />
<MCQExercise exercise={{ id: 'docker-vm-untrusted', title: 'Running untrusted code', description: 'A platform runs arbitrary code submitted by customers. Why would containers alone be a questionable isolation boundary?', options: [ { id: 'a', text: 'Containers share the host kernel, so a kernel vulnerability or container escape reaches the host and every other tenant' }, { id: 'b', text: 'Containers cannot enforce CPU or memory limits on untrusted workloads' }, { id: 'c', text: 'Containers have no way to restrict network access between tenants' }, { id: 'd', text: 'Container filesystems are always shared between all containers on a host' }, ], correctId: 'a', explanation: 'Isolation between containers is enforced by kernel namespaces and cgroups, and all containers on a host share one kernel. That makes the kernel a shared attack surface: a privilege escalation bug there can cross the boundary. A VM escape must additionally defeat the hypervisor, a much smaller and more hardened interface. This is why platforms running customer code use VMs or microVM runtimes such as Firecracker.', wrongExplanations: { b: 'cgroups enforce CPU and memory limits effectively. Resource control is not the weak point.', c: 'Network isolation is well supported through user-defined networks and network policies.', d: 'Each container has its own filesystem view. Sharing only happens through volumes you explicitly mount.', }, }} />
No. VMs provide stronger isolation. All containers on a host share one kernel, so a kernel vulnerability or a container escape can reach the host and other containers, whereas a VM escape must also get past the hypervisor, a far smaller and more hardened boundary. For your own code the difference rarely matters in practice. For genuinely untrusted multi-tenant code it does, which is why such platforms use VMs or microVM runtimes like Firecracker and gVisor.
Not natively, because containers share the host kernel. Windows containers need a Windows kernel and Linux containers need a Linux one. Docker Desktop makes Linux containers work on macOS and Windows by running a lightweight Linux VM behind the scenes, on Windows it typically uses WSL 2. Everything behaves transparently, but it explains why bind-mounted file access is noticeably slower on those platforms than on native Linux.
No, and in practice they are used together. Almost every cloud container platform runs containers inside VMs: an EKS or GKE node is a VM hosting many pods, and Fargate runs each task in its own microVM. You get container packaging and density within a VM boundary that separates tenants. Containers changed how applications are packaged and scheduled; VMs remain the unit of hardware-level isolation underneath.
When you need a different operating system or kernel version than the host, when running genuinely untrusted third-party code, for legacy applications that expect a full operating system with init and multiple services, when compliance requires hardware-level isolation between workloads, and for kernel-level work such as drivers or custom modules. For everything else, stateless services, CI jobs, development environments, containers are lighter and faster.
Because Linux containers cannot run natively there. Docker Desktop runs a Linux VM and your containers live inside it, so every bind-mounted file access crosses the boundary between the host filesystem and the VM. For a project with many small files, such as a large node_modules, this is the dominant cost. Mitigate it by bind-mounting only the source directory, keeping dependency directories in named volumes, and enabling VirtioFS in Docker Desktop's settings.
Choose between Docker Desktop and Docker Engine, check the requirements, verify the install with docker run hello-world, and fix the three most common failures.
Install Docker Desktop on Windows 10 or 11 with the WSL 2 backend. Covers prerequisites, settings, troubleshooting and filesystem choice for build speed.
Install Docker Desktop on macOS for Intel or Apple Silicon (M1, M2, M3). Covers setup, resource settings, the Dashboard, verifying the install and common fixes.