Skip to content

build: compress linux binary with UPX in builder-linux stage#2934

Closed
zampani-docker wants to merge 1 commit into
mainfrom
zampani/upx-binary-compression
Closed

build: compress linux binary with UPX in builder-linux stage#2934
zampani-docker wants to merge 1 commit into
mainfrom
zampani/upx-binary-compression

Conversation

@zampani-docker
Copy link
Copy Markdown

Summary

  • Adds UPX compression to the builder-linux stage, reducing the production binary from ~110 MB to ~28.7 MB (73.8% reduction)
  • UPX installed in builder-base alongside clang and zig (all host-arch build tools), so it is cached once and inherited — no per-variant re-fetch
  • Compression runs in its own RUN layer after the Go build layer for correct BuildKit cache separation
  • upx -t validates packed binary integrity at build time before the image is finalized
  • builder-cross (macOS/Windows release binaries) intentionally unchanged — UPX on macOS requires re-signing and triggers AV on Windows

Test plan

  • UPX default compression (level 7) verified: 114,874,072 → 30,089,008 bytes (26.19%) in ~30s
  • Compressed binary tested inside Alpine container: version and --help both work correctly after UPX decompression
  • upx -t integrity check passes on the packed binary

Deferred findings (accepted tradeoffs)

  • RSS at runtime: The binary decompresses to ~110 MB in anonymous memory at startup — on-disk size is 28.7 MB but runtime footprint is unchanged. Containers with memory limits below ~200 MB may OOM during decompression.
  • Startup latency: ~300–800 ms decompression on cold invocations before the Go runtime initialises. Acceptable for a long-lived interactive agent; noticeable for very short-lived invocations.
  • AV/EDR false positives: UPX-packed binaries are flagged heuristically by some AV/EDR products. Teams deploying in regulated environments may need to pre-allowlist the binary.
  • Seccomp: UPX's decompression stub requires mmap and mprotect(PROT_EXEC) on anonymous mappings. Docker's default seccomp profile allows both; custom hardened profiles may need to be updated.

Comment drafted by Claude Code.

Adds UPX compression to the production Docker build, reducing the
binary from ~110 MB to ~28.7 MB (73.8% reduction). Uses default
compression level (7) rather than --best, which takes 9+ minutes on
this binary size with negligible additional compression gain.

UPX is installed in builder-base alongside clang and zig (all
host-arch build tools), so it is cached once in the base layer and
inherited by builder-linux without a per-variant network fetch.
Compression runs in its own RUN step after the Go build layer so the
two are cached independently. upx -t validates the packed binary
integrity before the image is finalized.

Operational tradeoffs (accepted):
- Runtime RSS is ~110 MB regardless of the 28.7 MB on-disk size
- ~300-800 ms startup decompression on cold invocations
- UPX-packed binaries may trigger AV/EDR heuristics in some environments

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@docker-agent docker-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

One medium-severity finding related to UPX version compatibility for targets.

Comment thread Dockerfile
xx-go build -trimpath -tags no_audio -ldflags "-s -w -linkmode=external -X 'github.com/docker/docker-agent/pkg/version.Version=$GIT_TAG' -X 'github.com/docker/docker-agent/pkg/version.Commit=$GIT_COMMIT'" -o /binaries/docker-agent-$TARGETOS-$TARGETARCH .
xx-verify --static /binaries/docker-agent-$TARGETOS-$TARGETARCH
EOT
RUN upx /binaries/docker-agent-$TARGETOS-$TARGETARCH && \
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] UPX arm64 compatibility depends on version — build may fail for linux/arm64

UPX support for linux/arm64 ELF binaries was only added in UPX 4.0 (2023). Alpine's apk add upx installs whatever version the Alpine release ships:

  • Alpine 3.17 and earlier → UPX 3.x → no arm64 supportupx will fail at build time with CantPackException: bad ELF e_machine when TARGETARCH=arm64
  • Alpine 3.18+ → UPX 4.x → arm64 supported

The Dockerfile pins ALPINE_VERSION="3.22", so today this is fine. However:

  1. There is no floor on the UPX version, so a future Alpine base image re-tag that ships an older UPX could silently break linux/arm64 builds.
  2. The PR description notes the binary was tested at ~114 MB → ~30 MB but does not explicitly confirm arm64 (TARGETARCH=arm64) was part of the test run.

Suggested mitigation: Add an explicit upx --version check or pin the upx version (e.g., apk add upx~=4) to ensure arm64 support is guaranteed, and confirm the test plan covers both linux/amd64 and linux/arm64.

@aheritier aheritier added area/distribution Agent registry, packaging, distribution, sharing kind/chore Maintenance, deps, CI, tooling (maps to chore: commit prefix) labels May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/distribution Agent registry, packaging, distribution, sharing kind/chore Maintenance, deps, CI, tooling (maps to chore: commit prefix)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants