diff --git a/tests/Feature/SettingsDropdownTest.php b/tests/Feature/SettingsDropdownTest.php
new file mode 100644
index 000000000..db6c70111
--- /dev/null
+++ b/tests/Feature/SettingsDropdownTest.php
@@ -0,0 +1,44 @@
+ 'test@example.com']);
+ $user->id = 1;
+
+ Auth::setUser($user);
+
+ app()->instance(ChangelogService::class, new class extends ChangelogService
+ {
+ public function getEntriesForUser(User $user): Collection
+ {
+ return collect([
+ (object) [
+ 'tag_name' => 'v1.0.0',
+ 'title' => 'Test Release',
+ 'content' => 'Release notes',
+ 'content_html' => '
Release notes
',
+ 'published_at' => Carbon::parse('2026-05-01'),
+ 'is_read' => false,
+ ],
+ ]);
+ }
+
+ public function getUnreadCountForUser(User $user): int
+ {
+ return 1;
+ }
+ });
+
+ Livewire::test(SettingsDropdown::class, ['trigger' => 'changelog-sidebar'])
+ ->call('openWhatsNewModal')
+ ->assertSee('Changelog')
+ ->assertSee('z-[60]', false)
+ ->assertSee('closeWhatsNewModal', false);
+});
diff --git a/tests/Unit/Service/StartServicePullLatestRestartTest.php b/tests/Unit/Service/StartServicePullLatestRestartTest.php
new file mode 100644
index 000000000..ce138ba65
--- /dev/null
+++ b/tests/Unit/Service/StartServicePullLatestRestartTest.php
@@ -0,0 +1,36 @@
+invoke(new StartService, pullLatestImages: true, stopBeforeStart: true))->toBeFalse();
+});
+
+it('still stops a service before a regular restart', function () {
+ $method = new ReflectionMethod(StartService::class, 'shouldStopBeforeStarting');
+
+ expect($method->invoke(new StartService, pullLatestImages: false, stopBeforeStart: true))->toBeTrue()
+ ->and($method->invoke(new StartService, pullLatestImages: false, stopBeforeStart: false))->toBeFalse();
+});
+
+it('routes service restart actions through start service with deferred stop semantics', function () {
+ $service = Mockery::mock(Service::class);
+
+ $stopService = Mockery::mock(StopService::class);
+ $stopService->shouldNotReceive('handle');
+ app()->instance(StopService::class, $stopService);
+
+ $startService = Mockery::mock(StartService::class);
+ $startService->shouldReceive('handle')
+ ->once()
+ ->with($service, true, true)
+ ->andReturn('restart queued');
+ app()->instance(StartService::class, $startService);
+
+ expect(RestartService::run($service, true))->toBe('restart queued');
+});
From 34f15c106c003645bf8cc0b6ba428e589ff1cbe5 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sun, 31 May 2026 21:46:23 +0200
Subject: [PATCH 077/117] fix(webhook): match GitLab SSH repos with custom
ports
Strip leading port segments from scp-style GitLab repository URLs so manual webhook matching compares the repository path consistently. Cover both ported and unported SSH URL forms.
---
.../MatchesManualWebhookApplications.php | 4 ++
tests/Feature/Webhook/WebhookHmacTest.php | 42 +++++++++++++++++++
2 files changed, 46 insertions(+)
diff --git a/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php b/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php
index f1fd0c40f..0463790eb 100644
--- a/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php
+++ b/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php
@@ -81,6 +81,10 @@ protected function canonicalManualWebhookRepository(?string $gitRepository): ?st
$path = data_get($parts, 'path');
} elseif (Str::startsWith($gitRepository, 'git@') && str_contains($gitRepository, ':')) {
$path = Str::after($gitRepository, ':');
+ // scp-style SSH URLs embed a custom port as "git@host:2222/owner/repo".
+ // Strip the leading numeric port segment so the path matches the webhook
+ // payload's owner/repo, consistent with convertGitUrl() in shared.php.
+ $path = preg_replace('#^\d+/#', '', $path) ?? $path;
} else {
$path = $gitRepository;
}
diff --git a/tests/Feature/Webhook/WebhookHmacTest.php b/tests/Feature/Webhook/WebhookHmacTest.php
index be2417462..3f49ff43d 100644
--- a/tests/Feature/Webhook/WebhookHmacTest.php
+++ b/tests/Feature/Webhook/WebhookHmacTest.php
@@ -509,6 +509,48 @@ function createApplicationWithWebhook(string $repo = 'test-org/test-repo', strin
expect($response->getContent())->not->toContain('No applications found');
});
+ test('gitlab matches scp-style ssh repository URL with custom port', function () {
+ $app = createApplicationWithWebhook(overrides: [
+ 'git_repository' => 'git@gitlab.example.com:2222/services/xyz.git',
+ 'git_branch' => 'master',
+ ]);
+ $secret = $app->manual_webhook_secret_gitlab;
+
+ $response = $this->postJson('/webhooks/source/gitlab/events/manual', [
+ 'object_kind' => 'push',
+ 'ref' => 'refs/heads/master',
+ 'project' => ['path_with_namespace' => 'services/xyz'],
+ 'after' => 'abc123',
+ 'commits' => [],
+ ], [
+ 'X-Gitlab-Token' => $secret,
+ ]);
+
+ $response->assertOk();
+ expect($response->getContent())->not->toContain('No applications found');
+ });
+
+ test('gitlab matches scp-style ssh repository URL without port', function () {
+ $app = createApplicationWithWebhook(overrides: [
+ 'git_repository' => 'git@gitlab.example.com:services/xyz.git',
+ 'git_branch' => 'master',
+ ]);
+ $secret = $app->manual_webhook_secret_gitlab;
+
+ $response = $this->postJson('/webhooks/source/gitlab/events/manual', [
+ 'object_kind' => 'push',
+ 'ref' => 'refs/heads/master',
+ 'project' => ['path_with_namespace' => 'services/xyz'],
+ 'after' => 'abc123',
+ 'commits' => [],
+ ], [
+ 'X-Gitlab-Token' => $secret,
+ ]);
+
+ $response->assertOk();
+ expect($response->getContent())->not->toContain('No applications found');
+ });
+
test('github matches repository case-insensitively', function () {
$app = createApplicationWithWebhook(overrides: [
'git_repository' => 'https://github.com/Test-Org/Test-Repo.git',
From 1b68f11ec0864f3de0a7dac017b2fd68aec0d6f3 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sun, 31 May 2026 21:47:18 +0200
Subject: [PATCH 078/117] fix(cleanup): disable unreachable self-hosted servers
Preserve self-hosted server IPs during unreachable cleanup and force-disable them instead. Keep cloud cleanup behavior overwriting the IP, with test coverage for both paths.
---
.../Commands/CleanupUnreachableServers.php | 10 +++++--
.../Feature/CleanupUnreachableServersTest.php | 28 ++++++++++++++++++-
2 files changed, 34 insertions(+), 4 deletions(-)
diff --git a/app/Console/Commands/CleanupUnreachableServers.php b/app/Console/Commands/CleanupUnreachableServers.php
index 09563a2c3..666e98a18 100644
--- a/app/Console/Commands/CleanupUnreachableServers.php
+++ b/app/Console/Commands/CleanupUnreachableServers.php
@@ -18,9 +18,13 @@ public function handle()
if ($servers->count() > 0) {
foreach ($servers as $server) {
echo "Cleanup unreachable server ($server->id) with name $server->name";
- $server->update([
- 'ip' => '1.2.3.4',
- ]);
+ if (isCloud()) {
+ $server->update([
+ 'ip' => '1.2.3.4',
+ ]);
+ } else {
+ $server->forceDisableServer();
+ }
}
}
}
diff --git a/tests/Feature/CleanupUnreachableServersTest.php b/tests/Feature/CleanupUnreachableServersTest.php
index c06944969..8849b1ca0 100644
--- a/tests/Feature/CleanupUnreachableServersTest.php
+++ b/tests/Feature/CleanupUnreachableServersTest.php
@@ -6,7 +6,30 @@
uses(RefreshDatabase::class);
-it('cleans up servers with unreachable_count >= 3 after 7 days', function () {
+it('disables (non-destructively) self-hosted servers with unreachable_count >= 3 after 7 days', function () {
+ config(['constants.coolify.self_hosted' => true]);
+
+ $team = Team::factory()->create();
+ $server = Server::factory()->create([
+ 'team_id' => $team->id,
+ 'unreachable_count' => 50,
+ 'unreachable_notification_sent' => true,
+ 'updated_at' => now()->subDays(8),
+ ]);
+
+ $originalIp = (string) $server->ip;
+
+ $this->artisan('cleanup:unreachable-servers')->assertSuccessful();
+
+ $server->refresh();
+ // IP must be preserved — never overwritten on self-hosted.
+ expect($server->ip)->toBe($originalIp);
+ expect($server->settings->force_disabled)->toBeTrue();
+});
+
+it('overwrites the IP with 1.2.3.4 on cloud for servers with unreachable_count >= 3 after 7 days', function () {
+ config(['constants.coolify.self_hosted' => false]);
+
$team = Team::factory()->create();
$server = Server::factory()->create([
'team_id' => $team->id,
@@ -36,6 +59,7 @@
$server->refresh();
expect($server->ip)->toBe($originalIp);
+ expect($server->settings->force_disabled)->toBeFalse();
});
it('does not clean up servers updated within 7 days', function () {
@@ -53,6 +77,7 @@
$server->refresh();
expect($server->ip)->toBe($originalIp);
+ expect($server->settings->force_disabled)->toBeFalse();
});
it('does not clean up servers without notification sent', function () {
@@ -70,4 +95,5 @@
$server->refresh();
expect($server->ip)->toBe($originalIp);
+ expect($server->settings->force_disabled)->toBeFalse();
});
From d423223d38ebe200427a235cdb213ff9bc78941a Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sun, 31 May 2026 21:50:10 +0200
Subject: [PATCH 079/117] 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.
---
app/Actions/Database/StartClickhouse.php | 11 ++-
app/Actions/Database/StartDragonfly.php | 11 ++-
app/Actions/Database/StartKeydb.php | 11 ++-
app/Actions/Database/StartMariadb.php | 11 ++-
app/Actions/Database/StartMongodb.php | 11 ++-
app/Actions/Database/StartMysql.php | 11 ++-
app/Actions/Database/StartPostgresql.php | 11 ++-
app/Actions/Database/StartRedis.php | 11 ++-
.../Controllers/Api/DatabasesController.php | 17 +++-
app/Livewire/Project/Database/Health.php | 81 +++++++++++++++++++
app/Models/StandaloneClickhouse.php | 14 +++-
app/Models/StandaloneDragonfly.php | 14 +++-
app/Models/StandaloneKeydb.php | 14 +++-
app/Models/StandaloneMariadb.php | 14 +++-
app/Models/StandaloneMongodb.php | 14 +++-
app/Models/StandaloneMysql.php | 14 +++-
app/Models/StandalonePostgresql.php | 14 +++-
app/Models/StandaloneRedis.php | 14 +++-
app/Traits/HasDatabaseHealthCheck.php | 34 ++++++++
...d_health_check_to_standalone_databases.php | 47 +++++++++++
openapi.json | 20 +++++
openapi.yaml | 15 ++++
.../project/database/configuration.blade.php | 4 +
.../project/database/health.blade.php | 26 ++++++
routes/web.php | 1 +
tests/Feature/DatabaseHealthCheckTest.php | 45 +++++++++++
26 files changed, 448 insertions(+), 42 deletions(-)
create mode 100644 app/Livewire/Project/Database/Health.php
create mode 100644 app/Traits/HasDatabaseHealthCheck.php
create mode 100644 database/migrations/2026_05_31_000000_add_health_check_to_standalone_databases.php
create mode 100644 resources/views/livewire/project/database/health.blade.php
create mode 100644 tests/Feature/DatabaseHealthCheckTest.php
diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php
index 30cae71f1..1128b8f8f 100644
--- a/app/Actions/Database/StartClickhouse.php
+++ b/app/Actions/Database/StartClickhouse.php
@@ -52,10 +52,10 @@ public function handle(StandaloneClickhouse $database)
'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 @@ public function handle(StandaloneClickhouse $database)
$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";
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index addc30be4..530ba7d23 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -108,10 +108,10 @@ public function handle(StandaloneDragonfly $database)
'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 @@ public function handle(StandaloneDragonfly $database)
$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";
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index e59d6f697..e9acd0b3c 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -110,10 +110,10 @@ public function handle(StandaloneKeydb $database)
'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 @@ public function handle(StandaloneKeydb $database)
// 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";
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index ceb1e8b85..17ed2a9a8 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -105,10 +105,10 @@ public function handle(StandaloneMariadb $database)
'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 @@ public function handle(StandaloneMariadb $database)
];
}
+ 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";
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index c79789718..e5973e807 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -115,10 +115,10 @@ public function handle(StandaloneMongodb $database)
'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 @@ public function handle(StandaloneMongodb $database)
$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";
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index 0394d50b6..f9d75e0c8 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -105,10 +105,10 @@ public function handle(StandaloneMysql $database)
'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 @@ public function handle(StandaloneMysql $database)
];
}
+ 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";
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index da8b5dc4e..520cbdec4 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -112,10 +112,10 @@ public function handle(StandalonePostgresql $database)
'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 @@ public function handle(StandalonePostgresql $database)
$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";
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index c31b099e4..e4bfd98e1 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -111,10 +111,10 @@ public function handle(StandaloneRedis $database)
'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 @@ public function handle(StandaloneRedis $database)
$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";
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index dc9b6f5b5..e2d2aad92 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -299,6 +299,11 @@ public function database_by_uuid(Request $request)
'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 @@ public function update_by_uuid(Request $request)
}
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.');
diff --git a/app/Livewire/Project/Database/Health.php b/app/Livewire/Project/Database/Health.php
new file mode 100644
index 000000000..33540d74e
--- /dev/null
+++ b/app/Livewire/Project/Database/Health.php
@@ -0,0 +1,81 @@
+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');
+ }
+}
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index 784e2c937..1e68046f9 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -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 @@
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 @@ protected function serverStatus(): Attribute
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');
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index e07053c03..5f76be884 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -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 @@
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 @@ protected function serverStatus(): Attribute
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');
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index 979f45a3d..6d3b5a82b 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -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 @@
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 @@ protected function serverStatus(): Attribute
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');
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index dba8a52f5..1058d8721 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -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 @@
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 @@ protected function serverStatus(): Attribute
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');
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index e72f4f1c6..4657f1d5e 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -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 @@
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 @@ protected function serverStatus(): Attribute
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');
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index 1c522d200..f3d0ad55f 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -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 @@
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 @@ protected function serverStatus(): Attribute
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');
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index 57dfe5988..39482892d 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -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 @@
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 @@ public function deleteVolumes()
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');
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index ef42d7f18..3d1d11138 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -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 @@
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 @@ protected function serverStatus(): Attribute
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');
diff --git a/app/Traits/HasDatabaseHealthCheck.php b/app/Traits/HasDatabaseHealthCheck.php
new file mode 100644
index 000000000..2602ecb23
--- /dev/null
+++ b/app/Traits/HasDatabaseHealthCheck.php
@@ -0,0 +1,34 @@
+health_check_enabled ?? true);
+ }
+
+ /**
+ * Build the Docker Compose healthcheck block for the given probe command.
+ *
+ * @param array
$test The Docker `test` array (e.g. ['CMD', 'pg_isready']).
+ * @return array
+ */
+ 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',
+ ];
+ }
+}
diff --git a/database/migrations/2026_05_31_000000_add_health_check_to_standalone_databases.php b/database/migrations/2026_05_31_000000_add_health_check_to_standalone_databases.php
new file mode 100644
index 000000000..63d7c3497
--- /dev/null
+++ b/database/migrations/2026_05_31_000000_add_health_check_to_standalone_databases.php
@@ -0,0 +1,47 @@
+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',
+ ]);
+ });
+ }
+ }
+};
diff --git a/openapi.json b/openapi.json
index e83538f2b..d3d25bacc 100644
--- a/openapi.json
+++ b/openapi.json
@@ -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"
diff --git a/openapi.yaml b/openapi.yaml
index 523d453ff..469a0c38d 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -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':
diff --git a/resources/views/livewire/project/database/configuration.blade.php b/resources/views/livewire/project/database/configuration.blade.php
index 73f87c0e3..c58232200 100644
--- a/resources/views/livewire/project/database/configuration.blade.php
+++ b/resources/views/livewire/project/database/configuration.blade.php
@@ -15,6 +15,8 @@
href="{{ route('project.database.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">
+
@can('update', $database)
@@ -57,6 +59,8 @@
@elseif ($currentRoute === 'project.database.persistent-storage')
+ @elseif ($currentRoute === 'project.database.health-checks')
+
@elseif ($currentRoute === 'project.database.import-backup')
@elseif ($currentRoute === 'project.database.webhooks')
diff --git a/resources/views/livewire/project/database/health.blade.php b/resources/views/livewire/project/database/health.blade.php
new file mode 100644
index 000000000..2e70f79b2
--- /dev/null
+++ b/resources/views/livewire/project/database/health.blade.php
@@ -0,0 +1,26 @@
+
diff --git a/routes/web.php b/routes/web.php
index aed37a086..e4d6f477f 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -243,6 +243,7 @@
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');
diff --git a/tests/Feature/DatabaseHealthCheckTest.php b/tests/Feature/DatabaseHealthCheckTest.php
new file mode 100644
index 000000000..8bc1e4e92
--- /dev/null
+++ b/tests/Feature/DatabaseHealthCheckTest.php
@@ -0,0 +1,45 @@
+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();
+});
From b46d8e260146a7f6b50f2e9572d08588ecc858ee Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sun, 31 May 2026 21:52:46 +0200
Subject: [PATCH 080/117] fix(terminal): keep sessions alive without hard
timeouts
---
app/Helpers/SshMultiplexingHelper.php | 5 +--
app/Livewire/Project/Shared/Terminal.php | 31 ++++++++++++++++--
config/constants.php | 1 +
docker/coolify-realtime/terminal-server.js | 32 ++-----------------
package-lock.json | 8 ++---
resources/js/terminal.js | 11 +------
.../project/shared/terminal.blade.php | 3 ++
.../Feature/RealtimeTerminalPackagingTest.php | 29 ++++++++++++-----
tests/Feature/SshMultiplexingLockTest.php | 13 ++++++++
9 files changed, 75 insertions(+), 58 deletions(-)
diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php
index 021ac3608..d0bd8d3a3 100644
--- a/app/Helpers/SshMultiplexingHelper.php
+++ b/app/Helpers/SshMultiplexingHelper.php
@@ -69,7 +69,7 @@ public static function generateScpCommand(Server $server, string $source, string
return $scpCommand.escapeshellarg($source).' '.self::escapedUserAtHost($server).':'.escapeshellarg($dest);
}
- public static function generateSshCommand(Server $server, string $command, bool $disableMultiplexing = false): string
+ public static function generateSshCommand(Server $server, string $command, bool $disableMultiplexing = false, ?int $commandTimeout = null): string
{
if ($server->settings->force_disabled) {
throw new \RuntimeException('Server is disabled.');
@@ -80,7 +80,8 @@ public static function generateSshCommand(Server $server, string $command, bool
self::validateSshKey($server->privateKey);
- $sshCommand = 'timeout '.config('constants.ssh.command_timeout').' ssh ';
+ $commandTimeout = $commandTimeout ?? (int) config('constants.ssh.command_timeout');
+ $sshCommand = $commandTimeout > 0 ? "timeout {$commandTimeout} ssh " : 'ssh ';
if (! $disableMultiplexing && self::isMultiplexingEnabled()) {
$sshCommand .= self::multiplexingOptions($server);
diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php
index bbc2b3e66..db65cdaad 100644
--- a/app/Livewire/Project/Shared/Terminal.php
+++ b/app/Livewire/Project/Shared/Terminal.php
@@ -12,6 +12,8 @@ class Terminal extends Component
{
public bool $hasShell = true;
+ public bool $isTerminalConnected = false;
+
private function checkShellAvailability(Server $server, string $container): bool
{
$escapedContainer = escapeshellarg($container);
@@ -65,12 +67,20 @@ public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
$dockerCommand = "sudo {$dockerCommand}";
}
- $command = SshMultiplexingHelper::generateSshCommand($server, $dockerCommand);
+ $command = SshMultiplexingHelper::generateSshCommand(
+ $server,
+ $dockerCommand,
+ commandTimeout: (int) config('constants.terminal.command_timeout')
+ );
} else {
$shellCommand = 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && '.
'if [ -f ~/.profile ]; then . ~/.profile; fi && '.
'if [ -n "$SHELL" ] && [ -x "$SHELL" ]; then exec $SHELL; else sh; fi';
- $command = SshMultiplexingHelper::generateSshCommand($server, $shellCommand);
+ $command = SshMultiplexingHelper::generateSshCommand(
+ $server,
+ $shellCommand,
+ commandTimeout: (int) config('constants.terminal.command_timeout')
+ );
}
// ssh command is sent back to frontend then to websocket
// this is done because the websocket connection is not available here
@@ -84,6 +94,23 @@ public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
$this->dispatch('send-back-command', $command);
}
+ #[On('terminalConnected')]
+ public function markTerminalConnected(): void
+ {
+ $this->isTerminalConnected = true;
+ }
+
+ #[On('terminalDisconnected')]
+ public function markTerminalDisconnected(): void
+ {
+ $this->isTerminalConnected = false;
+ }
+
+ public function keepTerminalPageAlive(): void
+ {
+ $this->isTerminalConnected = true;
+ }
+
public function render()
{
return view('livewire.project.shared.terminal');
diff --git a/config/constants.php b/config/constants.php
index 89b633650..fd66a682a 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -35,6 +35,7 @@
'protocol' => env('TERMINAL_PROTOCOL'),
'host' => env('TERMINAL_HOST'),
'port' => env('TERMINAL_PORT'),
+ 'command_timeout' => 0,
],
'pusher' => [
diff --git a/docker/coolify-realtime/terminal-server.js b/docker/coolify-realtime/terminal-server.js
index 42ca7c81d..c76383b9f 100755
--- a/docker/coolify-realtime/terminal-server.js
+++ b/docker/coolify-realtime/terminal-server.js
@@ -63,8 +63,8 @@ function createHttpError(response) {
}
const userSessions = new Map();
-const terminalDebugEnabled = ['1', 'true', 'yes'].includes(
- String(process.env.TERMINAL_DEBUG || '').toLowerCase()
+const terminalDebugEnabled = ['local', 'development'].includes(
+ String(process.env.APP_ENV || process.env.NODE_ENV || '').toLowerCase()
);
function logTerminal(level, message, context = {}) {
@@ -154,7 +154,6 @@ const verifyClient = async (info, callback) => {
const wss = new WebSocketServer({ server, path: '/terminal/ws', verifyClient: verifyClient });
const HEARTBEAT_INTERVAL_MS = 30000;
-const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
wss.on('connection', async (ws, req) => {
ws.isAlive = true;
@@ -168,7 +167,6 @@ wss.on('connection', async (ws, req) => {
ptyProcess: null,
isActive: false,
authorizedIPs: [],
- lastActivityAt: Date.now(),
authReady: false,
pendingMessages: [],
};
@@ -260,29 +258,6 @@ const heartbeat = setInterval(() => {
} catch (_) {
// ignore — close handler will follow
}
-
- const session = ws.userId ? userSessions.get(ws.userId) : null;
- if (session?.isActive && session.lastActivityAt && (Date.now() - session.lastActivityAt > IDLE_TIMEOUT_MS)) {
- const idleMs = Date.now() - session.lastActivityAt;
- logTerminal('warn', 'Closing terminal session due to idle timeout.', {
- userId: ws.userId,
- idleMs,
- idleTimeoutMs: IDLE_TIMEOUT_MS,
- });
- try {
- ws.send('idle-timeout');
- } catch (_) {
- // ignore — close still attempted below
- }
- killPtyProcess(ws.userId);
- setTimeout(() => {
- try {
- ws.close(1000, 'Idle timeout');
- } catch (_) {
- // ignore — already closed
- }
- }, 100);
- }
});
}, HEARTBEAT_INTERVAL_MS);
@@ -290,11 +265,9 @@ wss.on('close', () => clearInterval(heartbeat));
const messageHandlers = {
message: (session, data) => {
- session.lastActivityAt = Date.now();
session.ptyProcess.write(data);
},
resize: (session, { cols, rows }) => {
- session.lastActivityAt = Date.now();
cols = cols > 0 ? cols : 80;
rows = rows > 0 ? rows : 30;
session.ptyProcess.resize(cols, rows)
@@ -420,7 +393,6 @@ async function handleCommand(ws, command, userId) {
userSession.ptyProcess = ptyProcess;
userSession.isActive = true;
- userSession.lastActivityAt = Date.now();
ws.send('pty-ready');
diff --git a/package-lock.json b/package-lock.json
index bcacecc8b..9d495c412 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1261,8 +1261,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
@@ -1752,7 +1751,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -1946,8 +1944,7 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -1992,7 +1989,6 @@
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
diff --git a/resources/js/terminal.js b/resources/js/terminal.js
index 7a7fc8536..cb3a26b3a 100644
--- a/resources/js/terminal.js
+++ b/resources/js/terminal.js
@@ -44,7 +44,7 @@ export function initializeTerminalComponent() {
pendingCommand: null,
// Last successfully sent SSH command — replayed after a transient reconnect
// so the PTY respawns automatically. Cleared on intentional terminations
- // (pty-exited, idle-timeout, unprocessable).
+ // (pty-exited, unprocessable).
lastSentCommand: null,
// Resize handling
resizeObserver: null,
@@ -462,15 +462,6 @@ export function initializeTerminalComponent() {
// Notify parent component that terminal disconnected
this.$wire.dispatch('terminalDisconnected');
- } else if (event.data === 'idle-timeout') {
- this.$wire.dispatch('error', 'Terminal closed after 30 minutes of inactivity.');
- this.terminalActive = false;
- if (this.term) {
- this.term.reset();
- }
- this.commandBuffer = '';
- this.lastSentCommand = null;
- this.$wire.dispatch('terminalDisconnected');
} else if (
typeof event.data === 'string' &&
(event.data.startsWith('Unauthorized:') || event.data.startsWith('Invalid SSH command:'))
diff --git a/resources/views/livewire/project/shared/terminal.blade.php b/resources/views/livewire/project/shared/terminal.blade.php
index c46c5f316..abcb366f5 100644
--- a/resources/views/livewire/project/shared/terminal.blade.php
+++ b/resources/views/livewire/project/shared/terminal.blade.php
@@ -1,4 +1,7 @@
+ @if ($isTerminalConnected)
+
+ @endif
@if (!$hasShell)
diff --git a/tests/Feature/RealtimeTerminalPackagingTest.php b/tests/Feature/RealtimeTerminalPackagingTest.php
index ba01deca5..c478f79b8 100644
--- a/tests/Feature/RealtimeTerminalPackagingTest.php
+++ b/tests/Feature/RealtimeTerminalPackagingTest.php
@@ -58,22 +58,35 @@
->toContain("'Visibility-resume timeout'");
});
-it('closes idle terminal sessions after 30 minutes on the server', function () {
+it('does not hard close terminal sessions after 30 minutes on the server', function () {
$terminalServer = file_get_contents(base_path('docker/coolify-realtime/terminal-server.js'));
expect($terminalServer)
- ->toContain('IDLE_TIMEOUT_MS = 30 * 60 * 1000')
- ->toContain('lastActivityAt')
- ->toContain("ws.send('idle-timeout');")
- ->toContain("ws.close(1000, 'Idle timeout');");
+ ->not->toContain('IDLE_TIMEOUT_MS = 30 * 60 * 1000')
+ ->not->toContain("ws.send('idle-timeout');")
+ ->not->toContain("ws.close(1000, 'Idle timeout');");
});
-it('reacts to idle-timeout sentinel on the client and shows a user-facing error', function () {
+it('does not close the client terminal from an idle-timeout sentinel', function () {
$terminalClient = file_get_contents(base_path('resources/js/terminal.js'));
expect($terminalClient)
- ->toContain("event.data === 'idle-timeout'")
- ->toContain('Terminal closed after 30 minutes of inactivity.');
+ ->not->toContain("event.data === 'idle-timeout'")
+ ->not->toContain('Terminal closed after 30 minutes of inactivity.');
+});
+
+it('keeps Livewire alive in background tabs while a terminal is connected', function () {
+ $terminalComponent = file_get_contents(base_path('app/Livewire/Project/Shared/Terminal.php'));
+ $terminalView = file_get_contents(base_path('resources/views/livewire/project/shared/terminal.blade.php'));
+
+ expect($terminalComponent)
+ ->toContain('public bool $isTerminalConnected = false;')
+ ->toContain("#[On('terminalConnected')]")
+ ->toContain('public function markTerminalConnected(): void')
+ ->toContain('public function keepTerminalPageAlive(): void')
+ ->and($terminalView)
+ ->toContain('@if ($isTerminalConnected)')
+ ->toContain('wire:poll.keep-alive.30s="keepTerminalPageAlive"');
});
it('replays the last command on reconnect so the PTY respawns automatically', function () {
diff --git a/tests/Feature/SshMultiplexingLockTest.php b/tests/Feature/SshMultiplexingLockTest.php
index e97be1f50..b914765f6 100644
--- a/tests/Feature/SshMultiplexingLockTest.php
+++ b/tests/Feature/SshMultiplexingLockTest.php
@@ -73,6 +73,19 @@ function makeMuxServer(): Server
Process::assertNothingRan();
});
+it('can generate terminal ssh commands without a hard command timeout', function () {
+ config(['constants.ssh.mux_enabled' => true]);
+ $server = makeMuxServer();
+ Storage::disk('ssh-keys')->put("ssh_key@{$server->privateKey->uuid}", $server->privateKey->private_key);
+
+ $command = SshMultiplexingHelper::generateSshCommand($server, 'echo ok', commandTimeout: 0);
+
+ expect($command)
+ ->toStartWith('ssh ')
+ ->not->toStartWith('timeout ')
+ ->not->toContain('timeout 3600 ssh');
+});
+
it('omits native multiplexing options when ssh multiplexing is disabled for a command', function () {
config(['constants.ssh.mux_enabled' => true]);
$server = makeMuxServer();
From b5416859e5b5147831fafcad2fe848b6049eb728 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Derdaele?=
Date: Wed, 27 May 2026 12:45:56 +0200
Subject: [PATCH 081/117] fix(templates): generate valid Garage RPC secret
Garage requires a 32 bytes secret (which is a 64 char hex string)
---
app/Models/Service.php | 3 ++-
templates/compose/garage.yaml | 2 +-
templates/service-templates-latest.json | 2 +-
templates/service-templates.json | 2 +-
4 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 11189b4ac..cc8074b74 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -778,7 +778,8 @@ public function extraFields()
}
$rpc_secret = $this->environment_variables()->where('key', 'GARAGE_RPC_SECRET')->first();
if (is_null($rpc_secret)) {
- $rpc_secret = $this->environment_variables()->where('key', 'SERVICE_HEX_32_RPCSECRET')->first();
+ $rpc_secret = $this->environment_variables()->where('key', 'SERVICE_HEX_64_RPCSECRET')->first()
+ ?? $this->environment_variables()->where('key', 'SERVICE_HEX_32_RPCSECRET')->first();
}
$metrics_token = $this->environment_variables()->where('key', 'GARAGE_METRICS_TOKEN')->first();
if (is_null($metrics_token)) {
diff --git a/templates/compose/garage.yaml b/templates/compose/garage.yaml
index 0c559cebd..e3292dffc 100644
--- a/templates/compose/garage.yaml
+++ b/templates/compose/garage.yaml
@@ -12,7 +12,7 @@ services:
- GARAGE_S3_API_URL=$GARAGE_S3_API_URL
- GARAGE_WEB_URL=$GARAGE_WEB_URL
- GARAGE_ADMIN_URL=$GARAGE_ADMIN_URL
- - GARAGE_RPC_SECRET=${SERVICE_HEX_32_RPCSECRET}
+ - GARAGE_RPC_SECRET=${SERVICE_HEX_64_RPCSECRET}
- GARAGE_ADMIN_TOKEN=$SERVICE_PASSWORD_GARAGE
- GARAGE_METRICS_TOKEN=$SERVICE_PASSWORD_GARAGEMETRICS
- GARAGE_ALLOW_WORLD_READABLE_SECRETS=true
diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json
index d1fb5e387..147c3729f 100644
--- a/templates/service-templates-latest.json
+++ b/templates/service-templates-latest.json
@@ -1620,7 +1620,7 @@
"garage": {
"documentation": "https://garagehq.deuxfleurs.fr/documentation/?utm_source=coolify.io",
"slogan": "Garage is an S3-compatible distributed object storage service designed for self-hosting.",
- "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF8zMl9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
+ "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF82NF9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
"tags": [
"object",
"storage",
diff --git a/templates/service-templates.json b/templates/service-templates.json
index 699b97e55..e320aef68 100644
--- a/templates/service-templates.json
+++ b/templates/service-templates.json
@@ -1620,7 +1620,7 @@
"garage": {
"documentation": "https://garagehq.deuxfleurs.fr/documentation/?utm_source=coolify.io",
"slogan": "Garage is an S3-compatible distributed object storage service designed for self-hosting.",
- "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF8zMl9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
+ "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF82NF9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
"tags": [
"object",
"storage",
From 164ff40f044f7a977cbf1c69c7a57a591f4cf08d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Derdaele?=
Date: Wed, 27 May 2026 19:46:29 +0200
Subject: [PATCH 082/117] revert changes to json files
---
templates/service-templates-latest.json | 2 +-
templates/service-templates.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json
index 147c3729f..d1fb5e387 100644
--- a/templates/service-templates-latest.json
+++ b/templates/service-templates-latest.json
@@ -1620,7 +1620,7 @@
"garage": {
"documentation": "https://garagehq.deuxfleurs.fr/documentation/?utm_source=coolify.io",
"slogan": "Garage is an S3-compatible distributed object storage service designed for self-hosting.",
- "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF82NF9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
+ "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF8zMl9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
"tags": [
"object",
"storage",
diff --git a/templates/service-templates.json b/templates/service-templates.json
index e320aef68..699b97e55 100644
--- a/templates/service-templates.json
+++ b/templates/service-templates.json
@@ -1620,7 +1620,7 @@
"garage": {
"documentation": "https://garagehq.deuxfleurs.fr/documentation/?utm_source=coolify.io",
"slogan": "Garage is an S3-compatible distributed object storage service designed for self-hosting.",
- "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF82NF9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
+ "compose": "c2VydmljZXM6CiAgZ2FyYWdlOgogICAgaW1hZ2U6ICdkeGZscnMvZ2FyYWdlOnYyLjEuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEdBUkFHRV9TM19BUElfVVJMPSRHQVJBR0VfUzNfQVBJX1VSTAogICAgICAtIEdBUkFHRV9XRUJfVVJMPSRHQVJBR0VfV0VCX1VSTAogICAgICAtIEdBUkFHRV9BRE1JTl9VUkw9JEdBUkFHRV9BRE1JTl9VUkwKICAgICAgLSAnR0FSQUdFX1JQQ19TRUNSRVQ9JHtTRVJWSUNFX0hFWF8zMl9SUENTRUNSRVR9JwogICAgICAtIEdBUkFHRV9BRE1JTl9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0UKICAgICAgLSBHQVJBR0VfTUVUUklDU19UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9HQVJBR0VNRVRSSUNTCiAgICAgIC0gR0FSQUdFX0FMTE9XX1dPUkxEX1JFQURBQkxFX1NFQ1JFVFM9dHJ1ZQogICAgICAtICdSVVNUX0xPRz0ke1JVU1RfTE9HOi1nYXJhZ2U9aW5mb30nCiAgICB2b2x1bWVzOgogICAgICAtICdnYXJhZ2UtbWV0YTovdmFyL2xpYi9nYXJhZ2UvbWV0YScKICAgICAgLSAnZ2FyYWdlLWRhdGE6L3Zhci9saWIvZ2FyYWdlL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2dhcmFnZS50b21sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2dhcmFnZS50b21sCiAgICAgICAgY29udGVudDogIm1ldGFkYXRhX2RpciA9IFwiL3Zhci9saWIvZ2FyYWdlL21ldGFcIlxuZGF0YV9kaXIgPSBcIi92YXIvbGliL2dhcmFnZS9kYXRhXCJcbmRiX2VuZ2luZSA9IFwibG1kYlwiXG5cbnJlcGxpY2F0aW9uX2ZhY3RvciA9IDFcbmNvbnNpc3RlbmN5X21vZGUgPSBcImNvbnNpc3RlbnRcIlxuXG5jb21wcmVzc2lvbl9sZXZlbCA9IDFcbmJsb2NrX3NpemUgPSBcIjFNXCJcblxucnBjX2JpbmRfYWRkciA9IFwiWzo6XTozOTAxXCJcbnJwY19zZWNyZXRfZmlsZSA9IFwiZW52OkdBUkFHRV9SUENfU0VDUkVUXCJcbmJvb3RzdHJhcF9wZWVycyA9IFtdXG5cbltzM19hcGldXG5zM19yZWdpb24gPSBcImdhcmFnZVwiXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDBcIlxucm9vdF9kb21haW4gPSBcIi5zMy5nYXJhZ2UubG9jYWxob3N0XCJcblxuW3MzX3dlYl1cbmJpbmRfYWRkciA9IFwiWzo6XTozOTAyXCJcbnJvb3RfZG9tYWluID0gXCIud2ViLmdhcmFnZS5sb2NhbGhvc3RcIlxuXG5bYWRtaW5dXG5hcGlfYmluZF9hZGRyID0gXCJbOjpdOjM5MDNcIlxuYWRtaW5fdG9rZW5fZmlsZSA9IFwiZW52OkdBUkFHRV9BRE1JTl9UT0tFTlwiXG5tZXRyaWNzX3Rva2VuX2ZpbGUgPSBcImVudjpHQVJBR0VfTUVUUklDU19UT0tFTlwiIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9nYXJhZ2UKICAgICAgICAtIHN0YXRzCiAgICAgICAgLSAnLWEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
"tags": [
"object",
"storage",
From 5b0be9798ed0776e8d147445fa9d0c01b4daa283 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 1 Jun 2026 06:55:30 +0200
Subject: [PATCH 083/117] chore(database): rename health checks route to
healthcheck
---
.../views/livewire/project/database/configuration.blade.php | 4 ++--
resources/views/livewire/project/database/health.blade.php | 2 +-
routes/web.php | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/resources/views/livewire/project/database/configuration.blade.php b/resources/views/livewire/project/database/configuration.blade.php
index c58232200..d6aac7f30 100644
--- a/resources/views/livewire/project/database/configuration.blade.php
+++ b/resources/views/livewire/project/database/configuration.blade.php
@@ -16,7 +16,7 @@
+ href="{{ route('project.database.healthcheck', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">
@can('update', $database)
@@ -59,7 +59,7 @@
@elseif ($currentRoute === 'project.database.persistent-storage')
- @elseif ($currentRoute === 'project.database.health-checks')
+ @elseif ($currentRoute === 'project.database.healthcheck')
@elseif ($currentRoute === 'project.database.import-backup')
diff --git a/resources/views/livewire/project/database/health.blade.php b/resources/views/livewire/project/database/health.blade.php
index 2e70f79b2..d869defc1 100644
--- a/resources/views/livewire/project/database/health.blade.php
+++ b/resources/views/livewire/project/database/health.blade.php
@@ -1,6 +1,6 @@
diff --git a/resources/views/livewire/project/shared/health-checks.blade.php b/resources/views/livewire/project/shared/health-checks.blade.php
index 8662b0b50..ac2063c2e 100644
--- a/resources/views/livewire/project/shared/health-checks.blade.php
+++ b/resources/views/livewire/project/shared/health-checks.blade.php
@@ -1,6 +1,6 @@