Definition
- Virtual Machine – a hypervisor presents virtual hardware to each VM, and each VM runs its own complete guest OS including its own kernel.
- Container – no guest OS, no virtual hardware; an isolated process on the host kernel, with namespaces for view isolation and cgroups for resource limits.
- Hypervisor – software (KVM, VMware ESXi, Hyper-V) that manages virtual hardware; type 1 runs on bare metal, type 2 on a host OS.
- Namespaces – Linux kernel feature giving each container its own view of processes (PID), network, mounts, users, and hostname.
- Cgroups – Linux kernel feature capping and accounting CPU and memory per container.
Imagine the National Bank of India needs many independent offices. A VM is a separate self-contained building for each office, with its own foundation, plumbing, and power generator (its own kernel), sitting on a shared plot managed by a site manager (the hypervisor). A container is partitioning one large building into offices with walls and locked doors (namespaces) and a meter limiting each office's electricity (cgroups), all sharing one foundation (the host kernel). Containers are fast and dense, but a serious problem in the foundation affects everyone.
Understanding the Concepts
What is a Virtual Machine?
A VM virtualizes the hardware layer. The hypervisor presents virtual CPU, memory, disk, and network to each VM. Each VM boots a full guest operating system, including its own kernel, drivers, and system services.
Physical Hardware ┌────────────────────────────────────┐ │ Hypervisor (KVM/ESXi) │ ├──────────┬──────────┬──────────────┤ │ VM 1 │ VM 2 │ VM 3 │ │ Guest OS │ Guest OS │ Guest OS │ │ Kernel │ Kernel │ Kernel │ │ App │ App │ App │ └──────────┴──────────┴──────────────┘ Each VM: full OS, own kernel, GBs, boots in seconds
What is a Container?
A container shares the host kernel. There is no guest OS and no virtual hardware. The container is an isolated process using Linux namespaces for view isolation and cgroups for resource limits.
Physical Hardware ┌────────────────────────────────────┐ │ Host OS + Kernel │ │ Container Runtime │ ├──────────┬──────────┬──────────────┤ │ Ctr 1 │ Ctr 2 │ Ctr 3 │ │ Libs │ Libs │ Libs │ │ App │ App │ App │ └──────────┴──────────┴──────────────┘ Each container: app + libs only, MBs, starts in ms
How do VMs and containers compare?
| Aspect | Virtual Machine | Container |
|---|---|---|
| Virtualization | Hardware (CPU, memory, disk) | OS-level (namespaces, cgroups) |
| Guest OS | Full OS with its own kernel | None, shares host kernel |
| Image size | Gigabytes | Megabytes |
| Start time | Tens of seconds (OS boot) | Milliseconds (process start) |
| Density | Low (heavy per-VM overhead) | High (no kernel overhead) |
| Isolation | Strong (hypervisor boundary) | Weaker (shared kernel) |
| Resource overhead | High (guest kernel, drivers) | Low (only app + libs) |
| Use case | Multi-tenant, untrusted workloads | Microservices, fast scaling |
What are namespaces and cgroups?
Namespaces give each container its own isolated view. Cgroups enforce resource limits.
# PID namespace: container sees only its own processes
docker exec transfer-service-1 ps aux
# PID 1: java -jar app.jar
# The container cannot see processes from other containers or the host.
# Network namespace: container has its own network stack
docker exec transfer-service-1 ip addr
# Shows the container's own eth0 interface and IP, isolated from the host.
# Cgroups: cap memory and CPU
docker run -d --name transfer-service \
--memory=512m --cpus=1.0 \
registry.bank.local/transfer-service:1.2.0
# If the process exceeds 512 MB, the kernel OOM-kills it.
# Kubernetes: resource limits via cgroups
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: transfer-service
image: registry.bank.local/transfer-service:1.2.0
resources:
requests: { memory: "256Mi", cpu: "250m" }
limits: { memory: "512Mi", cpu: "1000m" }
What is the isolation trade-off?
All containers on a host share one kernel. A kernel vulnerability or a container escape can potentially affect the host and sibling containers. A hypervisor is a stronger boundary.
# Container hardening: reduce the attack surface
docker run -d --name transfer-service \
--user 1000:1000 \ # Non-root user
--read-only \ # Read-only filesystem
--cap-drop ALL \ # Drop all Linux capabilities
--security-opt no-new-privileges \ # No privilege escalation
registry.bank.local/transfer-service:1.2.0
# Kubernetes: security context hardening
spec:
containers:
- name: transfer-service
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
Why do containers run inside VMs in production?
In practice, Kubernetes worker nodes are VMs. This gives hypervisor isolation at the node level and container density within the node.
Cloud Infrastructure ┌───────────────────────────────────────────┐ │ VM (K8s Node 1) VM (K8s Node 2) │ │ ┌─────┬─────┐ ┌─────┬─────┐ │ │ │Ctr A│Ctr B│ │Ctr C│Ctr D│ │ │ └─────┴─────┘ └─────┴─────┘ │ │ Host Kernel Host Kernel │ ├───────────────────────────────────────────┤ │ Hypervisor (KVM) │ └───────────────────────────────────────────┘ Hypervisor isolates VMs. Containers give density within VMs.
What about stronger container isolation?
For highly sensitive workloads, sandboxed runtimes give each container a thin VM boundary while keeping much of the speed.
# gVisor: user-space kernel intercepts syscalls
# Kata Containers: lightweight microVM per container
# Firecracker: Amazon's microVM used by AWS Lambda
# Kubernetes: RuntimeClass for sensitive workloads
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
---
apiVersion: v1
kind: Pod
metadata:
name: sensitive-payment-pod
spec:
runtimeClassName: gvisor # Stronger isolation for payment workloads
containers:
- name: payment-service
image: registry.bank.local/payment-service:1.0.0
What are common mistakes?
# WRONG: running containers as root with all capabilities.
docker run -d --privileged registry.bank.local/transfer-service:1.2.0
# A container escape gives root access to the entire host.
# CORRECT: non-root, dropped capabilities, read-only.
docker run -d --user 1000 --cap-drop ALL --read-only \
registry.bank.local/transfer-service:1.2.0
What should a senior engineer know?
- The shared-kernel model means a container's userspace can differ from the host (different distro base) but cannot run a different kernel or OS family.
- For highly sensitive banking workloads, use microVM runtimes or dedicated node pools, accepting overhead for a stronger boundary.
- Cgroups prevent one runaway service from starving neighbours by capping CPU and memory per container.
- VMs remain the standard for mutually untrusted tenants. Containers are for trusted workloads within a VM boundary.
Visual Overview
Virtual Machine: ┌────────────────────────────────────┐ │ Hypervisor │ ├──────────┬──────────┬──────────────┤ │ VM 1 │ VM 2 │ VM 3 │ │ Guest OS │ Guest OS │ Guest OS │ │ Kernel │ Kernel │ Kernel │ │ App A │ App B │ App C │ └──────────┴──────────┴──────────────┘ Strong isolation, heavy, slow boot Container: ┌────────────────────────────────────┐ │ Host OS + Kernel │ │ Namespaces + Cgroups │ ├──────────┬──────────┬──────────────┤ │ Ctr A │ Ctr B │ Ctr C │ │ Libs │ Libs │ Libs │ │ App A │ App B │ App C │ └──────────┴──────────┴──────────────┘ Weaker isolation, light, ms boot Production: containers inside VMs ┌──── VM (K8s Node) ────┐ │ ┌─────┐ ┌─────┐ │ │ │Ctr A│ │Ctr B│ ... │ ← Container density │ └─────┘ └─────┘ │ │ Host Kernel │ └────────────────────────┘ ← VM isolation
Key Takeaway
The difference comes down to what each virtualizes and where the isolation boundary sits. A VM virtualizes hardware: a hypervisor presents virtual CPU, memory, and disk, and each VM boots its own complete guest OS with its own kernel. A container does not virtualize hardware and has no guest OS; it is an isolated process on the host kernel, with namespaces for view isolation and cgroups for resource limits.
That architectural difference drives every practical consequence. VMs are heavy (GBs), boot in seconds, and spend significant memory on the guest OS. Containers are light (MBs), start in milliseconds, and pack far more densely per host. The trade-off is isolation: a hypervisor is a strong boundary that contains even kernel-level compromises, while containers share one kernel, making the boundary thinner.
In practice, containers run inside VMs. Each Kubernetes worker node is a VM, giving hypervisor isolation at the node level and container density within the node. For highly sensitive banking workloads, microVM runtimes or dedicated node pools provide a stronger boundary per container.
Remember one sentence: a VM runs a full guest OS on virtualized hardware for strong isolation, while a container shares the host kernel for speed and density.
---