Docker 95 🐳 Reverse Engineering Docker Images: Extracting Dockerfiles using dive and Layer Inspection
Docker images do not store the original Dockerfile. The image is a stack of layers, and each layer records the instruction that created it, but the full build context—the files that were copied, the arguments passed to docker build—is not preserved in a form that can be reconstructed exactly . Reverse engineering a Dockerfile is therefore an approximation: it recovers the sequence of instructions from the layer metadata, and the result is useful for understanding how an image was built, but it is not a file that can be passed directly to docker build and expected to produce the same image .
This chapter covers the two tools that make this approximation practical. docker history reads the layer metadata that Docker stores and prints the instruction associated with each layer. dive provides an interactive view of the same data, showing which files each layer added, modified, or deleted, alongside the instruction that produced the layer. The combination answers the questions that matter when auditing a third-party image: what commands ran, what files were introduced, and where did the size come from.
Key point: The recovered Dockerfile is a reference, not a build file. COPY and ADD instructions appear with checksums rather than source paths because the original build context is gone. The instruction sequence is accurate; the exact inputs to that sequence are not recoverable from the image alone .
Why reverse engineering requires layer inspection
The metadata problem. Docker stores the instruction that created each layer in the image’s history. docker history reads this metadata and prints one row per history entry, with the CREATED BY column showing the instruction . For a RUN instruction, this is the full shell command. For ENV, EXPOSE, and CMD, the row shows the instruction but contributes zero bytes to the image size. The history is the closest thing to a build log that the image retains.
The filesystem problem. The history tells you what command ran, but not what files it produced. A RUN instruction that downloads a tarball, extracts it, and deletes the tarball shows the full command in the history, but the layer still contains the extracted files. dive shows the filesystem changes for each layer, which is what reveals the actual content of the image . The two views are complementary: history explains the command, dive explains the effect.
The context problem. The COPY and ADD instructions in a Dockerfile reference files from the build context. That context is not stored in the image. When dfimage or docker history recovers a COPY instruction, it can show that a file was copied to a destination, and it can show the checksum of the copied content, but it cannot show the original source path . This is the fundamental limitation of Dockerfile recovery: the instructions are recoverable, but the inputs are not.
The security audit problem. A third-party image can contain files that the Dockerfile never mentions. A malicious image author can add a layer that installs a backdoor, and the history will show the instruction that did it if the author did not take steps to obscure it. dive makes this visible by showing exactly which files each layer introduced, including files in unexpected paths like /usr/local/bin/ or /etc/cron.d/ . This is the primary use case for layer inspection in a security context: confirming that an image contains only what it claims to contain.
a. docker history: Reading the Instruction Sequence
docker history is the built-in command for inspecting layer metadata. It requires no additional tooling and works on any image in the local daemon. The output columns are the layer ID, creation time, the instruction that created the layer, the size contribution, and a comment field .
# View the layer history of an image
docker history nginx:latest
# Show the full instruction without truncation
docker history --no-trunc nginx:latest
# Format the output to show size and instruction only
docker history --format "{{.Size}}\t{{.CreatedBy}}" nginx:latest
The --no-trunc flag is essential for reverse engineering. Without it, long RUN commands are truncated, and the truncation often hides the part of the command that matters—the package names, the URLs, the specific files being copied . The formatted output with --format is useful for sorting by size to identify the largest layers.
# Find the largest layers in an image
docker history nginx:latest --format "{{.Size}}\t{{.CreatedBy}}" --no-trunc | sort -hr | head -10
The history entries for metadata instructions—ENV, EXPOSE, CMD, ENTRYPOINT—show a size of 0B. These entries are still part of the instruction sequence and belong in the recovered Dockerfile, but they did not create a filesystem layer . The entries with non-zero size are the ones that created layers, and those are the layers that dive can inspect for file changes.
A limitation of docker history is that it shows the history from the base image as well as the instructions added by the current image. The entries for the base image appear with <missing> in the IMAGE column because Docker does not store the intermediate image IDs for layers that came from a pulled base image . The instruction sequence is still readable, but the boundary between the base image’s instructions and the current image’s instructions is not always obvious.
b. dive: Layer Contents and Efficiency
dive is an interactive terminal tool for exploring image layers. It shows the layer list on the left and the filesystem contents on the right. Selecting a layer displays the files that layer added, modified, or deleted, combined with the state of the filesystem up to that layer . The bottom pane shows basic layer information and an experimental efficiency metric that estimates wasted space.
# Run dive against an image in the local daemon
dive nginx:latest
# Run dive without installing it locally, using the Docker socket
docker run -ti --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive nginx:latest
The interface uses Tab to switch between the layer list and the file tree, Ctrl+U to hide files that were not modified in the selected layer, and Ctrl+F to filter the file tree . The layer list shows the instruction for each layer in the Command field, which is the same data that docker history provides. The file tree shows what that instruction actually did to the filesystem.
For reverse engineering, the value of dive is in correlating the instruction with its effect. A RUN instruction that installs packages will show the package files in the file tree. A COPY instruction will show the copied files, and selecting a file will show its contents if the file is readable. This is how a suspicious file can be identified: the layer that introduced it and the instruction that placed it there are both visible .
dive also reports the layer digest, which is the SHA256 content hash of the layer. This is useful for recording exactly which layer introduced a file, especially in a security audit where the finding needs to be traceable .
The efficiency metric is a secondary feature. It estimates wasted space by identifying files that were added and then deleted in later layers, or files that were duplicated across layers. The metric is not precise, but it highlights the layers that are worth investigating for size optimization .
c. dfimage: Generating the Dockerfile Text
dfimage is a tool that automates the extraction of instructions from layer metadata and prints them in Dockerfile syntax. It walks the layer history from the topmost layer down to the base image, collects the instruction associated with each layer, and prints them in reverse order . The output is a Dockerfile-shaped document that can be read and compared, even though it cannot be used directly for a build.
# Pull the dfimage tool
docker pull ghcr.io/laniksj/dfimage
# Run dfimage against a local image
docker run -v /var/run/docker.sock:/var/run/docker.sock --rm \
ghcr.io/laniksj/dfimage ruby:latest
The output begins with a FROM directive for the base image and then lists the instructions in order. The RUN instructions appear as they were executed. The COPY and ADD instructions appear with a checksum instead of a source path, reflecting the fact that the build context is not available .
The tool stops when it reaches the first tagged layer, which is assumed to be the base image. If the base image’s layers are also present and tagged, the tool will output a FROM directive with that tag and stop. If the base image is not tagged locally, the tool continues walking to the root layer, producing a longer instruction sequence that includes the base image’s instructions .
The output is a reference document, not a build artifact. The absence of the build context means that a COPY instruction cannot be replayed. The absence of build arguments means that any ARG values are not recoverable. The instruction sequence is useful for understanding how an image was constructed, but rebuilding it requires reconstructing the context from other sources.
Complete Example Session
# ============================================
# PART 1: INSPECT THE HISTORY
# ============================================
# View the layer history with full instructions
docker history --no-trunc nginx:latest
# ============================================
# PART 2: IDENTIFY THE LARGEST LAYERS
# ============================================
# Sort layers by size to find the biggest contributors
docker history nginx:latest --format "{{.Size}}\t{{.CreatedBy}}" --no-trunc | sort -hr | head -10
# ============================================
# PART 3: RUN DIVE FOR LAYER CONTENTS
# ============================================
# Explore the image layer by layer
dive nginx:latest
# ============================================
# PART 4: USE DIVE TO FIND FILE CHANGES
# ============================================
# Inside dive: select a layer, press Ctrl+U to hide unmodified files
# The file tree shows only what that layer changed
# ============================================
# PART 5: CHECK A SUSPICIOUS LAYER
# ============================================
# Look for files in unexpected paths: /usr/local/bin/, /etc/cron.d/
# Select a file to view its contents if readable
# Note the layer digest for the finding
# ============================================
# PART 6: GENERATE THE DOCKERFILE TEXT
# ============================================
# Use dfimage to print the instruction sequence
docker run -v /var/run/docker.sock:/var/run/docker.sock --rm \
ghcr.io/laniksj/dfimage nginx:latest
# ============================================
# PART 7: COMPARE WITH A KNOWN IMAGE
# ============================================
# If you have the original Dockerfile, compare the RUN instructions
# The COPY/ADD instructions will differ because of the checksum
# ============================================
# PART 8: DOCUMENT THE RECOVERED SEQUENCE
# ============================================
# Save the dfimage output for reference
docker run -v /var/run/docker.sock:/var/run/docker.sock --rm \
ghcr.io/laniksj/dfimage nginx:latest > nginx-recovered.txt
# ============================================
# PART 9: CHECK LAYER DIGESTS FOR TRACEABILITY
# ============================================
# Dive shows the digest of the selected layer
# Record it for any security finding
# ============================================
# PART 10: CLEAN UP
# ============================================
# No persistent state is created by dive or dfimage
# The recovered Dockerfile is a reference document only
The ten parts trace the workflow from the built-in history inspection through interactive layer exploration to automated Dockerfile extraction. The critical steps are Parts 2 and 3: the history identifies which layers are large or interesting, and dive shows what those layers actually contain.
Quick Reference
Docker History Options
| Option | Purpose |
|---|---|
--no-trunc | Show full instructions without truncation |
--format | Custom output format with Go templates |
--human | Human-readable sizes (default) |
--quiet | Show only layer IDs |
Dive Keybindings
| Key | Action |
|---|---|
Tab | Switch between layer list and file tree |
Ctrl+U | Hide files unmodified in selected layer |
Ctrl+F | Filter files in the tree |
Ctrl+Space | Collapse/expand directory |
Q | Quit |
Tool Comparison
| Tool | Input | Output | Use Case |
|---|---|---|---|
docker history | Image ID/tag | Instruction list with sizes | Quick inspection |
dive | Image ID/tag | Interactive layer/file view | Security audit, size analysis |
dfimage | Image ID/tag | Dockerfile-shaped text | Instruction sequence recovery |
Limitations
| Limitation | Cause |
|---|---|
COPY/ADD show checksums | Build context not stored |
ARG values missing | Not stored in layer metadata |
| Base image instructions included | History walks to root |
| Output not directly buildable | Missing context and arguments |
Best Practices
✅ Do This:
# Use --no-trunc to see full RUN instructions
docker history --no-trunc my-image # ✅
# Sort by size to find optimization targets
docker history my-image --format "{{.Size}}\t{{.CreatedBy}}" --no-trunc | sort -hr # ✅
# Use dive to inspect file changes per layer
dive my-image # ✅
# Record layer digests for security findings
# Dive shows the digest of the selected layer # ✅
❌ Don’t Do This:
# Trust the recovered Dockerfile as a build file
# COPY/ADD context is missing; ARG values are lost # ❌
# Assume all instructions in history created layers
# ENV, EXPOSE, CMD show 0B and created no filesystem layer # ❌
# Ignore the base image instructions
# History includes the base image's layers # ❌
# Skip dive if the history looks clean
# History shows commands; dive shows what files they produced # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Recovered Dockerfile does not build | COPY/ADD context missing | Use as reference only |
| Truncated instructions hide details | Default docker history truncates | Use --no-trunc |
| Large layer not visible | Metadata instructions show 0B | Focus on non-zero layers |
| Dive shows no file changes | Selected layer is metadata-only | Select a layer with a RUN/COPY instruction |
| Base image confusion | <missing> in IMAGE column | Recognize the boundary by instruction patterns |
Real-World Examples
1. Full History with Instructions
docker history --no-trunc nginx:latest
2. Largest Layers First
docker history nginx --format "{{.Size}}\t{{.CreatedBy}}" --no-trunc | sort -hr
3. Run Dive
dive nginx:latest
4. Dive Without Local Install
docker run -ti --rm -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive nginx:latest
5. Generate Dockerfile Text
docker run -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/laniksj/dfimage ruby:latest
6. Hide Unmodified Files in Dive
Ctrl+U in the file tree
7. Filter Files in Dive
Ctrl+F in the file tree
8. Record Layer Digest
Dive shows Digest field for selected layer
9. Compare Two Image Versions
docker history nginx:1.24 --format "{{.Size}}\t{{.CreatedBy}}"
docker history nginx:1.25 --format "{{.Size}}\t{{.CreatedBy}}"
10. Save Recovered Dockerfile
docker run ... dfimage nginx:latest > nginx-recovered.txt
Visual
The Layer Stack and History
┌──────────────────────────────────────────────────────────────┐
│ IMAGE LAYERS (bottom to top) │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Layer 1: FROM debian:12 (base image) ││
│ │ Layer 2: RUN apt-get update && apt-get install -y nginx ││
│ │ Layer 3: COPY nginx.conf /etc/nginx/ ││
│ │ Layer 4: EXPOSE 80 ││
│ │ Layer 5: CMD ["nginx", "-g", "daemon off;"] ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ docker history shows (top to bottom): │
│ CMD ["nginx", ...] 0B │
│ EXPOSE 80 0B │
│ COPY nginx.conf ... checksum │
│ RUN apt-get ... large size │
│ (base image layers) <missing> │
│ │
│ dive shows the file tree for each layer. │
│ Layer 2 adds /usr/sbin/nginx, /etc/nginx/, etc. │
│ Layer 3 adds /etc/nginx/nginx.conf. │
│ │
└──────────────────────────────────────────────────────────────┘
History vs. Dive View
┌──────────────────────────────────────────────────────────────┐
│ docker history dive │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Instruction │ │ Layer list │ │
│ │ + size │ │ (same commands) │ │
│ └─────────────────┘ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ File tree │ │
│ │ (what changed) │ │
│ └─────────────────┘ │
│ │
│ History: "what command ran?" │
│ Dive: "what did that command produce?" │
│ │
└──────────────────────────────────────────────────────────────┘
Recovered Dockerfile Limitations
┌──────────────────────────────────────────────────────────────┐
│ RECOVERED DOCKERFILE │
│ │
│ FROM debian:12 │
│ RUN apt-get update && apt-get install -y nginx │
│ COPY dir:abc123... /etc/nginx/ ← checksum, not path │
│ EXPOSE 80 │
│ CMD ["nginx", "-g", "daemon off;"] │
│ │
│ What is missing: │
│ ┌─────────────────────────────────────────┐ │
│ │ Original build context (source files) │ │
│ │ ARG values passed at build time │ │
│ │ Exact file paths for COPY/ADD │ │
│ └─────────────────────────────────────────┘ │
│ │
│ The instruction sequence is recoverable. │
│ The inputs to that sequence are not. │
│ │
└──────────────────────────────────────────────────────────────┘
Security Audit Workflow
┌──────────────────────────────────────────────────────────────┐
│ THIRD-PARTY IMAGE │
│ │
│ ┌─────────────────┐ │
│ │ docker history │────► Check RUN instructions │
│ │ --no-trunc │ for unexpected commands │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ dive │────► Check file changes per layer │
│ │ │ for files in unexpected paths │
│ │ │ (/usr/local/bin/, /etc/cron.d/) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Select file │────► View contents if readable │
│ │ Record digest │ Trace finding to layer │
│ └─────────────────┘ │
│ │
│ History shows commands. Dive shows files. │
│ Both are needed for a complete audit. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Dockerfile storage | Not stored in image |
| History source | Layer metadata, CREATED BY field |
--no-trunc | Essential for full instructions |
| Dive purpose | Interactive layer and file inspection |
| Dive efficiency metric | Estimates wasted space |
| dfimage output | Dockerfile-shaped reference text |
COPY/ADD recovery | Checksum only, no source path |
| Base image boundary | <missing> in history IMAGE column |
| Security use | Correlate instructions with file changes |
Key takeaways:
- The original Dockerfile is not stored in the image. The image stores layer metadata, and each layer’s metadata includes the instruction that created it.
docker historyreads this metadata. The instruction sequence is recoverable; the build context is not . docker history --no-truncis the starting point. It shows the full instruction for each layer, including the completeRUNcommands that would otherwise be truncated. Sorting by size identifies the layers that contribute the most to the image .diveshows what the instructions actually did. The history tells you that aRUNinstruction installed packages.diveshows which files were added, modified, or deleted, and allows selecting a file to view its contents. This is the difference between knowing the command and knowing the effect .dfimageautomates the extraction. It walks the layer history and prints the instructions in Dockerfile syntax. The output is a reference document. TheCOPYandADDinstructions show checksums instead of source paths, andARGvalues are missing .- The recovered Dockerfile is not buildable. The build context that the original
docker buildcommand used is not part of the image. The instruction sequence is useful for understanding and auditing, but rebuilding requires reconstructing the context from other sources. - Security auditing requires both tools. The history shows the commands;
diveshows the files. A malicious image can have a history that looks clean while containing files in unexpected paths.diveis what reveals those files and the layers that introduced them .
Remember: Docker images are built to run, not to be reconstructed. The layer metadata preserves the instructions because Docker needs it for caching and debugging, but it does not preserve the inputs to those instructions. Reverse engineering is therefore a process of reading what the image records and inferring what it does not. docker history gives the command sequence. dive gives the filesystem changes. dfimage combines the command sequence into a readable document. None of them can recover the build context, and any recovered Dockerfile should be treated as a reference rather than a build artifact. The value of the process is in understanding and auditing, not in reproduction.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!