diff --git a/app/Actions/Proxy/SaveProxyConfiguration.php b/app/Actions/Proxy/SaveProxyConfiguration.php index 7196301729..ff143763b1 100644 --- a/app/Actions/Proxy/SaveProxyConfiguration.php +++ b/app/Actions/Proxy/SaveProxyConfiguration.php @@ -6,6 +6,8 @@ use App\Models\Server; use App\Services\ProxyPortParser; use Illuminate\Validation\ValidationException; use Lorisleiva\Actions\Concerns\AsAction; +use Symfony\Component\Yaml\Tag\TaggedValue; +use Symfony\Component\Yaml\Yaml; class SaveProxyConfiguration { @@ -23,6 +25,10 @@ class SaveProxyConfiguration ]); } + if (! $server->isSwarm()) { + $this->ensureDefaultContainerName($configuration); + } + $proxy_path = $server->proxyPath(); $docker_compose_yml_base64 = base64_encode($configuration); $new_hash = str($docker_compose_yml_base64)->pipe('md5')->value; @@ -60,4 +66,29 @@ class SaveProxyConfiguration instant_remote_process($commands, $server); } + + /** + * Coolify finds the proxy by its container name, so a renamed or unnamed proxy shows as stopped (#7083). + */ + private function ensureDefaultContainerName(string $configuration): void + { + $services = data_get(Yaml::parse($configuration, Yaml::PARSE_CUSTOM_TAGS), 'services'); + + foreach (['traefik', 'caddy'] as $proxyService) { + if (! is_array($services) || ! array_key_exists($proxyService, $services)) { + continue; + } + + $containerName = data_get($services, "{$proxyService}.container_name"); + if ($containerName instanceof TaggedValue) { + $containerName = $containerName->getValue(); + } + + if ($containerName !== 'coolify-proxy') { + throw ValidationException::withMessages([ + 'configuration' => ["The {$proxyService} container_name must be coolify-proxy. Coolify uses this name to manage the proxy."], + ]); + } + } + } } diff --git a/tests/Feature/Proxy/NonRootProxyCommandsTest.php b/tests/Feature/Proxy/NonRootProxyCommandsTest.php index f1cda7eb28..04ac7980e6 100644 --- a/tests/Feature/Proxy/NonRootProxyCommandsTest.php +++ b/tests/Feature/Proxy/NonRootProxyCommandsTest.php @@ -112,8 +112,8 @@ it('deploys the swarm proxy stack with an absolute compose file path', function it('builds restart commands without entering proxy directories as the SSH user', function (string $proxyType, string $proxyPath) { useNonRootProxy($this->server, $proxyType); $configuration = $this->server->proxyType() === ProxyTypes::CADDY->value - ? "services:\n caddy:\n image: lucaslorentz/caddy-docker-proxy:2.13-alpine\n" - : "services:\n traefik:\n image: traefik:v3.6\n"; + ? "services:\n caddy:\n container_name: coolify-proxy\n image: lucaslorentz/caddy-docker-proxy:2.13-alpine\n" + : "services:\n traefik:\n container_name: coolify-proxy\n image: traefik:v3.6\n"; $method = new ReflectionMethod(RestartProxyJob::class, 'buildRestartCommands'); $commands = $method->invoke(new RestartProxyJob($this->server), $configuration); @@ -133,7 +133,7 @@ it('backs up and prunes proxy configurations through sudo without bash', functio $this->server->proxy->last_saved_settings = 'a1b2c3d4e5f6'; $this->server->save(); - SaveProxyConfiguration::run($this->server, "services:\n caddy:\n image: lucaslorentz/caddy-docker-proxy:2.13-alpine\n"); + SaveProxyConfiguration::run($this->server, "services:\n caddy:\n container_name: coolify-proxy\n image: lucaslorentz/caddy-docker-proxy:2.13-alpine\n"); $lines = nonRootProxyScriptLines(); $backups = '/data/coolify/proxy/caddy/backups'; diff --git a/tests/Feature/Proxy/ProxyContainerNameTest.php b/tests/Feature/Proxy/ProxyContainerNameTest.php new file mode 100644 index 0000000000..296270d685 --- /dev/null +++ b/tests/Feature/Proxy/ProxyContainerNameTest.php @@ -0,0 +1,65 @@ + 0]); + Process::fake(); + + $team = Team::factory()->create(); + $this->server = Server::factory()->create([ + 'team_id' => $team->id, + 'private_key_id' => PrivateKey::factory()->create(['team_id' => $team->id])->id, + ]); + $this->server->settings->update(['is_reachable' => true, 'is_usable' => true, 'force_disabled' => false]); + $this->server->proxy->type = ProxyTypes::TRAEFIK->value; + $this->server->save(); + $this->server->refresh(); +}); + +it('rejects a renamed proxy container before database or remote writes', function () { + $originalProxy = $this->server->proxy->toArray(); + $configuration = "services:\n traefik:\n container_name: utility-servers-proxy\n image: traefik:v3.7\n"; + + expect(fn () => SaveProxyConfiguration::run($this->server, $configuration)) + ->toThrow(ValidationException::class, 'The traefik container_name must be coolify-proxy.'); + + expect($this->server->fresh()->proxy->toArray())->toBe($originalProxy); + Process::assertNothingRan(); +}); + +it('rejects a proxy service without a container name', function () { + $configuration = "services:\n traefik:\n image: traefik:v3.7\n"; + + expect(fn () => SaveProxyConfiguration::run($this->server, $configuration)) + ->toThrow(ValidationException::class, 'The traefik container_name must be coolify-proxy.'); + + Process::assertNothingRan(); +}); + +it('saves a proxy configuration with the default container name', function () { + $configuration = "services:\n traefik:\n container_name: coolify-proxy\n image: traefik:v3.7\n traefik-logrotate:\n container_name: coolify-proxy-logrotate\n"; + + SaveProxyConfiguration::run($this->server, $configuration); + + expect($this->server->fresh()->proxy->last_saved_proxy_configuration)->toBe($configuration); +}); + +it('allows any container name on a swarm server', function () { + $this->server->settings()->update(['is_swarm_manager' => true]); + $configuration = "services:\n traefik:\n container_name: utility-servers-proxy\n image: traefik:v3.7\n"; + + SaveProxyConfiguration::run($this->server->fresh(), $configuration); + + expect($this->server->fresh()->proxy->last_saved_proxy_configuration)->toBe($configuration); +}); diff --git a/tests/Feature/Proxy/ProxyPortVariablesTest.php b/tests/Feature/Proxy/ProxyPortVariablesTest.php index c6af6871a3..b76c423be2 100644 --- a/tests/Feature/Proxy/ProxyPortVariablesTest.php +++ b/tests/Feature/Proxy/ProxyPortVariablesTest.php @@ -54,7 +54,7 @@ dataset('proxy port variables', [ function proxyConfigurationWithPort(string $port): string { - return "services:\n traefik:\n image: traefik:v3.6\n ports:\n - '{$port}'\n - '443:443'\n"; + return "services:\n traefik:\n container_name: coolify-proxy\n image: traefik:v3.6\n ports:\n - '{$port}'\n - '443:443'\n"; } it('saves a proxy configuration whose ports use variables', function (string $port) { @@ -87,7 +87,7 @@ it('restarts a proxy whose ports use variables', function (string $port) { })->with('proxy port variables'); it('asks to start a stopped proxy whose ports use variables and checks only concrete ports', function () { - $this->server->proxy->last_saved_proxy_configuration = "services:\n traefik:\n image: traefik:v3.6\n ports:\n - '\${HTTP_PORT:-8081}:80'\n - '\${HTTPS_PORT}:443'\n"; + $this->server->proxy->last_saved_proxy_configuration = "services:\n traefik:\n container_name: coolify-proxy\n image: traefik:v3.6\n ports:\n - '\${HTTP_PORT:-8081}:80'\n - '\${HTTPS_PORT}:443'\n"; $this->server->save(); expect(CheckProxy::run($this->server))->toBeTrue(); @@ -97,7 +97,7 @@ it('asks to start a stopped proxy whose ports use variables and checks only conc }); it('asks to start a stopped proxy whose ports all use variables without defaults', function () { - $this->server->proxy->last_saved_proxy_configuration = "services:\n traefik:\n image: traefik:v3.6\n ports:\n - '\${HTTP_PORT}:80'\n - '\${HTTPS_PORT}:443'\n"; + $this->server->proxy->last_saved_proxy_configuration = "services:\n traefik:\n container_name: coolify-proxy\n image: traefik:v3.6\n ports:\n - '\${HTTP_PORT}:80'\n - '\${HTTPS_PORT}:443'\n"; $this->server->save(); expect(CheckProxy::run($this->server))->toBeTrue(); @@ -106,14 +106,14 @@ it('asks to start a stopped proxy whose ports all use variables without defaults }); it('does not ask to start a stopped proxy without published ports', function () { - $this->server->proxy->last_saved_proxy_configuration = "services:\n traefik:\n image: traefik:v3.6\n ports:\n"; + $this->server->proxy->last_saved_proxy_configuration = "services:\n traefik:\n container_name: coolify-proxy\n image: traefik:v3.6\n ports:\n"; $this->server->save(); expect(CheckProxy::run($this->server))->toBeFalse(); }); it('saves and starts a proxy whose ports key is empty', function () { - $configuration = "services:\n traefik:\n image: traefik:v3.6\n ports:\n"; + $configuration = "services:\n traefik:\n container_name: coolify-proxy\n image: traefik:v3.6\n ports:\n"; SaveProxyConfiguration::run($this->server, $configuration); StartProxy::run($this->server->fresh(), async: false, force: true);