What an image actually is, where images are stored, and the range of tools that can build them. Docker basics are in 1. Docker; what runs the images is in Orchestration Platforms.
Tools at a glance
| Category | Tools |
|---|---|
| Dockerfile builders | Docker BuildKit / buildx, Podman, Buildah, Kaniko, img |
| No-Dockerfile builders | Cloud Native Buildpacks (pack), Paketo, Nixpacks, Source-to-Image (S2I) |
| Language-native | Jib (Java), ko (Go), .NET SDK container publish, Spring Boot bootBuildImage |
| Build systems | Bazel rules_oci, Nix dockerTools, Earthly |
| Registries | Docker Hub, GHCR, Quay, AWS ECR, Azure ACR, Google Artifact Registry, Harbor, GitLab Registry |
| Cloud build services | AWS CodeBuild, Azure ACR Tasks, Google Cloud Build, GitHub Actions |
| Security | Trivy, Grype, Docker Scout, Snyk, Cosign, Syft (SBOM) |
Anatomy of an Image
An OCI image is made of:
flowchart TD
I[Image reference: registry/name:tag or @sha256 digest] --> M[Manifest: lists config + layers]
M --> C[Config JSON: env, entrypoint, user, ports, architecture]
M --> L1[Layer 1: base OS files - tar.gz]
M --> L2[Layer 2: runtime / packages]
M --> L3[Layer 3: app dependencies]
M --> L4[Layer 4: your code]
- Layers are content-addressed tarballs of filesystem changes, identified by a SHA-256 digest; identical layers are stored and downloaded once and shared between images
- Manifest points to the config and layers; an image index (manifest list) points to one manifest per platform (
linux/amd64,linux/arm64), which is how one tag works on both Intel and ARM - Config holds runtime defaults (
ENV,CMD,ENTRYPOINT,USER,EXPOSE) - At run time a container adds a thin writable layer on top via a union filesystem (overlayfs)
$ docker history shop-api:1.0 # layers and sizes
$ docker inspect shop-api:1.0 # config + layer digests
$ docker manifest inspect nginx:1.27 # multi-platform index
$ docker save shop-api:1.0 -o shop-api.tar # export as a tarball
$ docker load -i shop-api.tar
Each Part Explained
One running example for every part: a small Python web app built from this Dockerfile.
# base image layers
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
# dependencies layer
RUN pip install -r requirements.txt
# your code layer
COPY . .
ENV PORT=8000
USER 1000
EXPOSE 8000
CMD ["python", "app.py"]
1. Image reference: the address
Tells Docker where to fetch the image and which version.
ghcr.io / sharif / shop-api : 1.0
registry owner name tag
- No registry shown (
nginx:1.27) means Docker Hub - Think of it as a street address: it points to the image but is not the image
- A
@sha256:...digest instead of a tag points at exactly one build forever (see below)
2. Manifest: the table of contents
A small JSON file listing the config and every layer by digest and size. Docker reads this first, then downloads only the layers it does not already have.
Simplified, with // comments added for explanation (real JSON has none):
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": { "digest": "sha256:9a1b...", "size": 1450 },
"layers": [
{ "digest": "sha256:aa11...", "size": 29100000 }, // base OS
{ "digest": "sha256:bb22...", "size": 41800000 }, // Python runtime
{ "digest": "sha256:cc33...", "size": 12400000 }, // pip dependencies
{ "digest": "sha256:dd44...", "size": 18000 } // your code
]
}
- Like a recipe card: "to make this image, stack these four layers in this order"
- See it with
docker manifest inspect python:3.12-slim - For multi-platform images, an image index sits above it and points to one manifest per CPU type (
amd64,arm64), so one tag works on a laptop and a Raspberry Pi
3. Config: the settings
A JSON file describing how to run the image, not what is inside it.
{
"architecture": "amd64",
"os": "linux",
"config": {
"Env": ["PORT=8000", "PATH=/usr/local/bin:..."],
"Cmd": ["python", "app.py"],
"User": "1000",
"WorkingDir": "/app",
"ExposedPorts": { "8000/tcp": {} }
},
"rootfs": { "type": "layers", "diff_ids": ["sha256:...", "sha256:..."] }
}
- Every
ENV,CMD,ENTRYPOINT,USER,WORKDIRandEXPOSEline from the Dockerfile ends up here, not in a layer - This is why you can override the command with
docker run image other-commandwithout rebuilding - Also holds build history and the list of layer ids
4. Layer 1: base OS files
The bottom layer comes from FROM. It holds a minimal operating system: /bin, /lib, /etc, package tools.
/bin/bash /lib/x86_64-linux-gnu/ /etc/os-release /usr/bin/apt ...
- Roughly 30-120 MB for Debian or Ubuntu, 5 MB for Alpine, near zero for distroless or
scratch - Shared by every image built on the same base, so it is downloaded once and reused
- Does not include a kernel: containers borrow the host's
5. Layer 2: runtime and language tools
Added by the base image's own Dockerfile (the python:3.12-slim part, or via your own RUN apt-get install).
/usr/local/bin/python3.12
/usr/local/lib/python3.12/
- Contains the interpreter or runtime: Python, Node.js, a JRE
- Changes rarely, so it caches well
6. Layer 3: dependencies
Created by RUN pip install -r requirements.txt (or npm ci, mvn dependency:resolve).
/usr/local/lib/python3.12/site-packages/flask/
/usr/local/lib/python3.12/site-packages/requests/
- Files that changed during the
RUNbecome the layer, so only the installed packages are stored - Placed before your code on purpose: dependencies change less often than code, so Docker can reuse this layer when only your code changes
- Biggest cache win in most Dockerfiles
7. Layer 4: your application code
Created by COPY . .
/app/app.py
/app/templates/index.html
/app/static/style.css
- Smallest and fastest-changing layer, so it sits on top
- Edit one line of code and only this layer is rebuilt and pushed (a few KB), not the other 80 MB
8. The writable layer (at runtime only)
When you docker run, Docker adds a thin read-write layer on top of the read-only image layers.
Container writable layer ← logs, temp files, anything the app writes
Layer 4 your code ┐
Layer 3 dependencies │ read-only, shared by every
Layer 2 Python runtime │ container from this image
Layer 1 base OS ┘
- Delete the container and this layer disappears; use a volume for data you want to keep (1. Docker)
- Many containers can run from one image and share all the read-only layers
How a Dockerfile maps to parts
| Dockerfile line | Becomes | In a layer? |
|---|---|---|
FROM python:3.12-slim |
Layers 1 and 2 (base) | Yes |
WORKDIR /app |
Config WorkingDir (creates the directory) |
Metadata |
COPY requirements.txt . |
Small file layer | Yes |
RUN pip install ... |
Layer 3 | Yes |
COPY . . |
Layer 4 | Yes |
ENV PORT=8000 |
Config Env |
Metadata |
USER 1000 |
Config User |
Metadata |
EXPOSE 8000 |
Config ExposedPorts |
Metadata |
CMD [...] |
Config Cmd |
Metadata |
Only FROM, COPY, ADD and RUN add filesystem layers; the rest only edit the config.
See it yourself
$ docker history shop-api:1.0
IMAGE CREATED BY SIZE
dd44... COPY . . # buildkit 18kB
cc33... RUN pip install -r requirements.txt 12.4MB
bb22... COPY requirements.txt . # buildkit 45B
<base> python:3.12-slim layers ~120MB
$ docker save shop-api:1.0 -o shop.tar && mkdir x && tar -xf shop.tar -C x
$ ls x/blobs/sha256 # each file is a layer tarball, the config, or the manifest
$ docker inspect --format '{{json .RootFS.Layers}}' shop-api:1.0
Why it matters:
- Fast pulls and pushes: only new layers move
- Cache: order Dockerfile lines from least to most changing
- Size: a file deleted in a later layer still exists in the earlier one; clean up in the same RUN, or use a multi-stage build
- Security: a secret copied into any layer stays in the image history
Naming: tags vs digests
registry.example.com/team/shop-api:1.4.2
registry.example.com/team/shop-api@sha256:3f1c... # immutable
- A tag is a movable label;
:latestis just a tag, not "newest" - A digest is immutable and identifies exact content. Pin digests (or immutable tags) for reproducible production deploys
- Recommended tags: the git commit SHA and a semantic version; avoid reusing a tag
Base image choices
| Base | Size | Notes |
|---|---|---|
ubuntu / debian |
30-120 MB | Familiar, apt, glibc |
-slim variants |
Smaller | Debian without extras |
| Alpine | ~5 MB | musl libc, can break some binaries / Python wheels |
| Distroless | 2-20 MB | No shell or package manager, smaller attack surface |
| Chainguard / Wolfi | Small | Minimal, frequently patched |
scratch |
0 | Empty; for static binaries (Go, Rust) |
| Red Hat UBI | Medium | Supported base for OpenShift / enterprise |
Registries
Where images are stored and pulled from.
| Registry | Notes |
|---|---|
| Docker Hub | Default, public images, pull rate limits |
GHCR (ghcr.io) |
GitHub Packages, tied to repos and Actions |
Quay (quay.io) |
Red Hat; scanning built in |
| Amazon ECR | Private / public, IAM auth, scanning, replication (3. AWS Compute) |
| Azure Container Registry | Geo-replication, ACR Tasks, Entra auth (2. Azure Compute) |
| Google Artifact Registry | Docker, Maven, npm etc. in one place |
| Harbor | Open-source, self-hosted, RBAC, replication, scanning |
| GitLab / Gitea / Nexus / Artifactory | Often combined with CI |
$ docker login ghcr.io
$ docker tag shop-api:1.0 ghcr.io/sharif/shop-api:1.0
$ docker push ghcr.io/sharif/shop-api:1.0
# ECR example
$ aws ecr get-login-password | docker login --username AWS --password-stdin 123456789012.dkr.ecr.af-south-1.amazonaws.com
# ACR example
$ az acr login --name myregistry123
Run a local registry for testing: docker run -d -p 5000:5000 registry:2.
Kubernetes pulls private images using an imagePullSecret or cloud identity (4. Running Your Containers in Kubernetes).
Build Tools
flowchart TD
S[Source code] --> Q{How do you want to build?}
Q -->|I write a Dockerfile| D[BuildKit / buildx, Podman, Buildah, Kaniko]
Q -->|No Dockerfile, detect the language| B[Cloud Native Buildpacks, Nixpacks, S2I]
Q -->|Java, Go or .NET project| N[Jib, ko, dotnet publish]
Q -->|Large monorepo, hermetic builds| X[Bazel rules_oci, Nix]
D --> R[(Registry)]
B --> R
N --> R
X --> R
Docker BuildKit and buildx
The default Docker builder. Parallel stages, better caching, secrets and SSH mounts, multi-platform output.
$ docker build -t shop-api:1.0 .
$ docker buildx create --use --name multi
$ docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/sharif/shop-api:1.0 --push .
$ docker buildx build --secret id=npmrc,src=$HOME/.npmrc . # build secret never stored in a layer
$ docker buildx build --cache-from type=registry,ref=ghcr.io/sharif/cache \
--cache-to type=registry,ref=ghcr.io/sharif/cache,mode=max .
In the Dockerfile, a build secret and cache mount:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
--mount=type=cache,target=/root/.npm \
npm ci
Podman and Buildah (daemonless, rootless)
Red Hat tools, no central daemon, runs as a normal user. podman is a drop-in CLI replacement (alias docker=podman); buildah builds images with or without a Dockerfile.
$ podman build -t shop-api:1.0 .
$ podman run --rm -p 8080:80 shop-api:1.0
$ podman generate kube shop-api > pod.yaml # export a running container as Kubernetes YAML
$ podman play kube pod.yaml
$ buildah bud -t shop-api:1.0 .
Kaniko (build inside Kubernetes / CI)
Builds from a Dockerfile without a Docker daemon and without privileged mode, ideal in CI pods and Tekton. (Google archived the original repo in 2025; maintained forks exist, evaluate before adopting.)
$ /kaniko/executor --context=dir:///workspace --dockerfile=Dockerfile \
--destination=myregistry/shop-api:1.0
Cloud Native Buildpacks (CNB), pack, Paketo
Turn source into an image with no Dockerfile: detect language, pick a build, add a runtime, produce reproducible layers with an SBOM. Used by Heroku, Google Cloud, Spring Boot, Cloud Foundry.
$ pack build shop-api:1.0 --builder paketobuildpacks/builder-jammy-base
$ pack inspect shop-api:1.0
Nixpacks, Source-to-Image (S2I)
- Nixpacks (used by Railway) detects the app and generates an image
- S2I (OpenShift) injects source into a builder image:
s2i build . ubi8/python-39 shop-api
Language-native builders
| Tool | Language | Idea |
|---|---|---|
| Jib | Java (Maven / Gradle) | Layers dependencies and classes separately, no Docker daemon, fast rebuilds |
ko |
Go | ko build ./cmd/app builds a distroless image directly |
| .NET SDK | C# | dotnet publish -t:PublishContainer |
| Spring Boot | Java | ./mvnw spring-boot:build-image (uses Buildpacks) |
$ mvn compile jib:build -Dimage=ghcr.io/sharif/shop-api:1.0
$ KO_DOCKER_REPO=ghcr.io/sharif ko build ./cmd/app
Bazel, Nix, Earthly
- Bazel
rules_oci: deterministic, cacheable builds for large monorepos - Nix
dockerTools: bit-for-bit reproducible minimal images - Earthly: Dockerfile-like syntax with Makefile-style targets, runs identically in CI and locally
Cloud build services
| Service | Example |
|---|---|
| Azure ACR Tasks | az acr build -r myregistry123 -t shop-api:1.0 . |
| AWS CodeBuild | buildspec.yml with docker build / docker push (AWS CodePipeline) |
| Google Cloud Build | gcloud builds submit --tag gcr.io/PROJECT/shop-api |
| GitHub Actions | docker/build-push-action (GitHub Actions) |
Comparison
| Tool | Needs daemon | Rootless | Dockerfile needed | Best for |
|---|---|---|---|---|
| BuildKit / buildx | Yes (or built-in) | Optional | Yes | General use, multi-arch, caching |
| Podman / Buildah | No | Yes | Optional | RHEL / Fedora, security-conscious |
| Kaniko | No | In-container | Yes | Building inside Kubernetes CI |
| Buildpacks | Needs a builder | Yes | No | Standardised builds across many apps |
| Jib / ko | No | Yes | No | JVM or Go teams wanting fast layered builds |
| Bazel / Nix | No | Yes | No | Reproducible monorepos |
Optimising Images
- Order Dockerfile steps from rarely to often changing for cache hits (1. Docker)
- Multi-stage builds: compile in one stage, copy artifacts into a tiny runtime stage
- Use small, pinned base images and a
.dockerignore - Combine
RUNsteps and clean package caches in the same layer - Do not bake secrets,
.git, test data or build tools into the final image - Inspect bloat with
docker historyor thedivetool - Build multi-platform if you run on ARM nodes (AWS Graviton, Apple Silicon, Raspberry Pi)
Supply Chain Security
flowchart LR
A[Build image] --> B[Scan: Trivy / Grype / Scout]
B --> C[Generate SBOM: Syft]
C --> D[Sign: Cosign]
D --> E[(Registry)]
E --> F[Admission policy verifies signature - Kyverno / Gatekeeper]
F --> G[Deploy to cluster]
- Vulnerability scanning:
trivy image shop-api:1.0, registry scanners (ECR, ACR, Harbor, Quay) - SBOM (software bill of materials):
syft shop-api:1.0 -o spdx-json - Signing and verification: Cosign (Sigstore), Notary; enforce in the cluster with Kyverno, OPA Gatekeeper or the cloud's policy engine
- Provenance attestations (SLSA) from the build system
- Rebuild regularly to pick up base image fixes; use digests and an automated updater (Renovate, Dependabot)
- Run as non-root, read-only root filesystem, drop capabilities
Image Lifecycle in a Pipeline
- Commit triggers CI (CI-CD Pipelines)
- Build image (BuildKit / Kaniko / Buildpacks), tag with the commit SHA
- Scan and sign
- Push to a registry
- Deployment references the immutable tag or digest
- A retention policy cleans old images (ECR lifecycle rules, ACR purge tasks)