fix(proxy): require coolify-proxy container name outside Swarm (#12225)

This commit is contained in:
Andras Bacsai
2026-10-19 21:23:12 +02:00
committed by GitHub
parent c089f8a328
commit 6d36605176
4 changed files with 104 additions and 8 deletions
@@ -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."],
]);
}
}
}
}
@@ -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';
@@ -0,0 +1,65 @@
<?php
use App\Actions\Proxy\SaveProxyConfiguration;
use App\Enums\ProxyTypes;
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Process;
use Illuminate\Validation\ValidationException;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::forceCreate(['id' => 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);
});
@@ -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);