build: publish multi-platform image (amd64, arm64, arm/v7)
CI / Go (test / vet / build) (push) Successful in 27s
CI / Rust (fmt / clippy / test / build) (push) Successful in 1m5s
CI / Go ↔ Rust differential + SQLite takeover (push) Successful in 1m38s
Docker publish / Build and push image (push) Successful in 1h6m5s

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 <noreply@anthropic.com>
This commit is contained in:
wtandClaude Opus 5.5 committed 2026-10-30 14:27:42 +07:00
1 parent 7bdf7b79d3
commit ae49c8f8c3
3 files changed
+41 -3

No files matched your search

+8
View File
@@ -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 }}
+26 -3
View File
@@ -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
+7
View File
@@ -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