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