From 596cb3984988d55cea5b430d967d8d737bd03f95 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 15 Sep 2026 10:14:03 +0200
Subject: [PATCH 1/6] chore(traefik): update supported image versions
---
other/nightly/versions.json | 6 +++---
versions.json | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/other/nightly/versions.json b/other/nightly/versions.json
index 6818286b29..8ae9763c72 100644
--- a/other/nightly/versions.json
+++ b/other/nightly/versions.json
@@ -17,14 +17,14 @@
}
},
"traefik": {
- "v3.7": "3.7.8",
- "v3.6": "3.6.23",
+ "v3.7": "3.7.13",
+ "v3.6": "3.6.25",
"v3.5": "3.5.6",
"v3.4": "3.4.5",
"v3.3": "3.3.7",
"v3.2": "3.2.5",
"v3.1": "3.1.7",
"v3.0": "3.0.4",
- "v2.11": "2.11.52"
+ "v2.11": "2.11.57"
}
}
diff --git a/versions.json b/versions.json
index 6818286b29..8ae9763c72 100644
--- a/versions.json
+++ b/versions.json
@@ -17,14 +17,14 @@
}
},
"traefik": {
- "v3.7": "3.7.8",
- "v3.6": "3.6.23",
+ "v3.7": "3.7.13",
+ "v3.6": "3.6.25",
"v3.5": "3.5.6",
"v3.4": "3.4.5",
"v3.3": "3.3.7",
"v3.2": "3.2.5",
"v3.1": "3.1.7",
"v3.0": "3.0.4",
- "v2.11": "2.11.52"
+ "v2.11": "2.11.57"
}
}
From 776c56f9471c5d6ed46c5aa13f4d7c7f195edd62 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 15 Sep 2026 11:38:33 +0200
Subject: [PATCH 2/6] fix(helper): hide deployment details unless explicitly
requested
---
bootstrap/helpers/shared.php | 98 +++++++++++----------
tests/Unit/RunningDeploymentsOutputTest.php | 38 ++++++++
2 files changed, 91 insertions(+), 45 deletions(-)
create mode 100644 tests/Unit/RunningDeploymentsOutputTest.php
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index f40c51a335..a7a606330e 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -2400,7 +2400,7 @@ function get_public_ips()
}
}
-function isAnyDeploymentInprogress()
+function isAnyDeploymentInprogress(bool $showAll = false)
{
$runningJobs = ApplicationDeploymentQueue::where('horizon_job_worker', gethostname())->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)->get();
@@ -2417,34 +2417,31 @@ function isAnyDeploymentInprogress()
if ($horizonJobStatus === 'unknown' || $horizonJobStatus === 'reserved') {
$horizonJobIds[] = $runningJob->horizon_job_id;
- // Get application and team information
- $application = Application::find($runningJob->application_id);
- $teamMembers = [];
- $deploymentUrl = '';
+ if ($showAll) {
+ $application = Application::find($runningJob->application_id);
+ $teamMembers = [];
+ $deploymentUrl = '';
- if ($application) {
- // Get team members through the application's project
- $team = $application->team();
- if ($team) {
- $teamMembers = $team->members()->pluck('email')->toArray();
+ if ($application) {
+ $team = $application->team();
+ if ($team) {
+ $teamMembers = $team->members()->pluck('email')->toArray();
+ }
+
+ if ($runningJob->deployment_url) {
+ $deploymentUrl = base_url().$runningJob->deployment_url;
+ }
}
- // Construct the full deployment URL
- if ($runningJob->deployment_url) {
- $baseUrl = base_url();
- $deploymentUrl = $baseUrl.$runningJob->deployment_url;
- }
+ $deploymentDetails[] = [
+ 'application_name' => $runningJob->application_name ?? 'Unknown',
+ 'server_name' => $runningJob->server_name ?? 'Unknown',
+ 'deployment_url' => $deploymentUrl,
+ 'team_members' => $teamMembers,
+ 'created_at' => $runningJob->created_at->format('Y-m-d H:i:s'),
+ 'horizon_job_id' => $runningJob->horizon_job_id,
+ ];
}
-
- $deploymentDetails[] = [
- 'id' => $runningJob->id,
- 'application_name' => $runningJob->application_name ?? 'Unknown',
- 'server_name' => $runningJob->server_name ?? 'Unknown',
- 'deployment_url' => $deploymentUrl,
- 'team_members' => $teamMembers,
- 'created_at' => $runningJob->created_at->format('Y-m-d H:i:s'),
- 'horizon_job_id' => $runningJob->horizon_job_id,
- ];
}
}
@@ -2453,30 +2450,41 @@ function isAnyDeploymentInprogress()
exit(0);
}
- // Display enhanced deployment information
- echo "\n=== Running Deployments ===\n";
- echo 'Total active deployments: '.count($horizonJobIds)."\n\n";
-
- foreach ($deploymentDetails as $index => $deployment) {
- echo 'Deployment #'.($index + 1).":\n";
- echo ' Application: '.$deployment['application_name']."\n";
- echo ' Server: '.$deployment['server_name']."\n";
- echo ' Started: '.$deployment['created_at']."\n";
- if ($deployment['deployment_url']) {
- echo ' URL: '.$deployment['deployment_url']."\n";
- }
- if (! empty($deployment['team_members'])) {
- echo ' Team members: '.implode(', ', $deployment['team_members'])."\n";
- } else {
- echo " Team members: No team members found\n";
- }
- echo ' Horizon Job ID: '.$deployment['horizon_job_id']."\n";
- echo "\n";
- }
+ echo formatRunningDeploymentsOutput(count($horizonJobIds), $deploymentDetails, $showAll);
exit(1);
}
+function formatRunningDeploymentsOutput(int $activeDeploymentCount, array $deploymentDetails = [], bool $showAll = false): string
+{
+ $output = "\n=== Running Deployments ===\n";
+ $output .= 'Total active deployments: '.$activeDeploymentCount."\n";
+
+ if (! $showAll) {
+ return $output;
+ }
+
+ $output .= "\n";
+
+ foreach ($deploymentDetails as $index => $deployment) {
+ $output .= 'Deployment #'.($index + 1).":\n";
+ $output .= ' Application: '.$deployment['application_name']."\n";
+ $output .= ' Server: '.$deployment['server_name']."\n";
+ $output .= ' Started: '.$deployment['created_at']."\n";
+ if ($deployment['deployment_url']) {
+ $output .= ' URL: '.$deployment['deployment_url']."\n";
+ }
+ if (! empty($deployment['team_members'])) {
+ $output .= ' Team members: '.implode(', ', $deployment['team_members'])."\n";
+ } else {
+ $output .= " Team members: No team members found\n";
+ }
+ $output .= ' Horizon Job ID: '.$deployment['horizon_job_id']."\n\n";
+ }
+
+ return $output;
+}
+
function isBase64Encoded($strValue)
{
return base64_encode(base64_decode($strValue, true)) === $strValue;
diff --git a/tests/Unit/RunningDeploymentsOutputTest.php b/tests/Unit/RunningDeploymentsOutputTest.php
new file mode 100644
index 0000000000..62d367514c
--- /dev/null
+++ b/tests/Unit/RunningDeploymentsOutputTest.php
@@ -0,0 +1,38 @@
+ 'secret-application',
+ 'server_name' => 'secret-server',
+ 'deployment_url' => 'https://example.com/secret-deployment',
+ 'team_members' => ['member@example.com'],
+ 'created_at' => '2026-09-15 08:52:42',
+ 'horizon_job_id' => 'secret-job-id',
+ ]]);
+
+ expect($output)
+ ->toBe("\n=== Running Deployments ===\nTotal active deployments: 1\n")
+ ->not->toContain('secret-application')
+ ->not->toContain('https://example.com/secret-deployment')
+ ->not->toContain('member@example.com');
+});
+
+it('shows all deployment details when requested', function () {
+ $output = formatRunningDeploymentsOutput(1, [[
+ 'application_name' => 'example-application',
+ 'server_name' => 'example-server',
+ 'deployment_url' => 'https://example.com/deployment',
+ 'team_members' => ['member@example.com'],
+ 'created_at' => '2026-09-15 08:52:42',
+ 'horizon_job_id' => 'example-job-id',
+ ]], true);
+
+ expect($output)
+ ->toContain('Deployment #1:')
+ ->toContain('Application: example-application')
+ ->toContain('Server: example-server')
+ ->toContain('Started: 2026-09-15 08:52:42')
+ ->toContain('URL: https://example.com/deployment')
+ ->toContain('Team members: member@example.com')
+ ->toContain('Horizon Job ID: example-job-id');
+});
From 8ab54da2ac56c5cf69c547549f501d3a5a655eda Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 15 Sep 2026 12:28:16 +0200
Subject: [PATCH 3/6] style(ui): remove description placeholders and normalize
control heights
---
resources/css/utilities.css | 2 +-
resources/views/livewire/dashboard.blade.php | 4 ++--
resources/views/livewire/project/index.blade.php | 2 +-
resources/views/livewire/server/index.blade.php | 2 +-
.../livewire/server/private-key/show.blade.php | 2 +-
.../shared-variables/environment/index.blade.php | 4 ++--
.../shared-variables/project/index.blade.php | 4 ++--
tests/Feature/FormControlHeightTest.php | 9 +++++----
tests/Unit/DescriptionPlaceholderViewTest.php | 16 ++++++++++++++++
9 files changed, 31 insertions(+), 14 deletions(-)
create mode 100644 tests/Unit/DescriptionPlaceholderViewTest.php
diff --git a/resources/css/utilities.css b/resources/css/utilities.css
index 9851180c2b..67d898da3a 100644
--- a/resources/css/utilities.css
+++ b/resources/css/utilities.css
@@ -59,7 +59,7 @@
/* input, select before */
@utility input-select {
- @apply block h-9 px-3 py-1.5 w-full text-sm text-black rounded-md border border-neutral-200 bg-[var(--coollabs-recessed)] dark:text-fg dark:border-white/[0.08] transition-colors disabled:bg-neutral-100 disabled:text-neutral-400 dark:disabled:bg-white/[0.03] dark:disabled:text-fg-faint;
+ @apply block h-8 px-3 py-1.5 w-full text-sm text-black rounded-md border border-neutral-200 bg-[var(--coollabs-recessed)] dark:text-fg dark:border-white/[0.08] transition-colors disabled:bg-neutral-100 disabled:text-neutral-400 dark:disabled:bg-white/[0.03] dark:disabled:text-fg-faint;
box-shadow: none;
&:where(.dark, .dark *) {
diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php
index f4bf96fbb7..d9eb201c5d 100644
--- a/resources/views/livewire/dashboard.blade.php
+++ b/resources/views/livewire/dashboard.blade.php
@@ -66,7 +66,7 @@
{{ $project->name }}
- {{ $project->description ?: 'No description' }}
+ {{ $project->description }}
@@ -181,7 +181,7 @@
{{ $server->name }}
- {{ $server->description ?: 'No description' }}
+ {{ $server->description }}
@if ($serverStatusType !== 'success')
diff --git a/resources/views/livewire/project/index.blade.php b/resources/views/livewire/project/index.blade.php
index 4cd0e22d19..108b4024ac 100644
--- a/resources/views/livewire/project/index.blade.php
+++ b/resources/views/livewire/project/index.blade.php
@@ -121,7 +121,7 @@
class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg"
x-text="project.name">
+ x-text="project.description || ''">
diff --git a/resources/views/livewire/server/index.blade.php b/resources/views/livewire/server/index.blade.php
index 9f5680160e..52a0c3a990 100644
--- a/resources/views/livewire/server/index.blade.php
+++ b/resources/views/livewire/server/index.blade.php
@@ -55,7 +55,7 @@
return [
'uuid' => $server->uuid,
'name' => $server->name,
- 'description' => $server->description ?: 'No description',
+ 'description' => $server->description,
'href' => route('server.show', ['server_uuid' => $server->uuid]),
'status' => $status,
'statusType' => $statusType,
diff --git a/resources/views/livewire/server/private-key/show.blade.php b/resources/views/livewire/server/private-key/show.blade.php
index bf938a888d..0663dd1168 100644
--- a/resources/views/livewire/server/private-key/show.blade.php
+++ b/resources/views/livewire/server/private-key/show.blade.php
@@ -78,7 +78,7 @@
@endif
- {{ $privateKey->description ?: 'No description provided.' }}
+ {{ $privateKey->description }}
diff --git a/resources/views/livewire/shared-variables/environment/index.blade.php b/resources/views/livewire/shared-variables/environment/index.blade.php
index 15c6dd461d..645f2e61be 100644
--- a/resources/views/livewire/shared-variables/environment/index.blade.php
+++ b/resources/views/livewire/shared-variables/environment/index.blade.php
@@ -33,7 +33,7 @@
href="{{ route('shared-variables.environment.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}" {{ wireNavigate() }}>
-
{{ $environment->name }}
{{ $environment->description ?: 'No description' }}
+
{{ $environment->name }}
{{ $environment->description }}
@endforeach
@@ -50,7 +50,7 @@
href="{{ route('shared-variables.environment.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}" {{ wireNavigate() }}
class="flex min-h-14 items-center gap-3 border-b border-neutral-200 px-4 py-2.5 last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
- {{ $environment->name }}
{{ $environment->description ?: 'No description' }}
+ {{ $environment->name }}
{{ $environment->description }}
{{ $project->name }}
@endforeach
diff --git a/resources/views/livewire/shared-variables/project/index.blade.php b/resources/views/livewire/shared-variables/project/index.blade.php
index 71fd497b66..d19fc39f5e 100644
--- a/resources/views/livewire/shared-variables/project/index.blade.php
+++ b/resources/views/livewire/shared-variables/project/index.blade.php
@@ -33,7 +33,7 @@
{{ $project->name }}
-
{{ $project->description ?: 'No description' }}
+
{{ $project->description }}
@@ -54,7 +54,7 @@
@else
@endif
-
{{ $project->name }}
{{ $project->description ?: 'No description' }}
+
{{ $project->name }}
{{ $project->description }}
{{ $project->environment_variables()->count() }} {{ Str::plural('variable', $project->environment_variables()->count()) }}
@endforeach
diff --git a/tests/Feature/FormControlHeightTest.php b/tests/Feature/FormControlHeightTest.php
index c1a073b480..589112b5bf 100644
--- a/tests/Feature/FormControlHeightTest.php
+++ b/tests/Feature/FormControlHeightTest.php
@@ -11,12 +11,13 @@ test('default form buttons and inputs share the same control height', function (
preg_match('/@utility button \{[^}]*\}/s', $utilities, $button);
expect($inputSelect[0] ?? '')
- ->toContain('h-9')
- ->and($button[0] ?? '')->toContain('h-9')
- ->and($button[0] ?? '')->toContain('min-h-9')
+ ->toContain('h-8')
+ ->not->toContain('h-9')
+ ->and($button[0] ?? '')->toContain('h-8')
+ ->and($button[0] ?? '')->toContain('min-h-8')
->and($button[0] ?? '')->toContain('whitespace-nowrap')
->and($button[0] ?? '')->toContain('shrink-0')
- ->and($button[0] ?? '')->not->toContain('h-8');
+ ->and($button[0] ?? '')->not->toContain('h-9');
});
test('settings form surfaces keep input and button heights equal', function () {
diff --git a/tests/Unit/DescriptionPlaceholderViewTest.php b/tests/Unit/DescriptionPlaceholderViewTest.php
new file mode 100644
index 0000000000..6dc733c0fc
--- /dev/null
+++ b/tests/Unit/DescriptionPlaceholderViewTest.php
@@ -0,0 +1,16 @@
+isFile() && $view->getExtension() === 'php') {
+ $viewContents .= file_get_contents($view->getPathname());
+ }
+ }
+
+ expect($viewContents)->not->toContain('No description');
+});
From ab30f95ad9d5cc118e656a31e5eea6560c53363f Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 15 Sep 2026 12:58:59 +0200
Subject: [PATCH 4/6] feat(restarts): make resource restart limits opt-in
Default restart limits to zero for applications, previews, and services, and bump the Coolify version to 4.3.21.
---
app/Livewire/Project/Application/Advanced.php | 4 +--
app/Livewire/Project/Service/Index.php | 4 +--
app/Models/Application.php | 4 +++
app/Models/ApplicationPreview.php | 4 +++
app/Models/ServiceApplication.php | 1 +
config/constants.php | 2 +-
...9_15_105627_make_restart_limits_opt_in.php | 32 +++++++++++++++++++
other/nightly/versions.json | 2 +-
.../Feature/AllResourceRestartLimitsTest.php | 28 ++++++++++++++++
tests/Unit/ProductionImageWorkflowTest.php | 4 +--
versions.json | 2 +-
11 files changed, 78 insertions(+), 9 deletions(-)
create mode 100644 database/migrations/2026_09_15_105627_make_restart_limits_opt_in.php
diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php
index a9e1c0be28..89d2011dcf 100644
--- a/app/Livewire/Project/Application/Advanced.php
+++ b/app/Livewire/Project/Application/Advanced.php
@@ -82,7 +82,7 @@ class Advanced extends Component
public bool $isConnectToDockerNetworkEnabled = false;
#[Validate(['integer', 'min:0'])]
- public int $maxRestartCount = 10;
+ public int $maxRestartCount = 0;
public function mount()
{
@@ -142,7 +142,7 @@ class Advanced extends Component
$this->disableBuildCache = $this->application->settings->disable_build_cache;
$this->injectBuildArgsToDockerfile = $this->application->settings->inject_build_args_to_dockerfile ?? true;
$this->includeSourceCommitInBuild = $this->application->settings->include_source_commit_in_build ?? false;
- $this->maxRestartCount = $this->application->max_restart_count ?? 10;
+ $this->maxRestartCount = $this->application->max_restart_count ?? 0;
}
// Load stop_grace_period separately since it has its own save handler
diff --git a/app/Livewire/Project/Service/Index.php b/app/Livewire/Project/Service/Index.php
index c73e8bb06e..f4b6bec2d4 100644
--- a/app/Livewire/Project/Service/Index.php
+++ b/app/Livewire/Project/Service/Index.php
@@ -84,7 +84,7 @@ class Index extends Component
public bool $isStripprefixEnabled = false;
- public mixed $maxRestartCount = 10;
+ public mixed $maxRestartCount = 0;
protected $listeners = ['generateDockerCompose', 'refreshScheduledBackups' => '$refresh', 'refreshFileStorages'];
@@ -432,7 +432,7 @@ class Index extends Component
$this->isLogDrainEnabled = data_get($this->serviceApplication, 'is_log_drain_enabled', false);
$this->isGzipEnabled = data_get($this->serviceApplication, 'is_gzip_enabled', true);
$this->isStripprefixEnabled = data_get($this->serviceApplication, 'is_stripprefix_enabled', true);
- $this->maxRestartCount = $this->serviceApplication->max_restart_count ?? 10;
+ $this->maxRestartCount = $this->serviceApplication->max_restart_count ?? 0;
}
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 38b8c5b0e2..14adbcef38 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -135,6 +135,10 @@ class Application extends BaseModel
private static $parserVersion = '5';
+ protected $attributes = [
+ 'max_restart_count' => 0,
+ ];
+
protected $fillable = [
'name',
'description',
diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php
index bffbdab621..d15142b6ad 100644
--- a/app/Models/ApplicationPreview.php
+++ b/app/Models/ApplicationPreview.php
@@ -13,6 +13,10 @@ class ApplicationPreview extends BaseModel
{
use HasRestartLimit, SoftDeletes;
+ protected $attributes = [
+ 'max_restart_count' => 0,
+ ];
+
protected $fillable = [
'uuid',
'application_id',
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
index cba18c1f23..066a86fac8 100644
--- a/app/Models/ServiceApplication.php
+++ b/app/Models/ServiceApplication.php
@@ -54,6 +54,7 @@ class ServiceApplication extends BaseModel
protected $attributes = [
'is_force_https_enabled' => true,
+ 'max_restart_count' => 0,
];
protected function casts(): array
diff --git a/config/constants.php b/config/constants.php
index 3f8335134f..6a37550e4f 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,7 +2,7 @@
return [
'coolify' => [
- 'version' => env('COOLIFY_VERSION') ?: '4.3.20',
+ 'version' => env('COOLIFY_VERSION') ?: '4.3.21',
'helper_version' => '1.0.17',
'realtime_version' => '1.0.19',
'railpack_version' => '0.23.0',
diff --git a/database/migrations/2026_09_15_105627_make_restart_limits_opt_in.php b/database/migrations/2026_09_15_105627_make_restart_limits_opt_in.php
new file mode 100644
index 0000000000..89d3efb333
--- /dev/null
+++ b/database/migrations/2026_09_15_105627_make_restart_limits_opt_in.php
@@ -0,0 +1,32 @@
+integer('max_restart_count')->default(0)->change();
+ });
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ foreach (['applications', 'application_previews', 'service_applications'] as $tableName) {
+ Schema::table($tableName, function (Blueprint $table) {
+ $table->integer('max_restart_count')->default(10)->change();
+ });
+ }
+ }
+};
diff --git a/other/nightly/versions.json b/other/nightly/versions.json
index 8ae9763c72..e010c30695 100644
--- a/other/nightly/versions.json
+++ b/other/nightly/versions.json
@@ -1,7 +1,7 @@
{
"coolify": {
"v4": {
- "version": "4.3.20"
+ "version": "4.3.21"
},
"nightly": {
"version": "4.4-rc.1"
diff --git a/tests/Feature/AllResourceRestartLimitsTest.php b/tests/Feature/AllResourceRestartLimitsTest.php
index 7da82f7d65..62594fc8b6 100644
--- a/tests/Feature/AllResourceRestartLimitsTest.php
+++ b/tests/Feature/AllResourceRestartLimitsTest.php
@@ -3,6 +3,7 @@
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Service\StopServiceApplication;
use App\Jobs\PushServerUpdateJob;
+use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
@@ -209,6 +210,33 @@ it('limits restarts only for applications', function () {
->toContain('$resetRestartCount && $serviceApplication instanceof ServiceApplication');
});
+it('makes restart limits opt in for new application resources', function () {
+ $migrationPaths = glob(database_path('migrations/*_make_restart_limits_opt_in.php'));
+
+ expect($migrationPaths)->toHaveCount(1);
+
+ $migration = file_get_contents($migrationPaths[0]);
+
+ expect($migration)
+ ->toContain("['applications', 'application_previews', 'service_applications']")
+ ->toContain("integer('max_restart_count')->default(0)->change()")
+ ->not->toContain('->update(');
+
+ $applicationSettings = file_get_contents(app_path('Livewire/Project/Application/Advanced.php'));
+ $serviceSettings = file_get_contents(app_path('Livewire/Project/Service/Index.php'));
+
+ expect($applicationSettings)
+ ->toContain('public int $maxRestartCount = 0;')
+ ->toContain('$this->application->max_restart_count ?? 0')
+ ->and($serviceSettings)
+ ->toContain('public mixed $maxRestartCount = 0;')
+ ->toContain('$this->serviceApplication->max_restart_count ?? 0');
+
+ foreach ([Application::class, ApplicationPreview::class, ServiceApplication::class] as $modelClass) {
+ expect((new $modelClass)->max_restart_count)->toBe(0);
+ }
+});
+
it('does not render restart limit warnings for service databases', function () {
$html = Blade::render(
'
',
diff --git a/tests/Unit/ProductionImageWorkflowTest.php b/tests/Unit/ProductionImageWorkflowTest.php
index 8d64d5b12a..588b405310 100644
--- a/tests/Unit/ProductionImageWorkflowTest.php
+++ b/tests/Unit/ProductionImageWorkflowTest.php
@@ -23,8 +23,8 @@ it('publishes v4 branch builds under the commit sha with a traceable internal ve
->toContain('ARG COOLIFY_VERSION')
->toContain('ENV COOLIFY_VERSION=${COOLIFY_VERSION}')
->and($constants)
- ->toContain("'version' => env('COOLIFY_VERSION') ?: '4.3.19'")
- ->and($versions['coolify']['v4']['version'])->toBe('4.3.19')
+ ->toContain("'version' => env('COOLIFY_VERSION') ?: '4.3.21'")
+ ->and($versions['coolify']['v4']['version'])->toBe('4.3.21')
->and($versions['coolify']['nightly']['version'])->toBe('4.4-rc.1')
->and($nightlyVersions)->toBe($versions);
});
diff --git a/versions.json b/versions.json
index 8ae9763c72..e010c30695 100644
--- a/versions.json
+++ b/versions.json
@@ -1,7 +1,7 @@
{
"coolify": {
"v4": {
- "version": "4.3.20"
+ "version": "4.3.21"
},
"nightly": {
"version": "4.4-rc.1"
From 113a2f229d7fa2391119d9acf149e9b0b70382f5 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 15 Sep 2026 13:28:50 +0200
Subject: [PATCH 5/6] fix(restarts): reset legacy restart limits during
migration
Normalize existing resources with the old restart limit defaults so restart limits remain opt-in.
---
...26_09_15_105627_make_restart_limits_opt_in.php | 15 +++++++++++++++
tests/Feature/AllResourceRestartLimitsTest.php | 7 ++++++-
2 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/database/migrations/2026_09_15_105627_make_restart_limits_opt_in.php b/database/migrations/2026_09_15_105627_make_restart_limits_opt_in.php
index 89d3efb333..9c1c927c01 100644
--- a/database/migrations/2026_09_15_105627_make_restart_limits_opt_in.php
+++ b/database/migrations/2026_09_15_105627_make_restart_limits_opt_in.php
@@ -2,10 +2,13 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
+ public $withinTransaction = false;
+
/**
* Run the migrations.
*/
@@ -15,6 +18,18 @@ return new class extends Migration
Schema::table($tableName, function (Blueprint $table) {
$table->integer('max_restart_count')->default(0)->change();
});
+
+ DB::table($tableName)
+ ->select('id')
+ ->where('max_restart_count', 10)
+ ->chunkById(5000, function ($resources) use ($tableName): void {
+ DB::table($tableName)
+ ->whereIn('id', $resources->pluck('id'))
+ ->update([
+ 'max_restart_count' => 0,
+ 'restart_limit_reached' => false,
+ ]);
+ });
}
}
diff --git a/tests/Feature/AllResourceRestartLimitsTest.php b/tests/Feature/AllResourceRestartLimitsTest.php
index 62594fc8b6..d1ff39d8ee 100644
--- a/tests/Feature/AllResourceRestartLimitsTest.php
+++ b/tests/Feature/AllResourceRestartLimitsTest.php
@@ -220,7 +220,12 @@ it('makes restart limits opt in for new application resources', function () {
expect($migration)
->toContain("['applications', 'application_previews', 'service_applications']")
->toContain("integer('max_restart_count')->default(0)->change()")
- ->not->toContain('->update(');
+ ->toContain('public $withinTransaction = false;')
+ ->toContain("->where('max_restart_count', 10)")
+ ->toContain('->chunkById(5000')
+ ->toContain("->whereIn('id', \$resources->pluck('id'))")
+ ->toContain("'max_restart_count' => 0")
+ ->toContain("'restart_limit_reached' => false");
$applicationSettings = file_get_contents(app_path('Livewire/Project/Application/Advanced.php'));
$serviceSettings = file_get_contents(app_path('Livewire/Project/Service/Index.php'));
From ede9d8417a200198eb101bdc82afe35b2728ba52 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 15 Sep 2026 23:00:45 +0200
Subject: [PATCH 6/6] build(minio): use packaged mx client in development
images
---
docker-compose-maxio.dev.yml | 4 ++--
docker-compose.dev-multi.yml | 2 +-
docker-compose.dev.yml | 2 +-
docker/development/Dockerfile | 2 +-
tests/Unit/MinioClientPackagingTest.php | 22 ++++++++++++++--------
5 files changed, 19 insertions(+), 13 deletions(-)
diff --git a/docker-compose-maxio.dev.yml b/docker-compose-maxio.dev.yml
index b429244166..885d17f3c8 100644
--- a/docker-compose-maxio.dev.yml
+++ b/docker-compose-maxio.dev.yml
@@ -162,7 +162,7 @@ services:
networks:
- coolify
# maxio-init:
- # image: quay.io/minio/aistor/mc:RELEASE.2026-09-06T02-44-40Z
+ # image: ghcr.io/coollabsio/mx:0.1.0
# pull_policy: always
# container_name: coolify-maxio-init
# restart: no
@@ -182,7 +182,7 @@ services:
# networks:
# - coolify
minio-init:
- image: quay.io/minio/aistor/mc:RELEASE.2026-09-06T02-44-40Z
+ image: ghcr.io/coollabsio/mx:0.1.0
pull_policy: always
container_name: coolify-minio-init
restart: no
diff --git a/docker-compose.dev-multi.yml b/docker-compose.dev-multi.yml
index 31908436f4..c8f2833117 100644
--- a/docker-compose.dev-multi.yml
+++ b/docker-compose.dev-multi.yml
@@ -195,7 +195,7 @@ services:
minio-init:
profiles: ["minio"]
- image: quay.io/minio/aistor/mc:RELEASE.2026-09-06T02-44-40Z
+ image: ghcr.io/coollabsio/mx:0.1.0
pull_policy: always
restart: "no"
depends_on:
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 869a059c94..d33d9aab5c 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -150,7 +150,7 @@ services:
networks:
- coolify
minio-init:
- image: quay.io/minio/aistor/mc:RELEASE.2026-09-06T02-44-40Z
+ image: ghcr.io/coollabsio/mx:0.1.0
pull_policy: always
container_name: coolify-minio-init
restart: no
diff --git a/docker/development/Dockerfile b/docker/development/Dockerfile
index e0df74b220..62bb0ee241 100644
--- a/docker/development/Dockerfile
+++ b/docker/development/Dockerfile
@@ -12,7 +12,7 @@ ARG NGINX_VERSION=1.31.2-r1
# =================================================================
# Get MinIO client
# =================================================================
-FROM quay.io/minio/aistor/mc:RELEASE.2026-09-06T02-44-40Z AS minio-client
+FROM ghcr.io/coollabsio/mx:0.1.0 AS minio-client
# =================================================================
# Final Stage: Production image
diff --git a/tests/Unit/MinioClientPackagingTest.php b/tests/Unit/MinioClientPackagingTest.php
index d5e029601b..569759a11f 100644
--- a/tests/Unit/MinioClientPackagingTest.php
+++ b/tests/Unit/MinioClientPackagingTest.php
@@ -1,21 +1,27 @@
toContain('quay.io/minio/aistor/mc:RELEASE.2026-09-06T02-44-40Z')
- ->not->toContain('quay.io/minio/aistor/mc:latest')
+ ->toContain('ghcr.io/coollabsio/mx:0.1.0')
+ ->not->toContain('quay.io/minio/aistor/mc:')
->not->toContain('minio/mc:');
}
});
+
+it('keeps the mc command path when packaging mx', function () {
+ $dockerfile = file_get_contents(dirname(__DIR__, 2).'/docker/development/Dockerfile');
+
+ expect($dockerfile)
+ ->toContain('COPY --from=minio-client /usr/bin/mc /usr/bin/mc')
+ ->not->toContain('COPY --from=minio-client /usr/bin/mx');
+});