Merge remote-tracking branch 'origin/next' into feat/railpack

This commit is contained in:
Andras Bacsai 2026-04-17 07:01:27 +02:00
commit 451b7376ed
49 changed files with 631 additions and 289 deletions

View file

@ -88,6 +88,14 @@ private function processFile(string $file): false|array
$payload['envs'] = base64_encode($envFileContent);
}
if (str($data->get('amd_only'))->toBoolean()) {
$payload['amd_only'] = true;
}
if (str($data->get('arm_only'))->toBoolean()) {
$payload['arm_only'] = true;
}
return $payload;
}
@ -160,6 +168,14 @@ private function processFileWithFqdn(string $file): false|array
$payload['envs'] = base64_encode($modifiedEnvContent);
}
if (str($data->get('amd_only'))->toBoolean()) {
$payload['amd_only'] = true;
}
if (str($data->get('arm_only'))->toBoolean()) {
$payload['arm_only'] = true;
}
return $payload;
}
@ -229,6 +245,14 @@ private function processFileWithFqdnRaw(string $file): false|array
$payload['envs'] = $modifiedEnvContent;
}
if (str($data->get('amd_only'))->toBoolean()) {
$payload['amd_only'] = true;
}
if (str($data->get('arm_only'))->toBoolean()) {
$payload['arm_only'] = true;
}
return $payload;
}
}

View file

@ -217,7 +217,7 @@ public function applications(Request $request)
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io")'],
],
),
],
@ -383,7 +383,7 @@ public function create_public_application(Request $request)
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io")'],
],
),
],
@ -549,7 +549,7 @@ public function create_private_gh_app_application(Request $request)
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io")'],
],
),
],
@ -2397,7 +2397,7 @@ public function delete_by_uuid(Request $request)
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io")'],
],
),
],

View file

@ -221,7 +221,7 @@ public function services(Request $request)
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
'url' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io").'],
'url' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io").'],
],
),
],
@ -843,7 +843,7 @@ public function delete_by_uuid(Request $request)
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
'url' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io").'],
'url' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io").'],
],
),
],

View file

@ -3198,7 +3198,7 @@ private function generate_healthcheck_commands()
$scheme = $this->sanitizeHealthCheckValue($this->application->health_check_scheme, '/^https?$/', 'http');
$host = $this->sanitizeHealthCheckValue($this->application->health_check_host, '/^[a-zA-Z0-9.\-_]+$/', 'localhost');
$path = $this->application->health_check_path
? $this->sanitizeHealthCheckValue($this->application->health_check_path, '#^[a-zA-Z0-9/\-_.~%]+$#', '/')
? $this->sanitizeHealthCheckValue($this->application->health_check_path, '#^[a-zA-Z0-9/\-_.~%,;]+$#', '/')
: null;
$url = escapeshellarg("{$scheme}://{$host}:{$health_check_port}".($path ?? '/'));

View file

@ -1496,7 +1496,10 @@ public function getServicesProperty()
'category' => 'Services',
'resourceType' => 'service',
'logo' => data_get($service, 'logo'),
]);
] + array_filter([
'amd_only' => data_get($service, 'amd_only') ? true : null,
'arm_only' => data_get($service, 'arm_only') ? true : null,
]));
}
$cachedServices = $items->toArray();

View file

@ -34,7 +34,7 @@ class HealthChecks extends Component
#[Validate(['nullable', 'integer', 'min:1', 'max:65535'])]
public ?string $healthCheckPort = null;
#[Validate(['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'])]
#[Validate(['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%,;]+$#'])]
public string $healthCheckPath;
#[Validate(['integer'])]
@ -62,7 +62,7 @@ class HealthChecks extends Component
'healthCheckEnabled' => 'boolean',
'healthCheckType' => 'string|in:http,cmd',
'healthCheckCommand' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
'healthCheckPath' => ['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
'healthCheckPath' => ['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%,;]+$#'],
'healthCheckPort' => 'nullable|integer|min:1|max:65535',
'healthCheckHost' => ['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
'healthCheckMethod' => 'required|string|in:GET,HEAD,POST,OPTIONS',

View file

@ -233,6 +233,9 @@ public function subscriptionEnded()
'is_reachable' => false,
]);
ServerReachabilityChanged::dispatch($server);
$server->unreachable_count = 3;
$server->unreachable_notification_sent = true;
$server->save();
}
}
@ -344,5 +347,4 @@ public function webhookNotificationSettings()
{
return $this->hasOne(WebhookNotificationSettings::class);
}
}

View file

@ -203,10 +203,24 @@ public static function volumeNameMessages(string $field = 'name'): array
}
/**
* Pattern for port mappings (e.g. 3000:3000, 8080:80, 8000-8010:8000-8010)
* Each entry requires host:container format, where each side can be a number or a range (number-number)
* Pattern for port mappings with optional IP binding and protocol suffix on either side.
* Format: [ip:]port[:ip:port] where IP is IPv4 or [IPv6], port can be a range, protocol suffix optional.
* Examples: 8080:80, 127.0.0.1:8080:80, [::1]::80/udp, 127.0.0.1:8080:80/tcp
*/
public const PORT_MAPPINGS_PATTERN = '/^(\d+(-\d+)?:\d+(-\d+)?)(,\d+(-\d+)?:\d+(-\d+)?)*$/';
public const PORT_MAPPINGS_PATTERN = '/^
(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[\da-fA-F:]+\]):)? # optional IP
(?:\d+(?:-\d+)?(?:\/(?:tcp|udp|sctp))?)? # optional host port
:
(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[\da-fA-F:]+\]):)? # optional IP
\d+(?:-\d+)?(?:\/(?:tcp|udp|sctp))? # container port
(?:,
(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[\da-fA-F:]+\]):)?
(?:\d+(?:-\d+)?(?:\/(?:tcp|udp|sctp))?)?
:
(?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[\da-fA-F:]+\]):)?
\d+(?:-\d+)?(?:\/(?:tcp|udp|sctp))?
)*
$/x';
/**
* Get validation rules for container name fields
@ -230,7 +244,7 @@ public static function portMappingRules(): array
public static function portMappingMessages(string $field = 'portsMappings'): array
{
return [
"{$field}.regex" => 'Port mappings must be a comma-separated list of port pairs or ranges (e.g. 3000:3000,8080:80,8000-8010:8000-8010).',
"{$field}.regex" => 'Port mappings must be a comma-separated list of port pairs or ranges with optional IP and protocol (e.g. 3000:3000, 8080:80/udp, 127.0.0.1:8080:80, [::1]::80).',
];
}

View file

@ -106,7 +106,7 @@ function sharedDataApplications()
'health_check_enabled' => 'boolean',
'health_check_type' => 'string|in:http,cmd',
'health_check_command' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
'health_check_path' => ['string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
'health_check_path' => ['string', 'regex:#^[a-zA-Z0-9/\-_.~%,;]+$#'],
'health_check_port' => 'integer|nullable|min:1|max:65535',
'health_check_host' => ['string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
'health_check_method' => 'string|in:GET,HEAD,POST,OPTIONS',

View file

@ -3660,13 +3660,21 @@ function convertGitUrl(string $gitRepository, string $deploymentType, GithubApp|
}
}
preg_match('/(?<=:)\d+(?=\/)/', $gitRepository, $matches);
$normalizedRepository = $repository;
if (count($matches) === 1) {
$providerInfo['port'] = $matches[0];
$gitHost = str($gitRepository)->before(':');
$gitRepo = str($gitRepository)->after('/');
$repository = "$gitHost:$gitRepo";
if (str($normalizedRepository)->contains('://')) {
$parsedRepository = parse_url($normalizedRepository);
if ($parsedRepository !== false && array_key_exists('port', $parsedRepository)) {
$providerInfo['port'] = (string) $parsedRepository['port'];
}
} else {
preg_match('/^(?<host>[^:]+):(?<port>\d+)\/(?<path>.+)$/', $normalizedRepository, $matches);
if (! empty($matches['port'])) {
$providerInfo['port'] = $matches['port'];
$repository = "{$matches['host']}:{$matches['path']}";
}
}
return [

14
composer.lock generated
View file

@ -72,7 +72,6 @@
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/67b6b6210af47319c74c5666388d71bc1bc58276",
"reference": "67b6b6210af47319c74c5666388d71bc1bc58276",
"shasum": ""
},
"require": {
@ -157,7 +156,6 @@
"source": "https://github.com/aws/aws-sdk-php/tree/3.374.2"
},
"time": "2026-03-27T18:05:55+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -5158,16 +5156,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.50",
"version": "3.0.51",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b"
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748",
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748",
"shasum": ""
},
"require": {
@ -5248,7 +5246,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.50"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.51"
},
"funding": [
{
@ -5264,7 +5262,7 @@
"type": "tidelift"
}
],
"time": "2026-03-19T02:57:58+00:00"
"time": "2026-04-10T01:33:53+00:00"
},
{
"name": "phpstan/phpdoc-parser",

View file

@ -2,9 +2,9 @@
return [
'coolify' => [
'version' => '4.0.0-beta.473',
'version' => '4.0.0-beta.474',
'helper_version' => '1.0.13',
'realtime_version' => '1.0.12',
'realtime_version' => '1.0.13',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),

View file

@ -11,7 +11,7 @@
*/
public function up(): void
{
Server::query()->chunk(100, function ($servers) {
Server::query()->whereHas('team')->chunk(100, function ($servers) {
foreach ($servers as $server) {
$existingKeys = SharedEnvironmentVariable::where('type', 'server')
->where('server_id', $server->id)

View file

@ -10,14 +10,15 @@
{
public function up(): void
{
Schema::table('local_persistent_volumes', function (Blueprint $table) {
$table->string('uuid')->nullable()->after('id');
});
if (! Schema::hasColumn('local_persistent_volumes', 'uuid')) {
Schema::table('local_persistent_volumes', function (Blueprint $table) {
$table->string('uuid')->nullable()->after('id');
});
}
DB::table('local_persistent_volumes')
->whereNull('uuid')
->orderBy('id')
->chunk(1000, function ($volumes) {
->chunkById(1000, function ($volumes) {
foreach ($volumes as $volume) {
DB::table('local_persistent_volumes')
->where('id', $volume->id)

View file

@ -112,6 +112,7 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- dev_coolify_data:/data/coolify
- dev_coolify_data:/var/lib/docker/volumes/coolify_dev_coolify_data/_data
- dev_backups_data:/data/coolify/backups
- dev_postgres_data:/data/coolify/_volumes/database
- dev_redis_data:/data/coolify/_volumes/redis

View file

@ -60,7 +60,7 @@ services:
retries: 10
timeout: 2s
soketi:
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.12'
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.13'
ports:
- "${SOKETI_PORT:-6001}:6001"
- "6002:6002"

View file

@ -96,7 +96,7 @@ services:
retries: 10
timeout: 2s
soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.12'
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.13'
pull_policy: always
container_name: coolify-realtime
restart: always

View file

@ -7,7 +7,7 @@
"dependencies": {
"@xterm/addon-fit": "0.11.0",
"@xterm/xterm": "6.0.0",
"axios": "1.13.6",
"axios": "1.15.0",
"cookie": "1.1.1",
"dotenv": "17.3.1",
"node-pty": "1.1.0",
@ -36,14 +36,14 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
"proxy-from-env": "^2.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
@ -344,10 +344,13 @@
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/ws": {
"version": "8.19.0",

View file

@ -5,7 +5,7 @@
"@xterm/addon-fit": "0.11.0",
"@xterm/xterm": "6.0.0",
"cookie": "1.1.1",
"axios": "1.13.6",
"axios": "1.15.0",
"dotenv": "17.3.1",
"node-pty": "1.1.0",
"ws": "8.19.0"

View file

@ -362,7 +362,7 @@
},
"domain": {
"type": "string",
"description": "Comma-separated list of URLs (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")"
"description": "Comma-separated list of URLs (e.g. \"https:\/\/app.coolify.io,https:\/\/app2.coolify.io\")"
}
},
"type": "object"
@ -813,7 +813,7 @@
},
"domain": {
"type": "string",
"description": "Comma-separated list of URLs (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")"
"description": "Comma-separated list of URLs (e.g. \"https:\/\/app.coolify.io,https:\/\/app2.coolify.io\")"
}
},
"type": "object"
@ -1264,7 +1264,7 @@
},
"domain": {
"type": "string",
"description": "Comma-separated list of URLs (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")"
"description": "Comma-separated list of URLs (e.g. \"https:\/\/app.coolify.io,https:\/\/app2.coolify.io\")"
}
},
"type": "object"
@ -2697,7 +2697,7 @@
},
"domain": {
"type": "string",
"description": "Comma-separated list of URLs (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")"
"description": "Comma-separated list of URLs (e.g. \"https:\/\/app.coolify.io,https:\/\/app2.coolify.io\")"
}
},
"type": "object"
@ -10816,7 +10816,7 @@
},
"url": {
"type": "string",
"description": "Comma-separated list of URLs (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")."
"description": "Comma-separated list of URLs (e.g. \"https:\/\/app.coolify.io,https:\/\/app2.coolify.io\")."
}
},
"type": "object"
@ -11147,7 +11147,7 @@
},
"url": {
"type": "string",
"description": "Comma-separated list of URLs (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")."
"description": "Comma-separated list of URLs (e.g. \"https:\/\/app.coolify.io,https:\/\/app2.coolify.io\")."
}
},
"type": "object"

View file

@ -258,7 +258,7 @@ paths:
docker_compose_domains:
type: array
description: 'Array of URLs to be applied to containers of a dockercompose application.'
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")' } }, type: object }
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io")' } }, type: object }
watch_paths:
type: string
description: 'The watch paths.'
@ -546,7 +546,7 @@ paths:
docker_compose_domains:
type: array
description: 'Array of URLs to be applied to containers of a dockercompose application.'
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")' } }, type: object }
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io")' } }, type: object }
watch_paths:
type: string
description: 'The watch paths.'
@ -834,7 +834,7 @@ paths:
docker_compose_domains:
type: array
description: 'Array of URLs to be applied to containers of a dockercompose application.'
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")' } }, type: object }
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io")' } }, type: object }
watch_paths:
type: string
description: 'The watch paths.'
@ -1735,7 +1735,7 @@ paths:
docker_compose_domains:
type: array
description: 'Array of URLs to be applied to containers of a dockercompose application.'
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")' } }, type: object }
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io")' } }, type: object }
watch_paths:
type: string
description: 'The watch paths.'
@ -6886,7 +6886,7 @@ paths:
urls:
type: array
description: 'Array of URLs to be applied to containers of a service.'
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, url: { type: string, description: 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io").' } }, type: object }
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, url: { type: string, description: 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io").' } }, type: object }
force_domain_override:
type: boolean
default: false
@ -7075,7 +7075,7 @@ paths:
urls:
type: array
description: 'Array of URLs to be applied to containers of a service.'
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, url: { type: string, description: 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io").' } }, type: object }
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, url: { type: string, description: 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io").' } }, type: object }
force_domain_override:
type: boolean
default: false

View file

@ -60,7 +60,7 @@ services:
retries: 10
timeout: 2s
soketi:
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.12'
image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.13'
ports:
- "${SOKETI_PORT:-6001}:6001"
- "6002:6002"

View file

@ -96,7 +96,7 @@ services:
retries: 10
timeout: 2s
soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.12'
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.13'
pull_policy: always
container_name: coolify-realtime
restart: always

View file

@ -539,6 +539,15 @@ install_docker_manually() {
echo "Docker installed successfully."
fi
}
install_docker_from_rhel_repo() {
echo " - Installing Docker from the RHEL repository for Rocky Linux..."
rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl --now enable docker
}
log_section "Step 3/9: Checking Docker installation"
echo "3/9 Checking Docker installation..."
if ! [ -x "$(command -v docker)" ]; then
@ -579,6 +588,13 @@ if ! [ -x "$(command -v docker)" ]; then
exit 1
fi
;;
"rocky")
install_docker_from_rhel_repo
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
;;
"almalinux" | "tencentos")
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1

View file

@ -1,7 +1,7 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.473"
"version": "4.0.0-beta.474"
},
"nightly": {
"version": "4.0.0"
@ -10,7 +10,7 @@
"version": "1.0.13"
},
"realtime": {
"version": "1.0.12"
"version": "1.0.13"
},
"sentinel": {
"version": "0.0.21"

25
package-lock.json generated
View file

@ -16,7 +16,7 @@
"devDependencies": {
"@tailwindcss/postcss": "4.1.18",
"@vitejs/plugin-vue": "6.0.3",
"axios": "1.13.2",
"axios": "1.15.0",
"laravel-echo": "2.2.7",
"laravel-vite-plugin": "2.0.1",
"postcss": "8.5.6",
@ -1474,15 +1474,15 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
@ -2501,11 +2501,14 @@
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/pusher-js": {
"version": "8.4.0",

View file

@ -9,7 +9,7 @@
"devDependencies": {
"@tailwindcss/postcss": "4.1.18",
"@vitejs/plugin-vue": "6.0.3",
"axios": "1.13.2",
"axios": "1.15.0",
"laravel-echo": "2.2.7",
"laravel-vite-plugin": "2.0.1",
"postcss": "8.5.6",

View file

@ -16,6 +16,8 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
</a>
<a class="sub-menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.index', $parameters) }}"><span class="menu-item-label">General</span></a>
<a class="sub-menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.index.advanced', $parameters) }}"><span class="menu-item-label">Advanced</span></a>
@if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
<a class="sub-menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.database.backups', $parameters) }}"><span class="menu-item-label">Backups</span></a>

View file

@ -835,6 +835,20 @@ class="h-5 w-5 text-warning-600 dark:text-warning-400"
<div class="font-medium text-neutral-900 dark:text-white truncate"
x-text="item.name">
</div>
<template x-if="item.amd_only">
<span
class="px-2 py-0.5 text-xs rounded-full bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200 shrink-0"
title="This service only supports AMD64/x86_64 architecture">
AMD only
</span>
</template>
<template x-if="item.arm_only">
<span
class="px-2 py-0.5 text-xs rounded-full bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200 shrink-0"
title="This service only supports ARM64/aarch64 architecture">
ARM only
</span>
</template>
<span
class="text-xs text-neutral-500 dark:text-neutral-400 shrink-0"
x-text="item.quickcommand"

View file

@ -5,19 +5,7 @@
</div>
<div>Advanced configuration for your application.</div>
<div class="flex flex-col gap-1 pt-4">
<h3>General</h3>
@if ($application->git_based())
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
id="isAutoDeployEnabled" label="Auto Deploy" canGate="update" :canResource="$application" />
<x-forms.checkbox
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update"
:canResource="$application" />
<x-forms.checkbox
helper="When enabled, anyone can trigger PR deployments. When disabled, only repository members, collaborators, and contributors can trigger PR deployments."
instantSave id="isPrDeploymentsPublicEnabled" label="Allow Public PR Deployments" canGate="update"
:canResource="$application" :disabled="!$isPreviewDeploymentsEnabled" />
@endif
<h3>Build</h3>
<x-forms.checkbox helper="Disable Docker build cache on every deployment." instantSave
id="disableBuildCache" label="Disable Build Cache" canGate="update" :canResource="$application" />
<x-forms.checkbox
@ -29,6 +17,55 @@
instantSave id="includeSourceCommitInBuild" label="Include Source Commit in Build" canGate="update"
:canResource="$application" />
<h3 class="pt-4">Container</h3>
<x-forms.checkbox
helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="isConsistentContainerNameEnabled" label="Consistent Container Names" canGate="update"
:canResource="$application" />
@if ($isConsistentContainerNameEnabled === false)
<form class="flex items-end gap-2 " wire:submit.prevent='saveCustomName'>
<x-forms.input
helper="You can add a custom name for your container.<br><br>The name will be converted to slug format when you save it. <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="customInternalName" label="Custom Container Name" canGate="update"
:canResource="$application" />
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
</form>
@endif
@if ($application->git_based())
<h3 class="pt-4">Deployment</h3>
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
id="isAutoDeployEnabled" label="Auto Deploy" canGate="update" :canResource="$application" />
<x-forms.checkbox
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update"
:canResource="$application" />
<x-forms.checkbox
helper="When enabled, anyone can trigger PR deployments. When disabled, only repository members, collaborators, and contributors can trigger PR deployments."
instantSave id="isPrDeploymentsPublicEnabled" label="Allow Public PR Deployments" canGate="update"
:canResource="$application" :disabled="!$isPreviewDeploymentsEnabled" />
<h3 class="pt-4">Git</h3>
<x-forms.checkbox instantSave id="isGitSubmodulesEnabled" label="Submodules"
helper="Allow Git Submodules during build process." canGate="update" :canResource="$application" />
<x-forms.checkbox instantSave id="isGitLfsEnabled" label="LFS"
helper="Allow Git LFS during build process." canGate="update" :canResource="$application" />
<x-forms.checkbox instantSave id="isGitShallowCloneEnabled" label="Shallow Clone"
helper="Use shallow cloning (--depth=1) to speed up deployments by only fetching the latest commit history. This reduces clone time and resource usage, especially for large repositories."
canGate="update" :canResource="$application" />
@endif
@if ($application->build_pack === 'dockercompose')
<h3 class="pt-4">Docker Compose</h3>
<x-forms.checkbox instantSave id="isRawComposeDeploymentEnabled" label="Raw Compose Deployment"
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/compose#raw-docker-compose-deployment'>documentation.</a>"
canGate="update" :canResource="$application" />
<x-forms.checkbox instantSave id="isConnectToDockerNetworkEnabled" label="Connect To Predefined Network"
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>."
canGate="update" :canResource="$application" />
@endif
<h3 class="pt-4">Proxy</h3>
@if ($application->settings->is_container_label_readonly_enabled)
<x-forms.checkbox
helper="Your application will be available only on https if your domain starts with https://..."
@ -49,45 +86,10 @@
helper="Readonly labels are disabled. You need to set the labels in the labels section." disabled
instantSave id="isStripprefixEnabled" label="Strip Prefixes" canGate="update" :canResource="$application" />
@endif
@if ($application->build_pack === 'dockercompose')
<h3>Docker Compose</h3>
<x-forms.checkbox instantSave id="isRawComposeDeploymentEnabled" label="Raw Compose Deployment"
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/compose#raw-docker-compose-deployment'>documentation.</a>"
canGate="update" :canResource="$application" />
@endif
<h3 class="pt-4">Container Names</h3>
<x-forms.checkbox
helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="isConsistentContainerNameEnabled" label="Consistent Container Names" canGate="update"
:canResource="$application" />
@if ($isConsistentContainerNameEnabled === false)
<form class="flex items-end gap-2 " wire:submit.prevent='saveCustomName'>
<x-forms.input
helper="You can add a custom name for your container.<br><br>The name will be converted to slug format when you save it. <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="customInternalName" label="Custom Container Name" canGate="update"
:canResource="$application" />
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
</form>
@endif
@if ($application->build_pack === 'dockercompose')
<h3 class="pt-4">Network</h3>
<x-forms.checkbox instantSave id="isConnectToDockerNetworkEnabled" label="Connect To Predefined Network"
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>."
canGate="update" :canResource="$application" />
@endif
<h3 class="pt-4">Logs</h3>
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
instantSave id="isLogDrainEnabled" label="Drain Logs" canGate="update" :canResource="$application" />
@if ($application->git_based())
<h3>Git</h3>
<x-forms.checkbox instantSave id="isGitSubmodulesEnabled" label="Submodules"
helper="Allow Git Submodules during build process." canGate="update" :canResource="$application" />
<x-forms.checkbox instantSave id="isGitLfsEnabled" label="LFS"
helper="Allow Git LFS during build process." canGate="update" :canResource="$application" />
<x-forms.checkbox instantSave id="isGitShallowCloneEnabled" label="Shallow Clone"
helper="Use shallow cloning (--depth=1) to speed up deployments by only fetching the latest commit history. This reduces clone time and resource usage, especially for large repositories."
canGate="update" :canResource="$application" />
@endif
</div>
</div>

View file

@ -61,7 +61,7 @@
@if (!isDatabaseImage(data_get($service, 'image')))
<div class="flex items-end gap-2">
<x-forms.input
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."
label="Domains for {{ $serviceName }}"
id="parsedServiceDomains.{{ str($serviceName)->replace('-', '_')->replace('.', '_') }}.domain"
x-bind:disabled="shouldDisable()"></x-forms.input>
@ -115,7 +115,7 @@
x-bind:disabled="!canUpdate" />
@else
<x-forms.input placeholder="https://coolify.io" wire:model="fqdn" label="Domains"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."
x-bind:disabled="!canUpdate" />
@can('update', $application)
<x-forms.button wire:click="getWildcardDomain">Generate Domain

View file

@ -173,6 +173,34 @@ class="w-full h-full p-2 transition-all duration-200 dark:bg-white/10 bg-black/1
</template>
</x-slot:logo>
</x-resource-view>
<template x-if="service.amd_only">
<div class="absolute top-2 right-10 group">
<span
class="px-2 py-0.5 text-xs rounded bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200 cursor-pointer">
AMD only
</span>
<div class="info-helper-popup right-0 w-sm">
<div class="p-4">
This service only supports AMD64/x86_64 architecture. It will not work
on ARM-based servers (e.g., Apple Silicon, Raspberry Pi, AWS Graviton).
</div>
</div>
</div>
</template>
<template x-if="service.arm_only">
<div class="absolute top-2 right-10 group">
<span
class="px-2 py-0.5 text-xs rounded-full bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200 cursor-pointer">
ARM only
</span>
<div class="info-helper-popup right-0 w-sm">
<div class="p-4">
This service only supports ARM64/aarch64 architecture. It will not work
on AMD64/x86_64-based servers.
</div>
</div>
</div>
</template>
<template x-if="shouldShowDocIcon(service)">
<a :href="getDocLink(service) || coolifyDocsUrl(service.name)" target="_blank"
@click.stop @mouseenter="resolveDocLink(service)"

View file

@ -1,16 +1,16 @@
<div class="w-full">
<form wire:submit.prevent='submit' class="flex flex-col w-full gap-2">
@if($requiredPort)
<x-callout type="warning" title="Required Port: {{ $requiredPort }}" class="mb-2">
<x-callout type="info" title="Required Port: {{ $requiredPort }}" class="mb-2">
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
<br><br>
<strong>Example:</strong> http://app.coolify.io:{{ $requiredPort }}
<strong>Example:</strong> https://app.coolify.io:{{ $requiredPort }},https://www.app.coolify.io:{{ $requiredPort }}
</x-callout>
@endif
<x-forms.input canGate="update" :canResource="$application" placeholder="https://app.coolify.io" label="Domains"
id="fqdn"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."></x-forms.input>
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
</form>

View file

@ -14,7 +14,10 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
</svg>
<span class="menu-item-label">Back</span>
</a>
<a class="sub-menu-item menu-item-active" href="#"><span class="menu-item-label">General</span></a>
<a class="sub-menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.index', $parameters) }}"><span class="menu-item-label">General</span></a>
<a class="sub-menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
href="{{ route('project.service.index.advanced', $parameters) }}"><span class="menu-item-label">Advanced</span></a>
</div>
@endif
<div class="w-full">
@ -23,63 +26,9 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
{{ data_get_str($service, 'name')->limit(10) }} >
{{ data_get_str($serviceApplication, 'name')->limit(10) }} | Coolify
</x-slot>
<form wire:submit='submitApplication'>
<div class="flex items-center gap-2 pb-4">
@if ($serviceApplication->human_name)
<h2>{{ Str::headline($serviceApplication->human_name) }}</h2>
@else
<h2>{{ Str::headline($serviceApplication->name) }}</h2>
@endif
<x-forms.button canGate="update" :canResource="$serviceApplication" type="submit">Save</x-forms.button>
@can('update', $serviceApplication)
<x-modal-confirmation wire:click="convertToDatabase" title="Convert to Database"
buttonTitle="Convert to Database" submitAction="convertToDatabase" :actions="['The selected resource will be converted to a service database.']"
confirmationText="{{ Str::headline($serviceApplication->name) }}"
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
shortConfirmationLabel="Service Application Name" />
@endcan
@can('delete', $serviceApplication)
<x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton
submitAction="deleteApplication" :actions="['The selected service application container will be stopped and permanently deleted.']"
confirmationText="{{ Str::headline($serviceApplication->name) }}"
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
shortConfirmationLabel="Service Application Name" />
@endcan
</div>
<div class="flex flex-col gap-2">
@if ($requiredPort && !$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':')))
<x-callout type="warning" title="Required Port: {{ $requiredPort }}" class="mb-2">
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
<br><br>
<strong>Example:</strong> http://app.coolify.io:{{ $requiredPort }}
</x-callout>
@endif
<div class="flex gap-2">
<x-forms.input canGate="update" :canResource="$serviceApplication" label="Name" id="humanName"
placeholder="Human readable name"></x-forms.input>
<x-forms.input canGate="update" :canResource="$serviceApplication" label="Description"
id="description"></x-forms.input>
</div>
<div class="flex gap-2">
@if (!$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':')))
@if ($serviceApplication->required_fqdn)
<x-forms.input canGate="update" :canResource="$serviceApplication" required placeholder="https://app.coolify.io"
label="Domains" id="fqdn"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
@else
<x-forms.input canGate="update" :canResource="$serviceApplication" placeholder="https://app.coolify.io"
label="Domains" id="fqdn"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
@endif
@endif
<x-forms.input canGate="update" :canResource="$serviceApplication"
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
label="Image" id="image"></x-forms.input>
</div>
</div>
<h3 class="py-2 pt-4">Advanced</h3>
<div class="w-96 flex flex-col gap-1">
@if ($currentRoute === 'project.service.index.advanced')
<h2>Advanced</h2>
<div class="w-full sm:w-96 flex flex-col gap-1 pt-4">
@if (str($serviceApplication->image)->contains('pocketbase'))
<x-forms.checkbox canGate="update" :canResource="$serviceApplication" instantSave="instantSaveApplicationSettings" id="isGzipEnabled"
label="Enable Gzip Compression"
@ -99,77 +48,134 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ
helper="Drain logs to your configured log drain endpoint in your Server settings."
instantSave="instantSaveApplicationAdvanced" id="isLogDrainEnabled" label="Drain Logs" />
</div>
</form>
@else
<form wire:submit='submitApplication'>
<div class="flex items-center gap-2 pb-4">
@if ($serviceApplication->human_name)
<h2>{{ Str::headline($serviceApplication->human_name) }}</h2>
@else
<h2>{{ Str::headline($serviceApplication->name) }}</h2>
@endif
<x-forms.button canGate="update" :canResource="$serviceApplication" type="submit">Save</x-forms.button>
@can('update', $serviceApplication)
<x-modal-confirmation wire:click="convertToDatabase" title="Convert to Database"
buttonTitle="Convert to Database" submitAction="convertToDatabase" :actions="['The selected resource will be converted to a service database.']"
confirmationText="{{ Str::headline($serviceApplication->name) }}"
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
shortConfirmationLabel="Service Application Name" />
@endcan
@can('delete', $serviceApplication)
<x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton
submitAction="deleteApplication" :actions="['The selected service application container will be stopped and permanently deleted.']"
confirmationText="{{ Str::headline($serviceApplication->name) }}"
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
shortConfirmationLabel="Service Application Name" />
@endcan
</div>
<div class="flex flex-col gap-2">
@if ($requiredPort && !$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':')))
<x-callout type="info" title="Required Port: {{ $requiredPort }}" class="mb-2">
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
<br><br>
<strong>Example:</strong> https://app.coolify.io:{{ $requiredPort }},https://www.app.coolify.io:{{ $requiredPort }}
</x-callout>
@endif
<x-domain-conflict-modal
:conflicts="$domainConflicts"
:showModal="$showDomainConflictModal"
confirmAction="confirmDomainUsage">
<x-slot:consequences>
<ul class="mt-2 ml-4 list-disc">
<li>Only one service will be accessible at this domain</li>
<li>The routing behavior will be unpredictable</li>
<li>You may experience service disruptions</li>
<li>SSL certificates might not work correctly</li>
</ul>
</x-slot:consequences>
</x-domain-conflict-modal>
<div class="flex gap-2">
<x-forms.input canGate="update" :canResource="$serviceApplication" label="Name" id="humanName"
placeholder="Human readable name"></x-forms.input>
<x-forms.input canGate="update" :canResource="$serviceApplication" label="Description"
id="description"></x-forms.input>
</div>
<div class="flex gap-2">
@if (!$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':')))
@if ($serviceApplication->required_fqdn)
<x-forms.input canGate="update" :canResource="$serviceApplication" required placeholder="https://app.coolify.io"
label="Domains" id="fqdn"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."></x-forms.input>
@else
<x-forms.input canGate="update" :canResource="$serviceApplication" placeholder="https://app.coolify.io"
label="Domains" id="fqdn"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- https://app.coolify.io,https://cloud.coolify.io/dashboard<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container.<br>- https://app.coolify.io:8080/api -> app.coolify.io/api will point to port 8080 inside the container."></x-forms.input>
@endif
@endif
<x-forms.input canGate="update" :canResource="$serviceApplication"
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
label="Image" id="image"></x-forms.input>
</div>
</div>
</form>
@if ($showPortWarningModal)
<div x-data="{ modalOpen: true }" x-init="$nextTick(() => { modalOpen = true })"
@keydown.escape.window="modalOpen = false; $wire.call('cancelRemovePort')"
:class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
<template x-teleport="body">
<div x-show="modalOpen"
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
<div class="flex justify-between items-center pb-3">
<h2 class="pr-8 font-bold">Remove Required Port?</h2>
<button @click="modalOpen = false; $wire.call('cancelRemovePort')"
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="relative w-auto">
<x-callout type="warning" title="Port Requirement Warning" class="mb-4">
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly.
One or more of your domains are missing a port number.
</x-callout>
<x-domain-conflict-modal
:conflicts="$domainConflicts"
:showModal="$showDomainConflictModal"
confirmAction="confirmDomainUsage">
<x-slot:consequences>
<ul class="mt-2 ml-4 list-disc">
<li>Only one service will be accessible at this domain</li>
<li>The routing behavior will be unpredictable</li>
<li>You may experience service disruptions</li>
<li>SSL certificates might not work correctly</li>
</ul>
</x-slot:consequences>
</x-domain-conflict-modal>
<x-callout type="danger" title="What will happen if you continue?" class="mb-4">
<ul class="mt-2 ml-4 list-disc">
<li>The service may become unreachable</li>
<li>The proxy may not be able to route traffic correctly</li>
<li>Environment variables may not be generated properly</li>
<li>The service may fail to start or function</li>
</ul>
</x-callout>
@if ($showPortWarningModal)
<div x-data="{ modalOpen: true }" x-init="$nextTick(() => { modalOpen = true })"
@keydown.escape.window="modalOpen = false; $wire.call('cancelRemovePort')"
:class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
<template x-teleport="body">
<div x-show="modalOpen"
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
<div class="flex justify-between items-center pb-3">
<h2 class="pr-8 font-bold">Remove Required Port?</h2>
<button @click="modalOpen = false; $wire.call('cancelRemovePort')"
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="relative w-auto">
<x-callout type="warning" title="Port Requirement Warning" class="mb-4">
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly.
One or more of your domains are missing a port number.
</x-callout>
<div class="flex flex-wrap gap-2 justify-between mt-4">
<x-forms.button @click="modalOpen = false; $wire.call('cancelRemovePort')"
class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
Cancel - Keep Port
</x-forms.button>
<x-forms.button wire:click="confirmRemovePort" @click="modalOpen = false" class="w-auto"
isError>
I understand, remove port anyway
</x-forms.button>
<x-callout type="danger" title="What will happen if you continue?" class="mb-4">
<ul class="mt-2 ml-4 list-disc">
<li>The service may become unreachable</li>
<li>The proxy may not be able to route traffic correctly</li>
<li>Environment variables may not be generated properly</li>
<li>The service may fail to start or function</li>
</ul>
</x-callout>
<div class="flex flex-wrap gap-2 justify-between mt-4">
<x-forms.button @click="modalOpen = false; $wire.call('cancelRemovePort')"
class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
Cancel - Keep Port
</x-forms.button>
<x-forms.button wire:click="confirmRemovePort" @click="modalOpen = false" class="w-auto"
isError>
I understand, remove port anyway
</x-forms.button>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
</div>
@endif
@endif
@elseif ($resourceType === 'database')
<x-slot:title>
@ -178,6 +184,17 @@ class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
</x-slot>
@if ($currentRoute === 'project.service.database.import')
<livewire:project.database.import :resource="$serviceDatabase" :key="'import-' . $serviceDatabase->uuid" />
@elseif ($currentRoute === 'project.service.index.advanced')
<h2>Advanced</h2>
<div class="w-full sm:w-96 flex flex-col gap-1 pt-4">
<x-forms.checkbox canGate="update" :canResource="$serviceDatabase" instantSave="instantSaveExclude"
label="Exclude from service status"
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
id="excludeFromStatus"></x-forms.checkbox>
<x-forms.checkbox canGate="update" :canResource="$serviceDatabase"
helper="Drain logs to your configured log drain endpoint in your Server settings."
instantSave="instantSaveLogDrain" id="isLogDrainEnabled" label="Drain Logs" />
</div>
@else
<form wire:submit='submitDatabase'>
<div class="flex items-center gap-2 pb-4">
@ -242,16 +259,6 @@ class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
@endif
</div>
</div>
<h3 class="pt-2">Advanced</h3>
<div class="w-full sm:w-96">
<x-forms.checkbox canGate="update" :canResource="$serviceDatabase" instantSave="instantSaveExclude"
label="Exclude from service status"
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
id="excludeFromStatus"></x-forms.checkbox>
<x-forms.checkbox canGate="update" :canResource="$serviceDatabase"
helper="Drain logs to your configured log drain endpoint in your Server settings."
instantSave="instantSaveLogDrain" id="isLogDrainEnabled" label="Drain Logs" />
</div>
</form>
@endif
@endif

View file

@ -20,6 +20,9 @@
placeholder="My super WordPress site" />
<x-forms.input canGate="update" :canResource="$service" id="description" label="Description" />
</div>
<div>
<h3>Network</h3>
</div>
<div class="w-full sm:w-96">
<x-forms.checkbox canGate="update" :canResource="$service" instantSave id="connectToDockerNetwork"
label="Connect To Predefined Network"

View file

@ -43,6 +43,12 @@
@endif
@else
@if ($is_shared)
<x-forms.checkbox instantSave id="is_buildtime"
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
label="Available at Buildtime" />
<x-forms.checkbox instantSave id="is_runtime"
helper="Make this variable available in the running container at runtime."
label="Available at Runtime" />
<x-forms.checkbox instantSave id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
@ -89,6 +95,12 @@
@endif
@else
@if ($is_shared)
<x-forms.checkbox disabled id="is_buildtime"
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
label="Available at Buildtime" />
<x-forms.checkbox disabled id="is_runtime"
helper="Make this variable available in the running container at runtime."
label="Available at Runtime" />
<x-forms.checkbox disabled id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
@ -209,6 +221,12 @@
@endif
@else
@if ($is_shared)
<x-forms.checkbox instantSave id="is_buildtime"
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
label="Available at Buildtime" />
<x-forms.checkbox instantSave id="is_runtime"
helper="Make this variable available in the running container at runtime."
label="Available at Runtime" />
<x-forms.checkbox instantSave id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />
@ -283,6 +301,12 @@
@endif
@else
@if ($is_shared)
<x-forms.checkbox disabled id="is_buildtime"
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
label="Available at Buildtime" />
<x-forms.checkbox disabled id="is_runtime"
helper="Make this variable available in the running container at runtime."
label="Available at Runtime" />
<x-forms.checkbox disabled id="is_literal"
helper="This means that when you use $VARIABLES in a value, it should be interpreted as the actual characters '$VARIABLES' and not as the value of a variable named VARIABLE.<br><br>Useful if you have $ sign in your value and there are some characters after it, but you would not like to interpolate it from another value. In this case, you should set this to true."
label="Is Literal?" />

View file

@ -18,24 +18,20 @@
label="CPU Weight" id="limitsCpuShares" />
</div>
<h3 class="pt-4">Limit Memory</h3>
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<x-forms.input canGate="update" :canResource="$resource"
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_reservation'>here</a>."
label="Soft Memory Limit" id="limitsMemoryReservation" />
<x-forms.input canGate="update" :canResource="$resource"
helper="0-100.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_swappiness'>here</a>."
type="number" min="0" max="100" label="Swappiness"
id="limitsMemorySwappiness" />
</div>
<div class="flex gap-2">
<x-forms.input canGate="update" :canResource="$resource"
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_limit'>here</a>."
label="Maximum Memory Limit" id="limitsMemory" />
<x-forms.input canGate="update" :canResource="$resource"
helper="Examples:69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#memswap_limit'>here</a>."
label="Maximum Swap Limit" id="limitsMemorySwap" />
</div>
<div class="flex gap-2">
<x-forms.input canGate="update" :canResource="$resource"
helper="<span class='text-helper'>Examples</span><br>• 69b (byte)<br>• 420k (kilobyte)<br>• 1337m (megabyte)<br>• 1g (gigabyte)<br><br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_reservation'>here</a>."
label="Soft Memory Limit" id="limitsMemoryReservation" />
<x-forms.input canGate="update" :canResource="$resource"
helper="Value between 0-100.<br><br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_swappiness'>here</a>."
type="number" min="0" max="100" label="Swappiness"
id="limitsMemorySwappiness" />
<x-forms.input canGate="update" :canResource="$resource"
helper="<span class='text-helper'>Examples</span><br>• 69b (byte)<br>• 420k (kilobyte)<br>• 1337m (megabyte)<br>• 1g (gigabyte)<br><br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_limit'>here</a>."
label="Maximum Memory Limit" id="limitsMemory" />
<x-forms.input canGate="update" :canResource="$resource"
helper="<span class='text-helper'>Examples</span><br>• 69b (byte)<br>• 420k (kilobyte)<br>• 1337m (megabyte)<br>• 1g (gigabyte)<br><br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#memswap_limit'>here</a>."
label="Maximum Swap Limit" id="limitsMemorySwap" />
</div>
</form>
</div>

View file

@ -63,6 +63,9 @@
}">
<h3 class="pt-4">Clone Resource</h3>
<div class="pb-2">Duplicate this resource to another server or network destination.</div>
<x-callout type="info" title="Important" class="mb-4">
Cloning only duplicates resource configuration (such as environment variables, build settings etc..). It does not include any resource data, such as databases or stored files.
</x-callout>
@can('update', $resource)
<div class="space-y-4 pb-8">

View file

@ -267,6 +267,7 @@
Route::get('/terminal', ExecuteContainerCommand::class)->name('project.service.command')->middleware('can.access.terminal');
Route::get('/{stack_service_uuid}/backups', ServiceDatabaseBackups::class)->name('project.service.database.backups');
Route::get('/{stack_service_uuid}/import', ServiceIndex::class)->name('project.service.database.import')->middleware('can.update.resource');
Route::get('/{stack_service_uuid}/advanced', ServiceIndex::class)->name('project.service.index.advanced');
Route::get('/{stack_service_uuid}', ServiceIndex::class)->name('project.service.index');
Route::get('/tasks/{task_uuid}', ServiceConfiguration::class)->name('project.service.scheduled-tasks');
});

View file

@ -539,6 +539,15 @@ install_docker_manually() {
echo "Docker installed successfully."
fi
}
install_docker_from_rhel_repo() {
echo " - Installing Docker from the RHEL repository for Rocky Linux..."
rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl --now enable docker
}
log_section "Step 3/9: Checking Docker installation"
echo "3/9 Checking Docker installation..."
if ! [ -x "$(command -v docker)" ]; then
@ -579,6 +588,13 @@ if ! [ -x "$(command -v docker)" ]; then
exit 1
fi
;;
"rocky")
install_docker_from_rhel_repo
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
;;
"almalinux" | "tencentos")
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1

View file

@ -4,6 +4,7 @@
# tags: calcom,calendso,scheduling,open,source
# logo: svgs/calcom.svg
# port: 3000
# amd_only: true
services:
calcom:

View file

@ -408,7 +408,8 @@
"category": "productivity",
"logo": "svgs/calcom.svg",
"minversion": "0.0.0",
"port": "3000"
"port": "3000",
"amd_only": true
},
"calibre-web-automated-book-downloader": {
"documentation": "https://github.com/calibrain/calibre-web-automated-book-downloader?utm_source=coolify.io",

View file

@ -408,7 +408,8 @@
"category": "productivity",
"logo": "svgs/calcom.svg",
"minversion": "0.0.0",
"port": "3000"
"port": "3000",
"amd_only": true
},
"calibre-web-automated-book-downloader": {
"documentation": "https://github.com/calibrain/calibre-web-automated-book-downloader?utm_source=coolify.io",

View file

@ -30,7 +30,7 @@
'updated_at' => now()->subDays(8),
]);
$originalIp = $server->ip;
$originalIp = (string) $server->ip;
$this->artisan('cleanup:unreachable-servers')->assertSuccessful();
@ -47,7 +47,7 @@
'updated_at' => now()->subDays(3),
]);
$originalIp = $server->ip;
$originalIp = (string) $server->ip;
$this->artisan('cleanup:unreachable-servers')->assertSuccessful();
@ -64,7 +64,7 @@
'updated_at' => now()->subDays(8),
]);
$originalIp = $server->ip;
$originalIp = (string) $server->ip;
$this->artisan('cleanup:unreachable-servers')->assertSuccessful();

View file

@ -0,0 +1,78 @@
<?php
use App\Models\Server;
use App\Models\Subscription;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('sets unreachable fields on servers when subscription ends', function () {
$team = Team::factory()->create();
Subscription::create([
'team_id' => $team->id,
'stripe_invoice_paid' => true,
]);
$server = Server::factory()->create([
'team_id' => $team->id,
'unreachable_count' => 0,
'unreachable_notification_sent' => false,
]);
$team->subscriptionEnded();
$server->refresh();
expect($server->unreachable_count)->toBe(3);
expect($server->unreachable_notification_sent)->toBeTrue();
});
it('cleans up unsubscribed server IP after 7 days via cleanup command', function () {
$team = Team::factory()->create();
$server = Server::factory()->create([
'team_id' => $team->id,
'unreachable_count' => 3,
'unreachable_notification_sent' => true,
'updated_at' => now()->subDays(8),
]);
$this->artisan('cleanup:unreachable-servers')->assertSuccessful();
$server->refresh();
expect($server->ip)->toBe('1.2.3.4');
});
it('does not clean up unsubscribed server IP within 7 day grace period', function () {
$team = Team::factory()->create();
$server = Server::factory()->create([
'team_id' => $team->id,
'unreachable_count' => 3,
'unreachable_notification_sent' => true,
'updated_at' => now()->subDays(3),
]);
$originalIp = (string) $server->ip;
$this->artisan('cleanup:unreachable-servers')->assertSuccessful();
$server->refresh();
expect((string) $server->ip)->toBe($originalIp);
});
it('does not affect servers with active subscriptions', function () {
$team = Team::factory()->create();
Subscription::create([
'team_id' => $team->id,
'stripe_invoice_paid' => true,
]);
$server = Server::factory()->create([
'team_id' => $team->id,
'unreachable_count' => 0,
'unreachable_notification_sent' => false,
]);
$originalCount = $server->unreachable_count;
$originalNotification = $server->unreachable_notification_sent;
expect($originalCount)->toBe(0);
expect($originalNotification)->toBeFalse();
});

View file

@ -60,3 +60,47 @@
'port' => '766',
]);
});
test('convertGitUrlsForSourceAndSshUrlSchemeWithCustomPort', function () {
$result = convertGitUrl('ssh://git@192.168.56.11:22222/User/Repo.git', 'source', null);
expect($result)->toBe([
'repository' => 'ssh://git@192.168.56.11:22222/User/Repo.git',
'port' => '22222',
]);
});
test('convertGitUrlsForSourceAndSshUrlSchemeWithCustomPortAndIpv6Host', function () {
$result = convertGitUrl('ssh://git@[2001:db8::10]:22222/group/project.git', 'source', null);
expect($result)->toBe([
'repository' => 'ssh://git@[2001:db8::10]:22222/group/project.git',
'port' => '22222',
]);
});
test('convertGitUrlsForDeployKeyAndGithubAppWithCustomPort', function () {
$githubApp = new GithubApp([
'html_url' => 'https://github.example.com',
'custom_user' => 'git',
'custom_port' => 22222,
]);
$result = convertGitUrl('andrasbacsai/coolify-examples.git', 'deploy_key', $githubApp);
expect($result)->toBe([
'repository' => 'ssh://git@github.example.com:22222/andrasbacsai/coolify-examples.git',
'port' => '22222',
]);
});
test('convertGitUrlsForDeployKeyAndGithubAppWithCustomPortAndIpv6Host', function () {
$githubApp = new GithubApp([
'html_url' => 'https://[2001:db8::10]',
'custom_user' => 'git',
'custom_port' => 22222,
]);
$result = convertGitUrl('andrasbacsai/coolify-examples.git', 'deploy_key', $githubApp);
expect($result)->toBe([
'repository' => 'ssh://git@[2001:db8::10]:22222/andrasbacsai/coolify-examples.git',
'port' => '22222',
]);
});

View file

@ -99,3 +99,23 @@
// The malicious payload should be escaped (escapeshellarg wraps and escapes quotes)
expect($command)->toContain("'https://github.com/user/repo.git'\\''");
});
it('preserves ssh scheme URLs with custom ports in deploy_key commands', function () {
$deploymentUuid = 'test-deployment-uuid';
$application = new Application;
$application->git_branch = 'master';
$application->git_repository = 'ssh://git@192.168.56.11:22222/User/Repo.git';
$application->private_key_id = 1;
$privateKey = new PrivateKey;
$privateKey->private_key = 'fake-private-key';
$application->setRelation('private_key', $privateKey);
$result = $application->generateGitLsRemoteCommands($deploymentUuid, false);
expect($result['commands'])
->toContain("'ssh://git@192.168.56.11:22222/User/Repo.git'")
->toContain('-p 22222')
->not->toContain('ssh:/git@192.168.56.11:22222/User/Repo.git');
});

View file

@ -0,0 +1,28 @@
<?php
function expectRockyInstallScriptToUseRhelRepo(string $path): void
{
$installScript = file_get_contents(base_path($path));
expect($installScript)
->toContain('install_docker_from_rhel_repo() {')
->toContain('echo " - Installing Docker from the RHEL repository for Rocky Linux..."')
->toContain('rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo')
->toContain('dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo')
->toContain('dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin')
->toContain('systemctl --now enable docker')
->toContain('"rocky")')
->toContain('install_docker_from_rhel_repo')
->not->toContain('dnf -y -q --setopt=install_weak_deps=False install dnf-plugins-core')
->not->toContain('dnf5 config-manager addrepo --overwrite --save-filename=docker-ce.repo --from-repofile=https://download.docker.com/linux/rhel/docker-ce.repo')
->not->toContain('dnf makecache')
->not->toContain('"ubuntu" | "debian" | "raspbian" | "centos" | "fedora" | "rhel" | "rocky" | "sles")');
}
it('uses the rocky linux documented docker install flow in the stable install script', function () {
expectRockyInstallScriptToUseRhelRepo('scripts/install.sh');
});
it('uses the rocky linux documented docker install flow in the nightly install script', function () {
expectRockyInstallScriptToUseRhelRepo('other/nightly/install.sh');
});

View file

@ -1,7 +1,7 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.473"
"version": "4.0.0-beta.474"
},
"nightly": {
"version": "4.0.0"
@ -10,7 +10,7 @@
"version": "1.0.13"
},
"realtime": {
"version": "1.0.12"
"version": "1.0.13"
},
"sentinel": {
"version": "0.0.21"