From 83b8b7ed22e457f45699418e1223b2de8390bdb2 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Wed, 1 Apr 2026 12:07:39 +0200 Subject: [PATCH] Fix Docker SQLite startup crash with production DB initialization SQLite auto-initializes via MigrateAsync regardless of environment since the DB file may not exist. Managed databases (PostgreSQL, SQL Server) still skip initialization in production. Build orchestrator limits Vite concurrency to prevent OOM in memory-constrained Docker builds. --- Dockerfile | 3 ++ .../SimpleModuleHostExtensions.cs | 16 +++++++---- .../SimpleModuleOptions.cs | 6 ++++ .../appsettings.Production.json | 11 ++++++++ tools/build-orchestrator.mjs | 28 ++++++++++++++----- 5 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 template/SimpleModule.Host/appsettings.Production.json diff --git a/Dockerfile b/Dockerfile index 2ddb1131..8167a1d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -133,6 +133,9 @@ RUN apt-get update \ COPY --from=build --chown=appuser:appgroup /app/publish . +# Writable directory for SQLite database and local storage +RUN mkdir -p /app/data /app/storage && chown appuser:appgroup /app/data /app/storage + USER appuser EXPOSE 8080 diff --git a/framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs b/framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs index da0c70e4..935c54f5 100644 --- a/framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs +++ b/framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using SimpleModule.Blazor; using SimpleModule.Core.Constants; using SimpleModule.Core.Events; @@ -43,7 +42,7 @@ public static WebApplicationBuilder AddSimpleModuleInfrastructure( builder.Services.AddSingleton(options); BridgeAspireConnectionString(builder.Configuration); - ValidateDatabaseConfiguration(builder.Configuration); + options.DatabaseProvider = ValidateDatabaseConfiguration(builder.Configuration); builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler(); @@ -108,8 +107,11 @@ public static WebApplicationBuilder AddSimpleModuleInfrastructure( /// public static async Task UseSimpleModuleInfrastructure(this WebApplication app) { - // Database initialization (non-production only) - if (!app.Environment.IsProduction()) + // Database initialization + // SQLite (file-based) always needs auto-initialization since the DB file may not exist. + // Managed databases (PostgreSQL, SQL Server) skip this in production — apply migrations externally. + var smOptions = app.Services.GetRequiredService(); + if (!app.Environment.IsProduction() || smOptions.DatabaseProvider == DatabaseProvider.Sqlite) { using var scope = app.Services.CreateScope(); var infos = scope.ServiceProvider.GetServices(); @@ -187,7 +189,9 @@ private static void BridgeAspireConnectionString(ConfigurationManager configurat } } - private static void ValidateDatabaseConfiguration(ConfigurationManager configuration) + private static DatabaseProvider ValidateDatabaseConfiguration( + ConfigurationManager configuration + ) { var dbOptions = configuration.GetSection(DatabaseConstants.SectionName).Get() @@ -202,7 +206,7 @@ private static void ValidateDatabaseConfiguration(ConfigurationManager configura ); } - _ = DatabaseProviderDetector.Detect(connString, dbOptions.Provider); + return DatabaseProviderDetector.Detect(connString, dbOptions.Provider); } private static void UseStaticFileCaching(WebApplication app) diff --git a/framework/SimpleModule.Hosting/SimpleModuleOptions.cs b/framework/SimpleModule.Hosting/SimpleModuleOptions.cs index 5fa9d6cc..f99f32c5 100644 --- a/framework/SimpleModule.Hosting/SimpleModuleOptions.cs +++ b/framework/SimpleModule.Hosting/SimpleModuleOptions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using SimpleModule.Core; +using SimpleModule.Database; namespace SimpleModule.Hosting; @@ -15,6 +16,11 @@ public class SimpleModuleOptions public bool EnableDevTools { get; set; } = true; + /// + /// The detected database provider, set during startup validation. + /// + internal DatabaseProvider DatabaseProvider { get; set; } + /// /// Configures options for a module. Called by generated Configure{Module}() extension methods. /// diff --git a/template/SimpleModule.Host/appsettings.Production.json b/template/SimpleModule.Host/appsettings.Production.json new file mode 100644 index 00000000..bacc57d2 --- /dev/null +++ b/template/SimpleModule.Host/appsettings.Production.json @@ -0,0 +1,11 @@ +{ + "Database": { + "DefaultConnection": "Data Source=/app/data/app.db", + "Provider": "Sqlite" + }, + "Storage": { + "Local": { + "BasePath": "/app/storage" + } + } +} diff --git a/tools/build-orchestrator.mjs b/tools/build-orchestrator.mjs index 9db305a9..a9df27fb 100644 --- a/tools/build-orchestrator.mjs +++ b/tools/build-orchestrator.mjs @@ -39,6 +39,21 @@ function runBuild(workspacePath, workspaceName) { }); } +async function runWithConcurrency(items, concurrency, fn) { + const results = []; + let index = 0; + + async function worker() { + while (index < items.length) { + const i = index++; + results[i] = await fn(items[i]); + } + } + + await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker())); + return results; +} + async function main() { try { log('startup', `Starting production build (VITE_MODE=${mode})...`); @@ -46,13 +61,12 @@ async function main() { const workspaces = discoverBuildableWorkspaces(rootDir); log('startup', `Found ${workspaces.length} workspaces to build`); - // Build all workspaces in parallel for better performance - await Promise.all( - workspaces.map((workspace) => { - const workspaceName = path.basename(workspace); - return runBuild(workspace, workspaceName); - }) - ); + // Limit concurrency to avoid OOM in memory-constrained environments (e.g. Docker) + const concurrency = parseInt(process.env.BUILD_CONCURRENCY, 10) || 4; + await runWithConcurrency(workspaces, concurrency, (workspace) => { + const workspaceName = path.basename(workspace); + return runBuild(workspace, workspaceName); + }); log('complete', 'All workspaces built successfully!'); process.exit(0);