mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
fix(compose): use explicit compose file paths and remove volumes by name (#12199)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9355728d03
commit
0eeb630ccd
6 files changed
+87
-25
No files matched your search
@@ -127,8 +127,9 @@ class StartDatabaseProxy
|
||||
"mkdir -p $configuration_dir",
|
||||
"echo '{$nginxconf_base64}' | base64 -d | tee $configuration_dir/nginx.conf > /dev/null",
|
||||
"echo '{$dockercompose_base64}' | base64 -d | tee $configuration_dir/docker-compose.yaml > /dev/null",
|
||||
"docker compose --project-directory {$configuration_dir} pull",
|
||||
"docker compose --project-directory {$configuration_dir} up -d",
|
||||
// Pass -f so Compose uses only the file that Coolify wrote.
|
||||
"docker compose --project-directory {$configuration_dir} -f {$configuration_dir}/docker-compose.yaml pull",
|
||||
"docker compose --project-directory {$configuration_dir} -f {$configuration_dir}/docker-compose.yaml up -d",
|
||||
], $server);
|
||||
} catch (\RuntimeException $e) {
|
||||
if ($this->isNonTransientError($e->getMessage())) {
|
||||
|
||||
@@ -45,16 +45,17 @@ class StartService
|
||||
// doesn't match configuration. Recreate?") would read the next script lines as the answer.
|
||||
// Give Compose no stdin so it uses the default answer (keep the volume). Never pass --yes:
|
||||
// it recreates the volume and deletes its data.
|
||||
// Pass -f so Compose uses only the file that Coolify wrote.
|
||||
$compose = "docker compose --project-directory {$workdir} -f {$workdir}/docker-compose.yml --project-name {$service->uuid}";
|
||||
if ($pullLatestImages) {
|
||||
$commands[] = "echo 'Pulling images.'";
|
||||
$commands[] = "docker compose --project-directory {$workdir} pull < /dev/null";
|
||||
$commands[] = "{$compose} pull < /dev/null";
|
||||
}
|
||||
if ($service->networks()->count() > 0) {
|
||||
$commands[] = "echo 'Creating Docker network.'";
|
||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
|
||||
}
|
||||
$commands[] = 'echo Starting service.';
|
||||
$compose = "docker compose --project-directory {$workdir} -f {$workdir}/docker-compose.yml --project-name {$service->uuid}";
|
||||
if (data_get($service, 'connect_to_docker_network')) {
|
||||
// Connect the created containers to the predefined network before they start. A container
|
||||
// that needs this network during a service_healthy wait never gets it after `up -d`.
|
||||
|
||||
+12
-15
@@ -546,24 +546,21 @@ class Application extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the volumes by the names that Coolify stored.
|
||||
*/
|
||||
public function deleteVolumes()
|
||||
{
|
||||
$persistentStorages = $this->persistentStorages()->get() ?? collect();
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$server = data_get($this, 'destination.server');
|
||||
// --project-directory instead of cd: a non-root SSH user cannot enter the directory on the Coolify host.
|
||||
instant_remote_process(["docker compose --project-directory {$this->dirOnServer()} down -v"], $server, false);
|
||||
} else {
|
||||
if ($persistentStorages->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$server = data_get($this, 'destination.server');
|
||||
foreach ($persistentStorages as $storage) {
|
||||
if ($storage->isSharedWithAnotherResource()) {
|
||||
continue;
|
||||
}
|
||||
instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false);
|
||||
$persistentStorages = $this->persistentStorages()->get();
|
||||
if ($persistentStorages->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
$server = data_get($this, 'destination.server');
|
||||
foreach ($persistentStorages as $storage) {
|
||||
if ($storage->isSharedWithAnotherResource()) {
|
||||
continue;
|
||||
}
|
||||
instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
@@ -794,8 +795,9 @@ describe('delete', function () {
|
||||
->not->toContain("{$uuid}_shared-data");
|
||||
});
|
||||
|
||||
it('lets Docker Compose keep the external volume when a Compose application is deleted with its volumes', function () {
|
||||
it('never removes the external volume when a Compose application is deleted with its volumes', function () {
|
||||
$application = externalVolumeApplication(EXTERNAL_VOLUME_SHORT_COMPOSE);
|
||||
$uuid = $application->uuid;
|
||||
applicationParser($application);
|
||||
$application->delete();
|
||||
$commands = [];
|
||||
@@ -804,11 +806,29 @@ describe('delete', function () {
|
||||
(new DeleteResourceJob($application))->handle();
|
||||
|
||||
$all = implode("\n", $commands);
|
||||
// --project-directory, not cd: a non-root SSH user cannot enter the directory on the Coolify host.
|
||||
expect($all)->toContain("docker compose --project-directory {$application->dirOnServer()} down -v")
|
||||
->not->toContain("cd {$application->dirOnServer()}")
|
||||
->not->toContain('docker volume rm')
|
||||
->and($application->docker_compose)->toContain("shared-data:\n external: true");
|
||||
expect($all)->toContain("docker volume rm -f '{$uuid}_app-data'")
|
||||
->not->toContain("'shared-data'")
|
||||
->not->toContain("{$uuid}_shared-data");
|
||||
});
|
||||
|
||||
it('removes the volumes by name without Docker Compose when a Compose application is deleted', function () {
|
||||
$application = externalVolumeApplication(EXTERNAL_VOLUME_SHORT_COMPOSE);
|
||||
applicationParser($application);
|
||||
LocalFileVolume::create([
|
||||
'fs_path' => $application->dirOnServer().'/compose.yaml',
|
||||
'mount_path' => '/compose.yaml',
|
||||
'content' => "services:\n other:\n image: busybox\n",
|
||||
'resource_id' => $application->id,
|
||||
'resource_type' => $application->getMorphClass(),
|
||||
]);
|
||||
$application->delete();
|
||||
$commands = [];
|
||||
fakeExternalVolumeServer($commands);
|
||||
|
||||
(new DeleteResourceJob($application, deleteConfigurations: false))->handle();
|
||||
|
||||
expect(implode("\n", $commands))->not->toContain('docker compose')
|
||||
->toContain("docker volume rm -f '{$application->uuid}_app-data'");
|
||||
});
|
||||
|
||||
it('removes only the old prefixed volume when a service with an old storage entry is deleted with its volumes', function () {
|
||||
|
||||
@@ -118,3 +118,34 @@ test('database proxy resolves the database container on every connection', funct
|
||||
->toContain('proxy_pass $upstream;')
|
||||
->not->toContain("proxy_pass {$database->uuid}:5432;");
|
||||
});
|
||||
|
||||
test('database proxy runs Docker Compose only with the compose file that Coolify wrote', function () {
|
||||
Server::flushIdentityMap();
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
config(['constants.ssh.mux_enabled' => false]);
|
||||
|
||||
$commands = [];
|
||||
Process::fake(function ($process) use (&$commands) {
|
||||
$commands[] = is_array($process->command) ? implode(' ', $process->command) : $process->command;
|
||||
|
||||
return Process::result(output: '');
|
||||
});
|
||||
|
||||
$team = Team::factory()->create();
|
||||
$privateKey = PrivateKey::factory()->create(['team_id' => $team->id]);
|
||||
$server = Server::factory()->create(['team_id' => $team->id, 'private_key_id' => $privateKey->id]);
|
||||
$destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail();
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$database = create_standalone_postgresql($environment->id, $destination);
|
||||
$database->update(['is_public' => true, 'public_port' => 15432]);
|
||||
|
||||
StartDatabaseProxy::run($database->fresh());
|
||||
|
||||
$proxyDir = database_proxy_dir($database->uuid);
|
||||
preg_match_all('/docker compose [^\n]*/', implode("\n", $commands), $matches);
|
||||
expect($matches[0])->toHaveCount(2);
|
||||
foreach ($matches[0] as $composeCommand) {
|
||||
expect($composeCommand)->toContain("--project-directory {$proxyDir} -f {$proxyDir}/docker-compose.yaml ");
|
||||
}
|
||||
});
|
||||
@@ -142,6 +142,18 @@ it('gives service compose up and pull no stdin and never answers the recreate pr
|
||||
}
|
||||
});
|
||||
|
||||
it('runs every service compose command with the compose file that Coolify wrote', function () {
|
||||
$service = serviceComposeStdinService('deploy');
|
||||
$workdir = $service->workdir();
|
||||
|
||||
$lines = serviceComposeCommandLines(StartService::run($service, pullLatestImages: true));
|
||||
|
||||
expect($lines)->toHaveCount(3);
|
||||
foreach ($lines as $line) {
|
||||
expect($line)->toContain("-f {$workdir}/docker-compose.yml --project-name {$service->uuid} ");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @param list<string> $calls
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user