Skip to content

Why I Removed Tools from My Container Image (And the Framework I Used to Decide)

DebugBox v1.0.0 shipped with every tool I could justify. The power variant included bird (a BGP routing daemon), bridge-utils, nano, py3-pip, and speedtest-cli. In v1.1.0, all five were removed. Several other tools were moved between variants. The result: balanced dropped from 51 MB to 47 MB, power from 112 MB to 91 MB.

This post covers the decision framework used to evaluate each tool, the specific changes made, and how the project's manifest system prevents tool lists from drifting across documentation.

The Problem with "Add Everything"

The first release of any tool project tends to be additive. When the question is "should we include this?", the default answer is yes. The cost of inclusion feels low (a few MB, one more line in the install script), and the cost of exclusion feels high (what if someone needs it?).

This logic breaks down over time for three reasons:

  1. Size compounds. Each tool adds not just its own binary but its dependencies. py3-pip alone pulls in Python 3 and its standard library. The marginal cost of "one more tool" is never just the tool.

  2. Scope creeps. bird is a BGP routing daemon. It runs a routing protocol. That is not debugging. But it ended up in the power variant because it is tangentially related to networking, and the boundary between "networking tool" and "debugging tool" was never defined.

  3. Maintenance burden scales. Every included tool is a potential CVE surface. When Trivy flags a vulnerability (the gate that does this flagging is covered in Building a Multi-Arch Container CI Pipeline with Hard-Fail Security Gates), you have to determine whether it is in a tool the variant actually needs or in something that was included speculatively.

The Evaluation Framework

For v1.1.0, every tool in every variant was evaluated against two questions:

1. Does this tool serve the variant's stated purpose?

Each variant has a defined scope (the full tool list and comparison against netshoot, Alpine, and busybox is in Choosing the Right Kubernetes Debugging Container):

  • Lite: Network connectivity and data inspection
  • Balanced: Daily Kubernetes troubleshooting
  • Power: Packet analysis, firewall debugging, and forensics

If a tool does not fit the variant's scope, it either moves to a different variant or gets removed entirely.

2. Does this tool overlap with something already included?

If two tools serve the same function, keep the one with broader utility. wget overlaps with curl. nano overlaps with vim. In both cases, the first tool was removed.

What Changed

Moved from balanced to power

nmap, iperf3, iftop, ethtool: These are specialized tools. nmap is a port scanner, not a daily debugging tool. iperf3 measures bandwidth between two endpoints, a task that comes up during capacity planning, not routine troubleshooting. iftop monitors network traffic by connection, useful for forensics but not for checking why a service returns 503. ethtool inspects network interface hardware settings, relevant in bare-metal or specific driver debugging scenarios.

All four remain available in power. Users who need them switch from debugbox:balanced to debugbox:power. The trade-off is explicit: you pay 91 MB instead of 47 MB, and you get the specialized tools.

Moved from power to balanced

openssl: TLS certificate debugging is not a specialized task. Checking certificate expiry, inspecting CA chains, and testing TLS connections are routine operations for anyone working with Kubernetes services. openssl belongs in the daily driver, not behind a 91 MB pull.

The shell helpers sniff-http, sniff-dns, and cert-check() were also moved from power-only to balanced, following the same logic: if the underlying tool (tcpdump, openssl) is in balanced, the helper that wraps it should be there too.

Removed entirely

bird: A BGP routing daemon. Runs a routing protocol. Not a debugging or inspection tool. It was included because it is in the "networking" category in Alpine's package repository, but category membership in a package manager is not a sufficient reason to include a tool in a debugging image.

bridge-utils: Provides brctl for managing Linux bridge devices. Rarely relevant in container networking, where bridges are managed by the container runtime (containerd, CRI-O) and CNI plugins, not by the user.

nano: A text editor. vim is already included in balanced and power. Including two editors in a debugging image is redundant. vim was kept because it has broader functionality (macros, scripting, regex search).

py3-pip: Allows installing arbitrary Python packages at runtime. This contradicts the design goal of a controlled, pre-built tool set. If a user needs a Python package for debugging, they can extend the image with their own Dockerfile. Including pip invites uncontrolled dependency installation into an image that is meant to be deterministic.

speedtest-cli: Measures internet bandwidth to Ookla servers. Not a debugging tool. Bandwidth between pods or nodes is measured with iperf3 (which is included in power). speedtest-cli tests connectivity to external servers, which is a different problem and not one that a Kubernetes debugging container should own.

wget: Overlaps entirely with curl. Both fetch URLs over HTTP/HTTPS. curl is more widely used, has richer output control, and was already present in lite. wget was in balanced with no functional gap that curl does not cover.

The Manifest System

Removing tools creates a documentation problem. If the tool list is hardcoded in multiple pages, some pages will show the old list and others the new one. This happened in v1.0.0: the README, the variant pages, the examples page, and the manifest page all had independent copies of the tool list.

DebugBox uses docs/manifest.yaml as the single source of truth for tool composition:

variants:
  balanced:
    packages:
      http-and-tls:
        - openssl    # TLS certificate inspection (curl inherited from lite)
      networking:
        - tcpdump
        - socat
        - mtr

Every documentation page that needs to reference tool lists links to the manifest or to reference/manifest.md (which renders the manifest in a readable format). No page maintains its own copy of the tool list.

When a tool moves between variants, the change happens in three places:

  1. The Dockerfile (the actual package installation)
  2. The verify script (confirms the tool is present after build)
  3. docs/manifest.yaml (the documented contract)

These three form a source-of-truth hierarchy. The Dockerfile is tier 1 (what is actually installed). The verify script is tier 2 (what is tested). The manifest is tier 3 (what is documented). If any of the three disagree, the build or the docs build will fail.

Results

Variant v1.0.0 v1.1.0 Change
Lite ~15 MB ~15 MB No change
Balanced ~51 MB ~47 MB -4 MB (removed wget, moved 4 tools to power, added openssl)
Power ~112 MB ~91 MB -21 MB (removed 5 tools, gained 4 from balanced)

The size reduction in power is significant: 21 MB, or about 19%. Most of that came from removing py3-pip (which pulls in the Python runtime) and bird (which includes protocol libraries).

The Harder Lesson

Adding a tool to an open-source project is easy. You add a line to the install script, verify it works, and ship. The user base grows to include people who depend on that tool.

Removing a tool requires understanding who depends on it and why. It requires documenting the removal, providing a migration path (in this case, switching to a different variant), and accepting that some users will disagree with the decision.

The framework (does it fit the variant's purpose? does it overlap?) is simple. Applying it honestly is the hard part. Every removal is a statement about what the project is and is not.


Series: DebugBox, From Variant Design to Release Pipeline (Part 2 of 4)