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;
}
$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;
}
if (! $server->isProxyShouldRun()) {
@@ -37,8 +37,12 @@ class CheckProxy
return false;
}
}
// Determine proxy container name based on environment
$proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
if ($server->isSwarm()) {
$status = getContainerStatus($server, 'coolify-proxy_traefik');
$status = getContainerStatus($server, $proxyContainerName);
$server->proxy->set('status', $status);
$server->save();
if ($status === 'running') {
@@ -47,7 +51,7 @@ class CheckProxy
return true;
} else {
$status = getContainerStatus($server, 'coolify-proxy');
$status = getContainerStatus($server, $proxyContainerName);
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
@@ -61,34 +65,11 @@ class CheckProxy
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$portsToCheck = ['80', '443'];
foreach ($portsToCheck as $port) {
// Try multiple methods to check port availability
$commands = [
// 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) {
// 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 {
@@ -126,4 +107,144 @@ class CheckProxy
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)
// {
// $teamId = getTeamIdFromToken();
private function validateDataApplications(Request $request, Server $server)
{
$teamId = getTeamIdFromToken();
// // Validate ports_mappings
// if ($request->has('ports_mappings')) {
// $ports = [];
// foreach (explode(',', $request->ports_mappings) as $portMapping) {
// $port = explode(':', $portMapping);
// if (in_array($port[0], $ports)) {
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => [
// 'ports_mappings' => 'The first number before : should be unique between mappings.',
// ],
// ], 422);
// }
// $ports[] = $port[0];
// }
// }
// // Validate custom_labels
// if ($request->has('custom_labels')) {
// if (! isBase64Encoded($request->custom_labels)) {
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => [
// 'custom_labels' => 'The custom_labels should be base64 encoded.',
// ],
// ], 422);
// }
// $customLabels = base64_decode($request->custom_labels);
// if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => [
// 'custom_labels' => 'The custom_labels should be base64 encoded.',
// ],
// ], 422);
// }
// }
// if ($request->has('domains') && $server->isProxyShouldRun()) {
// $uuid = $request->uuid;
// $fqdn = $request->domains;
// $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
// $fqdn = str($fqdn)->replaceStart(',', '')->trim();
// $errors = [];
// $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
// if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
// $errors[] = 'Invalid domain: '.$domain;
// }
// Validate ports_mappings
if ($request->has('ports_mappings')) {
$ports = [];
foreach (explode(',', $request->ports_mappings) as $portMapping) {
$port = explode(':', $portMapping);
if (in_array($port[0], $ports)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'ports_mappings' => 'The first number before : should be unique between mappings.',
],
], 422);
}
$ports[] = $port[0];
}
}
// Validate custom_labels
if ($request->has('custom_labels')) {
if (! isBase64Encoded($request->custom_labels)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'custom_labels' => 'The custom_labels should be base64 encoded.',
],
], 422);
}
$customLabels = base64_decode($request->custom_labels);
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'custom_labels' => 'The custom_labels should be base64 encoded.',
],
], 422);
}
}
if ($request->has('domains') && $server->isProxyShouldRun()) {
$uuid = $request->uuid;
$fqdn = $request->domains;
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
$errors = [];
$fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
$errors[] = 'Invalid domain: '.$domain;
}
// return str($domain)->trim()->lower();
// });
// if (count($errors) > 0) {
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => $errors,
// ], 422);
// }
// if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
// return response()->json([
// 'message' => 'Validation failed.',
// 'errors' => [
// 'domains' => 'One of the domain is already used.',
// ],
// ], 422);
// }
// }
// }
return str($domain)->trim()->lower();
});
if (count($errors) > 0) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'domains' => 'One of the domain is already used.',
],
], 422);
}
}
}
}

View File

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

View File

@@ -925,7 +925,7 @@ $schema://$host {
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) {
Storage::disk('ssh-mux')->delete($this->muxFilename());

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
<div x-init="$wire.checkProxy()" class="flex gap-2">
<x-forms.button wire:click='checkProxy(true)' :showLoadingIndicator="false">Refresh</x-forms.button>
<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')

View File

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