From ae49c8f8c3edc0d9a33212a0fdeb608a4a0e2b71 Mon Sep 17 00:00:00 2001 From: wt Date: Fri, 30 Oct 2026 14:27:42 +0700 Subject: [PATCH] build: publish multi-platform image (amd64, arm64, arm/v7) The builder stage runs on the build platform and cross-compiles Rust, with GCC cross compilers for bundled SQLite, so only the small runtime stage runs under QEMU. The workflow sets up QEMU via the Docker Hub mirror and pushes all three platforms under the same tags. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docker-publish.yml | 8 ++++++++ Dockerfile | 29 +++++++++++++++++++++++++--- docs/DEPLOYMENT.md | 7 +++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 354181e..546016a 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -55,6 +55,13 @@ jobs: echo "image=$image" >> "$GITHUB_OUTPUT" echo "Publishing $image:$tag as $REGISTRY_USER" + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + # Only the runtime stage runs emulated; Rust is cross-compiled natively. + image: ${{ vars.DOCKERHUB_MIRROR || 'mirror.gcr.io' }}/tonistiigi/binfmt:latest + platforms: arm64,arm + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: @@ -75,6 +82,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . + platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true tags: | ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.version_tagname }} diff --git a/Dockerfile b/Dockerfile index df8f1dd..707f217 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,39 @@ -FROM rust:1.90-bookworm AS build +# Rust always runs natively on the build machine and cross-compiles for the +# image platform; only the small runtime stage executes under emulation. +FROM --platform=$BUILDPLATFORM rust:1.90-bookworm AS build +ARG TARGETPLATFORM +# Bundled SQLite is C, so a cross target also needs a C cross compiler and +# bookworm's glibc for that architecture (matching the runtime image). +RUN set -eu; \ + case "$TARGETPLATFORM" in \ + linux/amd64) target=x86_64-unknown-linux-gnu; gnu=x86_64-linux-gnu; arch=amd64 ;; \ + linux/arm64) target=aarch64-unknown-linux-gnu; gnu=aarch64-linux-gnu; arch=arm64 ;; \ + linux/arm/v7) target=armv7-unknown-linux-gnueabihf; gnu=arm-linux-gnueabihf; arch=armhf ;; \ + *) echo "Unsupported platform: $TARGETPLATFORM" >&2; exit 1 ;; \ + esac; \ + echo "export RUST_TARGET=$target" > /cross.env; \ + if [ "$target" != "$(rustc -vV | sed -n 's/^host: //p')" ]; then \ + apt-get update; \ + apt-get install -y --no-install-recommends "gcc-$(echo "$gnu" | tr _ -)" "libc6-dev-$arch-cross"; \ + rm -rf /var/lib/apt/lists/*; \ + rustup target add "$target"; \ + env=$(echo "$target" | tr a-z- A-Z_); \ + echo "export CARGO_TARGET_${env}_LINKER=$gnu-gcc CC_$(echo "$target" | tr - _)=$gnu-gcc AR_$(echo "$target" | tr - _)=$gnu-ar" >> /cross.env; \ + fi WORKDIR /src COPY Cargo.toml Cargo.lock ./ COPY src ./src COPY internal/httpapi/web ./internal/httpapi/web -RUN cargo build --locked --release +RUN . /cross.env \ + && cargo build --locked --release --target "$RUST_TARGET" \ + && cp "target/$RUST_TARGET/release/balance-server" /balance-server FROM debian:bookworm-slim # Keep the original Alpine image's numeric identity so existing volumes remain writable. RUN groupadd --system --gid 101 balance \ && useradd --system --uid 100 --gid balance --home-dir /nonexistent --no-log-init balance \ && mkdir /data && chown balance:balance /data -COPY --from=build /src/target/release/balance-server /usr/local/bin/balance-server +COPY --from=build /balance-server /usr/local/bin/balance-server USER balance ENV BALANCE_ADDR=:8080 BALANCE_DB_PATH=/data/balance.db EXPOSE 8080 diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 7ad387f..17053cc 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -14,6 +14,13 @@ are compiled at image build time. The numeric identity remains UID 100 / GID 101, matching the original Alpine Go image, so existing volume permissions are preserved across the switch. +The image is multi-platform: `linux/amd64`, `linux/arm64` and `linux/arm/v7` +(armhf) are published under the same tag, and `docker pull` selects the matching +one. The builder stage runs on the build machine's platform and cross-compiles +Rust (with GCC cross compilers for bundled SQLite), so only the runtime stage +needs QEMU. Build all platforms locally with a `docker-container` buildx builder: +`docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 .` + `docker compose up -d --build` publishes **http://localhost:8123** (host 8123, container 8080). `GET /health` is the liveness endpoint. Direct binary execution uses `:8080` by default. Place a TLS reverse proxy in front of the published port