Containers solved the "works on my machine" problem that plagued software deployments for decades. By bundling an application with its exact runtime dependencies into a portable image, containers make deployment deterministic — the same image that passes CI is exactly what runs in production. In 2026, containers are not a trend; they are the default.
What changed in 2026
- Rootless by default. Docker Desktop 4.x and Podman 5 run containers as non-root out of the box. Security-sensitive teams moved to Podman or rootless Docker entirely.
- BuildKit is the only builder. The legacy Docker builder is deprecated; BuildKit enables parallel builds, cache mounts, and secrets that never land in image layers.
- OCI images everywhere. The Open Container Initiative image spec means images built with Docker, Podman, Kaniko, or Buildah are interchangeable.
- WebAssembly as a container alternative. WASM modules run in
wasmtime or wasmedge with sub-millisecond cold starts for short-lived workloads — not containers, but worth knowing.
Containers vs virtual machines
| Dimension |
Container |
Virtual Machine |
| Startup time |
Milliseconds |
Seconds to minutes |
| Size |
MBs |
GBs |
| Isolation |
Process + namespace |
Full kernel |
| Overhead |
Minimal |
~10–20 % CPU/memory |
| Use case |
App deployment, CI |
Full OS isolation, legacy apps |
Containers share the host kernel; VMs run their own. The trade-off is startup speed and density vs stronger isolation.
How containers work
A container is a Linux process isolated using three kernel primitives:
- Namespaces — isolate the process's view of the system (PID, network, filesystem, users).
- cgroups — limit and account for CPU and memory usage.
- Union filesystems (overlay) — layer read-only image layers under a writable container layer.
When you run docker run nginx, Docker unpacks the nginx image layers, creates namespaces and cgroups, and starts the nginx process inside that environment.
Multi-stage builds
The most impactful optimisation for production images:
# Stage 1: build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: production — only the output
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Non-root user
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER app
EXPOSE 3000
CMD ["node", "dist/index.js"]
This pattern strips build tools, dev dependencies, and source files — typical Node.js images shrink from ~1.2 GB to ~120 MB.
Image size comparison
| Base image |
Approximate size |
Use case |
ubuntu:24.04 |
~80 MB |
Debugging, legacy scripts |
node:22 |
~1.1 GB |
Avoid in production |
node:22-alpine |
~170 MB |
Most Node apps |
node:22-slim |
~240 MB |
When Alpine glibc causes issues |
gcr.io/distroless/nodejs22 |
~90 MB |
Maximum security |
| Scratch + static binary |
~5–20 MB |
Go, Rust production binaries |
Security baseline in 2026
# 1. Pin exact versions — never use :latest
FROM node:22.4.1-alpine3.20
# 2. Run as non-root
RUN addgroup -S app && adduser -S app -G app
USER app
# 3. Read-only root filesystem (set at runtime)
# docker run --read-only --tmpfs /tmp myimage
# 4. No secrets in layers — use BuildKit secrets
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
Scan images with docker scout, trivy, or grype in CI before pushing to registry.
How to pick
- Deploying any server workload? → Container. Full stop.
- Need full OS isolation (legacy app, kernel modules)? → VM or bare metal.
- Running short-lived functions? → Container, but consider Lambda/Cloud Run to avoid managing the runtime.
- Want the fastest possible cold start (<1 ms)? → Look at WASM for stateless compute.
- Building locally? → Docker Compose for multi-service dev stacks.
Common mistakes
Using :latest tags. Latest is not pinned — a base image update can silently change your runtime. Pin versions (node:22.4.1-alpine3.20).
Copying .env and secrets into the image. Secrets baked into layers are readable by anyone with pull access. Use BuildKit secrets, environment variables at runtime, or a secrets manager.
Installing tools in production images. curl, git, vim have no place in a production image. They increase attack surface and size.
Not setting resource limits. A container without CPU/memory limits can starve its neighbours on shared infrastructure. Always set --memory and --cpus (or Kubernetes resources.requests/limits).
What to skip
docker-compose in production — it lacks health-check-driven orchestration and auto-restart policies that Kubernetes or Nomad provide.
- Building images as root — rootless builds are secure by default and required in many enterprise environments.
- Fat images with full OS shells for debugging — use ephemeral debug containers (
kubectl debug) instead of baking shells into every image.
FAQ
What is the difference between an image and a container?
An image is the read-only template (like a class). A container is the running instance (like an object). You can run many containers from the same image.
Do I need Kubernetes to use containers?
No. Docker Compose, AWS ECS, Fly.io, Railway, and Render all run containers without Kubernetes. Use Kubernetes when you need auto-scaling, rolling deployments, and self-healing across many nodes.
How do I share data between containers?
Use named Docker volumes for persistent data, or bind mounts for development. For production, prefer managed storage (S3, Cloud SQL) outside the container entirely.
What is containerd and how does it relate to Docker?
containerd is the low-level container runtime that Docker uses internally. Kubernetes dropped the Docker shim and talks to containerd directly. You rarely interact with containerd unless you are building a platform.
Where to go next