Merge pull request #5558 from coollabsio/next

v4.0.0-beta.406
This commit is contained in:
Andras Bacsai
2025-04-05 15:09:22 +02:00
committed by GitHub
10 changed files with 4577 additions and 658 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ class CheckProxy
return false; return false;
} }
$proxyType = $server->proxyType(); $proxyType = $server->proxyType();
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) { if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) && ! $fromUI) {
return false; return false;
} }
if (! $server->isProxyShouldRun()) { if (! $server->isProxyShouldRun()) {
@@ -37,8 +37,12 @@ class CheckProxy
return false; return false;
} }
} }
// Determine proxy container name based on environment
$proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
if ($server->isSwarm()) { if ($server->isSwarm()) {
$status = getContainerStatus($server, 'coolify-proxy_traefik'); $status = getContainerStatus($server, $proxyContainerName);
$server->proxy->set('status', $status); $server->proxy->set('status', $status);
$server->save(); $server->save();
if ($status === 'running') { if ($status === 'running') {
@@ -47,7 +51,7 @@ class CheckProxy
return true; return true;
} else { } else {
$status = getContainerStatus($server, 'coolify-proxy'); $status = getContainerStatus($server, $proxyContainerName);
if ($status === 'running') { if ($status === 'running') {
$server->proxy->set('status', 'running'); $server->proxy->set('status', 'running');
$server->save(); $server->save();
@@ -61,34 +65,11 @@ class CheckProxy
if ($server->id === 0) { if ($server->id === 0) {
$ip = 'host.docker.internal'; $ip = 'host.docker.internal';
} }
$portsToCheck = ['80', '443']; $portsToCheck = ['80', '443'];
foreach ($portsToCheck as $port) { foreach ($portsToCheck as $port) {
// Try multiple methods to check port availability // Use the smart port checker that handles dual-stack properly
$commands = [ if ($this->isPortConflict($server, $port, $proxyContainerName)) {
// Method 1: Check /proc/net/tcp directly (convert port to hex)
"cat /proc/net/tcp | grep -q '00000000:".str_pad(dechex($port), 4, '0', STR_PAD_LEFT)."'",
// Method 2: Use ss command (modern alternative to netstat)
"ss -tuln | grep -q ':$port '",
// Method 3: Use lsof if available
"lsof -i :$port >/dev/null 2>&1",
// Method 4: Use fuser if available
"fuser $port/tcp >/dev/null 2>&1",
];
$portInUse = false;
foreach ($commands as $command) {
try {
instant_remote_process([$command], $server);
$portInUse = true;
break;
} catch (\Throwable $e) {
continue;
}
}
if ($portInUse) {
if ($fromUI) { 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>"); 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 { } else {
@@ -126,4 +107,144 @@ class CheckProxy
return true; return true;
} }
} }
/**
* Smart port checker that handles dual-stack configurations
* Returns true only if there's a real port conflict (not just dual-stack)
*/
private function isPortConflict(Server $server, string $port, string $proxyContainerName): bool
{
// First check if our own proxy is using this port (which is fine)
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
}
}
} 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;
}
}
} }

View File

@@ -3006,73 +3006,73 @@ class ApplicationsController extends Controller
// ]); // ]);
// } // }
// private function validateDataApplications(Request $request, Server $server) private function validateDataApplications(Request $request, Server $server)
// { {
// $teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
// // Validate ports_mappings // Validate ports_mappings
// if ($request->has('ports_mappings')) { if ($request->has('ports_mappings')) {
// $ports = []; $ports = [];
// foreach (explode(',', $request->ports_mappings) as $portMapping) { foreach (explode(',', $request->ports_mappings) as $portMapping) {
// $port = explode(':', $portMapping); $port = explode(':', $portMapping);
// if (in_array($port[0], $ports)) { if (in_array($port[0], $ports)) {
// return response()->json([ return response()->json([
// 'message' => 'Validation failed.', 'message' => 'Validation failed.',
// 'errors' => [ 'errors' => [
// 'ports_mappings' => 'The first number before : should be unique between mappings.', 'ports_mappings' => 'The first number before : should be unique between mappings.',
// ], ],
// ], 422); ], 422);
// } }
// $ports[] = $port[0]; $ports[] = $port[0];
// } }
// } }
// // Validate custom_labels // Validate custom_labels
// if ($request->has('custom_labels')) { if ($request->has('custom_labels')) {
// if (! isBase64Encoded($request->custom_labels)) { if (! isBase64Encoded($request->custom_labels)) {
// return response()->json([ return response()->json([
// 'message' => 'Validation failed.', 'message' => 'Validation failed.',
// 'errors' => [ 'errors' => [
// 'custom_labels' => 'The custom_labels should be base64 encoded.', 'custom_labels' => 'The custom_labels should be base64 encoded.',
// ], ],
// ], 422); ], 422);
// } }
// $customLabels = base64_decode($request->custom_labels); $customLabels = base64_decode($request->custom_labels);
// if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
// return response()->json([ return response()->json([
// 'message' => 'Validation failed.', 'message' => 'Validation failed.',
// 'errors' => [ 'errors' => [
// 'custom_labels' => 'The custom_labels should be base64 encoded.', 'custom_labels' => 'The custom_labels should be base64 encoded.',
// ], ],
// ], 422); ], 422);
// } }
// } }
// if ($request->has('domains') && $server->isProxyShouldRun()) { if ($request->has('domains') && $server->isProxyShouldRun()) {
// $uuid = $request->uuid; $uuid = $request->uuid;
// $fqdn = $request->domains; $fqdn = $request->domains;
// $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
// $fqdn = str($fqdn)->replaceStart(',', '')->trim(); $fqdn = str($fqdn)->replaceStart(',', '')->trim();
// $errors = []; $errors = [];
// $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
// if (filter_var($domain, FILTER_VALIDATE_URL) === false) { if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
// $errors[] = 'Invalid domain: '.$domain; $errors[] = 'Invalid domain: '.$domain;
// } }
// return str($domain)->trim()->lower(); return str($domain)->trim()->lower();
// }); });
// if (count($errors) > 0) { if (count($errors) > 0) {
// return response()->json([ return response()->json([
// 'message' => 'Validation failed.', 'message' => 'Validation failed.',
// 'errors' => $errors, 'errors' => $errors,
// ], 422); ], 422);
// } }
// if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
// return response()->json([ return response()->json([
// 'message' => 'Validation failed.', 'message' => 'Validation failed.',
// 'errors' => [ 'errors' => [
// 'domains' => 'One of the domain is already used.', 'domains' => 'One of the domain is already used.',
// ], ],
// ], 422); ], 422);
// } }
// } }
// } }
} }

View File

@@ -455,7 +455,6 @@ class General extends Component
{ {
$config = GenerateConfig::run($this->application, true); $config = GenerateConfig::run($this->application, true);
$fileName = str($this->application->name)->slug()->append('_config.json'); $fileName = str($this->application->name)->slug()->append('_config.json');
dd($config);
return response()->streamDownload(function () use ($config) { return response()->streamDownload(function () use ($config) {
echo $config; echo $config;

View File

@@ -925,7 +925,7 @@ $schema://$host {
public function isFunctional() public function isFunctional()
{ {
$isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4'; $isFunctional = data_get($this->settings, 'is_reachable') && data_get($this->settings, 'is_usable') && data_get($this->settings, 'force_disabled') === false && $this->ip !== '1.2.3.4';
if ($isFunctional === false) { if ($isFunctional === false) {
Storage::disk('ssh-mux')->delete($this->muxFilename()); Storage::disk('ssh-mux')->delete($this->muxFilename());

View File

@@ -2,7 +2,7 @@
return [ return [
'coolify' => [ 'coolify' => [
'version' => '4.0.0-beta.405', 'version' => '4.0.0-beta.406',
'helper_version' => '1.0.8', 'helper_version' => '1.0.8',
'realtime_version' => '1.0.6', 'realtime_version' => '1.0.6',
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),

View File

@@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.405" "version": "4.0.0-beta.406"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.406" "version": "4.0.0-beta.407"
}, },
"helper": { "helper": {
"version": "1.0.8" "version": "1.0.8"

View File

@@ -72,6 +72,7 @@
@script @script
<script> <script>
$wire.$on('checkProxyEvent', () => { $wire.$on('checkProxyEvent', () => {
$wire.$dispatch('info', 'Checking proxy.');
$wire.$call('checkProxy'); $wire.$call('checkProxy');
}); });
$wire.$on('restartEvent', () => { $wire.$on('restartEvent', () => {

View File

@@ -1,5 +1,7 @@
<div x-init="$wire.checkProxy()" class="flex gap-2"> <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> <x-forms.button wire:click='checkProxy(true)' :showLoadingIndicator="false">Refresh</x-forms.button>
@endif
@if (data_get($server, 'proxy.status') === 'running') @if (data_get($server, 'proxy.status') === 'running')
<x-status.running status="Proxy Running" /> <x-status.running status="Proxy Running" />
@elseif (data_get($server, 'proxy.status') === 'restarting') @elseif (data_get($server, 'proxy.status') === 'restarting')

View File

@@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.405" "version": "4.0.0-beta.406"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.406" "version": "4.0.0-beta.407"
}, },
"helper": { "helper": {
"version": "1.0.8" "version": "1.0.8"