From 2ea1afb81174df0f1b3bae85a0b456e1fd80275d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:32:14 +0200 Subject: [PATCH] fix(server): explain why the Docker Engine version check failed When "docker version --format json" did not return clean JSON (for example, text from a shell startup file), validation stored 0.0.0 and said that Docker was older than version 24. Validation now stores no version and tells the user to run the command on the server. When the version is too old, the message shows the detected version. Closes #11089 Co-Authored-By: Claude Opus 5.5 --- app/Actions/Server/ValidateServer.php | 2 +- app/Jobs/ValidateAndInstallServerJob.php | 4 +- app/Livewire/Server/ValidateAndInstall.php | 3 +- app/Models/Server.php | 27 ++++-- .../ServerDockerVersionValidationTest.php | 87 +++++++++++++++++++ 5 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 tests/Feature/ServerDockerVersionValidationTest.php diff --git a/app/Actions/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php index 7c973130c4..f4855b9ffa 100644 --- a/app/Actions/Server/ValidateServer.php +++ b/app/Actions/Server/ValidateServer.php @@ -114,7 +114,7 @@ class ValidateServer if ($this->docker_version) { return 'OK'; } else { - $this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: documentation.'; + $this->error = $server->dockerEngineVersionError(); $server->update([ 'validation_logs' => $this->error, ]); diff --git a/app/Jobs/ValidateAndInstallServerJob.php b/app/Jobs/ValidateAndInstallServerJob.php index 987b53e7f6..7ebfbbc142 100644 --- a/app/Jobs/ValidateAndInstallServerJob.php +++ b/app/Jobs/ValidateAndInstallServerJob.php @@ -160,14 +160,14 @@ class ValidateAndInstallServerJob implements ShouldBeEncrypted, ShouldQueue // Validate Docker version $dockerVersion = $this->server->validateDockerEngineVersion(); if (! $dockerVersion) { - $requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.'); - $errorMessage = 'Minimum Docker Engine version '.$requiredDockerVersion.' is not installed. Please install Docker manually before continuing: documentation.'; + $errorMessage = $this->server->dockerEngineVersionError(); $this->server->update([ 'validation_logs' => $errorMessage, 'is_validating' => false, ]); Log::error('ValidateAndInstallServer: Docker version not sufficient', [ 'server_id' => $this->server->id, + 'docker_version' => $this->server->dockerVersion(), ]); return; diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index 27a7c580e1..c7ce754732 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -320,8 +320,7 @@ class ValidateAndInstall extends Component instant_remote_process(ensureProxyNetworksExist($this->server)->toArray(), $this->server, false); StartProxy::dispatch($this->server); } else { - $requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.'); - $this->error = 'Minimum Docker Engine version '.$requiredDockerVersion.' is not installed. Please install Docker manually before continuing: documentation.'; + $this->error = $this->server->dockerEngineVersionError(); $this->server->update([ 'validation_logs' => $this->error, ]); diff --git a/app/Models/Server.php b/app/Models/Server.php index e8d3e7c12a..e1eac85d6a 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -2567,13 +2567,11 @@ $siteAddress { public function validateDockerEngineVersion() { $dockerVersionRaw = instant_remote_process(['docker version --format json'], $this, false); - $dockerVersionJson = json_decode($dockerVersionRaw, true); - $dockerVersion = data_get($dockerVersionJson, 'Server.Version', '0.0.0'); - $this->rememberDockerVersion(is_string($dockerVersion) ? $dockerVersion : null); + $dockerVersion = dockerEngineVersionFromJson($dockerVersionRaw); + $this->rememberDockerVersion($dockerVersion); $composeVersionRaw = instant_remote_process(['docker compose version --short'], $this, false); $this->rememberComposeVersion(is_string($composeVersionRaw) ? $composeVersionRaw : null); - $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion); - if (is_null($dockerVersion)) { + if (is_null(checkMinimumDockerEngineVersion($dockerVersion))) { $this->settings->is_usable = false; $this->settings->save(); @@ -2587,6 +2585,25 @@ $siteAddress { return true; } + /** + * Explains why validateDockerEngineVersion() failed: the version could not be read, or it is too old. + */ + public function dockerEngineVersionError(): string + { + $requiredVersion = str(config('constants.docker.minimum_required_version'))->before('.'); + $documentation = 'documentation'; + $dockerVersion = $this->dockerVersion(); + + if (is_null($dockerVersion)) { + $command = $this->isNonRoot() ? 'sudo docker version --format json' : 'docker version --format json'; + $user = e($this->user); + + return "Coolify could not read the Docker Engine version. Run \"{$command}\" on the server as the {$user} user. It must exit without an error and print only JSON. Text from shell startup files (for example ~/.bashrc) breaks the output."; + } + + return "Docker Engine {$dockerVersion} is installed, but Coolify needs version {$requiredVersion} or newer. Please upgrade Docker: {$documentation}."; + } + public function validateCoolifyNetwork($isSwarm = false, $isBuildServer = false) { if ($isBuildServer) { diff --git a/tests/Feature/ServerDockerVersionValidationTest.php b/tests/Feature/ServerDockerVersionValidationTest.php new file mode 100644 index 0000000000..f41992431c --- /dev/null +++ b/tests/Feature/ServerDockerVersionValidationTest.php @@ -0,0 +1,87 @@ + 0]); + Event::fake([ServerReachabilityChanged::class]); + + $user = User::factory()->create(); + $this->team = Team::factory()->create(); + $user->teams()->attach($this->team, ['role' => 'owner']); + $this->actingAs($user); + session(['currentTeam' => $this->team]); + + $this->server = Server::factory()->create([ + 'team_id' => $this->team->id, + 'user' => 'root', + 'private_key_id' => PrivateKey::factory()->create(['team_id' => $this->team->id])->id, + ]); +}); + +function fakeDockerVersionOutput(string $output, int $exitCode = 0): void +{ + Process::fake([ + '*compose*' => Process::result(output: 'v2.40.3', exitCode: 0), + '*' => Process::result(output: $output, exitCode: $exitCode), + ]); +} + +function validateDockerVersionError(Server $server): ?string +{ + $component = Livewire::test(ValidateAndInstall::class, ['server' => $server])->instance(); + $component->uptime = '1 day'; + $component->validateDockerVersion(); + + return $component->error; +} + +it('accepts Docker Engine 29', function () { + fakeDockerVersionOutput('{"Client":{"Version":"29.6.2"},"Server":{"Version":"29.6.2"}}'); + + expect($this->server->validateDockerEngineVersion())->toBeTrue() + ->and($this->server->fresh()->dockerVersion())->toBe('29.6.2'); +}); + +it('reports an unreadable docker version instead of a too old one', function (string $output, int $exitCode) { + fakeDockerVersionOutput($output, $exitCode); + + $error = validateDockerVersionError($this->server); + + expect($error)->toContain('could not read the Docker Engine version') + ->toContain('Run "docker version --format json" on the server as the root user') + ->not->toContain('Minimum Docker Engine version') + ->and($this->server->fresh()->dockerVersion())->toBeNull() + ->and($this->server->fresh()->validation_logs)->toBe($error); +})->with([ + 'text before the json' => ['Welcome to the server'."\n".'{"Server":{"Version":"29.6.2"}}', 0], + 'command failed' => ['', 1], +]); + +it('reports the detected version when docker is too old', function () { + fakeDockerVersionOutput('{"Server":{"Version":"23.0.6"}}'); + + expect(validateDockerVersionError($this->server)) + ->toContain('Docker Engine 23.0.6 is installed') + ->toContain('version 24 or newer'); +}); + +it('shows the sudo command for non-root servers', function () { + $this->server->update(['user' => 'deploy']); + fakeDockerVersionOutput('not json'); + + expect(validateDockerVersionError($this->server->fresh())) + ->toContain('Run "sudo docker version --format json" on the server as the deploy user'); +});