| |

Docker 95 🐳 Reverse Engineering Docker Images: Extracting Dockerfiles using dive and Layer Inspection

⏱️ Estimated read: 15 min | |

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

OptionPurpose
--no-truncShow full instructions without truncation
--formatCustom output format with Go templates
--humanHuman-readable sizes (default)
--quietShow only layer IDs

Dive Keybindings

KeyAction
TabSwitch between layer list and file tree
Ctrl+UHide files unmodified in selected layer
Ctrl+FFilter files in the tree
Ctrl+SpaceCollapse/expand directory
QQuit

Tool Comparison

ToolInputOutputUse Case
docker historyImage ID/tagInstruction list with sizesQuick inspection
diveImage ID/tagInteractive layer/file viewSecurity audit, size analysis
dfimageImage ID/tagDockerfile-shaped textInstruction sequence recovery

Limitations

LimitationCause
COPY/ADD show checksumsBuild context not stored
ARG values missingNot stored in layer metadata
Base image instructions includedHistory walks to root
Output not directly buildableMissing 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

PitfallWhy It HappensFix
Recovered Dockerfile does not buildCOPY/ADD context missingUse as reference only
Truncated instructions hide detailsDefault docker history truncatesUse --no-trunc
Large layer not visibleMetadata instructions show 0BFocus on non-zero layers
Dive shows no file changesSelected layer is metadata-onlySelect a layer with a RUN/COPY instruction
Base image confusion<missing> in IMAGE columnRecognize 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

ItemValue
Dockerfile storageNot stored in image
History sourceLayer metadata, CREATED BY field
--no-truncEssential for full instructions
Dive purposeInteractive layer and file inspection
Dive efficiency metricEstimates wasted space
dfimage outputDockerfile-shaped reference text
COPY/ADD recoveryChecksum only, no source path
Base image boundary<missing> in history IMAGE column
Security useCorrelate 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 history reads this metadata. The instruction sequence is recoverable; the build context is not .
  • docker history --no-trunc is the starting point. It shows the full instruction for each layer, including the complete RUN commands that would otherwise be truncated. Sorting by size identifies the layers that contribute the most to the image .
  • dive shows what the instructions actually did. The history tells you that a RUN instruction installed packages. dive shows 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 .
  • dfimage automates the extraction. It walks the layer history and prints the instructions in Dockerfile syntax. The output is a reference document. The COPY and ADD instructions show checksums instead of source paths, and ARG values are missing .
  • The recovered Dockerfile is not buildable. The build context that the original docker build command 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; dive shows the files. A malicious image can have a history that looks clean while containing files in unexpected paths. dive is 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.

Was this helpful?


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!