Chris Collins

Tools · containers

Container image layers

Open a docker save tar or an OCI image layout and see each layer with its Dockerfile instruction and size, the merged file tree with what every layer added, changed and deleted, wasted space and dive's efficiency score, the config, the platforms in the index, every digest checked — and the packages inside, handed to the SBOM viewer.

Added

Works with

How to use it

dive is the tool everyone reaches for when an image is bigger than it should be. This does the same job in a browser tab, from a file: drop the tar that docker save writes and it lists the layers with the instruction that made each one, shows the file system after any layer with the changes coloured in, and adds up the bytes you ship twice. It also reads what dive doesn’t: the index with its platforms and attestations, the digests (all of them recomputed), and the package databases, so you can see what is installed without running the image.

It opens with a small example image built for this page (Alpine’s package database, an npm app, two Python packages and some jars, with a file that a later layer deletes and one it overwrites).

Getting an image as a file

Docker

docker save alpine:3.20 -o alpine.tar — add --platform linux/amd64 for one platform of a multi-platform image

skopeo

skopeo copy docker://alpine:3.20 oci-archive:alpine.tar

crane

crane pull --format=oci alpine:3.20 alpine-oci writes a folder: use Open OCI folder

podman

podman save --format oci-archive -o alpine.tar alpine:3.20

BuildKit

docker buildx build --output type=oci,dest=app.tar . (compression=zstd works too)

It reads Docker’s manifest.json layout (old <id>/layer.tar archives and the Docker 25+ ones with blobs/sha256/) and the OCI image layout, with uncompressed, gzip or zstd layers. An index with several platforms is listed in full; pick any platform whose blobs are in the archive.

Registries don’t let web pages fetch from them (I checked Docker Hub, ghcr.io, ECR Public, quay.io, gcr.io, MCR, registry.k8s.io and GitLab’s registry: none sends CORS headers), so there is no "pull by name". To look at an image without downloading its layers, paste the output of docker buildx imagetools inspect --raw IMAGE, crane manifest IMAGE, crane config IMAGE or docker inspect IMAGE into the box: the index shows platforms and digests, a manifest shows the layer sizes, and a config shows the settings and history.

How to use it

  1. Open image tar…, Open OCI folder… or drop either on the box. Big images are fine: layers are streamed through the decompressor and the tar reader in chunks and never held in memory whole. Untick Check SHA-256 digests to go faster on a very large image.

  2. Layers lists every layer with the bytes of file content it adds, the instruction from the config’s history (docker history), and how many files it added, changed and deleted. Click a layer for its digests and the full list of its changes, largest first.

  3. Files is the file system after the layer you choose, as a tree with folder sizes. Files the layer added are green, changed amber, deleted red and struck through; only what this layer changed hides the rest. Whiteouts (.wh.name) and opaque directories (.wh..wh..opq) are applied as the container runtime applies them.

  4. Wasted space is dive’s analysis: a path that more than one layer writes ships every copy, so all of them count as potential waste, and the efficiency score is the smallest copy of each path over all copies. The numbers match dive’s own (dive --json) on the images I tested, including deleted directories and opaque whiteouts.

  5. Config shows what docker inspect shows — user, entrypoint, command, environment, ports, volumes, labels, health check — with notes on running as root, secrets in ENV and shell-form entrypoints, and the whole history including the steps that made no layer.

  6. Manifest & digests checks each layer twice: the blob as stored against the manifest digest, and the uncompressed tar against the config’s diff_id. It also lists the index: every platform and attestation manifest, and whether its blobs are in the archive.

  7. Packages lists what is installed, read from the final file system (a database a later layer deleted doesn’t count): Debian/Ubuntu (/var/lib/dpkg/status and distroless status.d, licences from /usr/share/doc//copyright), Alpine (/lib/apk/db/installed), Red Hat, Fedora and SUSE (rpmdb.sqlite), npm (node_modules//package.json, and package-lock.json for what isn’t installed), Python (.dist-info/METADATA), Java (pom.properties in jars, wars and the jars inside Spring Boot fat jars) and Go (the module list in a binary’s build info). *Open in SBOM viewer sends them as a CycloneDX 1.6 SBOM to the SBOM viewer, with the package dependencies from dpkg, apk and rpm, for licences and (if you ask) known vulnerabilities; Download CycloneDX saves the same file.

For the Dockerfile side of the story, the Dockerfile explainer shows which instructions make layers and what a change rebuilds; docker run ↔ Compose turns a config into a run command; the container simulator animates how overlay layers stack.

What I checked it against

The example image and the test images were built with Docker 29 (BuildKit) and exported four ways: docker save (OCI layout, gzip), BuildKit’s OCI exporter with gzip and with zstd, and skopeo’s docker-archive (the old layout). For each one, and for alpine:3.19.0, debian:bookworm-slim, node:22-bookworm-slim, python:3.12-alpine, postgres:16-alpine and ubi9/ubi-micro, the image size, wasted bytes and efficiency score equal dive 0.13.1’s --json output to the byte, and every blob digest and diff_id matches the manifest and config. The RPM list equals rpm -qa on the same database; the Go module list equals go version -m.

Not read: rpm’s older Berkeley DB and NDB databases (it says so), Go binaries built before Go 1.18, gzip layers made of several concatenated members, and Docker’s pre-2016 schema 1 manifests.

Browser support

Any current browser. Opening a folder needs Chrome, Edge, Firefox or Safari 14.1 or later. gzip uses the browser’s own DecompressionStream; zstd layers load a small decoder the first time one is met.

Privacy

The image is read in this page and never uploaded. The only network request is for zstd layers: the first one loads fzstd (MIT, 8 kB) from cdn.jsdelivr.net. The tab you were on is kept in your browser’s local storage, and pasted JSON while it is small.

Runs entirely in your browser — network requests go only to the services named on this page.