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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants