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');
+});