fix(docker): replace deprecated stop flags with version-aware ones

Store each server's Docker version and build stop commands via
dockerStopCommand() so newer Docker uses --timeout instead of
deprecated --time/-t. Also show Docker version in server details.
This commit is contained in:
Andras Bacsai committed 2026-08-13 11:17:44 +02:00
1 parent 6481ffffcf
commit e79230cf37
32 files changed
+697 -64

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
<?php
/**
* Docker 28.0 deprecated `--time` on `docker stop` / `docker restart` in favor of `--timeout`.
* `--timeout` is rejected by Docker < 28, so Coolify picks the flag from the stored engine version.
* Unknown versions keep the historical `--time` flag.
*
* @see https://github.com/coollabsio/coolify/issues/11244
*/
it('parses docker engine versions', function (?string $raw, ?string $expected) {
expect(parseDockerEngineVersion($raw))->toBe($expected);
})->with([
'semver' => ['29.4.3', '29.4.3'],
'two-part' => ['28.0', '28.0.0'],
'suffix' => ['28.0.1-ce', '28.0.1'],
'build metadata' => ['29.4.3+azure', '29.4.3'],
'null' => [null, null],
'empty' => ['', null],
'garbage' => ['not-a-version', null],
]);
it('extracts the server version from docker version json', function () {
expect(dockerEngineVersionFromJson('{"Server":{"Version":"29.4.3"}}'))->toBe('29.4.3')
->and(dockerEngineVersionFromJson('{"Client":{"Version":"29.4.3"}}'))->toBeNull()
->and(dockerEngineVersionFromJson('not-json'))->toBeNull();
});
it('uses --timeout on docker 28 and newer', function (string $version) {
expect(dockerStopCommand(30, 'app-1', $version))->toBe('docker stop --timeout=30 app-1');
})->with(['28.0.0', '28.0.1', '28.0', '29.4.3', '29.4.3-ce']);
it('uses --time on docker older than 28', function (string $version) {
expect(dockerStopCommand(30, 'app-1', $version))->toBe('docker stop --time=30 app-1');
})->with(['24.0.0', '26.1.4', '27.5.1']);
it('uses the historical --time flag when the docker version is unknown', function (?string $version) {
expect(dockerStopCommand(30, 'app-1', $version))->toBe('docker stop --time=30 app-1');
})->with([
'null' => [null],
'empty' => [''],
'garbage' => ['unknown'],
]);
@@ -0,0 +1,66 @@
<?php
/**
* Docker deprecated `--time` on `docker stop` / `docker restart` in v28.0.
* Call sites must go through dockerStopCommand() so the flag is chosen from
* the stored engine version (`--timeout` on 28+, `--time` before that or if unknown).
*
* @see https://github.com/coollabsio/coolify/issues/11244
* @see https://github.com/coollabsio/coolify/issues/10791
*/
it('does not hardcode docker --time outside the version-aware helper', function () {
$projectRoot = dockerStopDeprecatedFlagProjectRoot();
$helperFile = $projectRoot.DIRECTORY_SEPARATOR.'bootstrap'.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'docker.php';
$offenders = collect([
...dockerStopDeprecatedFlagPhpFiles($projectRoot.DIRECTORY_SEPARATOR.'app'),
...dockerStopDeprecatedFlagPhpFiles($projectRoot.DIRECTORY_SEPARATOR.'bootstrap'.DIRECTORY_SEPARATOR.'helpers'),
])
->reject(fn (string $file): bool => $file === $helperFile)
->mapWithKeys(function (string $file) use ($projectRoot) {
preg_match_all('/docker\s+(?:stop|restart)\s+--time\b/', file_get_contents($file), $matches);
return [str_replace($projectRoot.DIRECTORY_SEPARATOR, '', $file) => $matches[0]];
})
->filter(fn (array $matches): bool => $matches !== [])
->map(fn (array $matches, string $file): string => $file.': '.implode(', ', array_unique($matches)));
expect($offenders->values()->implode("\n"))->toBe('');
});
it('builds application container stop commands from the stored docker version', function (string $relativePath) {
$contents = file_get_contents(dockerStopDeprecatedFlagProjectRoot().DIRECTORY_SEPARATOR.$relativePath);
expect($contents)
->toContain('dockerStopCommand(')
->not->toContain('docker stop --time=')
->not->toContain('docker stop -t $timeout');
})->with([
'deployment graceful shutdown' => ['app/Jobs/ApplicationDeploymentJob.php'],
'stop application' => ['app/Actions/Application/StopApplication.php'],
'stop application on one server' => ['app/Actions/Application/StopApplicationOneServer.php'],
'stop preview containers' => ['app/Livewire/Project/Application/Previews.php'],
]);
function dockerStopDeprecatedFlagProjectRoot(): string
{
return dirname(__DIR__, 2);
}
/**
* @return list<string>
*/
function dockerStopDeprecatedFlagPhpFiles(string $directory): array
{
$files = [];
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)) as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$files[] = $file->getPathname();
}
}
sort($files);
return $files;
}