From e5f2d3c30e29d918bb48edbd65b16e06a9cd93c0 Mon Sep 17 00:00:00 2001 From: Hiroyuki Wada Date: Sat, 13 Jun 2026 20:58:00 +0900 Subject: [PATCH 1/2] ci: slim the release image to distroless/base-nossl (drop unused OpenSSL/C++) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release image used distroless/cc, which bundles OpenSSL (libssl3, ~8MB) and the C++ runtime (libstdc++/libgomp). scim-server needs neither: - it serves plain HTTP (TcpListener + axum::serve); TLS termination is expected at a reverse proxy (hence the X-Forwarded-* host resolution). No server TLS. - the only TLS is the PostgreSQL client via sqlx + rustls (pure Rust, no OpenSSL). - no C++ dependencies (SQLite is C, compiled into the binary). Switch Dockerfile.release to distroless/base-nossl-debian13 — ~12MB smaller and still has glibc + ca-certificates. Verified: the published v0.4.1 binary serves ServiceProviderConfig and User CRUD on base-nossl. The self-contained Dockerfile (dev/compose/CI) stays on distroless/cc: plain cargo binaries dynamically link libgcc_s.so.1, which base-nossl omits. The release binaries don't need it because cargo-zigbuild links libgcc statically. Co-Authored-By: Claude Opus 4.8 (1M context) --- Dockerfile | 8 +++++--- Dockerfile.release | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index aaccfd6..0750630 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,9 +25,11 @@ RUN rm -rf src COPY src ./src RUN cargo build --release --locked --features "${FEATURES}" -# Runtime stage: distroless/cc provides glibc + libgcc + ca-certificates and a -# non-root user (debian13 / trixie is the current latest). sqlx-postgres is pure -# Rust and rusqlite bundles SQLite, so no extra system libraries are required. +# Runtime stage. This self-contained image is built with plain cargo, whose +# binary dynamically links libgcc_s.so.1, so it needs distroless/cc (which ships +# libgcc). The published release image uses base-nossl instead — see +# Dockerfile.release — because the cargo-zigbuild binaries link libgcc +# statically and don't need it. distroless/cc is glibc-based (debian13, latest). FROM gcr.io/distroless/cc-debian13:nonroot COPY --from=builder /app/target/release/scim-server /usr/local/bin/scim-server diff --git a/Dockerfile.release b/Dockerfile.release index 40d9f92..29b96ab 100644 --- a/Dockerfile.release +++ b/Dockerfile.release @@ -5,9 +5,11 @@ # source of truth). Nothing is compiled here, so the multi-arch image is # assembled by COPY alone (no QEMU emulation needed). # -# distroless/cc provides glibc + libgcc + ca-certificates and a non-root user. -# debian13 (trixie) is the current latest distroless base. -FROM gcr.io/distroless/cc-debian13:nonroot +# distroless/base-nossl provides glibc + libgcc + ca-certificates and a non-root +# user (debian13 / trixie, latest). We use rustls (no OpenSSL) and have no C++ +# deps, so the `cc` image's OpenSSL + libstdc++ are unnecessary — base-nossl is +# ~11MB smaller and still satisfies every shared library the binary needs. +FROM gcr.io/distroless/base-nossl-debian13:nonroot # buildx sets TARGETARCH to "amd64" / "arm64" for each target platform; the # matching prebuilt binary is staged under bin/ by the release workflow. From 5426d57cdea7ff767f03d2dadad70cf782c83cc8 Mon Sep 17 00:00:00 2001 From: Hiroyuki Wada Date: Sat, 13 Jun 2026 22:03:03 +0900 Subject: [PATCH 2/2] ci: unify into a single multi-target Dockerfile (drop Dockerfile.release) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the two Dockerfiles with one multi-stage Dockerfile so the runtime settings (user/workdir/entrypoint/cmd/expose) live in a single place and can't drift: - `source` target (default) — compiles from source on rust:1.96-bookworm and runs on distroless/cc (plain cargo binaries dynamically link libgcc_s, which only cc ships). Used by `docker build .`, docker-compose, and the CI check. - `prebuilt` target — COPYs the prebuilt release binary onto base-nossl (the cargo-zigbuild binaries link libgcc statically, so the smaller base works). release.yml builds it with `--target prebuilt --build-arg RUNTIME=...base-nossl...`. buildkit only builds the requested target's stages, so `docker build .` skips the prebuilt stage's `COPY bin/` and the release build skips the Rust compile. No fragile hand-copying of system libraries; both paths use official distroless images with matched libs. Verified: source target builds/runs; prebuilt target builds from the v0.4.1 binary and runs on base-nossl (51.5MB). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 4 ++- Dockerfile | 58 +++++++++++++++++++---------------- Dockerfile.release | 29 ------------------ 3 files changed, 34 insertions(+), 57 deletions(-) delete mode 100644 Dockerfile.release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a74f86..dbcf8ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -223,7 +223,9 @@ jobs: uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . - file: Dockerfile.release + target: prebuilt + build-args: | + RUNTIME=gcr.io/distroless/base-nossl-debian13:nonroot platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} diff --git a/Dockerfile b/Dockerfile index 0750630..210e8cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,45 +1,49 @@ -# Self-contained image used for local development, docker-compose, and the CI -# build check. It compiles from source on a glibc toolchain so it matches the -# released image's runtime (distroless/cc). The published release image is built -# separately from the prebuilt binaries via Dockerfile.release. - -# Build stage (glibc, matches the distroless/cc runtime below) +# One image, two build paths that share a single runtime definition. +# +# * "source" (default) — compiles from source. Used for local development, +# docker-compose, and the CI build check. Plain cargo binaries dynamically +# link libgcc_s.so.1, so this path uses the distroless/cc base (which ships +# libgcc). +# * "prebuilt" — COPYs a prebuilt release binary, no compilation, so +# the published image is byte-identical to the released binary (single +# source of truth). Used by the release pipeline: +# --target prebuilt \ +# --build-arg RUNTIME=gcr.io/distroless/base-nossl-debian13:nonroot +# cargo-zigbuild links libgcc statically, so the smaller base-nossl works. +# +# buildkit only builds the stages in the requested target's graph, so the +# release build skips the Rust compile, and `docker build .` never evaluates +# the prebuilt stage's `COPY bin/`. + +ARG RUNTIME=gcr.io/distroless/cc-debian13:nonroot + +# --- build stage (used by the "source" path) --- FROM rust:1.96-bookworm AS builder - -# Cargo features to enable in the build (image supports both backends by default) ARG FEATURES="sqlite,postgresql" - WORKDIR /app - -# Copy manifests first for better layer caching COPY Cargo.toml Cargo.lock ./ - -# Create a dummy source to cache dependencies RUN mkdir src && echo "fn main() {}" > src/main.rs - -# Build dependencies (cached unless Cargo.toml/Cargo.lock change) RUN cargo build --release --locked --features "${FEATURES}" - -# Remove dummy source and build the real binary RUN rm -rf src COPY src ./src RUN cargo build --release --locked --features "${FEATURES}" -# Runtime stage. This self-contained image is built with plain cargo, whose -# binary dynamically links libgcc_s.so.1, so it needs distroless/cc (which ships -# libgcc). The published release image uses base-nossl instead — see -# Dockerfile.release — because the cargo-zigbuild binaries link libgcc -# statically and don't need it. distroless/cc is glibc-based (debian13, latest). -FROM gcr.io/distroless/cc-debian13:nonroot - -COPY --from=builder /app/target/release/scim-server /usr/local/bin/scim-server - +# --- shared runtime definition (settings declared once, here) --- +FROM ${RUNTIME} AS runtime EXPOSE 3000 WORKDIR /data STOPSIGNAL SIGTERM - ENTRYPOINT ["scim-server"] # Zero-config demo by default (in-memory SQLite, unauthenticated), bound to all # interfaces so a published port is reachable. For real use, mount a config and # override the command with `--config /data/config.yaml`. CMD ["--host", "0.0.0.0"] + +# --- release path: package the exact prebuilt binary (no compilation) --- +FROM runtime AS prebuilt +ARG TARGETARCH +COPY bin/scim-server-${TARGETARCH} /usr/local/bin/scim-server + +# --- default path: compile from source --- +FROM runtime AS source +COPY --from=builder /app/target/release/scim-server /usr/local/bin/scim-server diff --git a/Dockerfile.release b/Dockerfile.release deleted file mode 100644 index 29b96ab..0000000 --- a/Dockerfile.release +++ /dev/null @@ -1,29 +0,0 @@ -# Runtime-only image used by the release pipeline. -# -# It packages the *exact* prebuilt binary that is published as a GitHub Release -# asset — the container and the downloadable binary are byte-identical (single -# source of truth). Nothing is compiled here, so the multi-arch image is -# assembled by COPY alone (no QEMU emulation needed). -# -# distroless/base-nossl provides glibc + libgcc + ca-certificates and a non-root -# user (debian13 / trixie, latest). We use rustls (no OpenSSL) and have no C++ -# deps, so the `cc` image's OpenSSL + libstdc++ are unnecessary — base-nossl is -# ~11MB smaller and still satisfies every shared library the binary needs. -FROM gcr.io/distroless/base-nossl-debian13:nonroot - -# buildx sets TARGETARCH to "amd64" / "arm64" for each target platform; the -# matching prebuilt binary is staged under bin/ by the release workflow. -ARG TARGETARCH -COPY bin/scim-server-${TARGETARCH} /usr/local/bin/scim-server - -EXPOSE 3000 -WORKDIR /data -STOPSIGNAL SIGTERM - -ENTRYPOINT ["scim-server"] -# Zero-config demo by default: in-memory SQLite, unauthenticated, bound to all -# interfaces so a published port is reachable. For real use, mount a config and -# override the command, e.g.: -# docker run -p 3000:3000 -v $PWD/config.yaml:/data/config.yaml \ -# ghcr.io/wadahiro/scim-server:latest --config /data/config.yaml -CMD ["--host", "0.0.0.0"]