diff --git a/util/RustSdk/RustSdk.csproj b/util/RustSdk/RustSdk.csproj
index e9b22781606e..ea943f000ff7 100644
--- a/util/RustSdk/RustSdk.csproj
+++ b/util/RustSdk/RustSdk.csproj
@@ -27,6 +27,11 @@
true
runtimes/linux-x64/native/libsdk.so
+
+ Always
+ true
+ runtimes/linux-arm64/native/libsdk.so
+
Always
true
@@ -50,6 +55,11 @@
true
runtimes/linux-x64/native/libsdk.so
+
+ Always
+ true
+ runtimes/linux-arm64/native/libsdk.so
+
Always
true
diff --git a/util/SeederUtility/Configuration/ServiceCollectionExtension.cs b/util/SeederUtility/Configuration/ServiceCollectionExtension.cs
index 57adda2b2e4e..059739856881 100644
--- a/util/SeederUtility/Configuration/ServiceCollectionExtension.cs
+++ b/util/SeederUtility/Configuration/ServiceCollectionExtension.cs
@@ -33,7 +33,15 @@ public static void ConfigureServices(ServiceCollection services, bool enableMang
services.AddSingleton, PasswordHasher>();
services.TryAddSingleton();
- services.AddDataProtection().SetApplicationName("Bitwarden");
+ var dpBuilder = services.AddDataProtection().SetApplicationName("Bitwarden");
+ // Persist DataProtection keys to a shared directory when configured, so
+ // records this tool encrypts are decryptable by the running app. Needed
+ // when seeding an instance whose components share a DataProtection key ring
+ // (e.g. self-host). No-op when the directory isn't set.
+ if (!string.IsNullOrWhiteSpace(globalSettings.DataProtection.Directory))
+ {
+ dpBuilder.PersistKeysToFileSystem(new DirectoryInfo(globalSettings.DataProtection.Directory));
+ }
services.AddDatabaseRepositories(globalSettings);
diff --git a/util/SeederUtility/Dockerfile b/util/SeederUtility/Dockerfile
new file mode 100644
index 000000000000..f77cd9e3ad97
--- /dev/null
+++ b/util/SeederUtility/Dockerfile
@@ -0,0 +1,115 @@
+###############################################
+# Build stage #
+###############################################
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build
+
+# Docker buildx supplies these values
+ARG TARGETPLATFORM
+ARG BUILDPLATFORM
+
+# Install base build dependencies
+RUN apt-get update && apt-get install -y \
+ build-essential \
+ curl \
+ pkg-config \
+ libssl-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+# Rust toolchain: RustSdk builds a native library via cargo during publish.
+RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
+ --default-toolchain stable \
+ --profile minimal \
+ --no-modify-path
+
+ENV PATH="/root/.cargo/bin:${PATH}"
+
+# Determine target architecture and install cross-compilation tools
+RUN case "$TARGETPLATFORM" in \
+ "linux/amd64") \
+ RUST_TARGET=x86_64-unknown-linux-gnu && \
+ RID=linux-x64 && \
+ ARCH_PACKAGES="" \
+ ;; \
+ "linux/arm64") \
+ RUST_TARGET=aarch64-unknown-linux-gnu && \
+ RID=linux-arm64 && \
+ ARCH_PACKAGES="gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-dev-arm64-cross" \
+ ;; \
+ *) \
+ echo "Unsupported platform: $TARGETPLATFORM" && exit 1 \
+ ;; \
+ esac \
+ && if [ -n "$ARCH_PACKAGES" ]; then \
+ apt-get update && apt-get install -y $ARCH_PACKAGES && rm -rf /var/lib/apt/lists/* ; \
+ fi \
+ && echo "RUST_TARGET=${RUST_TARGET}" >> /etc/build-env \
+ && echo "RID=${RID}" >> /etc/build-env \
+ && . /etc/build-env \
+ && rustup target add ${RUST_TARGET} \
+ && echo "Rust target: ${RUST_TARGET}, .NET RID: ${RID}"
+
+# Configure Rust for cross-compilation with proper linkers
+RUN . /etc/build-env \
+ && mkdir -p /root/.cargo \
+ && case "$TARGETPLATFORM" in \
+ "linux/amd64") \
+ echo "[target.x86_64-unknown-linux-gnu]" >> /root/.cargo/config.toml \
+ && echo "linker = \"gcc\"" >> /root/.cargo/config.toml \
+ ;; \
+ "linux/arm64") \
+ echo "[target.aarch64-unknown-linux-gnu]" >> /root/.cargo/config.toml \
+ && echo "linker = \"aarch64-linux-gnu-gcc\"" >> /root/.cargo/config.toml \
+ ;; \
+ esac
+
+# Copy project files
+WORKDIR /source
+COPY . ./
+
+# Restore .NET dependencies
+WORKDIR /source/util/SeederUtility
+RUN . /etc/build-env && dotnet restore -r ${RID}
+
+# Publish a self-contained, single-file binary
+WORKDIR /source/util/SeederUtility
+RUN . /etc/build-env \
+ && export NoWarn="CA1305;CS1591" \
+ && rustc --version \
+ && cargo --version \
+ && echo "Building for Rust target: ${RUST_TARGET}, .NET RID: ${RID}" \
+ && dotnet publish SeederUtility.csproj \
+ -c Release \
+ --no-restore \
+ --self-contained \
+ /p:PublishSingleFile=true \
+ -r ${RID} \
+ -o /app/out
+
+###############################################
+# App stage #
+###############################################
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble AS app
+
+ARG TARGETPLATFORM
+LABEL com.bitwarden.product="bitwarden"
+
+ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
+ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
+ENV ASPNETCORE_ENVIRONMENT=Production
+# The single-file binary extracts native libs at startup; it runs non-root, so
+# point extraction at a writable dir.
+ENV DOTNET_BUNDLE_EXTRACT_BASE_DIR=/tmp/.net
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ gosu \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy app from the build stage
+WORKDIR /app
+COPY --from=build /app/out /app
+COPY ./util/SeederUtility/entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+
+# CLI entrypoint; args pass through, e.g. `docker run preset --name `.
+# DB connection comes from globalSettings__* env vars.
+ENTRYPOINT ["/entrypoint.sh"]
diff --git a/util/SeederUtility/entrypoint.sh b/util/SeederUtility/entrypoint.sh
new file mode 100755
index 000000000000..d97b27220ea9
--- /dev/null
+++ b/util/SeederUtility/entrypoint.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+GROUPNAME="bitwarden"
+USERNAME="bitwarden"
+
+LUID=${LOCAL_UID:-0}
+LGID=${LOCAL_GID:-0}
+
+# Step down from host root to well-known nobody/nogroup user
+if [ $LUID -eq 0 ]
+then
+ LUID=65534
+fi
+if [ $LGID -eq 0 ]
+then
+ LGID=65534
+fi
+
+if [ "$(id -u)" = "0" ]
+then
+ groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
+ groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
+ useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
+ usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
+
+ chown -R $USERNAME:$GROUPNAME /app
+ mkdir -p /etc/bitwarden/core
+ mkdir -p /etc/bitwarden/logs
+ mkdir -p /etc/bitwarden/ca-certificates
+ chown -R $USERNAME:$GROUPNAME /etc/bitwarden
+
+ gosu_cmd="gosu $USERNAME:$GROUPNAME"
+else
+ gosu_cmd=""
+fi
+
+# Pass through any CLI subcommand and arguments (e.g. `preset --name ...`).
+exec $gosu_cmd /app/SeederUtility "$@"