refactor(proxy-status): refactored how the proxy status is handled on the UI and on the backend

feat(cloudflare): improved cloudflare tunnel automated installation
This commit is contained in:
Andras Bacsai
2025-06-06 14:47:54 +02:00
parent 8e8400f595
commit ddcb14500d
51 changed files with 1277 additions and 829 deletions

View File

@@ -104,14 +104,11 @@ class RunRemoteProcess
$this->activity->save();
if ($this->call_event_on_finish) {
try {
if ($this->call_event_data) {
event(resolve("App\\Events\\$this->call_event_on_finish", [
'data' => $this->call_event_data,
]));
$eventClass = "App\\Events\\$this->call_event_on_finish";
if (! is_null($this->call_event_data)) {
event(new $eventClass($this->call_event_data));
} else {
event(resolve("App\\Events\\$this->call_event_on_finish", [
'userId' => $this->activity->causer_id,
]));
event(new $eventClass($this->activity->causer_id));
}
} catch (\Throwable $e) {
Log::error('Error calling event: '.$e->getMessage());

View File

@@ -3,6 +3,7 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -12,12 +13,38 @@ class CheckProxy
{
use AsAction;
// It should return if the proxy should be started (true) or not (false)
public function handle(Server $server, $fromUI = false): bool
/**
* Determine if the proxy should be started
*
* @param bool $fromUI Whether this check is initiated from the UI
* @return bool True if proxy should be started, false otherwise
*/
public function handle(Server $server, bool $fromUI = false): bool
{
// Early validation checks
if (! $this->shouldProxyRun($server, $fromUI)) {
return false;
}
// Handle swarm vs standalone differently
if ($server->isSwarm()) {
return $this->checkSwarmProxy($server);
}
return $this->checkStandaloneProxy($server, $fromUI);
}
/**
* Basic validation to determine if proxy can run at all
*/
private function shouldProxyRun(Server $server, bool $fromUI): bool
{
// Server must be functional
if (! $server->isFunctional()) {
return false;
}
// Build servers don't need proxy
if ($server->isBuildServer()) {
if ($server->proxy) {
$server->proxy = null;
@@ -26,86 +53,308 @@ class CheckProxy
return false;
}
$proxyType = $server->proxyType();
// Check if proxy is disabled or force stopped
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) && ! $fromUI) {
return false;
}
// Validate proxy configuration
if (! $server->isProxyShouldRun()) {
if ($fromUI) {
throw new \Exception('Proxy should not run. You selected the Custom Proxy.');
} else {
return false;
}
return false;
}
// Determine proxy container name based on environment
$proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
return true;
}
if ($server->isSwarm()) {
$status = getContainerStatus($server, $proxyContainerName);
/**
* Check proxy status for swarm mode
*/
private function checkSwarmProxy(Server $server): bool
{
$proxyContainerName = 'coolify-proxy_traefik';
$status = getContainerStatus($server, $proxyContainerName);
// Update status if changed
if ($server->proxy->status !== $status) {
$server->proxy->set('status', $status);
$server->save();
if ($status === 'running') {
return false;
}
return true;
} else {
$status = getContainerStatus($server, $proxyContainerName);
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
return false;
}
if ($server->settings->is_cloudflare_tunnel) {
return false;
}
$ip = $server->ip;
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$portsToCheck = ['80', '443'];
foreach ($portsToCheck as $port) {
// Use the smart port checker that handles dual-stack properly
if ($this->isPortConflict($server, $port, $proxyContainerName)) {
if ($fromUI) {
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br><br>Docs: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/discord'>https://coolify.io/discord</a>");
} else {
return false;
}
}
}
try {
if ($server->proxyType() !== ProxyTypes::NONE->value) {
$proxyCompose = CheckConfiguration::run($server);
if (isset($proxyCompose)) {
$yaml = Yaml::parse($proxyCompose);
$portsToCheck = [];
if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
$ports = data_get($yaml, 'services.traefik.ports');
} elseif ($server->proxyType() === ProxyTypes::CADDY->value) {
$ports = data_get($yaml, 'services.caddy.ports');
}
if (isset($ports)) {
foreach ($ports as $port) {
$portsToCheck[] = str($port)->before(':')->value();
}
}
}
} else {
$portsToCheck = [];
}
} catch (\Exception $e) {
Log::error('Error checking proxy: '.$e->getMessage());
}
if (count($portsToCheck) === 0) {
return false;
}
return true;
}
// If running, no need to start
return $status !== 'running';
}
/**
* Check proxy status for standalone mode
*/
private function checkStandaloneProxy(Server $server, bool $fromUI): bool
{
$proxyContainerName = 'coolify-proxy';
$status = getContainerStatus($server, $proxyContainerName);
if ($server->proxy->status !== $status) {
$server->proxy->set('status', $status);
$server->save();
ProxyStatusChanged::dispatch($server->id);
}
// If already running, no need to start
if ($status === 'running') {
return false;
}
// Cloudflare tunnel doesn't need proxy
if ($server->settings->is_cloudflare_tunnel) {
return false;
}
// Check for port conflicts
$portsToCheck = $this->getPortsToCheck($server);
if (empty($portsToCheck)) {
return false;
}
$this->validatePortsAvailable($server, $portsToCheck, $proxyContainerName, $fromUI);
return true;
}
/**
* Get list of ports that need to be available for the proxy
*/
private function getPortsToCheck(Server $server): array
{
$defaultPorts = ['80', '443'];
try {
if ($server->proxyType() === ProxyTypes::NONE->value) {
return [];
}
$proxyCompose = CheckConfiguration::run($server);
if (! $proxyCompose) {
return $defaultPorts;
}
$yaml = Yaml::parse($proxyCompose);
$ports = $this->extractPortsFromCompose($yaml, $server->proxyType());
return ! empty($ports) ? $ports : $defaultPorts;
} catch (\Exception $e) {
Log::error('Error checking proxy configuration: '.$e->getMessage());
return $defaultPorts;
}
}
/**
* Extract ports from docker-compose configuration
*/
private function extractPortsFromCompose(array $yaml, string $proxyType): array
{
$ports = [];
$servicePorts = null;
if ($proxyType === ProxyTypes::TRAEFIK->value) {
$servicePorts = data_get($yaml, 'services.traefik.ports');
} elseif ($proxyType === ProxyTypes::CADDY->value) {
$servicePorts = data_get($yaml, 'services.caddy.ports');
}
if ($servicePorts) {
foreach ($servicePorts as $port) {
$ports[] = str($port)->before(':')->value();
}
}
return array_unique($ports);
}
/**
* Validate that required ports are available (checked in parallel)
*/
private function validatePortsAvailable(Server $server, array $ports, string $proxyContainerName, bool $fromUI): void
{
if (empty($ports)) {
return;
}
\Log::info('Checking ports in parallel: '.implode(', ', $ports));
// Check all ports in parallel
$conflicts = $this->checkPortsInParallel($server, $ports, $proxyContainerName);
// Handle any conflicts found
foreach ($conflicts as $port) {
$message = "Port $port is in use";
if ($fromUI) {
$message = "Port $port is in use.<br>You must stop the process using this port.<br><br>".
"Docs: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>".
"Discord: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/discord'>https://coolify.io/discord</a>";
}
throw new \Exception($message);
}
}
/**
* Check multiple ports for conflicts in parallel using concurrent processes
*/
private function checkPortsInParallel(Server $server, array $ports, string $proxyContainerName): array
{
$conflicts = [];
// First, quickly filter out ports used by our own proxy
$ourProxyPorts = $this->getOurProxyPorts($server, $proxyContainerName);
$portsToCheck = array_diff($ports, $ourProxyPorts);
if (empty($portsToCheck)) {
\Log::info('All ports are used by our proxy, no conflicts to check');
return [];
}
// Build a single optimized command to check all ports at once
$command = $this->buildBatchPortCheckCommand($portsToCheck, $server);
if (! $command) {
\Log::warning('No suitable port checking method available, falling back to sequential');
return $this->checkPortsSequentially($server, $portsToCheck, $proxyContainerName);
}
try {
$output = instant_remote_process([$command], $server, true);
$results = $this->parseBatchPortCheckOutput($output, $portsToCheck);
foreach ($results as $port => $isConflict) {
if ($isConflict) {
$conflicts[] = $port;
}
}
} catch (\Throwable $e) {
\Log::warning('Batch port check failed, falling back to sequential: '.$e->getMessage());
return $this->checkPortsSequentially($server, $portsToCheck, $proxyContainerName);
}
return $conflicts;
}
/**
* Get ports currently used by our proxy container
*/
private function getOurProxyPorts(Server $server, string $proxyContainerName): array
{
try {
$getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'";
$containerId = trim(instant_remote_process([$getProxyContainerId], $server));
if (empty($containerId)) {
return [];
}
$getPortsCommand = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' 2>/dev/null || echo '{}'";
$portsJson = instant_remote_process([$getPortsCommand], $server, true);
$portsData = json_decode($portsJson, true);
$ports = [];
if (is_array($portsData)) {
foreach (array_keys($portsData) as $portSpec) {
if (preg_match('/^(\d+)\/tcp$/', $portSpec, $matches)) {
$ports[] = $matches[1];
}
}
}
return $ports;
} catch (\Throwable $e) {
\Log::warning('Failed to get proxy ports: '.$e->getMessage());
return [];
}
}
/**
* Build a single command to check multiple ports efficiently
*/
private function buildBatchPortCheckCommand(array $ports, Server $server): ?string
{
$portList = implode('|', $ports);
// Try different commands in order of preference
$commands = [
// ss - fastest and most reliable
[
'check' => 'command -v ss >/dev/null 2>&1',
'exec' => "ss -Htuln state listening | grep -E ':($portList) ' | awk '{print \$5}' | cut -d: -f2 | sort -u",
],
// netstat - widely available
[
'check' => 'command -v netstat >/dev/null 2>&1',
'exec' => "netstat -tuln | grep -E ':($portList) ' | grep LISTEN | awk '{print \$4}' | cut -d: -f2 | sort -u",
],
// lsof - last resort
[
'check' => 'command -v lsof >/dev/null 2>&1',
'exec' => "lsof -iTCP -sTCP:LISTEN -P -n | grep -E ':($portList) ' | awk '{print \$9}' | cut -d: -f2 | sort -u",
],
];
foreach ($commands as $cmd) {
try {
instant_remote_process([$cmd['check']], $server);
return $cmd['exec'];
} catch (\Throwable $e) {
continue;
}
}
return null;
}
/**
* Parse output from batch port check command
*/
private function parseBatchPortCheckOutput(string $output, array $portsToCheck): array
{
$results = [];
$usedPorts = array_filter(explode("\n", trim($output)));
foreach ($portsToCheck as $port) {
$results[$port] = in_array($port, $usedPorts);
}
return $results;
}
/**
* Fallback to sequential checking if parallel fails
*/
private function checkPortsSequentially(Server $server, array $ports, string $proxyContainerName): array
{
$conflicts = [];
foreach ($ports as $port) {
try {
if ($this->isPortConflict($server, $port, $proxyContainerName)) {
$conflicts[] = $port;
}
} catch (\Throwable $e) {
\Log::warning("Sequential port check failed for port $port: ".$e->getMessage());
// Continue checking other ports
}
}
return $conflicts;
}
/**
@@ -114,137 +363,168 @@ class CheckProxy
*/
private function isPortConflict(Server $server, string $port, string $proxyContainerName): bool
{
// First check if our own proxy is using this port (which is fine)
// Check if our own proxy is using this port (which is fine)
if ($this->isOurProxyUsingPort($server, $port, $proxyContainerName)) {
return false;
}
// Try different methods to detect port conflicts
$portCheckMethods = [
'checkWithSs',
'checkWithNetstat',
'checkWithLsof',
'checkWithNetcat',
];
foreach ($portCheckMethods as $method) {
try {
$result = $this->$method($server, $port);
if ($result !== null) {
return $result;
}
} catch (\Throwable $e) {
continue; // Try next method
}
}
// If all methods fail, assume port is free to avoid false positives
return false;
}
/**
* Check if our own proxy container is using the port
*/
private function isOurProxyUsingPort(Server $server, string $port, string $proxyContainerName): bool
{
try {
$getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'";
$containerId = trim(instant_remote_process([$getProxyContainerId], $server));
if (! empty($containerId)) {
$checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'";
try {
instant_remote_process([$checkProxyPort], $server);
// Our proxy is using the port, which is fine
return false;
} catch (\Throwable $e) {
// Our container exists but not using this port
}
if (empty($containerId)) {
return false;
}
$checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'";
instant_remote_process([$checkProxyPort], $server);
return true; // Our proxy is using the port
} catch (\Throwable $e) {
// Container not found or error checking, continue with regular checks
}
// Command sets for different ways to check ports, ordered by preference
$commandSets = [
// Set 1: Use ss to check listener counts by protocol stack
[
'available' => 'command -v ss >/dev/null 2>&1',
'check' => [
// Get listening process details
"ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"",
// Count IPv4 listeners
"echo \"\$ss_output\" | grep -c ':$port '",
],
],
// Set 2: Use netstat as alternative to ss
[
'available' => 'command -v netstat >/dev/null 2>&1',
'check' => [
// Get listening process details
"netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '",
// Count listeners
"echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'",
],
],
// Set 3: Use lsof as last resort
[
'available' => 'command -v lsof >/dev/null 2>&1',
'check' => [
// Get process using the port
"lsof -i :$port -P -n | grep 'LISTEN'",
// Count listeners
"lsof -i :$port -P -n | grep 'LISTEN' | wc -l",
],
],
];
// Try each command set until we find one available
foreach ($commandSets as $set) {
try {
// Check if the command is available
instant_remote_process([$set['available']], $server);
// Run the actual check commands
$output = instant_remote_process($set['check'], $server, true);
// Parse the output lines
$lines = explode("\n", trim($output));
// Get the detailed output and listener count
$details = trim($lines[0] ?? '');
$count = intval(trim($lines[1] ?? '0'));
// If no listeners or empty result, port is free
if ($count == 0 || empty($details)) {
return false;
}
// Try to detect if this is our coolify-proxy
if (strpos($details, 'docker') !== false || strpos($details, $proxyContainerName) !== false) {
// It's likely our docker or proxy, which is fine
return false;
}
// Check for dual-stack scenario - typically 1-2 listeners (IPv4+IPv6)
// If exactly 2 listeners and both have same port, likely dual-stack
if ($count <= 2) {
// Check if it looks like a standard dual-stack setup
$isDualStack = false;
// Look for IPv4 and IPv6 in the listing (ss output format)
if (preg_match('/LISTEN.*:'.$port.'\s/', $details) &&
(preg_match('/\*:'.$port.'\s/', $details) ||
preg_match('/:::'.$port.'\s/', $details))) {
$isDualStack = true;
}
// For netstat format
if (strpos($details, '0.0.0.0:'.$port) !== false &&
strpos($details, ':::'.$port) !== false) {
$isDualStack = true;
}
// For lsof format (IPv4 and IPv6)
if (strpos($details, '*:'.$port) !== false &&
preg_match('/\*:'.$port.'.*IPv4/', $details) &&
preg_match('/\*:'.$port.'.*IPv6/', $details)) {
$isDualStack = true;
}
if ($isDualStack) {
return false; // This is just a normal dual-stack setup
}
}
// If we get here, it's likely a real port conflict
return true;
} catch (\Throwable $e) {
// This command set failed, try the next one
continue;
}
}
// Fallback to simpler check if all above methods fail
try {
// Just try to bind to the port directly to see if it's available
$checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'";
$result = instant_remote_process([$checkCommand], $server, true);
return trim($result) === 'in-use';
} catch (\Throwable $e) {
// If everything fails, assume the port is free to avoid false positives
return false;
}
}
/**
* Check port usage with ss command
*/
private function checkWithSs(Server $server, string $port): ?bool
{
$commands = [
'command -v ss >/dev/null 2>&1',
"ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"",
"echo \"\$ss_output\" | grep -c ':$port '",
];
$output = instant_remote_process($commands, $server, true);
return $this->parsePortCheckOutput($output, $port);
}
/**
* Check port usage with netstat command
*/
private function checkWithNetstat(Server $server, string $port): ?bool
{
$commands = [
'command -v netstat >/dev/null 2>&1',
"netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '",
"echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'",
];
$output = instant_remote_process($commands, $server, true);
return $this->parsePortCheckOutput($output, $port);
}
/**
* Check port usage with lsof command
*/
private function checkWithLsof(Server $server, string $port): ?bool
{
$commands = [
'command -v lsof >/dev/null 2>&1',
"lsof -i :$port -P -n | grep 'LISTEN'",
"lsof -i :$port -P -n | grep 'LISTEN' | wc -l",
];
$output = instant_remote_process($commands, $server, true);
return $this->parsePortCheckOutput($output, $port);
}
/**
* Check port usage with netcat as fallback
*/
private function checkWithNetcat(Server $server, string $port): ?bool
{
$checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'";
$result = instant_remote_process([$checkCommand], $server, true);
return trim($result) === 'in-use';
}
/**
* Parse output from port checking commands
*/
private function parsePortCheckOutput(string $output, string $port): ?bool
{
$lines = explode("\n", trim($output));
$details = trim($lines[0] ?? '');
$count = intval(trim($lines[1] ?? '0'));
// No listeners found
if ($count === 0 || empty($details)) {
return false;
}
// Check if it's likely our docker/proxy process
if (strpos($details, 'docker') !== false || strpos($details, 'coolify-proxy') !== false) {
return false;
}
// Handle dual-stack scenarios (1-2 listeners for IPv4+IPv6)
if ($count <= 2 && $this->isDualStackSetup($details, $port)) {
return false;
}
// Real port conflict detected
return true;
}
/**
* Detect if this is a normal dual-stack setup (IPv4 + IPv6)
*/
private function isDualStackSetup(string $details, string $port): bool
{
$patterns = [
// ss output format
'/LISTEN.*:'.$port.'\s/',
'/\*:'.$port.'\s/',
'/:::'.$port.'\s/',
// netstat format
'/0\.0\.0\.0:'.$port.'/',
'/:::'.$port.'/',
// lsof format
'/\*:'.$port.'.*IPv[46]/',
];
$matches = 0;
foreach ($patterns as $pattern) {
if (preg_match($pattern, $details)) {
$matches++;
}
}
// If we see patterns indicating both IPv4 and IPv6, it's likely dual-stack
return $matches >= 2;
}
}

View File

@@ -3,7 +3,7 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Events\ProxyStarted;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Models\Activity;
@@ -57,20 +57,19 @@ class StartProxy
" echo 'Successfully stopped and removed existing coolify-proxy.'",
'fi',
"echo 'Starting coolify-proxy.'",
'docker compose up -d',
'docker compose up -d --wait',
"echo 'Successfully started coolify-proxy.'",
]);
$commands = $commands->merge(connectProxyToNetworks($server));
}
if ($async) {
return remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server);
return remote_process($commands, $server, callEventOnFinish: 'ProxyStatusChanged', callEventData: $server->id);
} else {
instant_remote_process($commands, $server);
$server->proxy->set('status', 'running');
$server->proxy->set('type', $proxyType);
$server->save();
ProxyStarted::dispatch($server);
ProxyStatusChanged::dispatch($server->id);
return 'OK';
}

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Proxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -24,6 +25,8 @@ class StopProxy
$server->save();
} catch (\Throwable $e) {
return handleError($e);
} finally {
ProxyStatusChanged::dispatch($server->id);
}
}
}

View File

@@ -2,16 +2,16 @@
namespace App\Actions\Server;
use App\Events\CloudflareTunnelConfigured;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Models\Activity;
use Symfony\Component\Yaml\Yaml;
class ConfigureCloudflared
{
use AsAction;
public function handle(Server $server, string $cloudflare_token)
public function handle(Server $server, string $cloudflare_token, string $ssh_domain): Activity
{
try {
$config = [
@@ -24,6 +24,13 @@ class ConfigureCloudflared
'command' => 'tunnel run',
'environment' => [
"TUNNEL_TOKEN={$cloudflare_token}",
'TUNNEL_METRICS=127.0.0.1:60123',
],
'healthcheck' => [
'test' => ['CMD', 'cloudflared', 'tunnel', '--metrics', '127.0.0.1:60123', 'ready'],
'interval' => '5s',
'timeout' => '30s',
'retries' => 5,
],
],
],
@@ -34,22 +41,20 @@ class ConfigureCloudflared
'mkdir -p /tmp/cloudflared',
'cd /tmp/cloudflared',
"echo '$docker_compose_yml_base64' | base64 -d | tee docker-compose.yml > /dev/null",
'echo Pulling latest Cloudflare Tunnel image.',
'docker compose pull',
'docker compose down -v --remove-orphans > /dev/null 2>&1',
'docker compose up -d --remove-orphans',
'echo Stopping existing Cloudflare Tunnel container.',
'docker rm -f coolify-cloudflared || true',
'echo Starting new Cloudflare Tunnel container.',
'docker compose up --wait --wait-timeout 15 --remove-orphans || docker logs coolify-cloudflared',
]);
instant_remote_process($commands, $server);
} catch (\Throwable $e) {
$server->settings->is_cloudflare_tunnel = false;
$server->settings->save();
throw $e;
} finally {
CloudflareTunnelConfigured::dispatch($server->team_id);
$commands = collect([
'rm -fr /tmp/cloudflared',
return remote_process($commands, $server, callEventOnFinish: 'CloudflareTunnelChanged', callEventData: [
'server_id' => $server->id,
'ssh_domain' => $ssh_domain,
]);
instant_remote_process($commands, $server);
} catch (\Throwable $e) {
throw $e;
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CloudflareTunnelChanged
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public $data) {}
}

View File

@@ -3,33 +3,12 @@
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProxyStatusChanged implements ShouldBroadcast
class ProxyStatusChanged
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?int $teamId = null;
public function __construct($teamId = null)
{
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
public function __construct(public $data) {}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProxyStatusChangedUI implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?int $teamId = null;
public function __construct(int $teamId)
{
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
}

View File

@@ -213,8 +213,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
if (! $application) {
return;
}
$application->status = $containerStatus;
$application->save();
if ($application->status !== $containerStatus) {
$application->status = $containerStatus;
$application->save();
}
}
private function updateApplicationPreviewStatus(string $applicationId, string $pullRequestId, string $containerStatus)
@@ -249,8 +251,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
return;
}
$application->status = 'exited';
$application->save();
if ($application->status !== 'exited') {
$application->status = 'exited';
$application->save();
}
}
});
}
@@ -286,8 +290,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
return;
}
$applicationPreview->status = 'exited';
$applicationPreview->save();
if ($applicationPreview->status !== 'exited') {
$applicationPreview->status = 'exited';
$applicationPreview->save();
}
}
});
}
@@ -318,8 +324,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
if (! $database) {
return;
}
$database->status = $containerStatus;
$database->save();
if ($database->status !== $containerStatus) {
$database->status = $containerStatus;
$database->save();
}
if ($this->isRunning($containerStatus) && $tcpProxy) {
$tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) {
return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running';
@@ -339,8 +347,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
$notFoundDatabaseUuids->each(function ($databaseUuid) {
$database = $this->databases->where('uuid', $databaseUuid)->first();
if ($database) {
$database->status = 'exited';
$database->save();
if ($database->status !== 'exited') {
$database->status = 'exited';
$database->save();
}
if ($database->is_public) {
StopDatabaseProxy::dispatch($database);
}
@@ -358,14 +368,18 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
if ($subType === 'application') {
$application = $service->applications()->where('id', $subId)->first();
if ($application) {
$application->status = $containerStatus;
$application->save();
if ($application->status !== $containerStatus) {
$application->status = $containerStatus;
$application->save();
}
}
} elseif ($subType === 'database') {
$database = $service->databases()->where('id', $subId)->first();
if ($database) {
$database->status = $containerStatus;
$database->save();
if ($database->status !== $containerStatus) {
$database->status = $containerStatus;
$database->save();
}
}
}
}
@@ -378,8 +392,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
$notFoundServiceApplicationIds->each(function ($serviceApplicationId) {
$application = ServiceApplication::find($serviceApplicationId);
if ($application) {
$application->status = 'exited';
$application->save();
if ($application->status !== 'exited') {
$application->status = 'exited';
$application->save();
}
}
});
}
@@ -387,8 +403,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
$notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) {
$database = ServiceDatabase::find($serviceDatabaseId);
if ($database) {
$database->status = 'exited';
$database->save();
if ($database->status !== 'exited') {
$database->status = 'exited';
$database->save();
}
}
});
}

View File

@@ -36,9 +36,10 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
$this->server->proxy->force_stop = false;
$this->server->save();
StartProxy::run($this->server, force: true);
CheckProxy::run($this->server, true);
// CheckProxy::run($this->server, true);
} catch (\Throwable $e) {
return handleError($e);
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Listeners;
use App\Events\CloudflareTunnelChanged;
use App\Events\CloudflareTunnelConfigured;
use App\Models\Server;
use Illuminate\Support\Sleep;
class CloudflareTunnelChangedNotification
{
public Server $server;
public function __construct() {}
public function handle(CloudflareTunnelChanged $event): void
{
$server_id = data_get($event, 'data.server_id');
$ssh_domain = data_get($event, 'data.ssh_domain');
$this->server = Server::find($server_id)->firstOrFail();
// Check if cloudflare tunnel is running (container is healthy) - try 3 times with 5 second intervals
$cloudflareHealthy = false;
$attempts = 3;
for ($i = 1; $i <= $attempts; $i++) {
\Log::debug("Cloudflare health check attempt {$i}/{$attempts}", ['server_id' => $server_id]);
$result = instant_remote_process_with_timeout(['docker inspect coolify-cloudflared | jq -e ".[0].State.Health.Status == \"healthy\""'], $this->server, false, 10);
if (blank($result)) {
\Log::debug("Cloudflare Tunnels container not found on attempt {$i}", ['server_id' => $server_id]);
} elseif ($result === 'true') {
\Log::debug("Cloudflare Tunnels container healthy on attempt {$i}", ['server_id' => $server_id]);
$cloudflareHealthy = true;
break;
} else {
\Log::debug("Cloudflare Tunnels container not healthy on attempt {$i}", ['server_id' => $server_id, 'result' => $result]);
}
// Sleep between attempts (except after the last attempt)
if ($i < $attempts) {
Sleep::for(5)->seconds();
}
}
if (! $cloudflareHealthy) {
\Log::error('Cloudflare Tunnels container failed all health checks.', ['server_id' => $server_id, 'attempts' => $attempts]);
return;
}
$this->server->settings->update([
'is_cloudflare_tunnel' => true,
]);
// Only update IP if it's not already set to the ssh_domain or if it's empty
if ($this->server->ip !== $ssh_domain && ! empty($ssh_domain)) {
\Log::debug('Cloudflare Tunnels configuration updated - updating IP address.', ['old_ip' => $this->server->ip, 'new_ip' => $ssh_domain]);
$this->server->update(['ip' => $ssh_domain]);
} else {
\Log::debug('Cloudflare Tunnels configuration updated - IP address unchanged.', ['current_ip' => $this->server->ip]);
}
$teamId = $this->server->team_id;
CloudflareTunnelConfigured::dispatch($teamId);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Listeners;
use App\Events\ProxyStarted;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
class ProxyStartedNotification
@@ -18,5 +19,6 @@ class ProxyStartedNotification
$this->server->setupDynamicProxyConfiguration();
$this->server->proxy->force_stop = false;
$this->server->save();
// ProxyStatusChanged::dispatch( $this->server->id);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Listeners;
use App\Events\ProxyStatusChanged;
use App\Events\ProxyStatusChangedUI;
use App\Models\Server;
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
class ProxyStatusChangedNotification implements ShouldQueueAfterCommit
{
public function __construct() {}
public function handle(ProxyStatusChanged $event)
{
$serverId = $event->data;
$server = Server::find($serverId);
if (is_null($server)) {
return;
}
$proxyContainerName = 'coolify-proxy';
$status = getContainerStatus($server, $proxyContainerName);
$server->proxy->set('status', $status);
$server->save();
if ($status === 'running') {
$server->setupDefaultRedirect();
$server->setupDynamicProxyConfiguration();
$server->proxy->force_stop = false;
$server->save();
}
ProxyStatusChangedUI::dispatch($server->team_id);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Server\ConfigureCloudflared;
use App\Models\Server;
use Livewire\Attributes\Validate;
use Livewire\Component;
class CloudflareTunnel extends Component
{
public Server $server;
#[Validate(['required', 'string'])]
public string $cloudflare_token;
#[Validate(['required', 'string'])]
public string $ssh_domain;
#[Validate(['required', 'boolean'])]
public bool $isCloudflareTunnelsEnabled;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh',
];
}
public function refresh()
{
$this->mount($this->server->uuid);
}
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
if ($this->server->isLocalhost()) {
return redirect()->route('server.show', ['server_uuid' => $server_uuid]);
}
$this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function toggleCloudflareTunnels()
{
try {
remote_process(['docker rm -f coolify-cloudflared'], $this->server, false, 10);
$this->isCloudflareTunnelsEnabled = false;
$this->server->settings->is_cloudflare_tunnel = false;
$this->server->settings->save();
if ($this->server->ip_previous) {
$this->server->update(['ip' => $this->server->ip_previous]);
$this->dispatch('success', 'Cloudflare Tunnel disabled.<br><br>Manually updated the server IP address to its previous IP address.');
} else {
$this->dispatch('success', 'Cloudflare Tunnel disabled. Please update the server IP address to its real IP address in the server settings.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function manualCloudflareConfig()
{
$this->isCloudflareTunnelsEnabled = true;
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnel enabled.');
}
public function automatedCloudflareConfig()
{
try {
if (str($this->ssh_domain)->contains('https://')) {
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
}
$activity = ConfigureCloudflared::run($this->server, $this->cloudflare_token, $this->ssh_domain);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.cloudflare-tunnel');
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Livewire\Attributes\Validate;
use Livewire\Component;
class CloudflareTunnels extends Component
{
public Server $server;
#[Validate(['required', 'boolean'])]
public bool $isCloudflareTunnelsEnabled;
public function mount(string $server_uuid)
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
if ($this->server->isLocalhost()) {
return redirect()->route('server.show', ['server_uuid' => $server_uuid]);
}
$this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
$this->validate();
$this->server->settings->is_cloudflare_tunnel = $this->isCloudflareTunnelsEnabled;
$this->server->settings->save();
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function manualCloudflareConfig()
{
$this->isCloudflareTunnelsEnabled = true;
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
}
public function render()
{
return view('livewire.server.cloudflare-tunnels');
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Server\ConfigureCloudflared;
use App\Models\Server;
use Livewire\Component;
class ConfigureCloudflareTunnels extends Component
{
public $server_id;
public string $cloudflare_token;
public string $ssh_domain;
public function alreadyConfigured()
{
try {
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
$server->settings->is_cloudflare_tunnel = true;
$server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
$this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
if (str($this->ssh_domain)->contains('https://')) {
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
// remove / from the end
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
}
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true;
$server->ip = $this->ssh_domain;
$server->save();
$server->settings->save();
$this->dispatch('info', 'Cloudflare Tunnels configuration started.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.configure-cloudflare-tunnels');
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
use App\Jobs\RestartProxyJob;
use App\Models\Server;
use Livewire\Component;
class Navbar extends Component
{
public Server $server;
public bool $isChecking = false;
public ?string $currentRoute = null;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChangedUI" => 'showNotification',
];
}
public function mount(Server $server)
{
$this->server = $server;
$this->currentRoute = request()->route()->getName();
}
public function restart()
{
try {
RestartProxyJob::dispatch($this->server);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkProxy()
{
try {
CheckProxy::run($this->server, true);
$this->dispatch('startProxy')->self();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startProxy()
{
try {
$activity = StartProxy::run($this->server, force: true);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function stop(bool $forceStop = true)
{
try {
StopProxy::dispatch($this->server, $forceStop);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkProxyStatus()
{
if ($this->isChecking) {
return;
}
try {
$this->isChecking = true;
CheckProxy::run($this->server, true);
$this->showNotification();
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->isChecking = false;
}
}
public function showNotification()
{
$status = $this->server->proxy->status ?? 'unknown';
$forceStop = $this->server->proxy->force_stop ?? false;
switch ($status) {
case 'running':
$this->dispatch('success', 'Proxy is running.');
break;
case 'restarting':
$this->dispatch('info', 'Initiating proxy restart.');
break;
case 'exited':
if ($forceStop) {
$this->dispatch('info', 'Proxy is stopped manually.');
} else {
$this->dispatch('info', 'Proxy is stopped manually. Starting in a moment.');
}
break;
default:
$this->dispatch('warning', 'Proxy is not running.');
break;
}
}
public function render()
{
return view('livewire.server.navbar');
}
}

View File

@@ -19,7 +19,7 @@ class Proxy extends Component
public ?string $redirect_url = null;
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
protected $listeners = ['saveConfiguration' => 'submit'];
protected $rules = [
'server.settings.generate_exact_labels' => 'required|boolean',
@@ -32,10 +32,10 @@ class Proxy extends Component
$this->redirect_url = data_get($this->server, 'proxy.redirect_url');
}
public function proxyStatusUpdated()
{
$this->dispatch('refresh')->self();
}
// public function proxyStatusUpdated()
// {
// $this->dispatch('refresh')->self();
// }
public function changeProxy()
{

View File

@@ -1,106 +0,0 @@
<?php
namespace App\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
use App\Events\ProxyStatusChanged;
use App\Jobs\RestartProxyJob;
use App\Models\Server;
use Livewire\Component;
class Deploy extends Component
{
public Server $server;
public bool $traefikDashboardAvailable = false;
public ?string $currentRoute = null;
public ?string $serverIp = null;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChanged" => 'proxyStarted',
'proxyStatusUpdated',
'traefikDashboardAvailable',
'serverRefresh' => 'proxyStatusUpdated',
'checkProxy',
'startProxy',
'proxyChanged' => 'proxyStatusUpdated',
];
}
public function mount()
{
if ($this->server->id === 0) {
$this->serverIp = base_ip();
} else {
$this->serverIp = $this->server->ip;
}
$this->currentRoute = request()->route()->getName();
}
public function traefikDashboardAvailable(bool $data)
{
$this->traefikDashboardAvailable = $data;
}
public function proxyStarted()
{
CheckProxy::run($this->server, true);
$this->dispatch('proxyStatusUpdated');
}
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function restart()
{
try {
RestartProxyJob::dispatch($this->server);
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkProxy()
{
try {
CheckProxy::run($this->server, true);
$this->dispatch('startProxyPolling');
$this->dispatch('proxyChecked');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startProxy()
{
try {
$this->server->proxy->force_stop = false;
$this->server->save();
$activity = StartProxy::run($this->server, force: true);
$this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function stop(bool $forceStop = true)
{
try {
StopProxy::run($this->server, $forceStop);
$this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -19,7 +19,7 @@ class DynamicConfigurations extends Component
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChanged" => 'loadDynamicConfigurations',
"echo-private:team.{$teamId},ProxyStatusChangedUI" => 'loadDynamicConfigurations',
'loadDynamicConfigurations',
];
}

View File

@@ -11,12 +11,12 @@ class Show extends Component
public $parameters = [];
protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated'];
// protected $listeners = ['proxyStatusUpdated'];
public function proxyStatusUpdated()
{
$this->server->refresh();
}
// public function proxyStatusUpdated()
// {
// $this->server->refresh();
// }
public function mount()
{

View File

@@ -1,77 +0,0 @@
<?php
namespace App\Livewire\Server\Proxy;
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Models\Server;
use Livewire\Component;
class Status extends Component
{
public Server $server;
public bool $polling = false;
public int $numberOfPolls = 0;
protected $listeners = [
'proxyStatusUpdated',
'startProxyPolling',
];
public function startProxyPolling()
{
$this->checkProxy();
}
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function checkProxy(bool $notification = false)
{
try {
if ($this->polling) {
if ($this->numberOfPolls >= 10) {
$this->polling = false;
$this->numberOfPolls = 0;
$notification && $this->dispatch('error', 'Proxy is not running.');
return;
}
$this->numberOfPolls++;
}
$shouldStart = CheckProxy::run($this->server, true);
if ($shouldStart) {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') {
$this->polling = false;
$notification && $this->dispatch('success', 'Proxy is running.');
} elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) {
$notification && $this->dispatch('error', 'Proxy has exited.');
} elseif ($this->server->proxy->force_stop) {
$notification && $this->dispatch('error', 'Proxy is stopped manually.');
} else {
$notification && $this->dispatch('error', 'Proxy is not running.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function getProxyStatus()
{
try {
GetContainersStatus::run($this->server);
// dispatch_sync(new ContainerStatusJob($this->server));
$this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -86,10 +86,7 @@ class Show extends Component
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh',
'refreshServerShow' => 'refresh',
];
}
@@ -211,7 +208,6 @@ class Show extends Component
$this->server->settings->is_usable = $this->isUsable = true;
$this->server->settings->save();
ServerReachabilityChanged::dispatch($this->server);
$this->dispatch('proxyStatusUpdated');
} else {
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);

View File

@@ -69,6 +69,11 @@ class Server extends BaseModel
}
if ($server->ip) {
$payload['ip'] = str($server->ip)->trim();
// Update ip_previous when ip is being changed
if ($server->isDirty('ip') && $server->getOriginal('ip')) {
$payload['ip_previous'] = $server->getOriginal('ip');
}
}
$server->forceFill($payload);
});

View File

@@ -2,10 +2,8 @@
namespace App\Providers;
use App\Events\ProxyStarted;
use App\Listeners\MaintenanceModeDisabledNotification;
use App\Listeners\MaintenanceModeEnabledNotification;
use App\Listeners\ProxyStartedNotification;
use Illuminate\Foundation\Events\MaintenanceModeDisabled;
use Illuminate\Foundation\Events\MaintenanceModeEnabled;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@@ -30,9 +28,6 @@ class EventServiceProvider extends ServiceProvider
GoogleExtendSocialite::class.'@handle',
InfomaniakExtendSocialite::class.'@handle',
],
ProxyStarted::class => [
ProxyStartedNotification::class,
],
];
public function boot(): void
@@ -42,6 +37,6 @@ class EventServiceProvider extends ServiceProvider
public function shouldDiscoverEvents(): bool
{
return false;
return true;
}
}

View File

@@ -91,21 +91,17 @@ function connectProxyToNetworks(Server $server)
if ($server->isSwarm()) {
$commands = $networks->map(function ($network) {
return [
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
"echo 'Successfully connected coolify-proxy to $network network.'",
"echo 'Proxy started and configured successfully!'",
];
});
} else {
$commands = $networks->map(function ($network) {
return [
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
"echo 'Successfully connected coolify-proxy to $network network.'",
"echo 'Proxy started and configured successfully!'",
];
});
}

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->string('ip_previous')->nullable()->after('ip');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('ip_previous');
});
}
};

View File

@@ -1,60 +0,0 @@
<div class="pb-6">
<x-modal modalId="startProxy">
<x-slot:modalBody>
<livewire:activity-monitor header="Proxy Startup Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="startProxy.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
<div class="flex items-center gap-2">
<h1>Server</h1>
@if ($server->proxySet())
<livewire:server.proxy.status :server="$server" />
@endif
</div>
<div class="subtitle">{{ data_get($server, 'name') }}</div>
<div class="navbar-main">
<nav class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap">
<a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}"
href="{{ route('server.show', [
'server_uuid' => data_get($server, 'uuid'),
]) }}">
<button>Configuration</button>
</a>
@if (!$server->isSwarmWorker() && !$server->settings->is_build_server)
<a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }}"
href="{{ route('server.proxy', [
'server_uuid' => data_get($server, 'uuid'),
]) }}">
<button>Proxy</button>
</a>
@endif
<a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}"
href="{{ route('server.resources', [
'server_uuid' => data_get($server, 'uuid'),
]) }}">
<button>Resources</button>
</a>
<a class="{{ request()->routeIs('server.command') ? 'dark:text-white' : '' }}"
href="{{ route('server.command', [
'server_uuid' => data_get($server, 'uuid'),
]) }}">
<button>Terminal</button>
</a>
<a class="{{ request()->routeIs('server.security.patches') ? 'dark:text-white' : '' }}"
href="{{ route('server.security.patches', [
'server_uuid' => data_get($server, 'uuid'),
]) }}">
<button>Security</button>
</a>
</nav>
<div class="order-first sm:order-last">
<livewire:server.proxy.deploy :server="$server" />
</div>
</div>
</div>

View File

@@ -13,9 +13,9 @@
href="{{ route('server.ca-certificate', ['server_uuid' => $server->uuid]) }}">CA Certificate
</a>
@if (!$server->isLocalhost())
<a class="menu-item {{ $activeMenu === 'cloudflare-tunnels' ? 'menu-item-active' : '' }}"
href="{{ route('server.cloudflare-tunnels', ['server_uuid' => $server->uuid]) }}">Cloudflare
Tunnels</a>
<a class="menu-item {{ $activeMenu === 'cloudflare-tunnel' ? 'menu-item-active' : '' }}"
href="{{ route('server.cloudflare-tunnel', ['server_uuid' => $server->uuid]) }}">Cloudflare
Tunnel</a>
@endif
@if ($server->isFunctional())
<a class="menu-item {{ $activeMenu === 'docker-cleanup' ? 'menu-item-active' : '' }}"

View File

@@ -14,7 +14,7 @@
<livewire:project.shared.configuration-checker :resource="$resource" />
<livewire:project.service.heading :service="$resource" :parameters="$parameters" title="Terminal" />
@elseif ($type === 'server')
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.navbar :server="$server" :parameters="$parameters" />
@endif
<h2 class="pt-4">Terminal</h2>

View File

@@ -2,7 +2,7 @@
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Advanced | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<livewire:server.navbar :server="$server" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="advanced" />
<form wire:submit='submit' class="w-full">

View File

@@ -2,12 +2,12 @@
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > CA Certificate | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="ca-certificate" />
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<h3>CA SSL Certificate</h3>
<h2>CA Certificate</h2>
<div class="flex gap-2">
<x-modal-confirmation title="Confirm changing of CA Certificate?" buttonTitle="Save"
submitAction="saveCaCertificate" :actions="[

View File

@@ -2,7 +2,7 @@
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Metrics | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="metrics" />
<div class="w-full">

View File

@@ -0,0 +1,122 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Cloudflare Tunnel | Coolify
</x-slot>
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="cloudflare-tunnel" />
<div class="w-full">
<div class="flex flex-col">
<div class="flex gap-2 items-center">
<h2>Cloudflare Tunnel</h2>
<x-helper class="inline-flex"
helper="If you are using Cloudflare Tunnel, enable this. It will proxy all SSH requests to your server through Cloudflare.<br> You then can close your server's SSH port in the firewall of your hosting provider.<br><span class='dark:text-warning'>If you choose manual configuration, Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" />
@if ($isCloudflareTunnelsEnabled)
<span
class="px-2 py-1 text-xs font-semibold text-green-800 bg-green-100 rounded dark:text-green-100 dark:bg-green-800">
Enabled
</span>
@endif
</div>
<div>Secure your servers with Cloudflare Tunnel.</div>
</div>
<div class="flex flex-col gap-2 pt-6">
@if ($isCloudflareTunnelsEnabled)
<div class="flex flex-col gap-4">
<div
class="w-full px-4 py-2 text-yellow-800 rounded-xs border-l-4 border-yellow-500 bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300 dark:border-yellow-600">
<p class="font-bold">Warning!</p>
<p>If you disable Cloudflare Tunnel, you will need to update the server's IP address back
to
its real IP address in the server "General" settings. The server may become inaccessible
if the IP
address is not updated correctly.</p>
</div>
<div class="w-64">
@if ($server->ip_previous)
<x-modal-confirmation title="Disable Cloudflare Tunnel?"
buttonTitle="Disable Cloudflare Tunnel" isErrorButton
submitAction="toggleCloudflareTunnels" :actions="[
'Cloudflare Tunnel will be disabled for this server.',
'The server IP address will be updated to its previous IP address.',
]"
confirmationText="DISABLE CLOUDFLARE TUNNEL"
confirmationLabel="Please type the confirmation text to disable Cloudflare Tunnel."
shortConfirmationLabel="Confirmation text"
step3ButtonText="Disable Cloudflare Tunnel" />
@else
<x-modal-confirmation title="Disable Cloudflare Tunnel?"
buttonTitle="Disable Cloudflare Tunnel" isErrorButton
submitAction="toggleCloudflareTunnels" :actions="[
'Cloudflare Tunnel will be disabled for this server.',
'You will need to update the server IP address to its real IP address.',
'The server may become inaccessible if the IP address is not updated correctly.',
'SSH access will revert to the standard port configuration.',
]"
confirmationText="DISABLE CLOUDFLARE TUNNEL"
confirmationLabel="Please type the confirmation text to disable Cloudflare Tunnel."
shortConfirmationLabel="Confirmation text"
step3ButtonText="Disable Cloudflare Tunnel" />
@endif
</div>
</div>
@elseif (!$server->isFunctional())
<div
class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded-sm dark:bg-yellow-900 dark:text-yellow-300">
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnel, please
validate your server first.</span> Then you will need a Cloudflare token and an SSH
domain configured.
<br />
To <span class="font-semibold">manually</span> configure Cloudflare Tunnel, please
click <span wire:click="manualCloudflareConfig" class="underline cursor-pointer">here</span>,
then you should validate the server.
<br /><br />
For more information, please read our <a
href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/server-ssh" target="_blank"
class="underline ">documentation</a>.
</div>
@endif
@if (!$isCloudflareTunnelsEnabled && $server->isFunctional())
<h3>Automated <a href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/server-ssh"
target="_blank"
class="text-xs underline hover:text-yellow-600 dark:hover:text-yellow-200">Docs</a></h3>
<div class="flex gap-2">
<x-slide-over @automated.window="slideOverOpen = true" fullScreen>
<x-slot:title>Cloudflare Tunnel Configuration</x-slot:title>
<x-slot:content>
<livewire:activity-monitor header="Logs" showWaiting fullHeight />
</x-slot:content>
</x-slide-over>
<form @submit.prevent="$wire.dispatch('automatedCloudflareConfig')"
class="flex flex-col gap-2 w-full">
<x-forms.input id="cloudflare_token" required label="Cloudflare Token" type="password" />
<x-forms.input id="ssh_domain" label="Configured SSH Domain" required
helper="The SSH domain you configured in Cloudflare. Make sure there is no protocol like http(s):// so you provide a FQDN not a URL. <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels/server-ssh' target='_blank'>Documentation</a>" />
<x-forms.button type="submit" isHighlighted>Continue</x-forms.button>
</form>
</div>
@script
<script>
$wire.$on('automatedCloudflareConfig', () => {
window.dispatchEvent(new CustomEvent('automated'));
$wire.$call('automatedCloudflareConfig');
});
</script>
@endscript
</div>
<h3 class="pt-6 pb-2">Manual</h3>
<x-modal-confirmation buttonFullWidth title="I manually configured Cloudflare Tunnel?"
buttonTitle="I manually configured Cloudflare Tunnel" submitAction="manualCloudflareConfig"
:actions="[
'You set everything up manually, including in Cloudflare and on the server (cloudflared is running).',
'If you missed something, the connection will not work.',
]" confirmationText="I manually configured Cloudflare Tunnel"
confirmationLabel="Please type the confirmation text to confirm that you manually configured Cloudflare Tunnel."
shortConfirmationLabel="Confirmation text" step3ButtonText="Confirm" />
@endif
</div>
</div>
</div>
</div>

View File

@@ -1,53 +0,0 @@
<div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Cloudflare Tunnels | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="cloudflare-tunnels" />
<div class="w-full">
<div class="flex flex-col">
<div class="flex gap-1 items-center">
<h2>Cloudflare Tunnels</h2>
<x-helper class="inline-flex"
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all SSH requests to your server through Cloudflare.<br> You then can close your server's SSH port in the firewall of your hosting provider.<br><span class='dark:text-warning'>If you choose manual configuration, Coolify does not install or set up Cloudflare (cloudflared) on your server.</span>" />
</div>
<div>Secure your servers with Cloudflare Tunnels.</div>
</div>
<div class="flex flex-col gap-2 pt-6">
@if ($isCloudflareTunnelsEnabled)
<div class="w-64">
<x-forms.checkbox instantSave id="isCloudflareTunnelsEnabled" label="Enabled" />
</div>
@elseif (!$server->isFunctional())
<div
class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded-sm dark:bg-yellow-900 dark:text-yellow-300">
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnels, please
validate your server first.</span> Then you will need a Cloudflare token and an SSH
domain configured.
<br />
To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please
click <span wire:click="manualCloudflareConfig" class="underline cursor-pointer">here</span>,
then you should validate the server.
<br /><br />
For more information, please read our <a
href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/overview" target="_blank"
class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">documentation</a>.
</div>
@endif
@if (!$isCloudflareTunnelsEnabled && $server->isFunctional())
<h4>Configuration</h4>
<div class="flex gap-2">
<x-modal-input buttonTitle="Automated" title="Cloudflare Tunnels" :closeOutside="false"
isHighlightedButton>
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
</x-modal-input>
<x-forms.button wire:click="manualCloudflareConfig" class="w-20">
Manual
</x-forms.button>
</div>
@endif
</div>
</div>
</div>
</div>

View File

@@ -1,6 +0,0 @@
<form wire:submit.prevent='submit' class="flex flex-col gap-2 w-full">
<x-forms.input id="cloudflare_token" required label="Cloudflare Token" type="password" />
<x-forms.input id="ssh_domain" label="Configured SSH Domain" required
helper="The SSH domain you configured in Cloudflare. Make sure there is no protocol like http(s):// so you provide a FQDN not a URL. <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels/server-ssh' target='_blank'>Documentation</a>" />
<x-forms.button type="submit" isHighlighted @click="modalOpen=false">Continue</x-forms.button>
</form>

View File

@@ -2,7 +2,7 @@
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Delete Server | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="danger" />
<div class="w-full">

View File

@@ -2,7 +2,7 @@
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Destinations | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="destinations" />
<div class="w-full">

View File

@@ -2,7 +2,7 @@
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Docker Cleanup | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<livewire:server.navbar :server="$server" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="docker-cleanup" />
<div class="w-full">
@@ -34,8 +34,8 @@
label="Docker cleanup frequency" required
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." />
@if (!$forceDockerCleanup)
<x-forms.input id="dockerCleanupThreshold" label="Docker cleanup threshold (%)" required
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
<x-forms.input id="dockerCleanupThreshold" label="Docker cleanup threshold (%)" required
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
@endif
<div class="w-96">
<x-forms.checkbox
@@ -77,7 +77,8 @@
</form>
<div class="mt-8">
<h3 class="mb-4">Recent executions <span class="text-xs text-neutral-500">(click to check output)</span></h3>
<h3 class="mb-4">Recent executions <span class="text-xs text-neutral-500">(click to check
output)</span></h3>
<livewire:server.docker-cleanup-executions :server="$server" />
</div>
</div>

View File

@@ -2,7 +2,7 @@
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Log Drains | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="log-drains" />
<div class="w-full">

View File

@@ -0,0 +1,178 @@
<div class="pb-6">
<x-modal modalId="startProxy">
<x-slot:modalBody>
<livewire:activity-monitor header="Proxy Startup Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="startProxy.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
<div class="flex items-center gap-2">
<h1>Server</h1>
@if ($server->proxySet())
<div class="flex gap-2">
@if (data_get($server, 'proxy.force_stop', false) === false)
<x-forms.button wire:click='checkProxyStatus()' :disabled="$isChecking" wire:loading.attr="disabled"
wire:target="checkProxyStatus">
<span wire:loading.remove wire:target="checkProxyStatus">Refresh</span>
<span wire:loading wire:target="checkProxyStatus">Checking...</span>
</x-forms.button>
@endif
<div wire:loading.remove wire:target="checkProxyStatus" class="flex items-center gap-2">
@if (data_get($server, 'proxy.status') === 'running')
<x-status.running status="Proxy Running" />
@elseif (data_get($server, 'proxy.status') === 'restarting')
<x-status.restarting status="Proxy Restarting" />
@elseif (data_get($server, 'proxy.force_stop'))
<x-status.stopped status="Proxy Stopped" />
@elseif (data_get($server, 'proxy.status') === 'exited')
<x-status.stopped status="Proxy Exited" />
@else
<x-status.stopped status="Proxy Not Running" />
@endif
</div>
</div>
@endif
</div>
<div class="subtitle">{{ data_get($server, 'name') }}</div>
<div class="navbar-main">
<nav
class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap">
<a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}"
href="{{ route('server.show', [
'server_uuid' => data_get($server, 'uuid'),
]) }}">
<button>Configuration</button>
</a>
@if (!$server->isSwarmWorker() && !$server->settings->is_build_server)
<a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }}"
href="{{ route('server.proxy', [
'server_uuid' => data_get($server, 'uuid'),
]) }}">
<button>Proxy</button>
</a>
@endif
<a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}"
href="{{ route('server.resources', [
'server_uuid' => data_get($server, 'uuid'),
]) }}">
<button>Resources</button>
</a>
<a class="{{ request()->routeIs('server.command') ? 'dark:text-white' : '' }}"
href="{{ route('server.command', [
'server_uuid' => data_get($server, 'uuid'),
]) }}">
<button>Terminal</button>
</a>
<a class="{{ request()->routeIs('server.security.patches') ? 'dark:text-white' : '' }}"
href="{{ route('server.security.patches', [
'server_uuid' => data_get($server, 'uuid'),
]) }}">
<button>Security</button>
</a>
</nav>
<div class="order-first sm:order-last">
@php use App\Enums\ProxyTypes; @endphp
<div>
@if ($server->proxySet())
<x-slide-over fullScreen @startproxy.window="slideOverOpen = true">
<x-slot:title>Proxy Status</x-slot:title>
<x-slot:content>
<livewire:activity-monitor header="Logs" />
</x-slot:content>
</x-slide-over>
@if (data_get($server, 'proxy.status') === 'running')
<div class="flex gap-2">
@if (
$currentRoute === 'server.proxy' &&
$traefikDashboardAvailable &&
$server->proxyType() === ProxyTypes::TRAEFIK->value)
<button>
<a target="_blank" href="http://{{ $serverIp }}:8080">
Traefik Dashboard
<x-external-link />
</a>
</button>
@endif
<x-modal-confirmation title="Confirm Proxy Restart?" buttonTitle="Restart Proxy"
submitAction="restart" :actions="[
'This proxy will be stopped and started again.',
'All resources hosted on coolify will be unavailable during the restart.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Restart Proxy" :dispatchEvent="true" dispatchEventType="restartEvent">
<x-slot:button-title>
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<path
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart Proxy
</x-slot:button-title>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Proxy Stopping?" buttonTitle="Stop Proxy"
submitAction="stop(true)" :actions="[
'The coolify proxy will be stopped.',
'All resources hosted on coolify will be unavailable.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Stop Proxy" :dispatchEvent="true" dispatchEventType="stopEvent">
<x-slot:button-title>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop Proxy
</x-slot:button-title>
</x-modal-confirmation>
</div>
@else
<button @click="$wire.dispatch('checkProxyEvent')" class="gap-2 button">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Start Proxy
</button>
@endif
@endif
@script
<script>
$wire.$on('checkProxyEvent', () => {
$wire.$dispatch('info', 'Checking if the required ports are not used by other services.');
$wire.$call('checkProxy');
});
$wire.$on('restartEvent', () => {
$wire.$dispatch('info', 'Initiating proxy restart.');
$wire.$call('restart');
});
$wire.$on('startProxy', () => {
window.dispatchEvent(new CustomEvent('startproxy'))
$wire.$call('startProxy');
});
$wire.$on('stopEvent', () => {
$wire.$dispatch('info', 'Stopping proxy.');
$wire.$call('stop');
});
</script>
@endscript
</div>
</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Private Key | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="private-key" />
<div class="w-full">

View File

@@ -1,93 +0,0 @@
@php use App\Enums\ProxyTypes; @endphp
<div>
@if ($server->proxySet())
<x-slide-over closeWithX fullScreen @startproxy.window="slideOverOpen = true">
<x-slot:title>Proxy Status</x-slot:title>
<x-slot:content>
<livewire:activity-monitor header="Logs" />
</x-slot:content>
</x-slide-over>
@if (data_get($server, 'proxy.status') === 'running')
<div class="flex gap-2">
@if (
$currentRoute === 'server.proxy' &&
$traefikDashboardAvailable &&
$server->proxyType() === ProxyTypes::TRAEFIK->value)
<button>
<a target="_blank" href="http://{{ $serverIp }}:8080">
Traefik Dashboard
<x-external-link />
</a>
</button>
@endif
<x-modal-confirmation title="Confirm Proxy Restart?" buttonTitle="Restart Proxy" submitAction="restart"
:actions="[
'This proxy will be stopped and started again.',
'All resources hosted on coolify will be unavailable during the restart.',
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Proxy"
:dispatchEvent="true" dispatchEventType="restartEvent">
<x-slot:button-title>
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart Proxy
</x-slot:button-title>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Proxy Stopping?" buttonTitle="Stop Proxy" submitAction="stop(true)"
:actions="[
'The coolify proxy will be stopped.',
'All resources hosted on coolify will be unavailable.',
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Stop Proxy" :dispatchEvent="true"
dispatchEventType="stopEvent">
<x-slot:button-title>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop Proxy
</x-slot:button-title>
</x-modal-confirmation>
</div>
@else
<button @click="$wire.dispatch('checkProxyEvent')" class="gap-2 button">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Start Proxy
</button>
@endif
@endif
@script
<script>
$wire.$on('checkProxyEvent', () => {
$wire.$dispatch('info', 'Checking proxy.');
$wire.$call('checkProxy');
});
$wire.$on('restartEvent', () => {
$wire.$dispatch('info', 'Restarting proxy.');
$wire.$call('restart');
});
$wire.$on('proxyChecked', () => {
window.dispatchEvent(new CustomEvent('startproxy'))
$wire.$call('startProxy');
});
$wire.$on('stopEvent', () => {
$wire.$dispatch('info', 'Stopping proxy.');
$wire.$call('stop');
});
</script>
@endscript
</div>

View File

@@ -2,7 +2,7 @@
<x-slot:title>
Proxy Dynamic Configuration | Coolify
</x-slot>
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.navbar :server="$server" :parameters="$parameters" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar-proxy :server="$server" :parameters="$parameters" />
<div class="w-full">

View File

@@ -2,7 +2,7 @@
<x-slot:title>
Proxy Logs | Coolify
</x-slot>
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.navbar :server="$server" :parameters="$parameters" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar-proxy :server="$server" :parameters="$parameters" />
<div class="w-full">

View File

@@ -2,7 +2,7 @@
<x-slot:title>
Proxy Configuration | Coolify
</x-slot>
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.navbar :server="$server" :parameters="$parameters" />
@if ($server->isFunctional())
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar-proxy :server="$server" :parameters="$parameters" />

View File

@@ -1,17 +0,0 @@
<div @if (data_get($server, 'proxy.force_stop', false) === false) x-init="$wire.checkProxy()" @endif class="flex gap-2">
@if (data_get($server, 'proxy.force_stop', false) === false)
<x-forms.button wire:click='checkProxy(true)' :showLoadingIndicator="false">Refresh</x-forms.button>
@endif
@if (data_get($server, 'proxy.status') === 'running')
<x-status.running status="Proxy Running" />
@elseif (data_get($server, 'proxy.status') === 'restarting')
<x-status.restarting status="Proxy Restarting" />
@elseif (data_get($server, 'proxy.force_stop'))
<x-status.stopped status="Proxy Stopped" />
@elseif (data_get($server, 'proxy.status') === 'exited')
<x-status.stopped status="Proxy Exited" />
@else
<x-status.stopped status="Proxy Not Running" />
@endif
</div>

View File

@@ -2,7 +2,7 @@
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Resources | Coolify
</x-slot>
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.navbar :server="$server" :parameters="$parameters" />
<div x-data="{ activeTab: 'managed' }" class="flex flex-col h-full gap-8 md:flex-row">
<div class="w-full">
<div class="flex flex-col">

View File

@@ -2,7 +2,7 @@
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Security | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<livewire:server.navbar :server="$server" />
<x-slide-over closeWithX fullScreen @startupdate.window="slideOverOpen = true">
<x-slot:title>Updating Packages</x-slot:title>
<x-slot:content>

View File

@@ -2,7 +2,7 @@
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > General | Coolify
</x-slot>
<x-server.navbar :server="$server" />
<livewire:server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="general" />
<div class="w-full">

View File

@@ -39,7 +39,7 @@ use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow;
use App\Livewire\Server\Advanced as ServerAdvanced;
use App\Livewire\Server\CaCertificate\Show as CaCertificateShow;
use App\Livewire\Server\Charts as ServerCharts;
use App\Livewire\Server\CloudflareTunnels;
use App\Livewire\Server\CloudflareTunnel;
use App\Livewire\Server\Delete as DeleteServer;
use App\Livewire\Server\Destinations as ServerDestinations;
use App\Livewire\Server\DockerCleanup;
@@ -245,7 +245,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key');
Route::get('/ca-certificate', CaCertificateShow::class)->name('server.ca-certificate');
Route::get('/resources', ResourcesShow::class)->name('server.resources');
Route::get('/cloudflare-tunnels', CloudflareTunnels::class)->name('server.cloudflare-tunnels');
Route::get('/cloudflare-tunnel', CloudflareTunnel::class)->name('server.cloudflare-tunnel');
Route::get('/destinations', ServerDestinations::class)->name('server.destinations');
Route::get('/log-drains', LogDrains::class)->name('server.log-drains');
Route::get('/metrics', ServerCharts::class)->name('server.charts');