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 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-24 00:32:14 +02:00
co-authored by Claude Opus 5.5
parent 8429b6e94b
commit 2ea1afb811
5 changed files with 113 additions and 10 deletions
+1 -1
View File
@@ -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: <a target="_blank" class="text-black underline dark:text-white" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$this->error = $server->dockerEngineVersionError();
$server->update([
'validation_logs' => $this->error,
]);
+2 -2
View File
@@ -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: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$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;
+1 -2
View File
@@ -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: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$this->error = $this->server->dockerEngineVersionError();
$this->server->update([
'validation_logs' => $this->error,
]);
+22 -5
View File
@@ -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 = '<a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>';
$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) {
@@ -0,0 +1,87 @@
<?php
use App\Events\ServerReachabilityChanged;
use App\Livewire\Server\ValidateAndInstall;
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Server;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Process;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::forceCreate(['id' => 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');
});