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);