Containers vs. VMs: The Foundation
- 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:
| Namespace | What It Isolates |
|---|---|
| PID | Process IDs — container sees only its own processes |
| NET | Network interfaces, IP addresses, routing tables |
| MNT | Filesystem mount points |
| UTS | Hostname and domain name |
| IPC | Inter-process communication (shared memory, semaphores) |
| USER | User 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)
| Characteristic | Virtual Machine | Container |
|---|---|---|
| Virtualization level | Hardware (via hypervisor) | Operating System (via kernel) |
| Includes | Full guest OS + kernel + apps | App + libs only (shares host kernel) |
| Size | Gigabytes (full OS image) | Megabytes (app + dependencies) |
| Boot time | Minutes | Milliseconds to seconds |
| Isolation | Strong (separate kernel) | Process-level (shared kernel) |
| Resource overhead | High (each VM runs its own kernel) | Low (containers share host kernel) |
| Density | ~10-20 VMs per host | ~100-1000+ containers per host |
| Use case | Running different OS types, strong security boundaries | Microservices, 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:
- Takes the read-only image layers
- Adds a writable layer on top (the "container layer")
- Sets up namespaces, cgroups, and networking
- Executes the command defined in
CMDorENTRYPOINT
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."
| Registry | Type | Notes |
|---|---|---|
| Docker Hub | Public / Private | Default registry, rate-limited for free tier |
| GitHub Container Registry (GHCR) | Public / Private | Integrated with GitHub Actions |
| AWS ECR | Private | Integrated with IAM for auth |
| Google Artifact Registry | Private | Successor to GCR, multi-format |
| Azure Container Registry (ACR) | Private | Integrated with Azure AD |
| Quay.io | Public / Private | Red Hat managed, vulnerability scanning |
| Harbor | Self-hosted | CNCF 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).
| Runtime | Description |
|---|---|
| containerd | Industry standard. Default runtime in most Kubernetes distributions. Graduated CNCF project. |
| CRI-O | Lightweight runtime built specifically for Kubernetes. Used by OpenShift. |
| Docker Engine | The 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:
- Image Specification: How container images are structured (layers, manifests, config)
- Runtime Specification: How containers are created and run (
runcis 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
- You (or your CI/CD pipeline) build your container image using Docker, Podman, or Buildah
- You push the image to a container registry
- 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
-
Using
latesttag in production: Thelatesttag is mutable. Twokubectl applyruns may pull different images, causing inconsistent deployments. Always pin to a specific version or digest. -
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.
-
Running as root: By default, containers run as root inside the container. This is a security risk. Use
USERin your Dockerfile to run as a non-root user. -
Ignoring layer caching: Put instructions that change frequently (like
COPY src/) after instructions that change rarely (likeRUN npm install) to maximize build cache hits. -
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
- Build a simple container image:
echo 'FROM nginx:alpine' > Dockerfile
docker build -t my-nginx:v1 . - Run it locally:
docker run -d -p 8080:80 my-nginx:v1
curl http://localhost:8080 - Inspect the layers:
docker history my-nginx:v1
What's Next?
Now that you understand containers, proceed to:
- Kubernetes Architecture — Learn how the Control Plane and Worker Nodes manage containers
- Setting Up Your Lab — Run a Kubernetes cluster locally
- Pods — See how Kubernetes wraps containers in its smallest deployable unit