Understanding OCI Container Images
We write Dockerfiles, push to registries, pull and run containers every day. But what's actually inside that tarball?
$ docker save nginx:latest | tar -tv

$ docker save nginx:latest -o image.tar
$ mkdir image_contents
$ tar -xf image.tar -C image_contents
$ tree image_contents/
The blueprint — points to config and ordered layers
Runtime DNA — env vars, cmd, ports, history
Content-addressable filesystem layers (tarballs)

Each layer only contains changes from the previous one. Layers are immutable, shared across images, and are simply gzipped tar archives.
Containers use OverlayFS to merge layers into a single unified filesystem at runtime. A writable top layer captures all changes — the image layers below stay untouched.
How do you delete a file when layers can't be modified?
# Layer 1
/app/secret.key
/app/config.yml
# Layer 2
/app/.wh.secret.key ← "whiteout" marker
The file is hidden but still exists in Layer 1 — secrets can be extracted!
Don't
COPY . .
RUN npm install
Do
COPY package*.json ./
RUN npm install
COPY . .
Don't
COPY .env .
RUN ./configure.sh
RUN rm .env # still in layer 1!
Do
RUN --mount=type=secret,id=env \
source /run/secrets/env && \
./configure.sh
# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app
# Stage 2: Runtime (47x smaller!)
FROM scratch
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
| Approach | Size |
|---|---|
| Single stage (Go toolchain + cache + binary) | 715 MB |
| Multi-stage (scratch + binary) | 15 MB |
Interactive layer explorer — see what each layer adds
Vulnerability scanner for container images
Dockerfile linter — catch bad practices before build