0]); config([ 'app.maintenance.store' => 'array', 'constants.ssh.mux_enabled' => false, 'constants.coolify.dev_data_volume' => 'coolify-dev-feature_coolify_data', 'constants.coolify.dev_backups_volume' => 'coolify-dev-feature_backups_data', ]); $this->commands = []; Process::fake(function ($process) { $this->commands[] = is_array(remoteProcessScript($process)) ? implode(' ', remoteProcessScript($process)) : remoteProcessScript($process); return Process::result(output: ''); }); $this->team = Team::factory()->create(); $this->privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]); $project = Project::factory()->create(['team_id' => $this->team->id]); $this->environment = Environment::factory()->create(['project_id' => $project->id]); }); /** * Environment and server kinds: * - kvm: development, a dev KVM VM with its own Docker daemon and its own /data/coolify. * - testing-host: development, the testing-host container that uses the host Docker daemon. * - production: production, with the testing-host address (the address alone must not change paths). * * @return array */ function devPathKinds(): array { return [ 'dev KVM server' => ['kvm', '/data/coolify'], 'dev testing-host server' => ['testing-host', DEV_PATH_DATA], 'production' => ['production', '/data/coolify'], ]; } function devPathServer(string $kind, ?string $proxyType = null, bool $analytics = false): Server { config()->set('app.env', $kind === 'production' ? 'production' : 'local'); $server = Server::factory()->create([ 'team_id' => test()->team->id, 'private_key_id' => test()->privateKey->id, 'ip' => $kind === 'kvm' ? '10.221.1.10' : 'coolify-testing-host', ]); if ($proxyType) { $server->proxy->set('type', $proxyType); $server->save(); } $server->settings->is_traffic_analytics_enabled = $analytics; $server->settings->sentinel_custom_url = 'https://coolify.example.com'; $server->settings->save(); return $server->fresh(); } function devPathDestination(Server $server): StandaloneDocker { return StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail(); } function devPathAllCommands(): string { return implode("\n", test()->commands); } it('detects only the development testing-host server as a host Docker server', function (string $kind, string $base) { $server = devPathServer($kind); expect($server->sharesDevHostDocker())->toBe($kind === 'testing-host') ->and(devHostDockerPath($server, '/data/coolify'))->toBe($base); })->with(devPathKinds()); it('maps Coolify data and backup paths to the dev volumes on the testing-host server', function () { $server = devPathServer('testing-host'); expect(devHostDockerPath($server, '/data/coolify/applications/abc/data'))->toBe(DEV_PATH_DATA.'/applications/abc/data') ->and(devHostDockerPath($server, '/data/coolify/proxy/'))->toBe(DEV_PATH_DATA.'/proxy/') ->and(devHostDockerPath($server, '/data/coolify/backups/databases/x/file.dmp'))->toBe(DEV_PATH_BACKUPS.'/databases/x/file.dmp') ->and(devHostDockerPath($server, '/data/coolify-other/x'))->toBe('/data/coolify-other/x') ->and(devHostDockerPath($server, '/etc/localtime'))->toBe('/etc/localtime') ->and(devHostDockerPath(null, '/data/coolify/proxy'))->toBe('/data/coolify/proxy'); }); it('mounts application compose bind and content volumes from the server data path', function (string $kind, string $base) { $server = devPathServer($kind); $destination = devPathDestination($server); $application = Application::factory()->create([ 'environment_id' => $this->environment->id, 'destination_id' => $destination->id, 'destination_type' => $destination->getMorphClass(), 'build_pack' => 'dockercompose', 'docker_compose_raw' => DEV_PATH_COMPOSE, ]); $volumes = applicationParser($application)->get('services')->get('app')['volumes']; $directory = "{$base}/applications/{$application->uuid}"; expect(collect($volumes)->all())->toContain("{$directory}/data:/app/data", [ 'type' => 'bind', 'source' => "{$directory}/config/app.conf", 'target' => '/etc/app.conf', 'bind' => ['create_host_path' => true], ]) // Coolify writes the files through SSH to the normal path. ->and($application->fileStorages()->pluck('fs_path')->all()) ->toContain("/data/coolify/applications/{$application->uuid}/config/app.conf"); })->with(devPathKinds()); it('mounts service compose bind and content volumes from the server data path', function (string $kind, string $base) { $server = devPathServer($kind); $destination = devPathDestination($server); $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => $server->id, 'destination_id' => $destination->id, 'destination_type' => $destination->getMorphClass(), 'docker_compose_raw' => DEV_PATH_COMPOSE, ]); ServiceApplication::create(['name' => 'app', 'service_id' => $service->id]); $volumes = serviceParser($service->fresh())->get('services')->get('app')['volumes']; $directory = "{$base}/services/{$service->uuid}"; expect(collect($volumes)->all())->toContain("{$directory}/data:/app/data", [ 'type' => 'bind', 'source' => "{$directory}/config/app.conf", 'target' => '/etc/app.conf', 'bind' => ['create_host_path' => true], ]); })->with(devPathKinds()); it('mounts the Traefik proxy and log rotation directory from the server data path', function (string $kind, string $base) { $server = devPathServer($kind, 'TRAEFIK', analytics: true); $config = Yaml::parse(generateDefaultProxyConfiguration($server)); expect($config['services']['traefik']['volumes'])->toContain("{$base}/proxy/:/traefik") ->and($config['services']['traefik-logrotate']['volumes'])->toBe(["{$base}/proxy/:/traefik"]); })->with(devPathKinds()); it('mounts the Caddy proxy and traffic directories from the server data path', function (string $kind, string $base) { $server = devPathServer($kind, 'CADDY', analytics: true); $volumes = Yaml::parse(generateDefaultProxyConfiguration($server))['services']['caddy']['volumes']; expect($volumes)->toContain( "{$base}/proxy/caddy/dynamic:/dynamic", "{$base}/proxy/caddy/config:/config", "{$base}/proxy/caddy/data:/data", "{$base}/proxy/caddy:/traffic", )->and(StartSentinel::trafficLogDirectory($server))->toBe("{$base}/proxy/caddy"); })->with(devPathKinds()); it('mounts the Sentinel database and traffic log directory from the server data path', function (string $kind, string $base) { $server = devPathServer($kind, 'TRAEFIK', analytics: true); StartSentinel::run($server, latestVersion: '1.0.1'); $directory = "{$base}/proxy"; $script = collect($this->commands)->first(fn (string $command): bool => str_contains($command, 'docker run -d')); expect(StartSentinel::trafficLogDirectory($server))->toBe($directory) ->and($script)->toContain("-v {$base}/sentinel:/app/db") ->toContain(escapeshellarg("{$directory}:{$directory}:ro")) ->toContain(escapeshellarg("TRAFFIC_ACCESS_LOG_PATH={$directory}/access.log")); if ($kind !== 'testing-host') { expect($script)->not->toContain('/var/lib/docker/volumes'); } })->with(devPathKinds()); it('mounts the database proxy configuration from the server data path', function (string $kind, string $base) { $server = devPathServer($kind); $database = create_standalone_postgresql($this->environment->id, devPathDestination($server)); $database->update(['is_public' => true, 'public_port' => 15432]); StartDatabaseProxy::run($database->fresh()); $script = devPathAllCommands(); preg_match("/echo '([A-Za-z0-9+\/=]+)' \| base64 -d \| tee [^ ]+docker-compose\.yaml/", $script, $matches); $compose = Yaml::parse(base64_decode($matches[1])); $volume = $compose['services']["{$database->uuid}-proxy"]['volumes'][0]; expect($volume['source'])->toBe("{$base}/databases/{$database->uuid}/proxy/nginx.conf") // Coolify writes the file through SSH to the normal path. ->and($script)->toContain("mkdir -p /data/coolify/databases/{$database->uuid}/proxy"); })->with(devPathKinds()); it('mounts the database backup file from the server backups path', function (string $kind, string $base) { $server = devPathServer($kind); $backupsBase = $kind === 'testing-host' ? DEV_PATH_BACKUPS : '/data/coolify/backups'; $job = (new ReflectionClass(DatabaseBackupJob::class))->newInstanceWithoutConstructor(); $job->server = $server; $job->backup_location = '/data/coolify/backups/databases/team-1/postgres-abc/pg-dump-1.dmp'; $source = (new ReflectionMethod($job, 'backupMountSource'))->invoke($job); expect($source)->toBe("{$backupsBase}/databases/team-1/postgres-abc/pg-dump-1.dmp"); })->with(devPathKinds()); it('uses the normal Caddy proxy path on start and restart in development', function () { config()->set('constants.coolify.base_config_path', '/srv/coolify'); $server = devPathServer('kvm', 'CADDY'); StartProxy::run($server, async: false, force: true); $restartCommands = (new ReflectionMethod(RestartProxyJob::class, 'buildRestartCommands')) ->invoke(new RestartProxyJob($server), 'services: {}'); expect(devPathAllCommands())->toContain('mkdir -p /srv/coolify/proxy/caddy/dynamic') ->not->toContain('mkdir -p /data/coolify/proxy/caddy/dynamic') ->and(implode("\n", $restartCommands))->toContain('mkdir -p /srv/coolify/proxy/caddy/dynamic') ->not->toContain('/data/coolify/proxy/caddy'); }); it('replaces an old dev volume path in a saved proxy configuration in development', function (string $kind, string $expected) { $server = devPathServer($kind, 'TRAEFIK'); $saved = "services:\n traefik:\n image: 'traefik:v3.6'\n volumes:\n - '/var/run/docker.sock:/var/run/docker.sock:ro'\n - '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/:/traefik'\n"; $server->proxy->last_saved_proxy_configuration = $saved; $server->save(); $configuration = GetProxyConfiguration::run($server->fresh()); expect(Yaml::parse($configuration)['services']['traefik']['volumes'])->toContain("{$expected}:/traefik"); })->with([ 'dev KVM server' => ['kvm', '/data/coolify/proxy/'], 'dev testing-host server' => ['testing-host', DEV_PATH_DATA.'/proxy/'], 'production keeps the saved configuration' => ['production', '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/'], ]);