Add UPX Binary Compression to Reduce Release Sizes#85
Add UPX Binary Compression to Reduce Release Sizes#85remmody wants to merge 2 commits intoDanielLavrushin:mainfrom
Conversation
- Added UPX compression stage to Dockerfile (Stage 2.5) - Integrated UPX compression into GitHub Actions release workflow - Using --ultra-brute --lzma flags for maximum compression - Added compression validation with upx -t before replacing binary - Repackaging tar.gz archives with compressed binaries - Regenerating SHA256 checksums after compression - Installed upx-ucl package in Ubuntu runner - Compression applied to all Linux architectures in release builds - Fallback to original binary if UPX test fails - Reduces Docker image and release artifact sizes significantly
|
There was a problem hiding this comment.
Pull request overview
This PR introduces UPX compression in both the Docker build pipeline and the GitHub Actions release workflow to reduce the size of shipped Linux binaries and resulting artifacts.
Changes:
- Added a dedicated Docker build stage to compress the built
b4binary with UPX before copying it into the runtime image. - Updated the release workflow to install UPX, compress per-arch Linux binaries, and regenerate
tar.gzassets + SHA256 checksums after compression.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| Dockerfile | Adds an UPX compression stage and swaps the runtime image to use the compressed binary. |
| .github/workflows/release.yml | Installs UPX and compresses per-arch outputs before repackaging release tarballs and checksums. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| -o /b4 | ||
|
|
||
| # Stage 2.5: UPX compression | ||
| FROM --platform=$TARGETPLATFORM alpine:3.23.3 AS upx-compressor |
There was a problem hiding this comment.
The UPX stage is pinned to --platform=$TARGETPLATFORM, which means during cross-platform builds (e.g., building linux/arm64 on an amd64 runner) the upx binary itself will run under QEMU emulation. That typically slows builds significantly and can fail if emulation isn’t available. Consider running the compressor stage on $BUILDPLATFORM (tooling runs native) while still compressing the target-arch /b4 output.
| FROM --platform=$TARGETPLATFORM alpine:3.23.3 AS upx-compressor | |
| FROM --platform=$BUILDPLATFORM alpine:3.23.3 AS upx-compressor |
| RUN apk add --no-cache upx | ||
|
|
||
| COPY --from=go-builder /b4 /b4 | ||
| RUN upx --ultra-brute --lzma /b4 -o /b4.upx && upx -t /b4.upx |
There was a problem hiding this comment.
upx --ultra-brute --lzma is extremely CPU-intensive and can add a large amount of time to Docker builds (especially under QEMU when cross-building). Consider switching to a less expensive preset (e.g., --best) or making the compression level configurable via a build arg so release builds can opt into ultra-brute when needed.
| RUN apk add --no-cache upx | |
| COPY --from=go-builder /b4 /b4 | |
| RUN upx --ultra-brute --lzma /b4 -o /b4.upx && upx -t /b4.upx | |
| ARG UPX_FLAGS="--best" | |
| RUN apk add --no-cache upx | |
| COPY --from=go-builder /b4 /b4 | |
| RUN upx ${UPX_FLAGS} /b4 -o /b4.upx && upx -t /b4.upx |
| upx --ultra-brute --lzma "$BINARY_PATH" -o "${BINARY_PATH}.upx" | ||
|
|
||
| # Test compressed binary | ||
| if upx -t "${BINARY_PATH}.upx"; then | ||
| echo "UPX compression successful, replacing original binary" | ||
| mv "${BINARY_PATH}.upx" "$BINARY_PATH" | ||
| else | ||
| echo "UPX test failed, keeping original binary" |
There was a problem hiding this comment.
This step intends to keep the original binary if UPX fails, but with GitHub Actions’ default bash -e behavior the command upx --ultra-brute --lzma ... will fail the step immediately on any non-zero exit code (e.g., unsupported arch/format), and the fallback logic below won’t run. Wrap the compression command in an if upx ...; then ... else ... block (or otherwise handle the exit status) so the fallback actually works.
| upx --ultra-brute --lzma "$BINARY_PATH" -o "${BINARY_PATH}.upx" | |
| # Test compressed binary | |
| if upx -t "${BINARY_PATH}.upx"; then | |
| echo "UPX compression successful, replacing original binary" | |
| mv "${BINARY_PATH}.upx" "$BINARY_PATH" | |
| else | |
| echo "UPX test failed, keeping original binary" | |
| if upx --ultra-brute --lzma "$BINARY_PATH" -o "${BINARY_PATH}.upx"; then | |
| # Test compressed binary | |
| if upx -t "${BINARY_PATH}.upx"; then | |
| echo "UPX compression successful, replacing original binary" | |
| mv "${BINARY_PATH}.upx" "$BINARY_PATH" | |
| else | |
| echo "UPX test failed, keeping original binary" | |
| rm -f "${BINARY_PATH}.upx" | |
| fi | |
| else | |
| echo "UPX compression failed (possibly unsupported arch/format), keeping original binary" |
| upx --ultra-brute --lzma "$BINARY_PATH" -o "${BINARY_PATH}.upx" | ||
|
|
There was a problem hiding this comment.
--ultra-brute --lzma can make the release workflow substantially slower across the full arch matrix. Unless maximum compression is required, consider a faster default (e.g., --best) and/or make the compression level configurable (workflow input/env) so you can trade off size vs. build time.



Overview
This PR adds UPX compression to both Docker builds and the GitHub Actions release workflow, significantly reducing binary sizes by ~60–70% without runtime performance impact.
Changes
Dockerfile (Stage 2.5: UPX Compression)
alpine:3.23.3with UPX--ultra-brute --lzmafor maximum size reductionupx -tbefore deployment/usr/local/bin/b4GitHub Actions Release Workflow
upx-uclpackage in Ubuntu runner386amd64arm64armv5-7mips*ppc64*riscv64s390xloong64tar.gzpackagingtar.gzarchives with compressed binariesBenefits
Technical Details
linux/amd64linux/arm64linux/arm/v7linux/arm/v6Testing
upx -tvalidationCommit
05e7226— feat(build): add UPX compression to reduce binary size