Skip to main content

Containers vs. VMs: The Foundation

Key Takeaways for AI & Readers
  • OS Virtualization: Containers share the host kernel but isolate user space using Linux namespaces and cgroups, making them far lighter than VMs.
  • Image vs. Container: An image is a read-only, layered filesystem blueprint; a container is a running instance of that image with its own writable layer.
  • OCI Standard: The Open Container Initiative defines the image and runtime specifications, ensuring portability across different tools (Docker, Podman, containerd).
  • Registry: Centralized storage (Docker Hub, ECR, GCR, GHCR) where images are pushed and pulled.
  • Runtime: The engine (containerd, CRI-O) that creates and manages container processes on a host.

Before diving into Kubernetes, you must understand the atom it manages: the Container. Kubernetes does not build your code. It does not build your container images. It takes pre-built container images and orchestrates their lifecycle across a cluster. Understanding what containers are — and are not — is essential.


What is a Container?

A container is a standard unit of software that packages up application code together with all its dependencies — runtime, libraries, system tools, and configuration — so the application runs quickly and reliably regardless of the computing environment.

Under the hood, containers are not a single technology. They are a combination of several Linux kernel features:

Linux Namespaces (Isolation)

Namespaces give each container its own isolated view of the system:

NamespaceWhat It Isolates
PIDProcess IDs — container sees only its own processes
NETNetwork interfaces, IP addresses, routing tables
MNTFilesystem mount points
UTSHostname and domain name
IPCInter-process communication (shared memory, semaphores)
USERUser and group IDs (allows UID remapping)

Control Groups (cgroups) — Resource Limits

While namespaces provide isolation, cgroups provide resource governance. They allow you to set hard limits on how much CPU, memory, disk I/O, and network bandwidth a container can consume. This prevents a runaway process in one container from starving others.

# Example: limit a container to 512MB RAM and 0.5 CPU cores
docker run --memory=512m --cpus=0.5 nginx:alpine

Union Filesystems (OverlayFS)

Container images use a layered filesystem. Each instruction in a Dockerfile creates a new read-only layer. When you run a container, a thin writable layer is added on top. This design means:

  • Sharing: Ten containers from the same image share the read-only layers, saving disk space.
  • Speed: Starting a container only requires creating the writable layer, which takes milliseconds.

Containers vs Virtual Machines (VMs)

App ABins/Libs
App BBins/Libs
App CBins/Libs
Guest OS
Guest OS
Guest OS
Hypervisor
Host Operating System
Infrastructure (Hardware)
Each App has its own Operating System. Heavy, slow to boot, excellent isolation.
CharacteristicVirtual MachineContainer
Virtualization levelHardware (via hypervisor)Operating System (via kernel)
IncludesFull guest OS + kernel + appsApp + libs only (shares host kernel)
SizeGigabytes (full OS image)Megabytes (app + dependencies)
Boot timeMinutesMilliseconds to seconds
IsolationStrong (separate kernel)Process-level (shared kernel)
Resource overheadHigh (each VM runs its own kernel)Low (containers share host kernel)
Density~10-20 VMs per host~100-1000+ containers per host
Use caseRunning different OS types, strong security boundariesMicroservices, CI/CD, density at scale

Key insight: Containers are not "lightweight VMs." They are isolated processes running on a shared Linux kernel. This is why a container hosting an Alpine Linux image is only ~5MB, whereas a VM running Alpine needs the entire kernel plus OS — hundreds of megabytes.

When VMs Still Win

Containers share the host kernel, which means:

  • You cannot run a Windows container on a Linux host (natively)
  • A kernel vulnerability affects all containers on that host
  • Some compliance frameworks require VM-level isolation (e.g., multi-tenant SaaS)

In practice, production Kubernetes clusters run containers inside VMs — you get the security boundary of VMs for the infrastructure layer and the speed and density of containers for the application layer.


Key Concepts

1. Container Image

An image is the read-only blueprint for a container. It contains the application binary, its dependencies, environment variables, and metadata about how to run it.

Images are built using a Dockerfile (or Containerfile for Podman):

# Example: a simple Node.js application image
FROM node:20-alpine # Base layer: Alpine Linux + Node.js
WORKDIR /app # Set working directory
COPY package*.json ./ # Copy dependency manifests
RUN npm ci --production # Install dependencies (new layer)
COPY src/ ./src/ # Copy application source code
EXPOSE 3000 # Document the port
CMD ["node", "src/index.js"] # Default command to run

Each instruction creates a layer. Layers are cached — if package.json hasn't changed, Docker reuses the cached npm ci layer, making rebuilds fast.

Image Tags and Digests

Images are identified by a tag (mutable) or a digest (immutable):

# Tag (mutable — can point to different images over time)
nginx:1.27

# Digest (immutable — always points to the exact same image)
nginx@sha256:3c4c1f42a89e343c7b050c5e5d6f803a...

Best Practice: In production Kubernetes manifests, use image digests or specific version tags (e.g., nginx:1.27.0), never latest. The latest tag is mutable and can lead to unpredictable deployments.

2. Container

A container is the running instance of an image. When you start a container, the runtime:

  1. Takes the read-only image layers
  2. Adds a writable layer on top (the "container layer")
  3. Sets up namespaces, cgroups, and networking
  4. Executes the command defined in CMD or ENTRYPOINT

You can run many containers from the same image, each with its own writable layer, network identity, and process tree.

3. Container Registry

A registry is a remote storage service for container images. Think of it as "GitHub for container images."

RegistryTypeNotes
Docker HubPublic / PrivateDefault registry, rate-limited for free tier
GitHub Container Registry (GHCR)Public / PrivateIntegrated with GitHub Actions
AWS ECRPrivateIntegrated with IAM for auth
Google Artifact RegistryPrivateSuccessor to GCR, multi-format
Azure Container Registry (ACR)PrivateIntegrated with Azure AD
Quay.ioPublic / PrivateRed Hat managed, vulnerability scanning
HarborSelf-hostedCNCF graduated project, enterprise features

Push and Pull Workflow

# Build the image
docker build -t my-app:v1.0 .

# Tag it for the registry
docker tag my-app:v1.0 ghcr.io/my-org/my-app:v1.0

# Push to the registry
docker push ghcr.io/my-org/my-app:v1.0

# Kubernetes pulls from the registry when creating Pods

4. Container Runtime

The runtime is the engine that actually creates and manages container processes on a host machine. In the Kubernetes context, the runtime must implement the Container Runtime Interface (CRI).

RuntimeDescription
containerdIndustry standard. Default runtime in most Kubernetes distributions. Graduated CNCF project.
CRI-OLightweight runtime built specifically for Kubernetes. Used by OpenShift.
Docker EngineThe original container runtime. Since Kubernetes v1.24, Docker is no longer supported as a CRI runtime (but images built with Docker still work everywhere).

Important clarification: When Kubernetes "deprecated Docker" in v1.20 (removed in v1.24), it deprecated the Docker shim — the bridge between Kubernetes and Docker Engine. Container images built with docker build are OCI-compliant and work with every runtime. Nothing changed for developers.


The OCI Standard

The Open Container Initiative (OCI), founded in 2015, defines two critical specifications:

  1. Image Specification: How container images are structured (layers, manifests, config)
  2. Runtime Specification: How containers are created and run (runc is the reference implementation)

This standardization means an image built with Docker can run with containerd, CRI-O, or Podman — interchangeably. You are never locked into a single tool.


The Kubernetes Connection

Kubernetes does not build images. Here is the workflow:

Developer → Build Image → Push to Registry → Kubernetes pulls & runs
  1. You (or your CI/CD pipeline) build your container image using Docker, Podman, or Buildah
  2. You push the image to a container registry
  3. Kubernetes pulls the image from the registry and runs containers from it according to your Pod specifications
# Kubernetes references your image by registry path + tag
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: web
image: ghcr.io/my-org/my-app:v1.0 # ← Registry + image + tag
ports:
- containerPort: 3000
imagePullSecrets:
- name: ghcr-secret # ← Credentials for private registries

Common Pitfalls

  1. Using latest tag in production: The latest tag is mutable. Two kubectl apply runs may pull different images, causing inconsistent deployments. Always pin to a specific version or digest.

  2. Large images: A 2GB image means slow pull times and slower Pod startup. Use multi-stage builds and minimal base images (Alpine, distroless) to keep images small.

  3. Running as root: By default, containers run as root inside the container. This is a security risk. Use USER in your Dockerfile to run as a non-root user.

  4. Ignoring layer caching: Put instructions that change frequently (like COPY src/) after instructions that change rarely (like RUN npm install) to maximize build cache hits.

  5. Missing health checks: A container that starts does not mean the application is ready. Always define liveness and readiness probes in your Kubernetes manifests.


Hands-On Exercise

  1. Build a simple container image:
    echo 'FROM nginx:alpine' > Dockerfile
    docker build -t my-nginx:v1 .
  2. Run it locally:
    docker run -d -p 8080:80 my-nginx:v1
    curl http://localhost:8080
  3. Inspect the layers:
    docker history my-nginx:v1

What's Next?

Now that you understand containers, proceed to: