Build fully static amd64 + arm64 Linux binaries for Rust projects with vendored OpenSSL and ring -- no flaky toolchain servers needed.
Cross-compiling Rust to aarch64-unknown-linux-musl on x86_64 GitHub Actions runners is painful:
| Approach | Problem |
|---|---|
gcc-aarch64-linux-gnu (apt) |
glibc cross-compiler -- vendored OpenSSL compiles against glibc headers, producing __memcpy_chk / __memset_chk undefined references against musl |
musl.cc toolchain download |
Single community server -- curl: (28) Failed to connect to musl.cc port 443 -- unreliable in CI |
| Manual CC/AR/linker env vars | Fragile -- must set CC_aarch64_unknown_linux_musl, AR_aarch64_unknown_linux_musl, CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER, plus .cargo/config.toml |
cargo-zigbuild solves all of this. Zig bundles its own musl libc, Clang-based C compiler, assembler, and linker. Two install steps, zero configuration, works for both architectures from a single install.
# Zig for C/C++ cross-compilation to musl
- uses: mlugg/setup-zig@v2
- name: Install cargo-zigbuild
run: pip3 install cargo-zigbuildThen just replace cargo build with cargo zigbuild:
- name: Build release binary
run: |
# Let cargo-zigbuild handle the zig integration automatically
cargo zigbuild --release --target ${{ matrix.rust_target }}
cp target/${{ matrix.rust_target }}/release/${{ env.BINARY_NAME }} ./${{ env.BINARY_NAME }}-${{ matrix.arch }}
# sanity check the binary and show dependencies
file ./${{ env.BINARY_NAME }}-${{ matrix.arch }} && (ldd ./${{ env.BINARY_NAME }}-${{ matrix.arch }} || true)No CC, AR, linker env vars needed. No .cargo/config.toml linker config. No musl toolchain downloads.
name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
SQLX_OFFLINE: "true"
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
BINARY_NAME: myapp
permissions:
contents: write
packages: write
jobs:
build-binaries:
name: Build ${{ matrix.arch }}
runs-on: ubuntu-latest
strategy:
matrix:
include:
- arch: amd64
rust_target: x86_64-unknown-linux-musl
- arch: arm64
rust_target: aarch64-unknown-linux-musl
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.91.0"
targets: ${{ matrix.rust_target }}
# Zig for C/C++ cross-compilation to musl
- uses: mlugg/setup-zig@v2
- name: Install cargo-zigbuild
run: pip3 install cargo-zigbuild
- name: Cache cargo registry and build artifacts
uses: Swatinem/rust-cache@v2
with:
key: ${{ runner.os }}-${{ matrix.rust_target }}-zigbuild-${{ hashFiles('**/Cargo.lock') }}
cache-targets: true
- name: Build release binary
run: |
# Let cargo-zigbuild handle the zig integration automatically
cargo zigbuild --release --target ${{ matrix.rust_target }}
cp target/${{ matrix.rust_target }}/release/${{ env.BINARY_NAME }} ./${{ env.BINARY_NAME }}-${{ matrix.arch }}
# sanity check the binary and show dependencies
file ./${{ env.BINARY_NAME }}-${{ matrix.arch }} && (ldd ./${{ env.BINARY_NAME }}-${{ matrix.arch }} || true)
- name: Stage artifacts
run: |
mkdir -p staging
cp ./${{ env.BINARY_NAME }}-${{ matrix.arch }} staging/
cp -r migrations staging/
cp -r config staging/
chmod +x staging/${{ env.BINARY_NAME }}-${{ matrix.arch }}
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.arch }}
path: staging/
docker:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs: build-binaries
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download all binary artifacts
uses: actions/download-artifact@v4
with:
pattern: binary-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push multi-arch image
uses: docker/build-push-action@v6
with:
context: .
file: deploy/docker/Dockerfile.release
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}Use Docker's TARGETARCH to select the right binary in a single Dockerfile:
FROM cgr.dev/chainguard/static:latest
ARG TARGETARCH
COPY --chmod=755 ./myapp-${TARGETARCH} /usr/local/bin/myapp
COPY ./migrations /app/migrations
COPY ./config /app/config
WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/myapp"]cgr.dev/chainguard/static is a zero-CVE image with no shell, no libc, no dynamic loader -- perfect for fully static musl binaries.
- C compiler (
zig cc): Clang-based, targets any architecture from any host - Assembler: Handles ring's aarch64 assembly files (
.S) - musl libc: Bundled -- no system musl-dev/musl-tools needed for cross targets
- Archiver (
zig ar): Creates static libraries for ring and openssl-src - Linker: Produces fully static binaries
openssl = { features = ["vendored"] }
-> openssl-src (build.rs)
-> cc crate -> zig cc --target=aarch64-linux-musl
-> OpenSSL ./Configure CC="zig cc"
-> C code compiled with musl headers (not glibc!)
-> No __memcpy_chk / __memset_chk issues
sqlx / reqwest / lettre -> rustls -> ring
ring build.rs -> cc crate -> zig cc --target=aarch64-linux-musl
-> C files + aarch64 assembly (.S) compiled correctly
-> zig ar creates static archives
| Aspect | gcc-aarch64-linux-gnu | musl.cc | cargo-zigbuild |
|---|---|---|---|
| Install reliability | apt (reliable) | curl from musl.cc (flaky) | pip/GitHub Action (reliable) |
| Works with musl target | No (glibc symbols) | Yes | Yes |
| Vendored OpenSSL | Fails (glibc headers) | Works (with CC/AR env) | Works (automatic) |
| ring assembly | Works | Works (with CC/AR env) | Works (automatic) |
| Config needed | CC, AR, linker, config.toml | CC, AR, linker, config.toml | None |
| Both arches from one install | No | No | Yes |
| Disk usage | ~50MB | ~180MB | ~45MB |
| Produces static binary | No (glibc) | Yes | Yes |
# Pin specific versions if needed
- uses: mlugg/setup-zig@v2
with:
version: "0.13.0"
- run: pip3 install cargo-zigbuild==0.21.8Some ring versions may need clang 18+ with zig 0.15's bundled libc++ 19 headers:
- run: sudo apt-get install -y clang-18 libclang-18-devOr pin to zig 0.13.0 (stable, no issues).
# Should show "statically linked" for amd64
file ./myapp-amd64
# ELF 64-bit LSB executable, x86-64, statically linked
# ldd should fail or show "not a dynamic executable"
ldd ./myapp-amd64 || true
# not a dynamic executable
# For arm64 (cross-compiled), file shows the arch
file ./myapp-arm64
# ELF 64-bit LSB executable, ARM aarch64, statically linked- cargo-zigbuild -- The tool itself
- mlugg/setup-zig -- GitHub Action with mirror fallback + signature verification
- cargo-zigbuild on PyPI --
pip3 install cargo-zigbuild - ring BUILDING.md -- ring's cross-compilation docs
- cgr.dev/chainguard/static -- Zero-CVE runtime image
- docker/metadata-action -- Docker tag generation
- Swatinem/rust-cache -- Rust build caching for GitHub Actions