feat(database): configure standalone health checks

Add configurable health check settings for standalone databases and apply them to generated Docker Compose services. Allow disabling health checks and cover the behavior with feature tests.
This commit is contained in:
Andras Bacsai
2026-05-31 21:50:10 +02:00
parent c9fcc0bc44
commit d423223d38
26 changed files with 448 additions and 42 deletions
+7 -4
View File
@@ -52,10 +52,10 @@ class StartClickhouse
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => ['CMD', 'clickhouse-client', '--user', (string) $this->database->clickhouse_admin_user, '--password', (string) $this->database->clickhouse_admin_password, '--query', 'SELECT 1'],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s',
'interval' => "{$this->database->health_check_interval}s",
'timeout' => "{$this->database->health_check_timeout}s",
'retries' => $this->database->health_check_retries,
'start_period' => "{$this->database->health_check_start_period}s",
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
@@ -98,6 +98,9 @@ class StartClickhouse
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
if (! $this->database->isHealthcheckEnabled()) {
unset($docker_compose['services'][$container_name]['healthcheck']);
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
+7 -4
View File
@@ -108,10 +108,10 @@ class StartDragonfly
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => ['CMD', 'redis-cli', '-a', (string) $this->database->dragonfly_password, 'ping'],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s',
'interval' => "{$this->database->health_check_interval}s",
'timeout' => "{$this->database->health_check_timeout}s",
'retries' => $this->database->health_check_retries,
'start_period' => "{$this->database->health_check_start_period}s",
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
@@ -182,6 +182,9 @@ class StartDragonfly
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
if (! $this->database->isHealthcheckEnabled()) {
unset($docker_compose['services'][$container_name]['healthcheck']);
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
+7 -4
View File
@@ -110,10 +110,10 @@ class StartKeydb
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => ['CMD', 'keydb-cli', '--pass', (string) $this->database->keydb_password, 'ping'],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s',
'interval' => "{$this->database->health_check_interval}s",
'timeout' => "{$this->database->health_check_timeout}s",
'retries' => $this->database->health_check_retries,
'start_period' => "{$this->database->health_check_start_period}s",
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
@@ -197,6 +197,9 @@ class StartKeydb
// Add custom docker run options
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
if (! $this->database->isHealthcheckEnabled()) {
unset($docker_compose['services'][$container_name]['healthcheck']);
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
+7 -4
View File
@@ -105,10 +105,10 @@ class StartMariadb
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s',
'interval' => "{$this->database->health_check_interval}s",
'timeout' => "{$this->database->health_check_timeout}s",
'retries' => $this->database->health_check_retries,
'start_period' => "{$this->database->health_check_start_period}s",
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
@@ -202,6 +202,9 @@ class StartMariadb
];
}
if (! $this->database->isHealthcheckEnabled()) {
unset($docker_compose['services'][$container_name]['healthcheck']);
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
+7 -4
View File
@@ -115,10 +115,10 @@ class StartMongodb
'echo',
'ok',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s',
'interval' => "{$this->database->health_check_interval}s",
'timeout' => "{$this->database->health_check_timeout}s",
'retries' => $this->database->health_check_retries,
'start_period' => "{$this->database->health_check_start_period}s",
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
@@ -253,6 +253,9 @@ class StartMongodb
$docker_compose['services'][$container_name]['command'] = $commandParts;
}
if (! $this->database->isHealthcheckEnabled()) {
unset($docker_compose['services'][$container_name]['healthcheck']);
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
+7 -4
View File
@@ -105,10 +105,10 @@ class StartMysql
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}"],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s',
'interval' => "{$this->database->health_check_interval}s",
'timeout' => "{$this->database->health_check_timeout}s",
'retries' => $this->database->health_check_retries,
'start_period' => "{$this->database->health_check_start_period}s",
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
@@ -203,6 +203,9 @@ class StartMysql
];
}
if (! $this->database->isHealthcheckEnabled()) {
unset($docker_compose['services'][$container_name]['healthcheck']);
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
+7 -4
View File
@@ -112,10 +112,10 @@ class StartPostgresql
'labels' => defaultDatabaseLabels($this->database)->toArray(),
'healthcheck' => [
'test' => ['CMD', 'psql', '-U', (string) $this->database->postgres_user, '-d', (string) $this->database->postgres_db, '-c', 'SELECT 1'],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s',
'interval' => "{$this->database->health_check_interval}s",
'timeout' => "{$this->database->health_check_timeout}s",
'retries' => $this->database->health_check_retries,
'start_period' => "{$this->database->health_check_start_period}s",
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
@@ -213,6 +213,9 @@ class StartPostgresql
$docker_compose['services'][$container_name]['command'] = $command;
}
if (! $this->database->isHealthcheckEnabled()) {
unset($docker_compose['services'][$container_name]['healthcheck']);
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
+7 -4
View File
@@ -111,10 +111,10 @@ class StartRedis
'redis-cli',
'ping',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s',
'interval' => "{$this->database->health_check_interval}s",
'timeout' => "{$this->database->health_check_timeout}s",
'retries' => $this->database->health_check_retries,
'start_period' => "{$this->database->health_check_start_period}s",
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
@@ -194,6 +194,9 @@ class StartRedis
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
if (! $this->database->isHealthcheckEnabled()) {
unset($docker_compose['services'][$container_name]['healthcheck']);
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -299,6 +299,11 @@ class DatabasesController extends Controller
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
'health_check_enabled' => ['type' => 'boolean', 'description' => 'Enable the database healthcheck probe.'],
'health_check_interval' => ['type' => 'integer', 'description' => 'Healthcheck interval in seconds.'],
'health_check_timeout' => ['type' => 'integer', 'description' => 'Healthcheck timeout in seconds.'],
'health_check_retries' => ['type' => 'integer', 'description' => 'Healthcheck retries count.'],
'health_check_start_period' => ['type' => 'integer', 'description' => 'Healthcheck start period in seconds.'],
],
),
)
@@ -565,9 +570,17 @@ class DatabasesController extends Controller
}
break;
}
$allowedFields = array_merge($allowedFields, ['health_check_enabled', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period']);
$healthCheckValidator = customApiValidator($request->all(), [
'health_check_enabled' => 'boolean',
'health_check_interval' => 'integer|min:1',
'health_check_timeout' => 'integer|min:1',
'health_check_retries' => 'integer|min:1',
'health_check_start_period' => 'integer|min:0',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if ($validator->fails() || $healthCheckValidator->fails() || ! empty($extraFields)) {
$errors = $validator->errors()->merge($healthCheckValidator->errors());
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace App\Livewire\Project\Database;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Health extends Component
{
use AuthorizesRequests;
public $database;
#[Validate(['boolean'])]
public bool $healthCheckEnabled = true;
#[Validate(['integer', 'min:1'])]
public int $healthCheckInterval = 15;
#[Validate(['integer', 'min:1'])]
public int $healthCheckTimeout = 5;
#[Validate(['integer', 'min:1'])]
public int $healthCheckRetries = 5;
#[Validate(['integer', 'min:0'])]
public int $healthCheckStartPeriod = 5;
public function mount()
{
$this->authorize('view', $this->database);
$this->syncData();
}
public function syncData(bool $toModel = false): void
{
if ($toModel) {
$this->validate();
$this->database->health_check_enabled = $this->healthCheckEnabled;
$this->database->health_check_interval = $this->healthCheckInterval;
$this->database->health_check_timeout = $this->healthCheckTimeout;
$this->database->health_check_retries = $this->healthCheckRetries;
$this->database->health_check_start_period = $this->healthCheckStartPeriod;
$this->database->save();
} else {
$this->healthCheckEnabled = $this->database->health_check_enabled;
$this->healthCheckInterval = $this->database->health_check_interval;
$this->healthCheckTimeout = $this->database->health_check_timeout;
$this->healthCheckRetries = $this->database->health_check_retries;
$this->healthCheckStartPeriod = $this->database->health_check_start_period;
}
}
public function instantSave()
{
$this->submit();
}
public function submit()
{
try {
$this->authorize('update', $this->database);
$this->syncData(true);
$this->dispatch('success', 'Health check updated. Restart the database to apply the changes.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
if (is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true);
} else {
$this->dispatch('configurationChanged');
}
}
}
public function render()
{
return view('livewire.project.database.health');
}
}
+13 -1
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasDatabaseHealthCheck;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneClickhouse extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
@@ -44,11 +45,21 @@ class StandaloneClickhouse extends BaseModel
'destination_type',
'destination_id',
'environment_id',
'health_check_enabled',
'health_check_interval',
'health_check_timeout',
'health_check_retries',
'health_check_start_period',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'health_check_enabled' => 'boolean',
'health_check_interval' => 'integer',
'health_check_timeout' => 'integer',
'health_check_retries' => 'integer',
'health_check_start_period' => 'integer',
'clickhouse_admin_password' => 'encrypted',
'public_port_timeout' => 'integer',
'restart_count' => 'integer',
@@ -111,6 +122,7 @@ class StandaloneClickhouse extends BaseModel
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image.$this->ports_mappings;
$newConfigHash .= $this->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
+13 -1
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasDatabaseHealthCheck;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneDragonfly extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
@@ -43,11 +44,21 @@ class StandaloneDragonfly extends BaseModel
'destination_type',
'destination_id',
'environment_id',
'health_check_enabled',
'health_check_interval',
'health_check_timeout',
'health_check_retries',
'health_check_start_period',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'health_check_enabled' => 'boolean',
'health_check_interval' => 'integer',
'health_check_timeout' => 'integer',
'health_check_retries' => 'integer',
'health_check_start_period' => 'integer',
'dragonfly_password' => 'encrypted',
'public_port_timeout' => 'integer',
'restart_count' => 'integer',
@@ -110,6 +121,7 @@ class StandaloneDragonfly extends BaseModel
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image.$this->ports_mappings;
$newConfigHash .= $this->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
+13 -1
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasDatabaseHealthCheck;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneKeydb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
@@ -44,11 +45,21 @@ class StandaloneKeydb extends BaseModel
'destination_type',
'destination_id',
'environment_id',
'health_check_enabled',
'health_check_interval',
'health_check_timeout',
'health_check_retries',
'health_check_start_period',
];
protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];
protected $casts = [
'health_check_enabled' => 'boolean',
'health_check_interval' => 'integer',
'health_check_timeout' => 'integer',
'health_check_retries' => 'integer',
'health_check_start_period' => 'integer',
'keydb_password' => 'encrypted',
'public_port_timeout' => 'integer',
'restart_count' => 'integer',
@@ -111,6 +122,7 @@ class StandaloneKeydb extends BaseModel
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image.$this->ports_mappings.$this->keydb_conf;
$newConfigHash .= $this->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
+13 -1
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasDatabaseHealthCheck;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -12,7 +13,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMariadb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
@@ -47,11 +48,21 @@ class StandaloneMariadb extends BaseModel
'destination_type',
'destination_id',
'environment_id',
'health_check_enabled',
'health_check_interval',
'health_check_timeout',
'health_check_retries',
'health_check_start_period',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'health_check_enabled' => 'boolean',
'health_check_interval' => 'integer',
'health_check_timeout' => 'integer',
'health_check_retries' => 'integer',
'health_check_start_period' => 'integer',
'mariadb_password' => 'encrypted',
'public_port_timeout' => 'integer',
'restart_count' => 'integer',
@@ -114,6 +125,7 @@ class StandaloneMariadb extends BaseModel
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image.$this->ports_mappings.$this->mariadb_conf;
$newConfigHash .= $this->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
+13 -1
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasDatabaseHealthCheck;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMongodb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
@@ -47,11 +48,21 @@ class StandaloneMongodb extends BaseModel
'destination_type',
'destination_id',
'environment_id',
'health_check_enabled',
'health_check_interval',
'health_check_timeout',
'health_check_retries',
'health_check_start_period',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'health_check_enabled' => 'boolean',
'health_check_interval' => 'integer',
'health_check_timeout' => 'integer',
'health_check_retries' => 'integer',
'health_check_start_period' => 'integer',
'public_port_timeout' => 'integer',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
@@ -120,6 +131,7 @@ class StandaloneMongodb extends BaseModel
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image.$this->ports_mappings.$this->mongo_conf;
$newConfigHash .= $this->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
+13 -1
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasDatabaseHealthCheck;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMysql extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
@@ -48,11 +49,21 @@ class StandaloneMysql extends BaseModel
'destination_type',
'destination_id',
'environment_id',
'health_check_enabled',
'health_check_interval',
'health_check_timeout',
'health_check_retries',
'health_check_start_period',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'health_check_enabled' => 'boolean',
'health_check_interval' => 'integer',
'health_check_timeout' => 'integer',
'health_check_retries' => 'integer',
'health_check_start_period' => 'integer',
'mysql_password' => 'encrypted',
'mysql_root_password' => 'encrypted',
'public_port_timeout' => 'integer',
@@ -116,6 +127,7 @@ class StandaloneMysql extends BaseModel
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image.$this->ports_mappings.$this->mysql_conf;
$newConfigHash .= $this->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
+13 -1
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasDatabaseHealthCheck;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandalonePostgresql extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
@@ -50,11 +51,21 @@ class StandalonePostgresql extends BaseModel
'destination_type',
'destination_id',
'environment_id',
'health_check_enabled',
'health_check_interval',
'health_check_timeout',
'health_check_retries',
'health_check_start_period',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'health_check_enabled' => 'boolean',
'health_check_interval' => 'integer',
'health_check_timeout' => 'integer',
'health_check_retries' => 'integer',
'health_check_start_period' => 'integer',
'init_scripts' => 'array',
'postgres_password' => 'encrypted',
'public_port_timeout' => 'integer',
@@ -158,6 +169,7 @@ class StandalonePostgresql extends BaseModel
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image.$this->ports_mappings.$this->postgres_initdb_args.$this->postgres_host_auth_method;
$newConfigHash .= $this->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
+13 -1
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasDatabaseHealthCheck;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneRedis extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $fillable = [
'uuid',
@@ -43,11 +44,21 @@ class StandaloneRedis extends BaseModel
'destination_type',
'destination_id',
'environment_id',
'health_check_enabled',
'health_check_interval',
'health_check_timeout',
'health_check_retries',
'health_check_start_period',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'health_check_enabled' => 'boolean',
'health_check_interval' => 'integer',
'health_check_timeout' => 'integer',
'health_check_retries' => 'integer',
'health_check_start_period' => 'integer',
'public_port_timeout' => 'integer',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
@@ -115,6 +126,7 @@ class StandaloneRedis extends BaseModel
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image.$this->ports_mappings.$this->redis_conf;
$newConfigHash .= $this->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Traits;
/**
* Shared healthcheck behaviour for standalone database models.
*
* Standalone databases use a fixed, type-specific probe command (psql, redis-cli, ...),
* so only the timing fields and the enable/disable flag are configurable.
*/
trait HasDatabaseHealthCheck
{
public function isHealthcheckEnabled(): bool
{
return (bool) ($this->health_check_enabled ?? true);
}
/**
* Build the Docker Compose healthcheck block for the given probe command.
*
* @param array<int, string> $test The Docker `test` array (e.g. ['CMD', 'pg_isready']).
* @return array<string, mixed>
*/
public function healthCheckConfiguration(array $test): array
{
return [
'test' => $test,
'interval' => ($this->health_check_interval ?? 15).'s',
'timeout' => ($this->health_check_timeout ?? 5).'s',
'retries' => $this->health_check_retries ?? 5,
'start_period' => ($this->health_check_start_period ?? 5).'s',
];
}
}
@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private array $tables = [
'standalone_postgresqls',
'standalone_mysqls',
'standalone_mariadbs',
'standalone_redis',
'standalone_clickhouses',
'standalone_dragonflies',
'standalone_keydbs',
'standalone_mongodbs',
];
public function up(): void
{
foreach ($this->tables as $table) {
Schema::table($table, function (Blueprint $table) {
$table->boolean('health_check_enabled')->default(true);
$table->integer('health_check_interval')->default(15);
$table->integer('health_check_timeout')->default(5);
$table->integer('health_check_retries')->default(5);
$table->integer('health_check_start_period')->default(5);
});
}
}
public function down(): void
{
foreach ($this->tables as $table) {
Schema::table($table, function (Blueprint $table) {
$table->dropColumn([
'health_check_enabled',
'health_check_interval',
'health_check_timeout',
'health_check_retries',
'health_check_start_period',
]);
});
}
}
};
+20
View File
@@ -4605,6 +4605,26 @@
"mysql_conf": {
"type": "string",
"description": "MySQL conf"
},
"health_check_enabled": {
"type": "boolean",
"description": "Enable the database healthcheck probe."
},
"health_check_interval": {
"type": "integer",
"description": "Healthcheck interval in seconds."
},
"health_check_timeout": {
"type": "integer",
"description": "Healthcheck timeout in seconds."
},
"health_check_retries": {
"type": "integer",
"description": "Healthcheck retries count."
},
"health_check_start_period": {
"type": "integer",
"description": "Healthcheck start period in seconds."
}
},
"type": "object"
+15
View File
@@ -2950,6 +2950,21 @@ paths:
mysql_conf:
type: string
description: 'MySQL conf'
health_check_enabled:
type: boolean
description: 'Enable the database healthcheck probe.'
health_check_interval:
type: integer
description: 'Healthcheck interval in seconds.'
health_check_timeout:
type: integer
description: 'Healthcheck timeout in seconds.'
health_check_retries:
type: integer
description: 'Healthcheck retries count.'
health_check_start_period:
type: integer
description: 'Healthcheck start period in seconds.'
type: object
responses:
'200':
@@ -15,6 +15,8 @@
href="{{ route('project.database.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Servers</span></a>
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Persistent Storage</span></a>
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.health-checks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Health Checks</span></a>
@can('update', $database)
<a class='sub-menu-item' wire:current.exact="menu-item-active"
href="{{ route('project.database.import-backup', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Import Backup</span></a>
@@ -57,6 +59,8 @@
<livewire:project.shared.destination :resource="$database" />
@elseif ($currentRoute === 'project.database.persistent-storage')
<livewire:project.service.storage :resource="$database" />
@elseif ($currentRoute === 'project.database.health-checks')
<livewire:project.database.health :database="$database" />
@elseif ($currentRoute === 'project.database.import-backup')
<livewire:project.database.import :resource="$database" />
@elseif ($currentRoute === 'project.database.webhooks')
@@ -0,0 +1,26 @@
<form wire:submit='submit' class="flex flex-col">
<div class="flex items-center gap-2">
<h2>Health Checks</h2>
<x-forms.button canGate="update" :canResource="$database" type="submit">Save</x-forms.button>
</div>
<div class="mt-1 pb-4">Configure how Docker checks this database's health. A higher interval lowers
<code>dockerd</code>/<code>containerd</code> CPU and load on servers running many databases. Restart the
database to apply changes.</div>
<div class="flex flex-col gap-4">
<x-forms.checkbox canGate="update" :canResource="$database" instantSave id="healthCheckEnabled"
label="Enabled"
helper="When disabled, Docker runs no healthcheck probe for this database and Coolify can no longer report a healthy/unhealthy state." />
@if ($healthCheckEnabled)
<div class="flex gap-2">
<x-forms.input canGate="update" :canResource="$database" min="1" type="number" id="healthCheckInterval"
placeholder="15" label="Interval (s)" required />
<x-forms.input canGate="update" :canResource="$database" min="1" type="number" id="healthCheckTimeout"
placeholder="5" label="Timeout (s)" required />
<x-forms.input canGate="update" :canResource="$database" min="1" type="number" id="healthCheckRetries"
placeholder="5" label="Retries" required />
<x-forms.input canGate="update" :canResource="$database" min="0" type="number"
id="healthCheckStartPeriod" placeholder="5" label="Start Period (s)" required />
</div>
@endif
</div>
</form>
+1
View File
@@ -243,6 +243,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/servers', DatabaseConfiguration::class)->name('project.database.servers');
Route::get('/import-backup', DatabaseConfiguration::class)->name('project.database.import-backup')->middleware('can.update.resource');
Route::get('/persistent-storage', DatabaseConfiguration::class)->name('project.database.persistent-storage');
Route::get('/health-checks', DatabaseConfiguration::class)->name('project.database.health-checks');
Route::get('/webhooks', DatabaseConfiguration::class)->name('project.database.webhooks');
Route::get('/resource-limits', DatabaseConfiguration::class)->name('project.database.resource-limits');
Route::get('/resource-operations', DatabaseConfiguration::class)->name('project.database.resource-operations');
+45
View File
@@ -0,0 +1,45 @@
<?php
use App\Models\StandalonePostgresql;
it('defaults to an enabled healthcheck when nothing is configured', function () {
$database = new StandalonePostgresql;
expect($database->isHealthcheckEnabled())->toBeTrue();
});
it('builds the compose healthcheck block from the model timing fields', function () {
$database = new StandalonePostgresql([
'health_check_interval' => 30,
'health_check_timeout' => 7,
'health_check_retries' => 4,
'health_check_start_period' => 12,
]);
$config = $database->healthCheckConfiguration(['CMD', 'pg_isready']);
expect($config)->toBe([
'test' => ['CMD', 'pg_isready'],
'interval' => '30s',
'timeout' => '7s',
'retries' => 4,
'start_period' => '12s',
]);
});
it('falls back to safe defaults when timing fields are missing', function () {
$database = new StandalonePostgresql;
$config = $database->healthCheckConfiguration(['CMD', 'pg_isready']);
expect($config['interval'])->toBe('15s')
->and($config['timeout'])->toBe('5s')
->and($config['retries'])->toBe(5)
->and($config['start_period'])->toBe('5s');
});
it('reports the healthcheck as disabled when the flag is false', function () {
$database = new StandalonePostgresql(['health_check_enabled' => false]);
expect($database->isHealthcheckEnabled())->toBeFalse();
});