Images vs Containers
An image is a template — an immutable, layered filesystem snapshot with a config (which command to run, ports, env vars). A container is a running instance of an image — a process with its own isolated view of the filesystem, network, PID, and user namespaces. The relationship is exactly class → object: you build an image once, run a container per replica.
The container’s isolation is provided by Linux kernel features, not virtualization:
- Namespaces — isolate the view (PID, network, mount, user, UTS, IPC).
- cgroups — limit and account resources (CPU, memory, I/O).
That’s why containers are light: no OS, no hypervisor — one shared kernel, isolated processes. The image holds the userspace of the app (binary, libraries, config), and the kernel comes from the host.
The Layered Filesystem
A Docker image is a stack of read-only layers, each produced by one Dockerfile instruction:
FROM node:20-slim # base layer
WORKDIR /app
COPY package.json . # layer
RUN npm ci # layer (dependencies)
COPY . . # layer (source)
CMD ["node", "server.js"] # runtime config
The superpower is layer caching: if a lower layer doesn’t change, Docker reuses it. That’s why you copy package.json before the source — dependency install (npm ci) only re-runs when the dependency manifest changes, not on every code edit. The ordering of instructions is your build performance.
The union filesystem (overlayfs) shows a merged view of the layers, with a thin container layer (read-write) on top for the running process.
Best Practices
- Order for caching — copy manifests first, install deps, then copy source.
- Pin base images —
node:20.11-slim@sha256:...or at least a minor version;latestis a moving target and a reproducibility bug. - Run as non-root — create a user in the image and
USERit; containers running as root are the #1 Docker security smell. - Multi-stage builds — compile in a fat builder image, copy only the artifacts into a slim runtime image. Your final image carries the binary, not the toolchain:
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app/bin .
FROM gcr.io/distroless/base
COPY --from=build /app/bin /bin/app
ENTRYPOINT ["/bin/app"]
The distroless runtime has no shell, no package manager, no compiler — smaller surface, smaller image, fewer CVEs.
Containers Are Ephemeral
A container’s writable layer dies with the container. For durable state you need:
- Volumes — a directory managed by the container runtime (
docker run -v vol_name:/data), surviving container restarts; the right place for databases and app state. - Bind mounts — a host directory mounted in (
-v /host/path:/data), useful in dev for live-reload; not portable across hosts.
Statelessness is the design goal: if your service can be killed and recreated without loss, you can scale it, roll it, and run it anywhere. Anything you must keep becomes an explicit volume (or, better, an external store — see Kubernetes for the managed story).
Container Networking
By default containers get an isolated network namespace with a bridge to the host:
- Bridge (default) — containers share a virtual bridge, communicate with each other, and reach the outside via NAT. Ports are published to the host with
-p 8080:80. - Host — the container shares the host’s network stack directly (no NAT); simpler, but no per-container isolation.
- Overlay (Kubernetes) — a virtual network spanning many hosts so pods on different machines talk as if on one L2 network. This is the layer Kubernetes networking is built on.
Networking and security meet here: an unpublished port is unreachable from outside; a container without an explicit network is isolated by default — the design gives you isolation, and you opt into exposure.
The Visualizer Mental Model
Think of docker build as producing a DAG of layers with cached nodes, docker run as instantiating the image with a fresh writable layer and network namespace, and docker compose as declaring a fleet of containers (app, db, cache) with their networks and volumes in YAML — the stepping stone to Kubernetes, where all of this is managed declaratively at cluster scale.
Practice Trajectory
- Write a Dockerfile for a tiny Go or Node app and
docker buildit; observe which steps were cached on rebuild. - Reorder your Dockerfile so a source change doesn’t re-run
npm install— measure the build-time difference. - Convert the build to a multi-stage build and compare the final image size (
docker images). - Run a container with
-p, connect to it from another container, and verify the isolation boundary. - Run a container as root vs non-root and list the container’s capabilities with
docker exec+id/capsh.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Reproducible app environments | Docker image as the artifact |
| “Works on my machine” | Containers kill the excuse |
| Stateless web services | Containers — ephemeral, scalable |
| Database / durable state | Container + volume (or managed DB) |
| Local dev parity | Docker Compose |