Container Images and Layers
A container image is a packaged root filesystem plus instructions for running a process in it. It is not a disk image or a single file. It is a small tree of content-addressed documents, defined by the Open Container Initiative (OCI) image specification, which is why images built by Docker, Buildah, Kaniko or Bazel all run on containerd, CRI-O or Podman.
Anatomy
image index (optional) "this tag has variants for several platforms"
├── manifest linux/amd64
│ ├── config (JSON) env, entrypoint, cmd, user, workdir, labels, layer diff IDs, history
│ ├── layer 1 (tar.gz) base OS files
│ ├── layer 2 (tar.gz) + runtime
│ └── layer 3 (tar.gz) + your application
└── manifest linux/arm64
└── …
| Object | Contains |
|---|---|
| Layer | A tar archive of filesystem changes: files added, modified or deleted |
| Config | How to run the image, and the ordered list of layers |
| Manifest | References to the config and layers, each by digest and size |
| Index | A list of manifests, one per platform |
Inspect one without pulling it:
docker buildx imagetools inspect nginx:1.27
crane manifest nginx:1.27 | jq
skopeo inspect docker://nginx:1.27Content addressing
Every object is named by the SHA-256 hash of its bytes: sha256:4f53cda1…. Three properties follow, and they explain most image behaviour:
- Immutable. Change one byte and the digest changes. A digest always refers to exactly the same content.
- Deduplicated. Two images built on the same base share those layers. They are stored once in the registry and once on each node, and pulled once.
- Verifiable. The client hashes what it downloads and rejects mismatches.
A tag such as nginx:1.27 is only a mutable pointer to a manifest digest. It can be moved. This is the reason to deploy by digest — covered in registries.
How layers become a filesystem
Layers are stacked with a union filesystem, normally OverlayFS:
container layer (read-write, per container, discarded on removal) ← "upperdir"
───────────────────────────────────────────────────────────────────
layer 3: app (read-only) ┐
layer 2: runtime (read-only) ├ "lowerdir"
layer 1: base OS (read-only) ┘
▼ merged view = the container's /
- Reads fall through to the highest layer that has the file.
- Writes are copy-on-write: the first modification copies the file up to the container layer. Editing one byte of a 1 GB file copies 1 GB.
- Deletes are recorded as whiteout markers in the upper layer. The file still exists in the lower layer.
That last point has an important consequence: a later layer cannot shrink an image. If one instruction adds a 500 MB archive and the next deletes it, the image still carries 500 MB. It also means a secret copied in one layer and removed in the next is still in the image for anyone to extract.
The writable container layer is slow and ephemeral. Anything that must persist, or that is written heavily, belongs in a volume — see volumes.
Dockerfile instructions and layers
| Instruction | Creates a filesystem layer? |
|---|---|
FROM | Brings in the base image’s layers |
RUN, COPY, ADD | Yes — one each |
ENV, WORKDIR, USER, EXPOSE, CMD, ENTRYPOINT, LABEL | No — they only change the config |
docker history --no-trunc myapp:1.4.2 # each step and its size
dive myapp:1.4.2 # browse layers and see wasted spaceThe build cache
For each instruction the builder computes a key from the previous layer, the instruction text and — for COPY/ADD — the content of the files being copied. If the key matches a cached layer, the step is skipped. The first cache miss invalidates everything after it.
# Slow: any source change invalidates the dependency install
COPY . .
RUN npm ci
# Fast: dependencies are reinstalled only when the lockfile changes
COPY package.json package-lock.json ./
RUN npm ci
COPY . .Order instructions from least to most frequently changing. This single habit accounts for most of the difference between a ten-second and a ten-minute rebuild. BuildKit adds cache mounts (RUN --mount=type=cache,target=/root/.npm …) that persist package-manager caches across builds without putting them in a layer, and registry-backed cache export for CI runners that start empty. More in Dockerfile best practices.
Multi-architecture images
A tag can point to an index with a manifest per platform. The runtime picks the entry matching the node, so the same image: line works on x86 and Arm.
docker buildx build --platform linux/amd64,linux/arm64 -t registry.example.com/app:1.4.2 --push .Cross-building uses emulation (slow) or, better, cross-compilation in a builder stage with --platform=$BUILDPLATFORM and GOARCH=$TARGETARCH. An image that lacks the node’s architecture fails at start with exec format error — a frequent surprise when adding Graviton nodes.
Size
Smaller images pull faster, start faster, and contain fewer packages to patch.
| Base | Approximate size | Contents |
|---|---|---|
ubuntu, debian | 30–120 MB | Full distribution user space |
debian:*-slim | ~30 MB | Trimmed Debian |
alpine | ~8 MB | musl libc and BusyBox — occasional compatibility issues |
| Distroless, Chainguard | 2–30 MB | Runtime only: no shell, no package manager |
scratch | 0 | Nothing — for fully static binaries |
The most effective technique is a multi-stage build: compile in a large image, copy only the artifact into a minimal one. A Go service typically drops from over 800 MB to under 20 MB.
Beyond the filesystem
The same registry machinery stores other artifacts attached to an image by digest: signatures, SBOMs, provenance attestations and vulnerability reports, as well as Helm charts and WebAssembly modules. See SBOMs and Helm OCI registries.
Lazy pulling addresses large images: with formats such as eStargz or SOCI, a container starts as soon as the files it needs first are fetched, instead of waiting for the full download.
Related
- Containers hub
- Registries — distribution, tags and digests
- Container runtimes — what unpacks and mounts the layers
- Image hardening
- Image pull failures
- OCI image specification
Across the wiki
- Container Runtimes — container internals (Linux)
- The Pause Container — container internals (Kubernetes)
- Runtime Sandboxing (gVisor, Kata Containers) — container internals (Kubernetes)
- Linux Packaging — packaging (Linux)
- Helm Charts — packaging (Kubernetes)
- Release and Versioning — packaging (DevOps)
- 05 — Package Management — packaging (Linux)