diff --git a/deploy/Dockerfile.api b/deploy/Dockerfile.api
index ebb8663..3046f6e 100644
--- a/deploy/Dockerfile.api
+++ b/deploy/Dockerfile.api
@@ -32,7 +32,7 @@ ENV DOTNET_NOLOGO=1
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
EXPOSE 8080
-HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
- CMD curl -fsS http://localhost:8080/health || exit 1
+HEALTHCHECK --interval=30s --timeout=5s --start-period=30s \
+ CMD curl -fsS http://localhost:8080/health/ready || exit 1
ENTRYPOINT ["dotnet", "foodmarket.Api.dll"]
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
index 1f4f8f8..58d0d68 100644
--- a/deploy/docker-compose.yml
+++ b/deploy/docker-compose.yml
@@ -34,6 +34,14 @@ services:
ports:
- "8080:8080" # api
+ healthcheck:
+ # Готовность = БД отвечает + миграции применены (см. /health/ready).
+ test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health/ready || exit 1"]
+ interval: 30s
+ timeout: 5s
+ retries: 5
+ start_period: 30s
+
volumes:
- api-data:/app/App_Data
- api-logs:/app/logs
@@ -44,7 +52,8 @@ services:
container_name: food-market-web
restart: unless-stopped
depends_on:
- - api
+ api:
+ condition: service_healthy
ports:
- "8081:80" # web SPA, not on 80 (legacy nginx holds it)
diff --git a/src/food-market.api/Infrastructure/Health/DatabaseReadyHealthCheck.cs b/src/food-market.api/Infrastructure/Health/DatabaseReadyHealthCheck.cs
new file mode 100644
index 0000000..20f2a76
--- /dev/null
+++ b/src/food-market.api/Infrastructure/Health/DatabaseReadyHealthCheck.cs
@@ -0,0 +1,43 @@
+using foodmarket.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace foodmarket.Api.Infrastructure.Health;
+
+/// Readiness-проба: приложение готово принимать трафик только когда
+/// БД отвечает И все миграции применены. Если есть неприменённые миграции —
+/// схема не соответствует коду (например, контейнер поднялся раньше, чем
+/// отработал Migrate(), или откатили не ту версию) — отдаём Unhealthy,
+/// чтобы оркестратор/прокси не слал на этот инстанс запросы.
+public sealed class DatabaseReadyHealthCheck : IHealthCheck
+{
+ private readonly AppDbContext _db;
+
+ public DatabaseReadyHealthCheck(AppDbContext db) => _db = db;
+
+ public async Task CheckHealthAsync(
+ HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ if (!await _db.Database.CanConnectAsync(cancellationToken))
+ {
+ return HealthCheckResult.Unhealthy("Нет соединения с БД.");
+ }
+
+ var pending = (await _db.Database.GetPendingMigrationsAsync(cancellationToken)).ToList();
+ if (pending.Count > 0)
+ {
+ return HealthCheckResult.Unhealthy(
+ $"Неприменённые миграции: {pending.Count} (первая: {pending[0]}).",
+ data: new Dictionary { ["pendingMigrations"] = pending });
+ }
+
+ return HealthCheckResult.Healthy("БД доступна, миграции применены.");
+ }
+ catch (Exception ex)
+ {
+ return HealthCheckResult.Unhealthy("Ошибка проверки готовности БД.", ex);
+ }
+ }
+}
diff --git a/src/food-market.api/Program.cs b/src/food-market.api/Program.cs
index d2c1c4e..23c8aad 100644
--- a/src/food-market.api/Program.cs
+++ b/src/food-market.api/Program.cs
@@ -145,6 +145,12 @@
// Anti-brute-force на /connect/token и /api/auth/signup (5/мин + 20/час на IP).
builder.Services.AddAuthRateLimiting();
+ // Health-пробы: liveness (процесс жив) и readiness (БД + миграции применены).
+ // Readiness-чек помечен тегом "ready", чтобы /health/live его не запускал.
+ builder.Services.AddHealthChecks()
+ .AddCheck(
+ "database", tags: ["ready"]);
+
// Email-отправка через MailKit. Singleton — внутри открывает scope для
// свежего DbContext'а, поэтому конфиг (PlatformSettings) перечитывается
// на каждой отправке без рестарта приложения.
@@ -220,6 +226,22 @@
app.MapControllers();
+ // Liveness: процесс отвечает — без обращения к зависимостям (Predicate=false
+ // => ни один чек не запускается). Используется для рестарта зависшего контейнера.
+ app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
+ {
+ Predicate = _ => false,
+ ResponseWriter = WriteHealthResponse,
+ });
+
+ // Readiness: запускает чеки с тегом "ready" (БД + миграции). 503 если не готово.
+ app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
+ {
+ Predicate = check => check.Tags.Contains("ready"),
+ ResponseWriter = WriteHealthResponse,
+ });
+
+ // Backward-compat: /health = liveness (Dockerfile/nginx исторически бьют сюда).
app.MapGet("/health", () => Results.Ok(new { status = "ok", time = DateTime.UtcNow }));
app.MapGet("/api/_debug/whoami", (HttpContext ctx) =>
@@ -290,3 +312,22 @@
{
Log.CloseAndFlush();
}
+
+// JSON-ответ health-проб: общий статус + по каждому чеку. text/plain по умолчанию
+// неудобен для мониторинга; отдаём структурировано.
+static Task WriteHealthResponse(HttpContext context,
+ Microsoft.Extensions.Diagnostics.HealthChecks.HealthReport report)
+{
+ context.Response.ContentType = "application/json";
+ return context.Response.WriteAsJsonAsync(new
+ {
+ status = report.Status.ToString(),
+ totalDurationMs = report.TotalDuration.TotalMilliseconds,
+ checks = report.Entries.Select(e => new
+ {
+ name = e.Key,
+ status = e.Value.Status.ToString(),
+ description = e.Value.Description,
+ }),
+ });
+}