You've built your application, packaged it into a container image, and pushed it to a registry. But what actually IS that image? Is it a single file? How do registries store and serve it? Why does the sha256 digest matter more than the tag? This guide explains the anatomy of container images — the standard format that makes containers portable across Docker, Podman, Kubernetes, and every other container runtime.
Table of Contents
- What a Container Image Actually Is
- The OCI Image Specification
- Image Structure: Manifests, Config, and Layers
- How Docker Build Creates Images
- Image Digests vs Tags
- Understanding Image Size
- Base Images and Inheritance
- Multi-Architecture Images
- How Registries Store and Serve Images
- Next Steps
The following diagram illustrates the layered architecture of a container image, showing how an application is built on top of multiple immutable layers:
1Container Image Layers (Stacked)Language RuntimePython, Node, GoSystem Librariesglibc, OpenSSL, zlibPackage Manager & Toolsapt, dnf, apkBase OS ImageUbuntu, Alpine, Debianscratchempty foundationWhat a Container Image Actually Is
Stop thinking of a container image as a single "file" sitting on a registry. That's wrong.
A container image is a versioned collection of filesystem artifacts (called layers) plus metadata that describes how to run them. It's more like a tarball of tarballs with instructions.
When you run docker pull alpine:latest, you're not downloading one thing. The registry sends you several components. First, there's a manifest file (JSON, ~1 KB) that lists what you're downloading. Second, there's a config blob (JSON, ~5-10 KB) with runtime settings. Finally, there are multiple layer blobs (compressed filesystem snapshots, often MBs or GBs). The container runtime assembles these pieces locally into a single filesystem view using kernel features like OverlayFS or UnionFS. That stacked filesystem is what your application sees when it runs.
This distinction matters for several fundamental reasons. Layers are immutable and content-addressable: Docker knows if a layer already exists because it's stored by its sha256 hash, not by name. This means you don't download duplicate data—if another image uses the same layer, it's referenced from the same storage. Multiple images can share layers: If you have 10 applications all based on Ubuntu 22.04, the Ubuntu layer is stored once on disk and referenced by all 10 images. This dramatically reduces storage usage. Images are reproducible: The same Dockerfile always produces the same image hash (assuming deterministic builds), making it possible to verify that an image is exactly what you expect.
The OCI Image Specification
In 2015, Docker donated the container image format to the Open Container Initiative (OCI), creating an open standard that any runtime can implement. This is critical: your image is not "Docker format." It's OCI Image Spec v1.1, and it works with Docker, Podman, containerd, CRI-O, and dozens of other tools.
The OCI Image Specification defines several key components. It establishes mediaTypes for different blob types so systems can distinguish between different kinds of data. It specifies the manifest structure defining what layers to download and in what order. It defines the image config as a JSON blob containing architecture, environment variables, entrypoint, volumes, and working directory. It describes the image layout for how files are organized on disk or in a registry. It includes support for annotations as key-value metadata that can be attached to images.
Because the format is standardized, you gain significant portability benefits. You can pull an image from Docker Hub with docker pull and then use the same image with podman pull without any conversion. You can push it to a private registry and it remains compatible with all OCI-compliant tools. You can inspect it with crane or skopeo, tools that work with OCI images regardless of where they came from. You can sign it with Cosign, which works with any OCI-compliant registry. This portability is why container images have become the universal deployment artifact across the industry.
Image Structure: Manifests, Config, and Layers
To understand what lives in a registry, consider the following diagram showing how a manifest, config blob, and layer stack work together:
1Image (sha256:abc123...)Manifest.jsonList of referencesLayer 1sha256:ghi789...Layer 2sha256:jkl012...Layer 3sha256:mno345...An image (identified by sha256:abc123...) contains three main components working together to define a complete container.
The manifest.json is a list that describes what's in the image. It contains references to a config blob and lists the layers in the order they should be stacked to reconstruct the filesystem.
The config blob (sha256:def456...) is metadata that describes how to run the image. It contains the target architecture (amd64, arm64, etc.), environment variables that should be set when the container starts, the entrypoint command that should execute, the working directory, exposed ports, and other runtime metadata that tells the container runtime how to configure and execute the image.
The layer blobs form an ordered stack of filesystem changes that are applied in sequence. Layer 1 contains base OS files, layer 2 adds the package manager and dependencies, layer 3 includes application code, and layer 4 adds configuration settings. Each layer is identified by its own sha256 hash, making it possible to reference and deduplicate identical layers across different images.
The Manifest is a simple JSON file that says: "To reconstruct this image, download these layers in this order, then use this config blob." Here's an example:
{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "size": 7023, "digest": "sha256:b5b2b2c507a1d67f" }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 32654, "digest": "sha256:e7c96db7f03f" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 16724, "digest": "sha256:3c3a4604a545" } ]}The Config Blob tells the runtime how to execute the image:
{ "architecture": "amd64", "os": "linux", "config": { "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"], "Cmd": ["/bin/sh"], "WorkingDir": "/", "Entrypoint": ["/docker-entrypoint.sh"] }, "rootfs": { "type": "layers", "diff_ids": ["sha256:... hash of layer 1 uncompressed", ...] }}Layers are compressed tarballs (gzipped) containing filesystem changes. Layer 1 might have 50,000 files (the base OS). Layer 2 might add 100 files (libraries). Layer 3 might add 5 files (your app). The runtime downloads each, decompresses them, and stacks them using an overlay filesystem so all layers appear as one unified filesystem to the running application.
How Docker Build Creates Images
When you run docker build -t myapp:1.0 ., here's what happens:
FROM ubuntu:22.04RUN apt-get update && apt-get install -y python3COPY app.py /app/WORKDIR /appCMD ["python3", "app.py"]Step 1 executes the FROM ubuntu:22.04 directive, which pulls the Ubuntu image. That becomes the base layers and could contain 20 or more layers containing the entire operating system.
Step 2 processes the RUN apt-get update && apt-get install -y python3 command by creating a new layer on top. Docker runs that command inside a temporary container, captures any filesystem changes (new files, modified files), and saves them as layer 2. This layer represents all the changes made by the apt commands.
Step 3 handles COPY app.py /app/ by creating a new layer 3 containing only the app.py file that you're copying into the image. This layer adds your application code on top of everything that came before.
Step 4 processes WORKDIR /app and CMD [...] by updating the image config (metadata). No new layer is created because these commands don't change the filesystem itself—they just update how the container should run.
Step 5 has Docker create a manifest that references all layers and the config blob. The final image hash is computed from everything, ensuring any change results in a different hash.
Why layer caching matters: If you rebuild without changing anything, Docker reuses cached layers from the previous build. If you change app.py, Docker rebuilds from that point forward but reuses all the earlier layers (OS, Python). This is why instruction order in your Dockerfile matters significantly—putting frequently-changing instructions later in the file allows better caching.
Image Digests vs Tags
A tag is a human-readable label: ubuntu:22.04, myapp:latest, gcr.io/myproject/myservice:v1.2.3.
A digest is a cryptographic hash of the image contents: sha256:abc123def456.... It's immutable. If even one byte changes, the digest changes.
Critical difference: A tag can move. myapp:latest might point to digest sha256:abc123 today, and sha256:def456 tomorrow if you rebuild and push. The tag is a floating reference. The digest is permanent.
For example, initially the tag myapp:1.0 and myapp:latest both point to digest sha256:abc123def456789..., with myapp:latest being an alias to the same digest. However, when you create a new rebuild, myapp:1.0 remains pointing to the original digest sha256:abc123def456789..., while myapp:latest now points to the new digest sha256:xyz789uvw012345..., demonstrating how tags are mutable while digests stay constant.
In production, use digests for deployments: Instead of deploying myapp:latest, deploy myapp@sha256:abc123. This guarantees you get exactly what you tested, even if someone force-pushes a new latest tag.
Tags are convenient for humans. myapp:v1.2.3 is more readable than a 64-character hash. But tags are aliases. Digests are truth.
Understanding Image Size
Image size matters for multiple practical reasons. Larger images are slower to pull across the network, especially across geographic regions or over slower connections. Registries charge for storage, so every redundant layer costs money in terms of storage fees. Large images mean slower pod startup in Kubernetes since more data must be downloaded and decompressed before the container can start running. Larger images have more code, which generally means more potential vulnerabilities and a broader attack surface.
A typical image size breakdown illustrates where storage is consumed. The ubuntu:22.04 base layers account for approximately 80 MB and include the OS libraries, apt package manager, and shell utilities. The Python runtime contributes about 100 MB and includes the Python interpreter and standard library. Application dependencies typically consume around 50 MB (packages like numpy and requests). Your actual application code is usually minimal, around 2 MB. All of these layers combined result in a total image size of approximately 232 MB.
What contributes to size: The base image is a major factor—Alpine (~5 MB), Ubuntu (~77 MB), CentOS (~200 MB) vary wildly. Package manager caches also add bloat: apt-get update caches package lists and a single RUN apt-get install creates garbage in the layer unless cleaned up afterward. Build artifacts like compiler toolchains and source code downloaded during build but not needed at runtime also contribute. Intermediate files such as logs, caches, and test data also take up space.
Optimization pattern — multi-stage builds:
# Build stageFROM golang:1.20 AS builderWORKDIR /srcCOPY . .RUN go build -o app . # Runtime stageFROM scratchCOPY --from=builder /src/app /appCMD ["/app"]The builder stage is discarded after use. Only the final runtime stage is included in the image. The Go compiler (200+ MB) never makes it into the final image; only the 10 MB compiled binary does.
Base Images and Inheritance
Every image (except scratch) is built on a base image. The base image provides several essential components. It provides the OS kernel interface including Linux tools, shell, and package manager. It includes runtime dependencies—shared libraries your language needs. It provides conveniences like curl, wget, and debugging tools that save you time.
When you FROM ubuntu:22.04, you're getting the Linux filesystem kernel interface (glibc, core utilities), the apt package manager, 50+ MB of pre-installed system libraries, and the Bash shell.
When you FROM python:3.11-slim, you're getting everything from Debian slim (smaller than Ubuntu, ~50 MB), the Python 3.11 interpreter, the pip package manager, and common dependencies that Python needs to function.
Base image choices affect your final image significantly:
Base Image | Size | Use Case |
|---|---|---|
| 0 bytes | Static binaries only (Go, Rust) |
| 7 MB | Minimal; suitable for microservices |
| 50 MB | Lightweight with package manager |
| 77 MB | Full-featured; more tools pre-installed |
| 900 MB | Python + full toolchain |
| 180 MB | Node.js + npm |
Version pinning matters: You should use specific versions rather than floating tags. Using FROM ubuntu:22.04 ensures reproducible builds, while FROM ubuntu:latest might change the base OS tomorrow without your knowledge. When you use a specific base image version, your builds are reproducible and predictable. When you use latest, someone might rebuild your image in 6 months and get a different base OS, different library versions, and a different security profile without realizing it.
Multi-Architecture Images
A single image tag can actually represent multiple architectures. When you push myapp:latest to a registry, you're pushing variants for linux/amd64, linux/arm64, and linux/s390x. The registry stores all three versions. When a user pulls myapp:latest on an M1 Mac (which is arm64), they automatically get the arm64 version. When they pull on an x86 server, they get the amd64 version.
This functionality is implemented through an image index, also called a manifest list, which provides a layer of indirection. The image index for myapp:latest contains a manifest for each architecture. The amd64 manifest references the config and layers built for amd64. The arm64 manifest references the config and layers built for arm64. Similarly, the ppc64le manifest references its architecture-specific config and layers.
To build multi-arch images, use BuildKit:
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .BuildKit compiles your application for each architecture and pushes a separate image for each to the registry, then creates an index pointing to all of them.
How Registries Store and Serve Images
A registry is a server that stores and serves OCI images. Docker Hub is a registry. Google Container Registry (GCR) is a registry. Your private Harbor or Artifactory instance is a registry.
Storage model: Registries organize images in a directory structure. The /blobs/ directory contains sha256-named compressed layer files. For example, abc123.tar.gz is layer 1 from image A, and because image B uses the identical layer, it references the same blob without duplication. Other blobs like def456.tar.gz (layer 2 from image A) and ghi789.tar.gz (layer 1 from image C) are stored separately. The /manifests/ directory contains pointers and references: myapp/latest points to the manifest for myapp:latest, myapp/v1.0.0 points to the manifest for that version, and the manifest blob itself is stored and indexed by its hash.
Registries use content-addressable storage: blobs are stored by their sha256 hash. Two images with identical layers share the same blob. No duplication occurs, saving significant storage space.
Push operation (user → registry):
- User has a local image:
myapp:1.0with layers [A, B, C] - User runs
docker push myapp:1.0 - Docker computes hashes for each layer and the config
- Docker checks with registry: "Do you have layers A, B, C?"
- Registry says: "I have A and B, but not C"
- Docker uploads only layer C
- Docker uploads the manifest (which references A, B, C)
- Docker creates a tag
myapp:1.0pointing to that manifest
Pull operation (registry → user):
- User runs
docker pull myapp:1.0 - Docker contacts registry: "Give me the manifest for
myapp:1.0" - Registry returns the manifest (list of layers)
- Docker checks locally: "Do I already have layers A, B, C?"
- Docker downloads only missing layers
- Docker decompresses layers and stacks them with OverlayFS
- Container can now start using the assembled filesystem
Why this matters: Deduplication means one 500 MB layer can be referenced by 100 images and the registry stores it only once. Efficient pulls happen because if you pull myapp:1.0 then myapp:1.1 (which shares 90% of layers), the second pull is fast since most layers already exist locally. Distributed systems benefit because different registries in different regions can pull from each other and share blobs, enabling global content distribution networks.
Next Steps
Understanding container images is foundational. You now know that images are versioned collections of layers (immutable filesystem snapshots) plus metadata, OCI Image Spec is the standard that all tools use, tags are aliases while digests are immutable references, registries use content-addressable storage for efficiency, and multi-architecture support is baked into the standard.
Continue learning with related topics:
Image internals and security include deep dives into container image layers and composition, the illusion of single artifacts and why images are vulnerable, SBOMs for inventorying components in your image, and CVEs for understanding vulnerabilities in these layers.
Building and deploying securely covers using images in CI/CD pipelines, comparing strip-down vs source-built image composition approaches, end-to-end secure deployment practices, and modernizing builds through Dockerfile to YAML migration.
Operational concerns include running images at scale with Kubernetes fundamentals, understanding registry options through container registries comparison, and learning how enterprises patch containers in production.
Practical exercises:
- Explore layers: Use
divetool to inspect what's in your images - Optimize size: Rebuild an existing image with multi-stage builds and measure the difference
- Experiment with tags and digests: Use
docker inspectandskopeoto see how they work - Test reproducibility: Build the same Dockerfile twice; verify you get the same digest
- Study registries: Understand how Docker Hub, GCR, and Artifactory organize images differently
Key Concepts Summary
An image is a stack of immutable layers plus metadata, not a single file. OCI Image Spec is the portable standard all runtimes use. Manifests plus config plus layers equals complete image definition. Digests are immutable; tags are mutable aliases. Registries deduplicate using content-addressable storage. Multi-architecture images let one tag serve multiple platforms. Understanding images is essential for debugging, optimization, and security.
