From 5b6782df1378ec8f8e723423823507e4a60e48fb Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 6 Jul 2025 12:48:54 +0200 Subject: [PATCH] chore: switch to multiarch Docker image --- Dockerfile | 85 ++++++++++++++++++++++++++++++++++++++++++++---------- Justfile | 6 ++++ 2 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 Justfile diff --git a/Dockerfile b/Dockerfile index 9b64364..06f08b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,88 @@ -FROM rust:1.83-alpine3.21 AS builder +# This Dockerfile supports building multi-platform Rust applications for +# linux/amd64 and linux/arm64. It was largely inspired by the article linked +# below, with the difference being that it more closely follows Docker's +# caching system. In theory, it can support any architecture that Docker and +# Rust both support. +# +# All RUN directives that require the specific target platform modify the +# TARGETPLATFORM argument using a sed command. This is required because Rust +# and Docker use different names for referring to the same architecture. +# +# https://dev.to/vladkens/fast-multi-arch-docker-build-for-rust-projects-an1 -ARG DI_VER=1.2.5 +# We first create a base image that installs Zig (for zig cc) and Cargo Chef +# (for better dependency caching). This image is shared between all +# architectures. +FROM --platform=$BUILDPLATFORM rust:1.88-alpine3.21 AS chef WORKDIR /app -RUN apk update && apk add --no-cache build-base +RUN apk update && \ + apk add --no-cache build-base musl-dev openssl-dev zig && \ + cargo install --locked cargo-zigbuild cargo-chef -# Build dumb-init -RUN wget -O - "https://github.com/Yelp/dumb-init/archive/refs/tags/v${DI_VER}.tar.gz" | tar -xzf - && \ + +# The planner generates the Chef recipe.json file that allows the builder steps +# to efficiently cache dependency builds, greatly speeding up builds if +# dependencies haven't changed. This image is also shared between all +# dependencies. +FROM chef AS planner + +COPY . . + +RUN cargo chef prepare --recipe-path recipe.json + + +# The builder container is responsible for performing the actual build. It +# installs its respective Rust toolchain and builds dumb-init for the target +# platform. Then it uses the generated Chef recipe file to build the +# dependencies, before using zigbuild to build the actual final binary. +FROM chef AS builder + +ARG DI_VER=1.2.5 +ARG TARGETPLATFORM + +RUN export ARCH="$(echo "${TARGETPLATFORM}" | sed 's:linux/amd64:x86_64:;s:linux/arm64:aarch64:')" && \ + rustup target add "$ARCH-unknown-linux-musl" && \ + wget -O - "https://github.com/Yelp/dumb-init/archive/refs/tags/v${DI_VER}.tar.gz" | tar -xzf - && \ cd "dumb-init-${DI_VER}" && \ - make SHELL=/bin/sh && \ - mv dumb-init .. && \ + make CC="zig cc -target $ARCH-linux-musl" SHELL=/bin/sh && \ + mkdir -p "/app/${TARGETPLATFORM}" && \ + mv dumb-init "/app/${TARGETPLATFORM}/dumb-init" && \ cd .. -COPY Cargo.toml Cargo.lock ./ +# Using Chef, we can build the dependencies separately from the application +# itself. This allows dependency builds to be cached, greatly speeding up +# builds when dependencies haven't changed. Zigbuild is used to support +# building C-based dependencies, such as libsqlite3. +COPY --from=planner /app/recipe.json recipe.json -RUN cargo fetch --locked +RUN export ARCH="$(echo "${TARGETPLATFORM}" | sed 's:linux/amd64:x86_64:;s:linux/arm64:aarch64:')" && \ + cargo chef cook \ + --recipe-path recipe.json \ + --release --zigbuild \ + --target "$ARCH-unknown-linux-musl" -COPY . ./ +# Finally we copy the application source code and build the application binary, +# using the cached dependency build. +COPY . . -RUN cargo build --release --frozen +RUN export ARCH="$(echo "${TARGETPLATFORM}" | sed 's:linux/amd64:x86_64:;s:linux/arm64:aarch64:')" && \ + cargo zigbuild \ + --release \ + --target "$ARCH-unknown-linux-musl" && \ + cp target/"$ARCH-unknown-linux-musl"/release/site "/app/${TARGETPLATFORM}/site" +# We generate the final Alpine-based image by copying the built binaries from +# the respective target platform's builder container. This is the only part of +# the build that runs inside an emulator. FROM alpine:3.21 -COPY --from=builder /app/target/release/site /bin/site -COPY --from=builder /app/dumb-init /bin/dumb-init +ARG TARGETPLATFORM + +COPY --from=builder /app/${TARGETPLATFORM}/dumb-init /bin/dumb-init +COPY --from=builder /app/${TARGETPLATFORM}/site /bin/site # Create a non-root user & make sure it can write to the data directory RUN set -x && \ @@ -41,5 +98,3 @@ USER www-data:www-data ENTRYPOINT [ "/bin/dumb-init", "--" ] CMD [ "/bin/site" ] - - diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..bf935b3 --- /dev/null +++ b/Justfile @@ -0,0 +1,6 @@ +# Build the multi-arch release OCI image and push it to the registry +push-release-image: + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t chewingbever/site:latest \ + --push .