Merge remote-tracking branch 'origin/release/v4.5' into automation/sync-main-to-release-v4.5

# Conflicts:
#	config/constants.php
#	tests/Feature/Api/ApplicationSettingsApiTest.php
This commit is contained in:
Andras Bacsai
2026-10-08 08:36:19 +02:00
108 changed files with 8429 additions and 856 deletions
+5
View File
@@ -2,6 +2,7 @@
use App\Actions\Shared\MigrateResourceToDestination;
use App\Enums\BuildPackTypes;
use App\Enums\HttpBasicAuthHashAlgorithm;
use App\Enums\RedirectTypes;
use App\Enums\StaticImageTypes;
use App\Models\Application;
@@ -276,6 +277,10 @@ function sharedDataApplications(?Application $application = null): array
'docker_compose_custom_build_command' => ValidationPatterns::shellSafeCommandRules(),
'is_container_label_escape_enabled' => 'boolean',
'is_preserve_repository_enabled' => 'boolean',
'http_basic_auth_hash_algorithm' => Rule::enum(HttpBasicAuthHashAlgorithm::class),
'http_basic_auth_bcrypt_cost' => 'integer|min:4|max:14',
'http_basic_auth_argon2id_memory_cost' => 'integer|min:8192|max:262144',
'http_basic_auth_argon2id_time_cost' => 'integer|min:1|max:12',
];
}
+73 -8
View File
@@ -766,7 +766,20 @@ function isNoindexDomain(string $domain, ?Collection $noindex_domains): bool
->contains(ValidationPatterns::normalizeApplicationDomainUrl($domain));
}
function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $is_traffic_analytics_enabled = false, array $domainPortOverrides = [], bool $supports_log_append = false, bool $supports_basic_auth_directive = false)
/**
* Caddy site address of a domain. Maintenance blocks in the base Caddyfile must use the same
* address as the labels, so caddy-docker-proxy merges them into one site block.
*/
function caddySiteAddress(string $scheme, string $host, bool $isForceHttpsEnabled): string
{
if ($scheme === 'https' && ! $isForceHttpsEnabled) {
return "http://{$host}, https://{$host}";
}
return "{$scheme}://{$host}";
}
function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $is_traffic_analytics_enabled = false, array $domainPortOverrides = [], bool $supports_log_append = false, bool $supports_basic_auth_directive = false, ?int $http_basic_auth_bcrypt_cost = null, array $http_basic_auth_argon2id_options = [])
{
$labels = collect([]);
if ($serviceLabels) {
@@ -777,7 +790,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$is_http_basic_auth_enabled = $is_http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null;
if ($is_http_basic_auth_enabled) {
$hashedPassword = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => 10]);
$hashedPassword = $http_basic_auth_argon2id_options
? password_hash($http_basic_auth_password, PASSWORD_ARGON2ID, $http_basic_auth_argon2id_options)
: password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => $http_basic_auth_bcrypt_cost ?? 10]);
}
$trafficAppKey = caddyTrafficAppKey($uuid, $service_name);
@@ -788,10 +803,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$path = $url->getPath();
$host_without_www = str($host)->replace('www.', '');
$schema = $url->getScheme();
$siteAddress = "{$schema}://{$host}";
if ($schema === 'https' && ! $is_force_https_enabled) {
$siteAddress = "http://{$host}, https://{$host}";
}
$siteAddress = caddySiteAddress($schema, $host, $is_force_https_enabled);
$portlessDomain = ServiceApplication::withoutPort($domain);
$port = $url->getPort() ?? ($domainPortOverrides[$portlessDomain] ?? null);
$handle = 'handle_path';
@@ -834,6 +846,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
if ($is_http_basic_auth_enabled) {
// Caddy 2.8 renamed basicauth to basic_auth; see Server::caddySupportsBasicAuthDirective().
$basicAuthDirective = $supports_basic_auth_directive ? 'basic_auth' : 'basicauth';
if ($http_basic_auth_argon2id_options) {
$labels->push("caddy_{$loop}.{$basicAuthDirective}=argon2id");
}
$labels->push("caddy_{$loop}.{$basicAuthDirective}.{$http_basic_auth_username}=\"{$hashedPassword}\"");
}
if ($is_traffic_analytics_enabled) {
@@ -907,7 +922,7 @@ function dockerComposeServicePorts(?string $compose, ?string $serviceName): arra
->unique()->values()->all();
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true, array $domainPortOverrides = [])
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true, array $domainPortOverrides = [], ?int $http_basic_auth_bcrypt_cost = null)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
@@ -919,7 +934,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$is_http_basic_auth_enabled = $is_http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null;
$http_basic_auth_label = "http-basic-auth-{$uuid}";
if ($is_http_basic_auth_enabled) {
$hashedPassword = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => 10]);
$hashedPassword = password_hash($http_basic_auth_password, PASSWORD_BCRYPT, ['cost' => $http_basic_auth_bcrypt_cost ?? 10]);
}
if ($is_http_basic_auth_enabled) {
@@ -1186,6 +1201,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$appUuid = $appUuid.'-pr-'.$pull_request_id;
}
$labels = collect([]);
$httpBasicAuthArgon2idOptions = $application->usesArgon2idBasicAuth() ? [
'memory_cost' => $application->http_basic_auth_argon2id_memory_cost,
'time_cost' => $application->http_basic_auth_argon2id_time_cost,
] : [];
if ($pull_request_id === 0) {
if ($application->fqdn) {
$domains = str(data_get($application, 'fqdn'))->explode(',');
@@ -1205,6 +1224,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
noindex_domains: $noindexDomains,
domainPortOverrides: $application->domain_port_overrides ?? [],
));
@@ -1222,6 +1242,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
http_basic_auth_argon2id_options: $httpBasicAuthArgon2idOptions,
noindex_domains: $noindexDomains,
is_traffic_analytics_enabled: $application->destination->server->isTrafficAnalyticsEnabled(),
supports_log_append: $application->destination->server->caddySupportsLogAppend(),
@@ -1242,6 +1264,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
domainPortOverrides: $application->domain_port_overrides ?? [],
@@ -1258,6 +1281,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
http_basic_auth_argon2id_options: $httpBasicAuthArgon2idOptions,
noindex_domains: $noindexDomains,
is_traffic_analytics_enabled: $application->destination->server->isTrafficAnalyticsEnabled(),
supports_log_append: $application->destination->server->caddySupportsLogAppend(),
@@ -1288,6 +1313,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
domainPortOverrides: $preview->domain_port_overrides ?? [],
@@ -1305,6 +1331,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
http_basic_auth_argon2id_options: $httpBasicAuthArgon2idOptions,
noindex_domains: $noindexDomains,
is_traffic_analytics_enabled: $application->destination->server->isTrafficAnalyticsEnabled(),
supports_log_append: $application->destination->server->caddySupportsLogAppend(),
@@ -1324,6 +1352,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
domainPortOverrides: $preview->domain_port_overrides ?? [],
@@ -1339,6 +1368,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
http_basic_auth_bcrypt_cost: $application->http_basic_auth_bcrypt_cost,
http_basic_auth_argon2id_options: $httpBasicAuthArgon2idOptions,
noindex_domains: $noindexDomains,
is_traffic_analytics_enabled: $application->destination->server->isTrafficAnalyticsEnabled(),
supports_log_append: $application->destination->server->caddySupportsLogAppend(),
@@ -2078,3 +2109,37 @@ function injectDockerComposeBuildArgs(string $command, string $buildArgsString):
return $modifiedCommand ?? $command;
}
/**
* Run a command that uses the Railpack BuildKit builder, then stop the builder when no other
* process uses it. An idle buildkitd keeps the memory of earlier builds until it restarts.
*
* The lock file is in the buildx metadata directory, which all helper containers of a server share.
* The script has no `&&`, `||`, pipe, or `$(`, because the sudo parser would put sudo into it.
*/
function railpackBuilderLockedScript(string $command): string
{
return 'exec 9>/root/.docker/buildx/coolify-railpack.lock; if flock -s 9; then :; else exit $?; fi; '
."{$command}; status=\$?; "
.'if flock -n -x 9; then DOCKER_CONFIG=/root/.docker docker buildx stop coolify-railpack >/dev/null 2>&1; fi; '
.'exit $status';
}
/**
* The buildx metadata of a server, as the `docker run -v` source that the deployment helper uses.
*/
function railpackBuildxMetadataVolume(Server $server): string
{
return isDev() && $server->isLocalhost() ? 'coolify-buildx' : '$HOME/.docker/buildx';
}
/**
* Run a command for the Railpack builder from a temporary helper container, then stop the builder
* when no build uses it.
*/
function railpackBuilderHelperCommand(string $buildxMetadataVolume, string $helperImage, string $command): string
{
$script = railpackBuilderLockedScript($command);
return "docker run --rm -v {$buildxMetadataVolume}:/root/.docker/buildx -v /var/run/docker.sock:/var/run/docker.sock {$helperImage} bash -c '{$script}' 2>/dev/null || true";
}
+13
View File
@@ -950,3 +950,16 @@ function getTraefikVersionFromDockerCompose(Server $server): ?string
return null;
}
}
/**
* Validation rule for an HTML page that Coolify sends to the proxy in one shell command
* (see Server::PROXY_ERROR_PAGE_MAX_BYTES).
*/
function proxyPageSizeRule(string $name): Closure
{
return function (string $attribute, mixed $value, Closure $fail) use ($name): void {
if (strlen((string) $value) > Server::PROXY_ERROR_PAGE_MAX_BYTES) {
$fail("The {$name} must not be larger than ".(Server::PROXY_ERROR_PAGE_MAX_BYTES / 1024).' KB.');
}
};
}