From 9b07be2d68af39e46c131a7d36bf4c0b56edd4c3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:36:46 +0200 Subject: [PATCH 01/11] chore(helper): bump helper version to 1.0.17 --- config/constants.php | 2 +- other/nightly/versions.json | 2 +- versions.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/constants.php b/config/constants.php index 7e649668d8..a69b697f30 100644 --- a/config/constants.php +++ b/config/constants.php @@ -3,7 +3,7 @@ return [ 'coolify' => [ 'version' => env('COOLIFY_VERSION') ?: '4.3.19', - 'helper_version' => '1.0.16', + 'helper_version' => '1.0.17', 'realtime_version' => '1.0.19', 'railpack_version' => '0.23.0', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index d426bdb007..50e20a4066 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -7,7 +7,7 @@ "version": "4.4-rc.1" }, "helper": { - "version": "1.0.16" + "version": "1.0.17" }, "realtime": { "version": "1.0.18" diff --git a/versions.json b/versions.json index d426bdb007..50e20a4066 100644 --- a/versions.json +++ b/versions.json @@ -7,7 +7,7 @@ "version": "4.4-rc.1" }, "helper": { - "version": "1.0.16" + "version": "1.0.17" }, "realtime": { "version": "1.0.18" From 22fbbd74ffc18f152302ebf3649a0d74385b9d9f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:43:52 +0200 Subject: [PATCH 02/11] fix(sentinel): restore hourly version checks for enabled servers Dispatch Sentinel checks hourly for eligible servers, including healthy installations, while continuing to exclude build servers. --- app/Jobs/ServerManagerJob.php | 6 ++++++ .../Server/SentinelUpdateCheckIndependenceTest.php | 12 ++++++------ tests/Unit/ServerManagerJobSentinelCheckTest.php | 7 ++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app/Jobs/ServerManagerJob.php b/app/Jobs/ServerManagerJob.php index 171d4e6949..20b60b9fd2 100644 --- a/app/Jobs/ServerManagerJob.php +++ b/app/Jobs/ServerManagerJob.php @@ -166,6 +166,12 @@ class ServerManagerJob implements ShouldBeEncrypted, ShouldQueue } } + if ($server->isSentinelEnabled() + && shouldRunCronNow('0 * * * *', $serverTimezone, "sentinel-version-check:{$server->id}", $this->executionTime) + ) { + CheckAndStartSentinelJob::dispatch($server); + } + // Dispatch ServerStorageCheckJob if due (only when Sentinel is out of sync or disabled) // When Sentinel is active, PushServerUpdateJob handles storage checks with real-time data if ($sentinelOutOfSync) { diff --git a/tests/Feature/Server/SentinelUpdateCheckIndependenceTest.php b/tests/Feature/Server/SentinelUpdateCheckIndependenceTest.php index 080a3ee7c8..d3ccb5c478 100644 --- a/tests/Feature/Server/SentinelUpdateCheckIndependenceTest.php +++ b/tests/Feature/Server/SentinelUpdateCheckIndependenceTest.php @@ -13,6 +13,7 @@ uses(RefreshDatabase::class); beforeEach(function () { Queue::fake(); + InstanceSettings::forceCreate(['id' => 0]); // Create user (which automatically creates a team) $user = User::factory()->create(); @@ -123,10 +124,9 @@ it('respects server timezone when checking sentinel updates', function () { }); }); -it('does not dispatch sentinel check for servers without sentinel enabled', function () { - // Disable sentinel +it('does not dispatch sentinel check for build servers', function () { $this->server->settings->update([ - 'is_sentinel_enabled' => false, + 'is_build_server' => true, ]); $instanceSettings = InstanceSettings::first(); @@ -144,13 +144,13 @@ it('does not dispatch sentinel check for servers without sentinel enabled', func Queue::assertNotPushed(CheckAndStartSentinelJob::class); }); -it('handles multiple servers with different sentinel configurations', function () { - // Create a second server with sentinel disabled +it('handles multiple servers with different sentinel eligibility', function () { + // Create a second server that cannot run Sentinel $server2 = Server::factory()->create([ 'team_id' => $this->team->id, ]); $server2->settings->update([ - 'is_sentinel_enabled' => false, + 'is_build_server' => true, 'server_timezone' => 'UTC', ]); diff --git a/tests/Unit/ServerManagerJobSentinelCheckTest.php b/tests/Unit/ServerManagerJobSentinelCheckTest.php index 4666565fe4..f2a49bad3d 100644 --- a/tests/Unit/ServerManagerJobSentinelCheckTest.php +++ b/tests/Unit/ServerManagerJobSentinelCheckTest.php @@ -18,7 +18,7 @@ afterEach(function () { Carbon::setTestNow(); }); -it('does not dispatch CheckAndStartSentinelJob hourly anymore', function () { +it('dispatches an hourly Sentinel version check for a healthy Sentinel', function () { $settings = Mockery::mock(InstanceSettings::class); $settings->instance_timezone = 'UTC'; $this->app->instance(InstanceSettings::class, $settings); @@ -39,8 +39,9 @@ it('does not dispatch CheckAndStartSentinelJob hourly anymore', function () { $job = new ServerManagerJob; $job->handle(); - // Hourly CheckAndStartSentinelJob dispatch was removed — ServerCheckJob handles it when Sentinel is out of sync - Queue::assertNotPushed(CheckAndStartSentinelJob::class); + Queue::assertPushed(CheckAndStartSentinelJob::class, function ($job) use ($server) { + return $job->server->id === $server->id; + }); }); it('does not schedule periodic Sentinel restart checks', function () { From 246b2cfe0ac7f1ece50b46706f693df1b88ec07b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:01:53 +0200 Subject: [PATCH 03/11] chore(release): bump Coolify version to 4.3.20 --- config/constants.php | 2 +- other/nightly/versions.json | 2 +- versions.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/constants.php b/config/constants.php index a69b697f30..3f8335134f 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => env('COOLIFY_VERSION') ?: '4.3.19', + 'version' => env('COOLIFY_VERSION') ?: '4.3.20', 'helper_version' => '1.0.17', 'realtime_version' => '1.0.19', 'railpack_version' => '0.23.0', diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 50e20a4066..6818286b29 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,7 +1,7 @@ { "coolify": { "v4": { - "version": "4.3.19" + "version": "4.3.20" }, "nightly": { "version": "4.4-rc.1" diff --git a/versions.json b/versions.json index 50e20a4066..6818286b29 100644 --- a/versions.json +++ b/versions.json @@ -1,7 +1,7 @@ { "coolify": { "v4": { - "version": "4.3.19" + "version": "4.3.20" }, "nightly": { "version": "4.4-rc.1" From 398f32712aa431118255f3c68879c3b96d5791d2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:41:06 +0200 Subject: [PATCH 04/11] docs(ai): consolidate engineering lessons --- .ai/lessons.md | 127 +++++++++++++------------------------------------ 1 file changed, 34 insertions(+), 93 deletions(-) diff --git a/.ai/lessons.md b/.ai/lessons.md index db826478c3..43de6bf878 100644 --- a/.ai/lessons.md +++ b/.ai/lessons.md @@ -1,102 +1,43 @@ # Lessons -## Check prior fixes before changing a repeated symptom -- When a reported regression matches a recent fix, inspect that fix and reproduce why it no longer works before adding another workaround. -- Do not claim a redirect or lifecycle root cause from an effects assertion alone. Prove the reported HTTP or browser failure first. -- Preserve SPA navigation when it is a product requirement. Do not replace it with a full-page redirect to mask a deletion race; fix the ordering or state race instead. +## Prove regressions before changing code +- Reproduce the reported failure on the unchanged baseline before adding a fix. +- When a symptom matches an earlier fix, inspect that fix and prove why it no longer works before adding another workaround. +- Test old reports against the current branch because later changes can make the report obsolete. +- Use the same regression test before and after the production change so the result shows the behavior difference. -## Confirm which surface becomes the modal -- When a user wants two settings pages replaced by a modal, identify the parent page that owns the trigger and confirm that the complete child settings view moves into that modal. -- Do not make one child page a modal inside the other child page when the user wants both child URLs removed. -- When the modal itself supplies the title and subtitle, do not repeat page-style section cards inside it. Use a flat input layout and one footer for actions. -- Put destructive actions on the footer's left. Put conversion and the primary Save action on the right, with Save last. -- Do not repeat domain-port guidance in a resource settings modal when domain ports have their own input in the domain editor. -- A flat modal form can still use a bordered summary box for a distinct linked resource, such as the domain count and Manage domains action. -- For compact modal headers, show the descriptive subtitle as hover text on an underlined title instead of adding a second visible line. -- Reuse `x-helper` and the plain `underline underline-offset-4` trigger for title help. Do not use a native `title` tooltip or a dotted underline when the project already has a shared title-tooltip pattern. +## Verify the complete user flow +- Do not use a passing unit test, a successful build, or a healthy process as proof for a reported UI failure. +- Verify the exact live flow, persisted state, relevant logs, and queue state when they affect the result. +- When the request covers more than one interface or resource type, inventory and verify each supported path. -## Alpine x-transition + tw-animate-css exit animations flash at the end -- Symptom: a modal/overlay fades out, then flashes fully visible for 1-2 frames before it disappears. -- Cause: `animate-out` keyframes default to `animation-fill-mode: none`. The element snaps back to its natural state when the keyframe ends. Alpine hides the element (display: none) only after its own timer (read from `transition-duration`), which starts ~2 rAF later than the animation. The gap shows the element at full opacity. -- Rule: every `x-transition:leave` that uses tw-animate-css `animate-out` MUST also include `fill-mode-forwards`. -- Rule: when a user reports UI flicker, check ALL layers of the animation stack (state reset timing, spinner flash, keyframe fill mode, focus restore) before you report the fix as complete. My first fix covered state reset and spinner only; the fill-mode snap was the visible one. +## Preserve product scope +- Do not replace required SPA navigation with a full-page redirect to hide a lifecycle or ordering defect. +- Do not add billing restrictions, live reconciliation, or fallback behavior unless the request includes them. +- Treat implementation constraints as details. Do not expand a requested team-level control into a more complex policy model. -## Displayed defaults must not become stored overrides -- When an edit form shows an inherited or computed default, trace an unchanged save and a related-field edit through persistence. -- Preserve the inherited state when the displayed value still equals the computed default; store an override only when the user selects a different value. +## Keep dynamic Livewire identities stable +- In dynamic lists, key components and actions with immutable record identities, not counts, indexes, or array positions. +- Use targeted refresh events. Do not refresh a parent and a child that the parent can remove or hide during the same operation. +- Prove lifecycle and redirect causes directly; an effects assertion alone is not sufficient. -## Prove regressions against the unchanged baseline -- For a bug fix, run the same regression test before and after the production change. Use a stash when requested so the failure and success come from the exact same test. +## Keep modal structure consistent +- Identify the parent page that owns a modal trigger and move the complete requested workflow into that modal. +- Use a flat form layout when the modal already supplies its title and description. +- Put destructive actions on the footer's left and primary actions last on the right. +- Use shared section, helper, tooltip, and icon-button components instead of local variants. +- Keep validation, preview, and save controls in a fixed footer when the body is large. -## Apply shared domain UX to every supported resource type -- When a user asks for domain-management behavior, inventory every resource that can edit domains before implementation. -- Do not stop at the resource type named in the original report when the requested UX is meant to be consistent across Coolify. +## Verify layered UI behavior visually +- Inspect the real layout with all conditional elements visible, especially compound status badges. +- For animation flicker, inspect state timing, loading indicators, keyframe fill mode, and focus restoration. +- Add `fill-mode-forwards` to Alpine leave transitions that use tw-animate-css `animate-out` so the element does not flash before Alpine hides it. -## Verify manual and generated domain paths separately -- Domain regeneration and manual hostname edits must start the same post-save DNS check. -- Add explicit regression coverage for both entry paths across every active domain editor. +## Preserve inherited values and clear API semantics +- An unchanged displayed default must remain inherited; store an override only when the user selects a different value. +- Expose named API values for special modes. Keep existing numeric sentinels only as compatibility aliases unless a breaking change is requested. -## Do not treat a runtime restart as behavior verification -- A healthy restarted container proves only that the process started. -- For a reported UI failure, verify the exact user flow and inspect the resulting persisted state before claiming the fix works. - -## Prove the reported live flow before reporting a UI fix -- Do not use unit tests or a healthy process as proof for a reported live UI failure. -- After the user repeats the flow, inspect the exact persisted record, request logs, queue state, and deployed source before stating that it works. - -## Start DNS checks only for DNS-relevant edits -- Compare the previous and saved scheme and hostname before a post-save DNS check. -- Do not restart DNS checks for indexing, redirect, path, or internal-port-only changes. - -## Include automatically added domains in post-save DNS checks -- Compare the configured domain list before and after Save. -- Start checks for each newly added counterpart, even when the edited domain itself did not change. - -## Use one DNS progress pattern -- All DNS check entry points must set the domain badge to the same `checking` state. -- Do not use separate loading feedback on Check all or per-domain action buttons when the badge is the progress indicator. -- Verify the rendered badge uses the spinner slot instead of the default status dot. - -## Confirm whether old reports still apply before changing code -- For an old issue, first test the current branch and inspect later fixes. Do not assume that the historical reproduction still needs a new code change. - -## Compare routing identity, not complete domain URLs -- Domain-conflict checks must treat `http://host` and `https://host` as the same routing identity. -- Reproduce reports with the exact stored schemes before stating that duplicate detection works. - -## Verify reported fixes against the running development app -- When a user asks for before-and-after verification, test the unchanged and fixed production code against the same Jean Run environment. -- Cover each requested interface, such as UI and API, and record the exact URL, response, persisted state, and relevant logs. - -## Do not infer that “Pro” means paid -- When the user calls a setting “Pro,” confirm whether it means advanced-user functionality or a subscription entitlement. -- Do not add billing or Cloud-only checks unless the user explicitly requests them. - -## Verify compound status layouts visually -- When a status component can render more than one badge, give its root an explicit horizontal flex layout. -- Inspect the real top-bar layout with every conditional badge visible before calling a UI change complete. - -## Keep a requested security control at its stated scope -- If the user specifies one team-level redaction flag, do not introduce per-secret policy questions. -- Explain storage constraints as implementation details, then preserve the requested single control. - -## Use shared section title helpers in edit modals -- When modal section descriptions should appear on hover, use `x-application.settings-section` instead of a manual heading and visible paragraph. -- Keep text labels for direct actions such as Back up now. Use a standard icon button with a tooltip for familiar secondary actions such as settings. - - -## Keep modal actions in the footer -- When a modal has a large editable body, put preview, validation, and save controls in a fixed footer. Keep the title bar for the title and close action. - -## Prefer named API values over numeric sentinels -- When an API option means an unbounded or special mode, expose a clear named value such as `all`. -- Keep an existing numeric sentinel such as `-1` only as a compatibility alias unless the user requests a breaking change. - -## Do not auto-heal existing deployments without a request -- When a parser or label fix can apply only after container recreation, keep the change limited to new deployments and later user-initiated redeployments unless the user explicitly asks for live reconciliation. -- Do not add status lookup fallbacks that alter existing deployment behavior when the requested scope is new deployments only. - -## Trace image replacements through build stages -- When replacing a container image for development, inspect both Compose services and every development Dockerfile `FROM` stage. -- A successful Compose pull does not prove the application build is free of the old image; validate the complete build dependency chain. -- Do not replace a removed image with a floating `latest` tag. Find the newest stable release tag and pin it consistently in Compose and every Dockerfile stage. +## Trace infrastructure changes end to end +- For container image changes, inspect Compose services and every relevant Dockerfile build stage. +- Pin a stable release tag instead of using a floating `latest` tag. +- A successful image pull does not prove that the complete application build no longer uses the old image. From 065ca9c861511ae20ba3b467667cd28db5a60a79 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:50:16 +0200 Subject: [PATCH 05/11] fix(helper): prefer newer fetched helper version Use a CDN-fetched helper version when it is newer than the bundled version, while ignoring stale stored versions. --- bootstrap/helpers/shared.php | 10 +++++++- tests/Feature/HelperVersionTest.php | 38 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/HelperVersionTest.php diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index d1f5e4016b..f40c51a335 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -4234,6 +4234,8 @@ function coolifyHelperImage(): string function getHelperVersion(): string { + $configuredHelperVersion = config('constants.coolify.helper_version'); + if (isDev()) { $devHelperVersion = InstanceSettings::query()->whereKey(0)->value('dev_helper_version'); @@ -4242,7 +4244,13 @@ function getHelperVersion(): string } } - return config('constants.coolify.helper_version'); + $fetchedHelperVersion = InstanceSettings::query()->whereKey(0)->value('helper_version'); + + if (! empty($fetchedHelperVersion) && version_compare($fetchedHelperVersion, $configuredHelperVersion, '>')) { + return $fetchedHelperVersion; + } + + return $configuredHelperVersion; } function loggy($message = null, array $context = []) diff --git a/tests/Feature/HelperVersionTest.php b/tests/Feature/HelperVersionTest.php new file mode 100644 index 0000000000..64ef8466a7 --- /dev/null +++ b/tests/Feature/HelperVersionTest.php @@ -0,0 +1,38 @@ + 0]); + + config([ + 'app.env' => 'production', + 'constants.coolify.helper_version' => '1.0.17', + 'constants.coolify.versions_url' => 'https://cdn.example.com/coolify/versions.json', + ]); +}); + +it('uses a newer helper version after fetching it from the CDN', function () { + Http::preventStrayRequests(); + Http::fake([ + 'https://cdn.example.com/coolify/versions.json' => Http::response([ + 'coolify' => ['helper' => ['version' => '1.0.18']], + ]), + ]); + + (new CheckHelperImageJob)->handle(); + + expect(InstanceSettings::findOrFail(0)->helper_version)->toBe('1.0.18') + ->and(getHelperVersion())->toBe('1.0.18'); +}); + +it('does not use a stored helper version older than the bundled version', function () { + InstanceSettings::findOrFail(0)->update(['helper_version' => '1.0.16']); + + expect(getHelperVersion())->toBe('1.0.17'); +}); 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 06/11] 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 07/11] 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 08/11] 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 09/11] 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 10/11] 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 11/11] 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'); +});