From d09480973ebadc9c242123724f0391a6a9353902 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:20:44 +0100 Subject: [PATCH] fix(previews): give each Compose preview its own project name Closes #9014 --- app/Jobs/ApplicationDeploymentJob.php | 30 ++++++++++++------- .../Unit/DockerComposeImagePullOrderTest.php | 15 ++++++++-- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index fb74d1ded1..5c51791e5d 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1029,9 +1029,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue // Use build-time .env file from /artifacts (outside Docker context to prevent it from being in the image) $command .= ' --env-file '.self::BUILD_TIME_ENV_PATH; if ($this->force_rebuild) { - $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull --no-cache"; + $command .= " --project-name {$this->composeProjectName()} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull --no-cache"; } else { - $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull"; + $command .= " --project-name {$this->composeProjectName()} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull"; } if (! $this->application->settings->use_build_secrets && $this->build_args instanceof Collection && $this->build_args->isNotEmpty()) { @@ -1140,10 +1140,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->write_deployment_configurations(); $this->server = $this->mainServer; $workdir = $this->application->workdir(); - $command = serverDockerConfigPrefix($this->server)."{$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}"; + $command = serverDockerConfigPrefix($this->server)."{$this->coolify_variables} docker compose --env-file {$workdir}/.env --project-name {$this->composeProjectName()} --project-directory {$workdir} -f {$workdir}{$this->docker_compose_location} pull --ignore-buildable {$services}"; } else { $workdir = $this->workdir; - $command = executeInDocker($this->deployment_uuid, $this->withoutHelperBuildSecretEnvironment("{$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}")); + $command = executeInDocker($this->deployment_uuid, $this->withoutHelperBuildSecretEnvironment("{$this->coolify_variables} docker compose --env-file {$workdir}/.env --project-name {$this->composeProjectName()} --project-directory {$workdir} -f {$workdir}{$this->docker_compose_location} pull --ignore-buildable {$services}")); } $this->execute_remote_command([ @@ -1172,6 +1172,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ->values(); } + /** + * The Docker Compose project of this deployment. Each preview has its own project, so Docker + * Compose does not treat the containers of the application and of the other previews as orphans + * of the deployed preview, and networks that the Compose file declares are not shared between them. + */ + private function composeProjectName(): string + { + return addPreviewDeploymentSuffix($this->application->uuid, $this->pull_request_id); + } + /** * Removes the containers that `docker compose up` would give a volume that the user deleted * in Coolify (see composeContainersWithRemovedVolumes()). @@ -1276,7 +1286,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($this->preserveRepository) { // Always use .env file $command .= " --env-file {$server_workdir}/.env"; - $command .= " --project-name {$this->application->uuid} --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; + $command .= " --project-name {$this->composeProjectName()} --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; $this->write_deployment_configurations(); $this->execute_remote_command( @@ -1285,7 +1295,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } else { // Always use .env file $command .= " --env-file {$this->workdir}/.env"; - $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"; + $command .= " --project-name {$this->composeProjectName()} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"; $this->execute_remote_command( [executeInDocker($this->deployment_uuid, $this->withoutHelperBuildSecretEnvironment($command)), 'hidden' => false, 'type' => 'stdout', 'command_hidden' => true], ); @@ -5485,17 +5495,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); if ($this->application->build_pack === 'dockerimage') { $this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.'); $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} pull"), 'hidden' => true], - [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} up --build -d"), 'hidden' => true], + [executeInDocker($this->deployment_uuid, "docker compose --project-name {$this->composeProjectName()} --project-directory {$this->workdir} pull"), 'hidden' => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->composeProjectName()} --project-directory {$this->workdir} up --build -d"), 'hidden' => true], ); } else { if ($this->use_build_server) { $this->execute_remote_command( - [serverDockerConfigPrefix($this->server)."{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --pull always --build -d", 'hidden' => true], + [serverDockerConfigPrefix($this->server)."{$this->coolify_variables} docker compose --project-name {$this->composeProjectName()} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --pull always --build -d", 'hidden' => true], ); } else { $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true], + [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->composeProjectName()} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true], ); } } diff --git a/tests/Unit/DockerComposeImagePullOrderTest.php b/tests/Unit/DockerComposeImagePullOrderTest.php index fb3f95a508..8445e3f3d8 100644 --- a/tests/Unit/DockerComposeImagePullOrderTest.php +++ b/tests/Unit/DockerComposeImagePullOrderTest.php @@ -28,7 +28,7 @@ class TestableDockerComposeImagePullDeploymentJob extends ApplicationDeploymentJ } } -function makeDockerComposeImagePullJob(bool $useBuildServer = false, ?string $customBuildCommand = null, ?string $compose = null): array +function makeDockerComposeImagePullJob(bool $useBuildServer = false, ?string $customBuildCommand = null, ?string $compose = null, int $pullRequestId = 0): array { $compose ??= <<<'YAML' services: @@ -70,7 +70,7 @@ function makeDockerComposeImagePullJob(bool $useBuildServer = false, ?string $cu 'use_build_server' => $useBuildServer, 'workdir' => '/artifacts/application', 'configuration_dir' => '/runtime/application', - 'pull_request_id' => 0, + 'pull_request_id' => $pullRequestId, 'deployment_uuid' => 'deployment-uuid', 'docker_compose_location' => '/docker-compose.yaml', 'coolify_variables' => '', @@ -142,3 +142,14 @@ it('pulls on the runtime server when a custom build command runs on a build serv expect($job->commandServers)->each->toBe($mainServer) ->and(last($job->recordedCommands)[0][0])->toEndWith("pull --ignore-buildable 'db' 'cache'"); }); + +it('uses its own compose project for each preview deployment', function (int $pullRequestId, string $projectName) { + [$job, $reflection] = makeDockerComposeImagePullJob(pullRequestId: $pullRequestId); + + $reflection->getMethod('pull_docker_compose_images')->invoke($job); + + expect($job->recordedCommands[0][0][0])->toContain("--project-name {$projectName} --project-directory"); +})->with([ + 'application' => [0, 'application-uuid'], + 'preview' => [42, 'application-uuid-pr-42'], +]);