diff --git a/src/ScadaLink.ConfigurationDatabase/MigrationHelper.cs b/src/ScadaLink.ConfigurationDatabase/MigrationHelper.cs
index 4acec5e..b38c4bd 100644
--- a/src/ScadaLink.ConfigurationDatabase/MigrationHelper.cs
+++ b/src/ScadaLink.ConfigurationDatabase/MigrationHelper.cs
@@ -16,12 +16,21 @@ public static class MigrationHelper
///
/// The database context to migrate or validate.
/// When true, auto-applies migrations. When false, validates schema version matches.
+ /// Optional logger for readiness-wait diagnostics.
/// Cancellation token.
public static async Task ApplyOrValidateMigrationsAsync(
ScadaLinkDbContext dbContext,
bool isDevelopment,
+ ILogger? logger = null,
CancellationToken cancellationToken = default)
{
+ // Wait for the target database to accept connections before invoking MigrateAsync.
+ // On a fresh MSSQL container, user databases recover asynchronously after the server
+ // starts accepting connections — DB_ID(@dbName) returns null until recovery completes.
+ // Without this wait, MigrateAsync sees the database as missing and falls through to
+ // CREATE DATABASE, which fails for non-privileged app logins.
+ await WaitForDatabaseReadyAsync(dbContext, logger, cancellationToken);
+
if (isDevelopment)
{
await dbContext.Database.MigrateAsync(cancellationToken);
@@ -38,4 +47,48 @@ public static class MigrationHelper
}
}
}
+
+ private static async Task WaitForDatabaseReadyAsync(
+ ScadaLinkDbContext dbContext,
+ ILogger? logger,
+ CancellationToken cancellationToken)
+ {
+ var timeout = TimeSpan.FromSeconds(60);
+ var pollInterval = TimeSpan.FromSeconds(2);
+ var deadline = DateTimeOffset.UtcNow + timeout;
+ var attempt = 0;
+ Exception? lastException = null;
+
+ while (DateTimeOffset.UtcNow < deadline)
+ {
+ attempt++;
+ try
+ {
+ if (await dbContext.Database.CanConnectAsync(cancellationToken))
+ {
+ if (attempt > 1)
+ {
+ logger?.LogInformation(
+ "Configuration database ready after {Attempt} attempt(s).", attempt);
+ }
+ return;
+ }
+ logger?.LogDebug(
+ "Configuration database not yet reachable (attempt {Attempt}).", attempt);
+ }
+ catch (Exception ex)
+ {
+ lastException = ex;
+ logger?.LogDebug(ex,
+ "Configuration database not yet reachable (attempt {Attempt}).", attempt);
+ }
+
+ await Task.Delay(pollInterval, cancellationToken);
+ }
+
+ throw new InvalidOperationException(
+ $"Configuration database not ready after {timeout.TotalSeconds:N0}s ({attempt} attempts). " +
+ "Verify SQL Server is running and the configuration database exists and is attached.",
+ lastException);
+ }
}
diff --git a/src/ScadaLink.Host/Program.cs b/src/ScadaLink.Host/Program.cs
index 29116f8..637fdba 100644
--- a/src/ScadaLink.Host/Program.cs
+++ b/src/ScadaLink.Host/Program.cs
@@ -109,7 +109,10 @@ try
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService();
- await MigrationHelper.ApplyOrValidateMigrationsAsync(dbContext, isDevelopment);
+ var migrationLogger = scope.ServiceProvider
+ .GetRequiredService()
+ .CreateLogger(typeof(MigrationHelper).FullName!);
+ await MigrationHelper.ApplyOrValidateMigrationsAsync(dbContext, isDevelopment, migrationLogger);
}
}