diff --git a/dev/docker-compose-seeder.yml b/dev/docker-compose-seeder.yml
new file mode 100644
index 000000000000..931d7e2ee3a9
--- /dev/null
+++ b/dev/docker-compose-seeder.yml
@@ -0,0 +1,85 @@
+services:
+ # Identity Service
+ identity:
+ build:
+ context: ..
+ dockerfile: ./src/Identity/Dockerfile
+ environment:
+ ASPNETCORE_ENVIRONMENT: Development
+ globalSettings__selfHosted: "false"
+ globalSettings__baseServiceUri__vault: "http://localhost:8080"
+ globalSettings__baseServiceUri__api: "http://api:5000"
+ globalSettings__baseServiceUri__identity: "http://identity:5000"
+ globalSettings__databaseProvider: "sqlite"
+ globalSettings__sqlite__connectionString: "Data Source=/etc/bitwarden/db/vault_dev.db"
+ globalSettings__licenseDirectory: "/etc/bitwarden/core/licenses"
+ globalSettings__attachment__baseDirectory: "/etc/bitwarden/core/attachments"
+ globalSettings__dataProtection__directory: "/etc/bitwarden/core/aspnet-dataprotection"
+ globalSettings__developmentDirectory: "/etc/bitwarden/dev"
+ globalSettings__logDirectory: "/etc/bitwarden/logs"
+ ports:
+ - "33656:5000"
+ volumes:
+ - ./.data/db:/etc/bitwarden/db
+ - ./.data/dev:/etc/bitwarden/dev
+ networks:
+ - bitwarden
+
+ # API Service
+ api:
+ build:
+ context: ..
+ dockerfile: ./src/Api/Dockerfile
+ environment:
+ ASPNETCORE_ENVIRONMENT: Development
+ globalSettings__selfHosted: "false"
+ globalSettings__baseServiceUri__vault: "http://localhost:8080"
+ globalSettings__baseServiceUri__api: "http://api:5000"
+ globalSettings__baseServiceUri__identity: "http://identity:5000"
+ globalSettings__databaseProvider: "sqlite"
+ globalSettings__sqlite__connectionString: "Data Source=/etc/bitwarden/db/vault_dev.db"
+ globalSettings__licenseDirectory: "/etc/bitwarden/core/licenses"
+ globalSettings__attachment__baseDirectory: "/etc/bitwarden/core/attachments"
+ globalSettings__dataProtection__directory: "/etc/bitwarden/core/aspnet-dataprotection"
+ globalSettings__logDirectory: "/etc/bitwarden/logs"
+ ports:
+ - "4000:5000"
+ volumes:
+ - ./.data/api:/etc/bitwarden
+ - ./.data/db:/etc/bitwarden/db
+ depends_on:
+ - identity
+ networks:
+ - bitwarden
+
+ # Seeder API Service
+ seeder:
+ build:
+ context: ..
+ dockerfile: ./util/SeederApi/Dockerfile
+ environment:
+ ASPNETCORE_ENVIRONMENT: Development
+ globalSettings__selfHosted: "false"
+ globalSettings__baseServiceUri__vault: "http://localhost:8080"
+ globalSettings__baseServiceUri__api: "http://api:5000"
+ globalSettings__baseServiceUri__identity: "http://identity:5000"
+ globalSettings__databaseProvider: "sqlite"
+ globalSettings__sqlite__connectionString: "Data Source=/etc/bitwarden/db/vault_dev.db"
+ globalSettings__licenseDirectory: "/etc/bitwarden/core/licenses"
+ globalSettings__attachment__baseDirectory: "/etc/bitwarden/core/attachments"
+ globalSettings__dataProtection__directory: "/etc/bitwarden/core/aspnet-dataprotection"
+ globalSettings__logDirectory: "/etc/bitwarden/logs"
+ ports:
+ - "5100:5000"
+ volumes:
+ - ./.data/seeder:/etc/bitwarden
+ - ./.data/db:/etc/bitwarden/db
+ depends_on:
+ - api
+ - identity
+ networks:
+ - bitwarden
+
+networks:
+ bitwarden:
+ driver: bridge
diff --git a/util/RustSdk/RustSdk.csproj b/util/RustSdk/RustSdk.csproj
index 14cc01736567..2ed7f7c327f9 100644
--- a/util/RustSdk/RustSdk.csproj
+++ b/util/RustSdk/RustSdk.csproj
@@ -26,7 +26,7 @@
Always
true
- runtimes/linux-x64/native/libsdk.so
+ runtimes/linux-arm64/native/libsdk.so
Always
@@ -49,7 +49,7 @@
Always
true
- runtimes/linux-x64/native/libsdk.so
+ runtimes/linux-arm64/native/libsdk.so
Always
diff --git a/util/SeederApi/Dockerfile b/util/SeederApi/Dockerfile
new file mode 100644
index 000000000000..a0021311aa13
--- /dev/null
+++ b/util/SeederApi/Dockerfile
@@ -0,0 +1,83 @@
+###############################################
+# Build stage #
+###############################################
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+
+# Docker buildx supplies the value for this arg
+ARG TARGETPLATFORM
+
+# Determine proper runtime value for .NET
+# We put the value in a file to be read by later layers.
+RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
+ RID=linux-x64 ; \
+ RUST_TARGET=x86_64-unknown-linux-gnu ; \
+ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
+ RID=linux-arm64 ; \
+ RUST_TARGET=aarch64-unknown-linux-gnu ; \
+ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
+ RID=linux-arm ; \
+ RUST_TARGET=armv7-unknown-linux-gnueabihf ; \
+ fi \
+ && echo "RID=$RID" > /tmp/rid.txt \
+ && echo "$RUST_TARGET" > /tmp/rust_target.txt
+
+# Install Rust
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ build-essential \
+ curl \
+ && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal \
+ && . $HOME/.cargo/env \
+ && rustup target add $(cat /tmp/rust_target.txt) \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+ENV PATH="/root/.cargo/bin:${PATH}"
+
+# Copy required project files
+WORKDIR /source
+COPY . ./
+
+# Restore project dependencies and tools
+WORKDIR /source/util/SeederApi
+RUN . /tmp/rid.txt && dotnet restore -r $RID
+
+# Build project
+RUN . /tmp/rid.txt && dotnet publish \
+ -c release \
+ --no-restore \
+ --self-contained \
+ /p:PublishSingleFile=true \
+ -r $RID \
+ -o out
+
+###############################################
+# App stage #
+###############################################
+FROM mcr.microsoft.com/dotnet/aspnet:8.0
+
+ARG TARGETPLATFORM
+LABEL com.bitwarden.product="bitwarden"
+ENV ASPNETCORE_ENVIRONMENT=Production
+ENV ASPNETCORE_URLS=http://+:5000
+ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates
+ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
+EXPOSE 5000
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ curl \
+ krb5-user \
+ gosu \
+ tzdata \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy app from the build stage
+WORKDIR /app
+COPY --from=build /source/util/SeederApi/out /app
+COPY ./util/SeederApi/entrypoint.sh /entrypoint.sh
+RUN chmod +x /entrypoint.sh
+HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
+
+ENTRYPOINT ["/entrypoint.sh"]
\ No newline at end of file
diff --git a/util/SeederApi/HostedServices/DatabaseMigrationHostedService.cs b/util/SeederApi/HostedServices/DatabaseMigrationHostedService.cs
new file mode 100644
index 000000000000..8a71cef59a25
--- /dev/null
+++ b/util/SeederApi/HostedServices/DatabaseMigrationHostedService.cs
@@ -0,0 +1,49 @@
+using System.Data.Common;
+using Bit.Core.Utilities;
+
+namespace Bit.SeederApi.HostedServices;
+
+public sealed class DatabaseMigrationHostedService(
+ IDbMigrator dbMigrator,
+ ILogger logger)
+ : IHostedService, IDisposable
+{
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ logger.LogInformation("Starting database migration...");
+
+ // Wait 5 seconds to allow database file to be ready
+ await Task.Delay(5000, cancellationToken);
+
+ const int maxMigrationAttempts = 10;
+ for (var i = 1; i <= maxMigrationAttempts; i++)
+ {
+ try
+ {
+ dbMigrator.MigrateDatabase(true, cancellationToken);
+ logger.LogInformation("Database migration completed successfully");
+ break;
+ }
+ catch (DbException e)
+ {
+ if (i >= maxMigrationAttempts)
+ {
+ logger.LogError(e, "Database failed to migrate after {MaxAttempts} attempts", maxMigrationAttempts);
+ throw;
+ }
+
+ logger.LogWarning(e,
+ "Database unavailable for migration. Trying again (attempt #{AttemptNumber})...", i + 1);
+ await Task.Delay(5000, cancellationToken);
+ }
+ }
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ return Task.FromResult(0);
+ }
+
+ public void Dispose()
+ { }
+}
diff --git a/util/SeederApi/SeederApi.csproj b/util/SeederApi/SeederApi.csproj
index 53e9941c1cb4..b427eb59552e 100644
--- a/util/SeederApi/SeederApi.csproj
+++ b/util/SeederApi/SeederApi.csproj
@@ -11,6 +11,7 @@
+
diff --git a/util/SeederApi/Startup.cs b/util/SeederApi/Startup.cs
index a38ff8256bde..7b35f33b664b 100644
--- a/util/SeederApi/Startup.cs
+++ b/util/SeederApi/Startup.cs
@@ -1,6 +1,7 @@
using System.Globalization;
using Bit.Core.Settings;
using Bit.SeederApi.Extensions;
+using Bit.SeederApi.HostedServices;
using Bit.SharedWeb.Utilities;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -28,7 +29,17 @@ public void ConfigureServices(IServiceCollection services)
services.AddCustomDataProtectionServices(Environment, globalSettings);
services.AddTokenizers();
- services.AddDatabaseRepositories(globalSettings);
+ var databaseProvider = services.AddDatabaseRepositories(globalSettings);
+
+ // Register database migrator based on provider
+ switch (databaseProvider)
+ {
+ case Core.Enums.SupportedDatabaseProviders.Sqlite:
+ services.AddSingleton();
+ services.AddHostedService();
+ break;
+ }
+
services.AddTestPlayIdTracking(globalSettings);
services.AddManglerService(globalSettings);
diff --git a/util/SeederApi/entrypoint.sh b/util/SeederApi/entrypoint.sh
new file mode 100644
index 000000000000..b72b41683a50
--- /dev/null
+++ b/util/SeederApi/entrypoint.sh
@@ -0,0 +1,54 @@
+#!/bin/sh
+
+# Setup
+
+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
+ # Create user and group
+
+ 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
+ mkhomedir_helper $USERNAME
+
+ # The rest...
+
+ 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
+
+ if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then
+ chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos
+ fi
+
+ gosu_cmd="gosu $USERNAME:$GROUPNAME"
+else
+ gosu_cmd=""
+fi
+
+if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then
+ cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf
+ $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab
+fi
+
+exec $gosu_cmd /app/SeederApi
\ No newline at end of file