mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(deploy): do not pull locally built compose images before stop
Skip the pre-stop pull when a custom build command runs without a build server, and pull only services whose image does not come from a build, including services that reuse the image of a build service. Closes #11773 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
801274f5bb
commit
f5c88f7e4f
@@ -968,18 +968,35 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return $composeFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls the registry images of the Compose services before the current deployment stops. Images that
|
||||
* this deployment builds are not pulled: a pull fails for an image that is only local, and a pull of
|
||||
* an image that is also in a registry replaces the new build with the old registry image.
|
||||
*/
|
||||
private function pull_docker_compose_images(): void
|
||||
{
|
||||
if ($this->docker_compose_custom_build_command && ! $this->use_build_server) {
|
||||
$this->application_deployment_queue->addLogEntry('Skipping the image pull before stopping the current deployment, because the custom build command can build images outside of Docker Compose.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$services = $this->registry_image_compose_services();
|
||||
if ($services->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->application_deployment_queue->addLogEntry('Pulling image-based services before stopping the current deployment.');
|
||||
|
||||
$services = $services->map(fn (string $service) => escapeshellarg($service))->implode(' ');
|
||||
if ($this->use_build_server) {
|
||||
$this->write_deployment_configurations();
|
||||
$this->server = $this->mainServer;
|
||||
$workdir = $this->application->workdir();
|
||||
$command = "{$this->coolify_variables} docker compose --env-file {$workdir}/.env --project-name {$this->application->uuid} --project-directory {$workdir} -f {$workdir}{$this->docker_compose_location} pull --ignore-buildable";
|
||||
$command = "{$this->coolify_variables} docker compose --env-file {$workdir}/.env --project-name {$this->application->uuid} --project-directory {$workdir} -f {$workdir}{$this->docker_compose_location} pull --ignore-buildable {$services}";
|
||||
} else {
|
||||
$workdir = $this->workdir;
|
||||
$command = executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --env-file {$workdir}/.env --project-name {$this->application->uuid} --project-directory {$workdir} -f {$workdir}{$this->docker_compose_location} pull --ignore-buildable");
|
||||
$command = executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --env-file {$workdir}/.env --project-name {$this->application->uuid} --project-directory {$workdir} -f {$workdir}{$this->docker_compose_location} pull --ignore-buildable {$services}");
|
||||
}
|
||||
|
||||
$this->execute_remote_command([
|
||||
@@ -988,6 +1005,26 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of the Compose services that get their image from a registry. A service with a
|
||||
* `build` section, or a service that uses the image of such a service, gets its image from the build.
|
||||
*
|
||||
* @return Collection<int, string>
|
||||
*/
|
||||
private function registry_image_compose_services(): Collection
|
||||
{
|
||||
$services = collect(data_get(Yaml::parse($this->generated_docker_compose), 'services', []));
|
||||
$builtImages = $services->filter(fn ($service) => filled(data_get($service, 'build')))->pluck('image')->filter();
|
||||
|
||||
return $services
|
||||
->filter(fn ($service) => filled(data_get($service, 'image'))
|
||||
&& blank(data_get($service, 'build'))
|
||||
&& ! $builtImages->contains(data_get($service, 'image')))
|
||||
->keys()
|
||||
->map(fn ($service) => (string) $service)
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the Compose containers. Content files are written first, because `docker compose up`
|
||||
* creates a missing bind source as an empty directory.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\Server;
|
||||
|
||||
class TestableDockerComposeImagePullDeploymentJob extends ApplicationDeploymentJob
|
||||
@@ -27,17 +28,33 @@ class TestableDockerComposeImagePullDeploymentJob extends ApplicationDeploymentJ
|
||||
}
|
||||
}
|
||||
|
||||
function makeDockerComposeImagePullJob(bool $useBuildServer = false): array
|
||||
function makeDockerComposeImagePullJob(bool $useBuildServer = false, ?string $customBuildCommand = null, ?string $compose = null): array
|
||||
{
|
||||
$compose ??= <<<'YAML'
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
image: 'myapp:latest'
|
||||
worker:
|
||||
image: 'myapp:latest'
|
||||
db:
|
||||
image: 'postgres:16'
|
||||
cache:
|
||||
image: 'redis:7'
|
||||
YAML;
|
||||
|
||||
$job = new TestableDockerComposeImagePullDeploymentJob;
|
||||
$reflection = new ReflectionClass(ApplicationDeploymentJob::class);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->uuid = 'application-uuid';
|
||||
$application->name = 'application';
|
||||
$application->setRelation('settings', new ApplicationSetting(['is_raw_compose_deployment_enabled' => false]));
|
||||
$application->shouldReceive('workdir')->andReturn('/runtime/application');
|
||||
|
||||
$queue = Mockery::mock(ApplicationDeploymentQueue::class)->makePartial();
|
||||
$queue->shouldReceive('addLogEntry')->once()->andReturnNull();
|
||||
$queue->shouldReceive('getAttribute')->with('updated_at')->andReturn('2026-10-16 00:00:00');
|
||||
$queue->shouldReceive('addLogEntry')->andReturnNull();
|
||||
|
||||
$buildServer = new Server;
|
||||
$buildServer->id = 1;
|
||||
@@ -52,9 +69,13 @@ function makeDockerComposeImagePullJob(bool $useBuildServer = false): array
|
||||
'mainServer' => $mainServer,
|
||||
'use_build_server' => $useBuildServer,
|
||||
'workdir' => '/artifacts/application',
|
||||
'configuration_dir' => '/runtime/application',
|
||||
'pull_request_id' => 0,
|
||||
'deployment_uuid' => 'deployment-uuid',
|
||||
'docker_compose_location' => '/docker-compose.yaml',
|
||||
'coolify_variables' => '',
|
||||
'docker_compose_custom_build_command' => $customBuildCommand,
|
||||
'generated_docker_compose' => $compose,
|
||||
] as $property => $value) {
|
||||
$reflection->getProperty($property)->setValue($job, $value);
|
||||
}
|
||||
@@ -70,15 +91,54 @@ it('aborts the deployment when pulling a compose image fails', function () {
|
||||
})->throws(RuntimeException::class, 'Image pull failed.');
|
||||
|
||||
it('pulls compose images on the runtime server when using a build server', function () {
|
||||
[$job, $reflection, $buildServer, $mainServer] = makeDockerComposeImagePullJob(useBuildServer: true);
|
||||
[$job, $reflection, , $mainServer] = makeDockerComposeImagePullJob(useBuildServer: true);
|
||||
|
||||
$reflection->getMethod('pull_docker_compose_images')->invoke($job);
|
||||
|
||||
$command = $job->recordedCommands[0][0][0];
|
||||
$command = last($job->recordedCommands)[0][0];
|
||||
|
||||
expect($job->commandServers)->toHaveCount(1)
|
||||
->and($job->commandServers[0])->toBe($mainServer)
|
||||
->and($job->commandServers[0])->not->toBe($buildServer)
|
||||
expect($job->commandServers)->each->toBe($mainServer)
|
||||
->and($command)->toContain('pull --ignore-buildable')
|
||||
->and($command)->toContain('--env-file /runtime/application/.env');
|
||||
});
|
||||
|
||||
it('pulls only services whose image does not come from the build', function () {
|
||||
[$job, $reflection] = makeDockerComposeImagePullJob();
|
||||
|
||||
$reflection->getMethod('pull_docker_compose_images')->invoke($job);
|
||||
|
||||
expect($job->recordedCommands)->toHaveCount(1)
|
||||
->and($job->recordedCommands[0][0][0])->toEndWith("pull --ignore-buildable '\\''db'\\'' '\\''cache'\\'''");
|
||||
});
|
||||
|
||||
it('does not pull when all images come from the build', function () {
|
||||
[$job, $reflection] = makeDockerComposeImagePullJob(compose: <<<'YAML'
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
image: 'myapp:latest'
|
||||
worker:
|
||||
image: 'myapp:latest'
|
||||
YAML);
|
||||
|
||||
$reflection->getMethod('pull_docker_compose_images')->invoke($job);
|
||||
|
||||
expect($job->recordedCommands)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('does not pull when a custom build command builds the images', function () {
|
||||
[$job, $reflection] = makeDockerComposeImagePullJob(customBuildCommand: 'docker buildx bake -f docker-bake.prod.hcl');
|
||||
|
||||
$reflection->getMethod('pull_docker_compose_images')->invoke($job);
|
||||
|
||||
expect($job->recordedCommands)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('pulls on the runtime server when a custom build command runs on a build server', function () {
|
||||
[$job, $reflection, , $mainServer] = makeDockerComposeImagePullJob(useBuildServer: true, customBuildCommand: 'docker buildx bake');
|
||||
|
||||
$reflection->getMethod('pull_docker_compose_images')->invoke($job);
|
||||
|
||||
expect($job->commandServers)->each->toBe($mainServer)
|
||||
->and(last($job->recordedCommands)[0][0])->toEndWith("pull --ignore-buildable 'db' 'cache'");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user