mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-05 21:47:10 -04:00
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
eeda90a8a4
commit
700d476bac
@@ -56,10 +56,7 @@ class StartDatabaseProxy
|
||||
}
|
||||
|
||||
$configuration_dir = database_proxy_dir($database->uuid);
|
||||
$host_configuration_dir = $configuration_dir;
|
||||
if (isDev()) {
|
||||
$host_configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$database->uuid.'/proxy';
|
||||
}
|
||||
$host_configuration_dir = devHostDockerPath($server, $configuration_dir);
|
||||
$timeoutConfig = $this->buildProxyTimeoutConfig($database->public_port_timeout);
|
||||
$nginxconf = <<<EOF
|
||||
user nginx;
|
||||
|
||||
@@ -48,6 +48,10 @@ class GetProxyConfiguration
|
||||
if (! empty(trim($proxy_configuration ?? '')) && removeLegacyTraefikDashboardExposure($server)) {
|
||||
$proxy_configuration = $server->proxy->get('last_saved_proxy_configuration');
|
||||
}
|
||||
|
||||
if (! empty(trim($proxy_configuration ?? '')) && replaceDevHostDockerProxyPaths($server)) {
|
||||
$proxy_configuration = $server->proxy->get('last_saved_proxy_configuration');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate default configuration as last resort
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Models\Server;
|
||||
@@ -51,11 +50,6 @@ class StartProxy
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
} else {
|
||||
if (isDev()) {
|
||||
if ($proxyType === ProxyTypes::CADDY->value) {
|
||||
$proxy_path = '/data/coolify/proxy/caddy';
|
||||
}
|
||||
}
|
||||
$caddyfile = 'import /dynamic/*.caddy';
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path/dynamic",
|
||||
|
||||
@@ -10,11 +10,12 @@ class StartSentinel
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* Sentinel and the proxy both mount this host path, and Sentinel reads the access log at the same path.
|
||||
*/
|
||||
public static function trafficLogDirectory(Server $server): string
|
||||
{
|
||||
return isDev()
|
||||
? devCoolifyDataPath().'/proxy'
|
||||
: rtrim($server->proxyPath(), '/');
|
||||
return devHostDockerPath($server, rtrim($server->proxyPath(), '/'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +89,7 @@ class StartSentinel
|
||||
$token = $server->settings->ensureValidSentinelToken();
|
||||
$endpoint = $server->settings->ensureSentinelUrl();
|
||||
$debug = data_get($server, 'settings.is_sentinel_debug_enabled');
|
||||
$mountDir = '/data/coolify/sentinel';
|
||||
$mountDir = devHostDockerPath($server, base_configuration_dir().'/sentinel');
|
||||
$image = coolifyRegistryUrl().'/coollabsio/sentinel:'.$version;
|
||||
$environments = [
|
||||
'TOKEN' => $token,
|
||||
@@ -108,7 +109,6 @@ class StartSentinel
|
||||
if ($customImage && ! empty($customImage)) {
|
||||
$image = $customImage;
|
||||
}
|
||||
$mountDir = devCoolifyDataPath().'/sentinel';
|
||||
}
|
||||
$dockerEnvironments = implode(' ', array_map(fn ($key, $value) => '-e '.escapeshellarg("$key=$value"), array_keys($environments), $environments));
|
||||
$dockerLabels = implode(' ', array_map(fn ($key, $value) => "$key=$value", array_keys($labels), $labels));
|
||||
|
||||
@@ -801,6 +801,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return instant_remote_process(['du -b '.escapeshellarg($this->backup_location).' | cut -f1'], $this->server, false, false, null, disableMultiplexing: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Host path of the backup file for the upload container. It differs from backup_location only for the
|
||||
* development testing-host server (see devHostDockerPath()).
|
||||
*/
|
||||
private function backupMountSource(): string
|
||||
{
|
||||
return devHostDockerPath($this->server, $this->backup_location);
|
||||
}
|
||||
|
||||
private function upload_to_s3(): void
|
||||
{
|
||||
if (is_null($this->s3)) {
|
||||
@@ -835,16 +844,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
instant_remote_process(["docker rm -f backup-of-{$this->backup_log_uuid}"], $this->server, false, false, null, disableMultiplexing: true);
|
||||
}
|
||||
|
||||
if (isDev()) {
|
||||
if ($this->database->name === 'coolify-db') {
|
||||
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
|
||||
} else {
|
||||
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file;
|
||||
}
|
||||
} else {
|
||||
$backup_location_from = $this->backup_location;
|
||||
}
|
||||
$mount = escapeshellarg($backup_location_from.':'.$this->backup_location.':ro');
|
||||
$mount = escapeshellarg($this->backupMountSource().':'.$this->backup_location.':ro');
|
||||
$commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v {$mount} {$fullImageName}";
|
||||
|
||||
// Escape S3 credentials to prevent command injection
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Jobs;
|
||||
|
||||
use App\Actions\Proxy\GetProxyConfiguration;
|
||||
use App\Actions\Proxy\SaveProxyConfiguration;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Models\Server;
|
||||
use App\Services\ProxyDashboardCacheService;
|
||||
@@ -87,7 +86,6 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
|
||||
*/
|
||||
private function buildRestartCommands(string $configuration): array
|
||||
{
|
||||
$proxyType = $this->server->proxyType();
|
||||
$containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
$proxy_path = $this->server->proxyPath();
|
||||
$stopTimeout = 30;
|
||||
@@ -136,9 +134,6 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
} else {
|
||||
if (isDev() && $proxyType === ProxyTypes::CADDY->value) {
|
||||
$proxy_path = '/data/coolify/proxy/caddy';
|
||||
}
|
||||
$caddyfile = 'import /dynamic/*.caddy';
|
||||
$commands = $commands->merge([
|
||||
"echo 'Starting proxy...'",
|
||||
|
||||
+19
-5
@@ -136,6 +136,11 @@ class Server extends BaseModel
|
||||
*/
|
||||
public const MINIMUM_CURRENT_CADDY_PROXY_VERSION = [2, 9];
|
||||
|
||||
/**
|
||||
* Address of the development `testing-host` server (docker-compose.dev*.yml, ServerSeeder).
|
||||
*/
|
||||
public const DEV_TESTING_HOST_IP = 'coolify-testing-host';
|
||||
|
||||
public static $batch_counter = 0;
|
||||
|
||||
/**
|
||||
@@ -594,11 +599,6 @@ class Server extends BaseModel
|
||||
$proxy_type = $this->proxyType();
|
||||
$redirect_enabled = $this->proxy->redirect_enabled ?? true;
|
||||
$redirect_url = $this->proxy->redirect_url;
|
||||
if (isDev()) {
|
||||
if ($proxy_type === ProxyTypes::CADDY->value) {
|
||||
$dynamic_conf_path = '/data/coolify/proxy/caddy/dynamic';
|
||||
}
|
||||
}
|
||||
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
|
||||
$default_redirect_file = "$dynamic_conf_path/default_redirect_503.yaml";
|
||||
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
|
||||
@@ -956,6 +956,20 @@ $siteAddress {
|
||||
return $this->ip === 'host.docker.internal' || $this->id === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* True only in development for the `testing-host` server. That container runs containers on the
|
||||
* host Docker daemon (/var/run/docker.sock), but its /data/coolify is a Docker named volume. The host
|
||||
* daemon must therefore mount the volume's host path instead of /data/coolify (see devHostDockerPath()).
|
||||
*
|
||||
* Dev KVM VMs and all other servers have their own Docker daemon and their own /data/coolify.
|
||||
* A `host.docker.internal` server writes to the real host /data/coolify, so it also needs no change.
|
||||
*/
|
||||
public function sharesDevHostDocker(): bool
|
||||
{
|
||||
// The saving hook can leave a Stringable in `ip`, so compare the string value.
|
||||
return isDev() && (string) $this->ip === self::DEV_TESTING_HOST_IP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Usable dedicated (build-only) servers of a team. Servers with the combined role
|
||||
* host deployments, so they are never picked as build servers.
|
||||
|
||||
@@ -1111,13 +1111,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
'resource_type' => get_class($originalResource),
|
||||
]
|
||||
);
|
||||
if (isDev()) {
|
||||
if ((int) $resource->compose_parsing_version >= 4) {
|
||||
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid);
|
||||
} else {
|
||||
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid);
|
||||
}
|
||||
}
|
||||
// The file storage keeps the path that Coolify writes; the Docker daemon may need another one.
|
||||
$source = $source->replace($mainDirectory, devHostDockerPath($server, $mainDirectory->value()));
|
||||
$volume = "$source:$target";
|
||||
if (isset($parsed['mode']) && $parsed['mode']) {
|
||||
$volume .= ':'.$parsed['mode']->value();
|
||||
@@ -2465,13 +2460,8 @@ function serviceParser(Service $resource): Collection
|
||||
'resource_type' => get_class($originalResource),
|
||||
]
|
||||
);
|
||||
if (isDev()) {
|
||||
if ((int) $resource->compose_parsing_version >= 4) {
|
||||
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid);
|
||||
} else {
|
||||
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid);
|
||||
}
|
||||
}
|
||||
// The file storage keeps the path that Coolify writes; the Docker daemon may need another one.
|
||||
$source = $source->replace($mainDirectory, devHostDockerPath($server, $mainDirectory->value()));
|
||||
$volume = "$source:$target";
|
||||
if (isset($parsed['mode']) && $parsed['mode']) {
|
||||
$volume .= ':'.$parsed['mode']->value();
|
||||
|
||||
@@ -164,8 +164,8 @@ function applyTrafficAnalyticsToProxyConfigArray(Server $server, array $config):
|
||||
data_set($config, 'services.traefik.command', applyTraefikAccessLogCommands($server, $commands, $enabled));
|
||||
unset($config['services']['traefik-logrotate']);
|
||||
|
||||
if ($enabled && ! $server->isSwarm() && ! isDev()) {
|
||||
$proxyPath = $server->proxyPath();
|
||||
if ($enabled && ! $server->isSwarm()) {
|
||||
$proxyPath = devHostDockerPath($server, $server->proxyPath());
|
||||
$config['services']['traefik-logrotate'] = [
|
||||
'container_name' => 'coolify-proxy-logrotate',
|
||||
'image' => 'alpine:3.24',
|
||||
@@ -534,6 +534,47 @@ function removeLegacyTraefikDashboardExposure(Server $server): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Development only. Older dev builds wrote the dev data volume path (/var/lib/docker/volumes/<name>_coolify_data/_data)
|
||||
* into the proxy configuration of every server. Replace it with the path for this server (devHostDockerPath()):
|
||||
* /data/coolify on a dev KVM VM, the configured volume path on the testing-host server. The next proxy restart applies it.
|
||||
*/
|
||||
function replaceDevHostDockerProxyPaths(Server $server): bool
|
||||
{
|
||||
if (! app()->bound('config') || ! isDev()) {
|
||||
return false;
|
||||
}
|
||||
$configuration = $server->proxy->get('last_saved_proxy_configuration');
|
||||
if (! is_string($configuration) || blank($configuration)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$configuredVolume = preg_quote(basename(dirname(devDockerVolumeDataPath('constants.coolify.dev_data_volume', 'coolify_dev_coolify_data'))), '#');
|
||||
$pattern = "#/var/lib/docker/volumes/(?:[A-Za-z0-9][A-Za-z0-9_.-]*_coolify_data|{$configuredVolume})/_data(?=[/:'\"\\s]|$)#m";
|
||||
$fixed = preg_replace($pattern, devHostDockerPath($server, base_configuration_dir()), $configuration);
|
||||
if (! is_string($fixed) || $fixed === $configuration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// The Caddy /traffic mount and the Traefik log rotation mount depend on the proxy type, so rebuild them.
|
||||
$fixed = applyTrafficAnalyticsToProxyConfiguration($server, $fixed);
|
||||
} catch (Throwable) {
|
||||
// Keep the plain path replacement for a configuration that is not a YAML mapping.
|
||||
}
|
||||
|
||||
if (blank($server->proxy->get('last_applied_settings'))) {
|
||||
$server->proxy->last_applied_settings = md5(base64_encode($configuration));
|
||||
}
|
||||
$server->proxy->last_saved_proxy_configuration = $fixed;
|
||||
$server->proxy->last_saved_settings = md5(base64_encode($fixed));
|
||||
$server->save();
|
||||
|
||||
Log::info('Replaced the development data volume path in the proxy configuration', ['server_id' => $server->id]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function generateDefaultProxyConfiguration(Server $server, array $custom_commands = [])
|
||||
{
|
||||
Log::info('Generating default proxy configuration', [
|
||||
@@ -633,11 +674,10 @@ function generateDefaultProxyConfiguration(Server $server, array $custom_command
|
||||
$config['services']['traefik']['command'][] = '--api.insecure=true';
|
||||
$config['services']['traefik']['command'][] = '--log.level=debug';
|
||||
$config['services']['traefik']['command'][] = '--accesslog.bufferingsize=100';
|
||||
$config['services']['traefik']['volumes'][] = devCoolifyDataPath().'/proxy/:/traefik';
|
||||
} else {
|
||||
$config['services']['traefik']['command'][] = '--api.insecure=false';
|
||||
$config['services']['traefik']['volumes'][] = "{$proxy_path}:/traefik";
|
||||
}
|
||||
$config['services']['traefik']['volumes'][] = devHostDockerPath($server, $proxy_path).':/traefik';
|
||||
if ($server->isSwarm()) {
|
||||
data_forget($config, 'services.traefik.container_name');
|
||||
data_forget($config, 'services.traefik.restart');
|
||||
@@ -692,9 +732,9 @@ function generateDefaultProxyConfiguration(Server $server, array $custom_command
|
||||
],
|
||||
'volumes' => [
|
||||
'/var/run/docker.sock:/var/run/docker.sock:ro',
|
||||
"{$proxy_path}/dynamic:/dynamic",
|
||||
"{$proxy_path}/config:/config",
|
||||
"{$proxy_path}/data:/data",
|
||||
devHostDockerPath($server, "{$proxy_path}/dynamic").':/dynamic',
|
||||
devHostDockerPath($server, "{$proxy_path}/config").':/config',
|
||||
devHostDockerPath($server, "{$proxy_path}/data").':/data',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -971,14 +971,44 @@ function isDev(): bool
|
||||
}
|
||||
|
||||
/**
|
||||
* Host path of the Coolify data volume in development. The proxy and Sentinel on one server both mount
|
||||
* paths below it, so they must use this value. An invalid volume name falls back to the legacy name.
|
||||
* Path that the Docker daemon of $server must use as a bind mount source for $path, a path that Coolify
|
||||
* writes through SSH (below base_configuration_dir()).
|
||||
*
|
||||
* Only the development `testing-host` server needs a different path (Server::sharesDevHostDocker()):
|
||||
* it writes to Docker named volumes, but it starts containers on the host Docker daemon. The returned
|
||||
* paths match its mounts in docker-compose.dev*.yml:
|
||||
* - /data/coolify/backups/... -> /var/lib/docker/volumes/<DEV_COOLIFY_BACKUPS_VOLUME>/_data/...
|
||||
* - /data/coolify/... -> /var/lib/docker/volumes/<DEV_COOLIFY_DATA_VOLUME>/_data/...
|
||||
*
|
||||
* For all other servers (production, dev KVM VMs, remote servers) the function returns $path unchanged.
|
||||
*/
|
||||
function devCoolifyDataPath(): string
|
||||
function devHostDockerPath(?Server $server, string $path): string
|
||||
{
|
||||
$volume = (string) config('constants.coolify.dev_data_volume');
|
||||
if (! $server?->sharesDevHostDocker()) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
$mounts = [
|
||||
backup_dir() => devDockerVolumeDataPath('constants.coolify.dev_backups_volume', 'coolify_dev_backups_data'),
|
||||
base_configuration_dir() => devDockerVolumeDataPath('constants.coolify.dev_data_volume', 'coolify_dev_coolify_data'),
|
||||
];
|
||||
foreach ($mounts as $containerPath => $hostPath) {
|
||||
if ($path === $containerPath || str_starts_with($path, $containerPath.'/')) {
|
||||
return $hostPath.substr($path, strlen($containerPath));
|
||||
}
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Host path of a development Docker volume. An invalid volume name falls back to the legacy name.
|
||||
*/
|
||||
function devDockerVolumeDataPath(string $configKey, string $fallbackVolume): string
|
||||
{
|
||||
$volume = (string) config($configKey);
|
||||
if (preg_match('/^[A-Za-z0-9][A-Za-z0-9_.-]*$/', $volume) !== 1) {
|
||||
$volume = 'coolify_dev_coolify_data';
|
||||
$volume = $fallbackVolume;
|
||||
}
|
||||
|
||||
return "/var/lib/docker/volumes/{$volume}/_data";
|
||||
|
||||
@@ -8,8 +8,10 @@ return [
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
'autoupdate' => env('AUTOUPDATE'),
|
||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||
// Development only: Docker volume that holds /data/coolify; the proxy and Sentinel share traffic logs below it.
|
||||
// Development only: Docker volumes that the testing-host server mounts at /data/coolify and /data/coolify/backups.
|
||||
// devHostDockerPath() uses them only for that server. The defaults are the legacy docker-compose.dev.yml names.
|
||||
'dev_data_volume' => env('DEV_COOLIFY_DATA_VOLUME', 'coolify_dev_coolify_data'),
|
||||
'dev_backups_volume' => env('DEV_COOLIFY_BACKUPS_VOLUME', 'coolify_dev_backups_data'),
|
||||
'registry_url' => env('REGISTRY_URL', 'ghcr.io'),
|
||||
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'),
|
||||
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
|
||||
|
||||
@@ -60,6 +60,10 @@ services:
|
||||
DEVELOPMENT_QEMU_SLOT: "${DEVELOPMENT_QEMU_SLOT:-}"
|
||||
DEVELOPMENT_QEMU_DOCKER_NETWORK: "${COMPOSE_PROJECT_NAME:-coolify-dev}"
|
||||
DEVELOPMENT_QEMU_COOLIFY_CONTAINER: "${COMPOSE_PROJECT_NAME:-coolify-dev}"
|
||||
# Host paths of the volumes that testing-host mounts at /data/coolify and /data/coolify/backups.
|
||||
# Coolify uses them only for the testing-host server (it shares the host Docker daemon), not for KVM servers.
|
||||
DEV_COOLIFY_DATA_VOLUME: "${COMPOSE_PROJECT_NAME:-coolify-dev}_coolify_data"
|
||||
DEV_COOLIFY_BACKUPS_VOLUME: "${COMPOSE_PROJECT_NAME:-coolify-dev}_backups_data"
|
||||
healthcheck:
|
||||
test: curl -sf http://127.0.0.1:8080/api/health || exit 1
|
||||
interval: 5s
|
||||
|
||||
@@ -47,6 +47,7 @@ networks:
|
||||
YAML;
|
||||
|
||||
beforeEach(function () {
|
||||
Server::flushIdentityMap();
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
|
||||
config(['constants.ssh.mux_enabled' => false]);
|
||||
Queue::fake();
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Proxy\GetProxyConfiguration;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Server\StartSentinel;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Events\SentinelRestarted;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\RestartProxyJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
const DEV_PATH_DATA = '/var/lib/docker/volumes/coolify-dev-feature_coolify_data/_data';
|
||||
const DEV_PATH_BACKUPS = '/var/lib/docker/volumes/coolify-dev-feature_backups_data/_data';
|
||||
|
||||
const DEV_PATH_COMPOSE = <<<'YAML'
|
||||
services:
|
||||
app:
|
||||
image: nginx:alpine
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- type: bind
|
||||
source: ./config/app.conf
|
||||
target: /etc/app.conf
|
||||
content: |
|
||||
listen 8080;
|
||||
YAML;
|
||||
|
||||
beforeEach(function () {
|
||||
// StandaloneDocker::server uses a static identity map that other tests can fill with the same server id.
|
||||
Server::flushIdentityMap();
|
||||
Bus::fake();
|
||||
Notification::fake();
|
||||
Event::fake([SentinelRestarted::class, ProxyStatusChanged::class, ProxyStatusChangedUI::class]);
|
||||
InstanceSettings::forceCreate(['id' => 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($process->command) ? implode(' ', $process->command) : $process->command;
|
||||
|
||||
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<string, array{0: string, 1: string}>
|
||||
*/
|
||||
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", "{$directory}/config/app.conf:/etc/app.conf")
|
||||
// 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", "{$directory}/config/app.conf:/etc/app.conf");
|
||||
})->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/'],
|
||||
]);
|
||||
@@ -60,9 +60,13 @@ it('uses a default caddy image that supports per-app traffic attribution', funct
|
||||
->and($server->fresh()->caddySupportsLogAppend())->toBeTrue();
|
||||
});
|
||||
|
||||
function caddyTrafficServer(object $test, bool $analyticsEnabled = true): Server
|
||||
function caddyTrafficServer(object $test, bool $analyticsEnabled = true, ?string $ip = null): Server
|
||||
{
|
||||
$server = Server::factory()->create(['team_id' => $test->team->id, 'private_key_id' => $test->privateKey->id]);
|
||||
$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;
|
||||
@@ -80,40 +84,48 @@ it('mounts the Sentinel traffic log directory into Caddy in production', functio
|
||||
->and($config['services']['caddy']['volumes'])->toContain('/data/coolify/proxy/caddy:/traffic');
|
||||
});
|
||||
|
||||
it('mounts the Sentinel traffic log directory into Caddy in development', function () {
|
||||
it('mounts the Sentinel traffic log directory into Caddy in development', function (?string $ip, string $directory) {
|
||||
config()->set('app.env', 'local');
|
||||
$server = caddyTrafficServer($this);
|
||||
$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('/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy:/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', function () {
|
||||
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);
|
||||
$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]);
|
||||
$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')
|
||||
->and($caddyVolumes)->toContain('/var/lib/docker/volumes/coolify-dev-feature_coolify_data/_data/proxy:/traffic')
|
||||
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 data volume for an invalid volume name', function (?string $volume) {
|
||||
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(devCoolifyDataPath())->toBe('/var/lib/docker/volumes/coolify_dev_coolify_data/_data');
|
||||
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],
|
||||
|
||||
@@ -47,17 +47,21 @@ it('produces traffic + geoip env when enabled', function () {
|
||||
expect($env)->toHaveKey('TRAFFIC_ACCESS_LOG_PATH');
|
||||
});
|
||||
|
||||
it('uses the dev proxy volume for traffic logs locally', function () {
|
||||
it('uses the dev proxy volume for traffic logs only on the testing-host server locally', function (string $ip, string $directory) {
|
||||
config()->set('app.env', 'local');
|
||||
$server = trafficProxyServer($this);
|
||||
$server->update(['ip' => $ip]);
|
||||
$server->settings->is_traffic_analytics_enabled = true;
|
||||
$server->settings->save();
|
||||
|
||||
expect(StartSentinel::trafficLogDirectory($server->fresh()))
|
||||
->toBe('/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy');
|
||||
->toBe($directory);
|
||||
expect(StartSentinel::sentinelTrafficEnvironment($server->fresh())['TRAFFIC_ACCESS_LOG_PATH'])
|
||||
->toBe('/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/access.log');
|
||||
});
|
||||
->toBe("{$directory}/access.log");
|
||||
})->with([
|
||||
'dev KVM server' => ['10.221.1.10', '/data/coolify/proxy'],
|
||||
'dev testing-host server' => [Server::DEV_TESTING_HOST_IP, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy'],
|
||||
]);
|
||||
|
||||
it('passes custom traffic settings as sentinel env', function () {
|
||||
$server = trafficProxyServer($this);
|
||||
@@ -207,11 +211,13 @@ it('keeps the access log commands valid for non-root servers', function () {
|
||||
expect($syntax->isSuccessful())->toBeTrue($syntax->getErrorOutput());
|
||||
});
|
||||
|
||||
it('mounts the configured dev data volume for traffic logs and Sentinel data', function () {
|
||||
it('mounts the configured dev data volume for traffic logs and Sentinel data on the testing-host server', function () {
|
||||
config()->set('app.env', 'local');
|
||||
config()->set('constants.coolify.dev_data_volume', 'coolify-dev-feature_coolify_data');
|
||||
$server = sentinelTrafficServer($this, 'CADDY', analyticsEnabled: true);
|
||||
$directory = '/var/lib/docker/volumes/coolify-dev-feature_coolify_data/_data/proxy';
|
||||
$server->update(['ip' => Server::DEV_TESTING_HOST_IP]);
|
||||
$server = $server->fresh();
|
||||
$directory = '/var/lib/docker/volumes/coolify-dev-feature_coolify_data/_data/proxy/caddy';
|
||||
|
||||
$script = runStartSentinelAndCaptureScript($server);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user