Skip to content
QSWEQB
mediumConceptEntryMidSenior

What is a container image, and why is a container not a virtual machine?

An image is an ordered stack of read-only filesystem layers plus a config document, addressed by digest. A container is an ordinary host process with namespaces restricting what it can see and cgroups restricting what it can use, sharing the host kernel instead of booting its own.

5 min readUpdated 2026-07-26

What the interviewer is scoring

  • Does the candidate describe an image as layers plus a config document rather than as "a lightweight VM image"
  • Whether they can name the mechanism, namespaces for visibility and cgroups for consumption, instead of saying "isolation"
  • That they connect layer ordering in a Dockerfile to which builds hit the cache and which do not
  • Whether the security answer follows from the shared kernel rather than being a list of memorised hardening flags
  • Does the candidate know that a later instruction cannot remove data from an earlier layer

Answer

An image is a stack of diffs and a config document

An image is not a disk image, and there is no filesystem inside it in the sense a VM has one. It is a manifest listing a config blob and an ordered set of layer blobs. Each layer is a tar archive of the changes one build step made: files added, files modified, and markers recording files removed. The config blob holds what a runtime needs to start the thing — entrypoint and command, environment, working directory, declared user, and the ordered layer digests.

Everything is content-addressed, so a layer's identity is the digest of its contents. Two images sharing a base share one blob on disk and in the registry, a pull transfers only the layers you lack, and a reference by digest is reproducible where a tag is a mutable pointer someone can move under you. At run time the layers are stacked into a single view by a union filesystem, usually overlayfs, with a thin writable layer on top, which is why starting a container from a 400 MB image is cheap: nothing is copied, a mount is assembled, and the first write to an existing file copies up that one file.

Layers are additive, which is the part people get wrong

Because each instruction appends a layer, no later instruction can shrink an earlier one. Removing a file adds a whiteout marker that hides it from the merged view; the bytes remain in the layer below and travel with the image forever. That is a size problem and, more seriously, a security one.

# Wrong twice over. The key is baked into layer 1 and stays there.
COPY id_rsa /root/.ssh/id_rsa
RUN git clone git@internal:repo.git && rm /root/.ssh/id_rsa

# Right: the secret is mounted for one command and never enters a layer.
RUN --mount=type=secret,id=deploy_key \
    GIT_SSH_COMMAND="ssh -i /run/secrets/deploy_key" git clone git@internal:repo.git

Anyone who can pull that first image can unpack the layer and read the key, whatever the final filesystem looks like. So a credential must never be a build argument or a copied file; a BuildKit secret mount exposes it to one RUN and leaves no trace. The size version of the same rule is that a package cache must be deleted in the RUN that created it, because that is the only way it never becomes a layer at all.

The build cache and why instruction order is a design decision

The builder reuses a cached layer while the step is unchanged. For most instructions "unchanged" means identical text; for COPY and ADD it also means an identical checksum of the copied files. The moment one step misses, every step after it rebuilds, because each layer is defined relative to the one beneath it. That rule dictates the shape of every good Dockerfile: what changes rarely goes early, what changes on every commit goes late.

# Dependency manifests only. Changes when dependencies change, so the
# expensive install below stays cached across ordinary code edits.
COPY package.json package-lock.json ./
RUN npm ci

# Source last, because this checksum changes on every commit.
COPY src ./src

Invert those two blocks and every one-line code change reinstalls the dependency tree. A .dockerignore belongs in the same conversation, since without it COPY . . sends node_modules and .git into the context and changes the cache key for reasons unrelated to your code.

Multi-stage builds extend the idea to the artefact: a build stage carries the compiler and the full dependency tree, and the final stage starts from a minimal base and copies only the built output with COPY --from, so nothing from the discarded stages reaches the shipped layers. It is the largest size reduction available to most images and it improves security for the same reason — a compiler that is not present cannot be used by an attacker who gets a shell.

What a container is at run time

Start a container and look at the host process table: the process is there, scheduled by the host kernel like any other. No guest kernel, no bootloader, no virtual hardware, no hypervisor. Two kernel features do the work.

Namespaces control what the process can see. Mount gives it its own filesystem view, PID its own process tree where the entrypoint is PID 1, network its own interfaces and port space, while UTS, IPC, user and cgroup namespaces partition hostname, shared memory, user IDs and the cgroup hierarchy. The user namespace matters most for security, because it lets a process be root inside the container while mapping to an unprivileged UID on the host.

Cgroups control what it can consume: CPU shares and quota, memory limit, process count, I/O bandwidth. A container exceeding its memory limit is killed by the host kernel's OOM killer, which is why exit code 137 in a crash loop points at a limit rather than at a bug. Capabilities, seccomp profiles and an LSM such as AppArmor or SELinux then narrow what the process may ask the kernel to do. None of this creates a machine; it constrains a process.

Why the distinction has real consequences

A VM has its own kernel on virtualised hardware, so the boundary a guest must cross to reach the host is the hypervisor, which is small and heavily scrutinised. A container shares the host kernel, so the boundary is the whole syscall surface, and one kernel vulnerability reachable from inside a container is potentially a compromise of every other container on that host. This is why platforms running genuinely untrusted code put a VM boundary underneath the container, via something like Firecracker or Kata Containers, rather than trusting namespaces alone. The same shared kernel explains the rest: startup is milliseconds because nothing boots, density is high because no guest OS is duplicated, and a Linux container cannot run on a Windows or macOS kernel at all, which is why Docker Desktop quietly runs a Linux VM and your "native" containers are guests inside it.

Practices that follow rather than practices to memorise

Each recommendation worth making is a consequence of something above. Pin base images by digest, because a tag is mutable. Choose a distroless or slim base, because packages you do not ship cannot be exploited. Set a non-root USER and drop capabilities, because root in the container is uncomfortably close to root on the host without user namespaces. Scan the built image rather than the source tree, because the image is what ships, and rebuild regularly, since a base that was clean when you pinned it accumulates published CVEs without changing a byte.

The image is layers plus config; the container is a process the kernel has been told to restrict. Questions about size, caching, secrets and isolation strength all answer themselves once those two sentences are load-bearing rather than decorative.

Likely follow-ups

  • You add a dependency and every subsequent build reinstalls everything. What is wrong with the Dockerfile ordering?
  • How does a multi-stage build reduce the final image, and what exactly is left behind?
  • Why can a Linux container not run on a Windows kernel, and what is Docker Desktop doing about it?
  • Which is the stronger boundary for running untrusted code, and what would you use instead of a plain container?

Related questions

Further reading

containersdockerimage-layersnamespacescgroups