Files
Andras BacsaiandClaude Opus 5.5 700d476bac fix(dev): rewrite host paths only for the testing-host dev server
Development mode rewrote /data/coolify paths to the dev Docker volume
for every server. That is only right for the testing-host backend,
which uses the host Docker daemon. The KVM localhost and other dev VMs
have their own /data/coolify, so bind mounts, Compose content files,
the Traefik and Caddy traffic mounts, Sentinel, and database proxies
pointed to wrong paths there.

- Server::sharesDevHostDocker() and devHostDockerPath() decide per
  server; only the testing-host server (ip coolify-testing-host) gets
  the volume path. Production is unchanged.
- The testing-host volume names come from the Compose project
  (DEV_COOLIFY_DATA_VOLUME, DEV_COOLIFY_BACKUPS_VOLUME), with the
  legacy names as fallback.
- In development, saved proxy configurations with the old path are
  fixed when they load and ask for a proxy restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:21:39 +02:00

157 lines
7.1 KiB
PHP

<?php
use App\Actions\Server\StartSentinel;
use App\Models\PrivateKey;
use App\Models\Server;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Process;
use Symfony\Component\Yaml\Yaml;
uses(RefreshDatabase::class);
beforeEach(function () {
// generateDefaultProxyConfiguration() synchronously persists the config to the
// server over SSH (SaveProxyConfiguration); fake the process layer so tests
// don't attempt a real SSH connection.
Process::fake();
$user = User::factory()->create();
$this->team = $user->teams()->first();
$this->privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
});
it('does not mount the traffic volume for caddy when traffic analytics is disabled', function () {
$server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $this->privateKey->id]);
$server->proxy->set('type', 'CADDY');
$server->save();
$server->settings->is_traffic_analytics_enabled = false;
$server->settings->save();
$config = Yaml::parse(generateDefaultProxyConfiguration($server->fresh()));
expect($config['services']['caddy']['volumes'])
->not->toContain($server->proxyPath().':/traffic');
});
it('mounts the traffic volume for caddy when traffic analytics is enabled', function () {
$server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $this->privateKey->id]);
$server->proxy->set('type', 'CADDY');
$server->save();
$server->settings->is_traffic_analytics_enabled = true;
$server->settings->save();
$config = Yaml::parse(generateDefaultProxyConfiguration($server->fresh()));
expect($config['services']['caddy']['volumes'])
->toContain($server->proxyPath().':/traffic');
});
it('uses a default caddy image that supports per-app traffic attribution', function () {
$server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $this->privateKey->id]);
$server->proxy->set('type', 'CADDY');
$server->save();
$config = Yaml::parse(generateDefaultProxyConfiguration($server->fresh()));
// caddy-docker-proxy 2.13 ships Caddy 2.11, which knows log_append; the old 2.8 default shipped Caddy 2.7.6.
expect($config['services']['caddy']['image'])->toBe('lucaslorentz/caddy-docker-proxy:2.13-alpine')
->and($server->fresh()->caddySupportsLogAppend())->toBeTrue();
});
function caddyTrafficServer(object $test, bool $analyticsEnabled = true, ?string $ip = null): Server
{
$server = Server::factory()->create(array_filter([
'team_id' => $test->team->id,
'private_key_id' => $test->privateKey->id,
'ip' => $ip,
]));
$server->proxy->set('type', 'CADDY');
$server->save();
$server->settings->is_traffic_analytics_enabled = $analyticsEnabled;
$server->settings->save();
return $server->fresh();
}
it('mounts the Sentinel traffic log directory into Caddy in production', function () {
$server = caddyTrafficServer($this);
$config = Yaml::parse(generateDefaultProxyConfiguration($server));
expect(StartSentinel::trafficLogDirectory($server))->toBe('/data/coolify/proxy/caddy')
->and($config['services']['caddy']['volumes'])->toContain('/data/coolify/proxy/caddy:/traffic');
});
it('mounts the Sentinel traffic log directory into Caddy in development', function (?string $ip, string $directory) {
config()->set('app.env', 'local');
$server = caddyTrafficServer($this, ip: $ip);
$volumes = Yaml::parse(generateDefaultProxyConfiguration($server))['services']['caddy']['volumes'];
$trafficVolumes = array_values(array_filter($volumes, fn (string $volume): bool => str_ends_with($volume, ':/traffic')));
// Caddy writes /traffic/access.log, Sentinel reads <trafficLogDirectory>/access.log.
expect($trafficVolumes)->toBe([StartSentinel::trafficLogDirectory($server).':/traffic'])
->and($trafficVolumes[0])->toBe("{$directory}:/traffic");
})->with([
// A dev KVM server has its own Docker daemon and its own /data/coolify, like production.
'dev KVM server' => ['10.221.1.10', '/data/coolify/proxy/caddy'],
// The testing-host server uses the host Docker daemon, so it mounts the dev data volume.
'dev testing-host server' => [Server::DEV_TESTING_HOST_IP, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy'],
]);
it('uses the configured dev data volume for Caddy, Traefik, and Sentinel on the testing-host server', function () {
config()->set('app.env', 'local');
config()->set('constants.coolify.dev_data_volume', 'coolify-dev-feature_coolify_data');
$caddy = caddyTrafficServer($this, ip: Server::DEV_TESTING_HOST_IP);
$caddyVolumes = Yaml::parse(generateDefaultProxyConfiguration($caddy))['services']['caddy']['volumes'];
$traefik = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $this->privateKey->id, 'ip' => Server::DEV_TESTING_HOST_IP]);
$traefik->proxy->set('type', 'TRAEFIK');
$traefik->save();
$traefikVolumes = Yaml::parse(generateDefaultProxyConfiguration($traefik->fresh()))['services']['traefik']['volumes'];
expect(StartSentinel::trafficLogDirectory($caddy))->toBe('/var/lib/docker/volumes/coolify-dev-feature_coolify_data/_data/proxy/caddy')
->and($caddyVolumes)->toContain('/var/lib/docker/volumes/coolify-dev-feature_coolify_data/_data/proxy/caddy:/traffic')
->and($traefikVolumes)->toContain('/var/lib/docker/volumes/coolify-dev-feature_coolify_data/_data/proxy/:/traefik');
});
it('falls back to the legacy dev volumes for an invalid volume name', function (?string $volume) {
config()->set('app.env', 'local');
config()->set('constants.coolify.dev_data_volume', $volume);
config()->set('constants.coolify.dev_backups_volume', $volume);
$server = caddyTrafficServer($this, ip: Server::DEV_TESTING_HOST_IP);
expect(devHostDockerPath($server, '/data/coolify'))->toBe('/var/lib/docker/volumes/coolify_dev_coolify_data/_data')
->and(devHostDockerPath($server, '/data/coolify/backups/x'))->toBe('/var/lib/docker/volumes/coolify_dev_backups_data/_data/x');
})->with([
'empty' => [''],
'null' => [null],
'path traversal' => ['../../etc'],
'shell characters' => ['vol;rm -rf /'],
]);
it('replaces a stale Caddy traffic mount and removes it when analytics is disabled', function () {
config()->set('app.env', 'local');
$server = caddyTrafficServer($this);
$config = ['services' => ['caddy' => ['volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro',
'/data/coolify/proxy/caddy:/traffic',
]]]];
$enabled = applyTrafficAnalyticsToProxyConfigArray($server, $config);
$server->settings->is_traffic_analytics_enabled = false;
$server->settings->save();
$disabled = applyTrafficAnalyticsToProxyConfigArray($server->fresh(), $enabled);
expect($enabled['services']['caddy']['volumes'])->toBe([
'/var/run/docker.sock:/var/run/docker.sock:ro',
StartSentinel::trafficLogDirectory($server).':/traffic',
])->and($disabled['services']['caddy']['volumes'])->toBe([
'/var/run/docker.sock:/var/run/docker.sock:ro',
]);
});