fix(api): accept settings that the UI can change but the API rejected

- Applications: is_container_label_readonly_enabled (turning it back on
  regenerates labels), include_coolify_default_variables, and
  dockerfile_target_build on create.
- Databases: save is_log_drain_enabled on update instead of dropping it,
  include_coolify_default_variables, and disable_local_backup on backups
  (only with save_s3).
- Services: include_coolify_default_variables, and redirect on service
  applications (www/non-www needs both hosts).
- Servers: wildcard_domain, server_timezone,
  backup_compression_cpu_percentage, and the proxy error page.

Closes #11092
This commit is contained in:
Andras Bacsai
2026-10-24 00:40:19 +02:00
parent 33b369243b
commit e0b6ac1121
12 changed files with 760 additions and 13 deletions
@@ -67,6 +67,16 @@ class UpdateServiceApplicationFromApi
$serviceApplication->setNoindexDomains($payload['noindex_domains'] ?? []);
}
if (array_key_exists('redirect', $payload)) {
if (in_array($payload['redirect'], ['www', 'non-www'], true) && ! $this->hasWwwAndNonWwwDomains($serviceApplication->fqdn)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['redirect' => ['The www and non-www redirects need both a www and a non-www domain in url.']],
], 422);
}
$serviceApplication->redirect = $payload['redirect'];
}
if (array_key_exists('human_name', $payload)) {
$serviceApplication->human_name = $payload['human_name'];
}
@@ -130,4 +140,14 @@ class UpdateServiceApplicationFromApi
return null;
}
private function hasWwwAndNonWwwDomains(?string $fqdn): bool
{
$hosts = str($fqdn ?? '')->explode(',')
->map(fn (string $url) => strtolower((string) parse_url(trim($url), PHP_URL_HOST)))
->filter();
return $hosts->contains(fn (string $host) => str_starts_with($host, 'www.'))
&& $hosts->contains(fn (string $host) => ! str_starts_with($host, 'www.'));
}
}
@@ -53,6 +53,7 @@ class ApplicationsController extends Controller
'include_source_commit_in_build',
'is_env_sorting_enabled',
'redact_all_env_values_in_logs',
'include_coolify_default_variables',
'is_pr_deployments_public_enabled',
'stop_grace_period',
'docker_images_to_keep',
@@ -60,6 +61,7 @@ class ApplicationsController extends Controller
'is_stripprefix_enabled',
'is_permanent_redirect_enabled',
'traefik_certificate_resolver',
'is_container_label_readonly_enabled',
'is_raw_compose_deployment_enabled',
'is_log_drain_enabled',
'is_gpu_enabled',
@@ -81,10 +83,12 @@ class ApplicationsController extends Controller
'include_source_commit_in_build',
'is_env_sorting_enabled',
'redact_all_env_values_in_logs',
'include_coolify_default_variables',
'is_pr_deployments_public_enabled',
'is_gzip_enabled',
'is_stripprefix_enabled',
'is_permanent_redirect_enabled',
'is_container_label_readonly_enabled',
'is_raw_compose_deployment_enabled',
'is_log_drain_enabled',
'is_gpu_enabled',
@@ -231,15 +235,18 @@ class ApplicationsController extends Controller
return;
}
$wasLabelManagementEnabled = $application->settings->is_container_label_readonly_enabled;
$application->settings->fill($settings)->save();
$regenerateLabels = ! $application->wasRecentlyCreated
&& $application->settings->is_container_label_readonly_enabled
&& (array_key_exists('is_gzip_enabled', $settings)
&& (! $wasLabelManagementEnabled
|| array_key_exists('is_gzip_enabled', $settings)
|| array_key_exists('is_stripprefix_enabled', $settings)
|| array_key_exists('is_permanent_redirect_enabled', $settings)
|| array_key_exists('traefik_certificate_resolver', $settings));
$application->settings->fill($settings)->save();
if ($regenerateLabels) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
@@ -431,6 +438,7 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'],
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'The Docker build target (stage) in the Dockerfile.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
@@ -457,6 +465,7 @@ class ApplicationsController extends Controller
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -464,6 +473,7 @@ class ApplicationsController extends Controller
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
@@ -637,6 +647,7 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository'],
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'The Docker build target (stage) in the Dockerfile.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
@@ -663,6 +674,7 @@ class ApplicationsController extends Controller
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -670,6 +682,7 @@ class ApplicationsController extends Controller
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
@@ -843,6 +856,7 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'],
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'The Docker build target (stage) in the Dockerfile.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
@@ -869,6 +883,7 @@ class ApplicationsController extends Controller
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -876,6 +891,7 @@ class ApplicationsController extends Controller
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
@@ -1046,6 +1062,7 @@ class ApplicationsController extends Controller
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -1053,6 +1070,7 @@ class ApplicationsController extends Controller
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
@@ -1219,6 +1237,7 @@ class ApplicationsController extends Controller
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -1226,6 +1245,7 @@ class ApplicationsController extends Controller
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
@@ -1337,7 +1357,7 @@ class ApplicationsController extends Controller
if ($return instanceof JsonResponse) {
return $return;
}
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'noindex_domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'use_build_secrets', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'http_basic_auth_hash_algorithm', 'http_basic_auth_bcrypt_cost', 'http_basic_auth_argon2id_memory_cost', 'http_basic_auth_argon2id_time_cost', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags', 'is_preserve_repository_enabled', 'preview_url_template', 'max_restart_count', ...self::APPLICATION_SETTING_FIELDS];
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'noindex_domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'use_build_secrets', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'http_basic_auth_hash_algorithm', 'http_basic_auth_bcrypt_cost', 'http_basic_auth_argon2id_memory_cost', 'http_basic_auth_argon2id_time_cost', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags', 'is_preserve_repository_enabled', 'preview_url_template', 'max_restart_count', ...self::APPLICATION_SETTING_FIELDS];
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
@@ -3207,6 +3227,7 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository.'],
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'The Docker build target (stage) in the Dockerfile.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
@@ -3237,6 +3258,7 @@ class ApplicationsController extends Controller
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -3244,6 +3266,7 @@ class ApplicationsController extends Controller
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
@@ -3348,7 +3371,7 @@ class ApplicationsController extends Controller
$this->authorize('update', $application);
if ($request->has('redact_all_env_values_in_logs')) {
if ($request->has('redact_all_env_values_in_logs') || $request->has('include_coolify_default_variables')) {
$this->authorize('manageEnvironment', $application);
}
@@ -594,6 +594,8 @@ class DatabasesController extends Controller
'health_check_retries' => ['type' => 'integer', 'description' => 'Healthcheck retries count.', 'minimum' => 1, 'default' => 5],
'health_check_start_period' => ['type' => 'integer', 'description' => 'Healthcheck start period in seconds.', 'minimum' => 0, 'default' => 5],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values, database passwords, and remote secrets are redacted. Requires team admin permissions.'],
'include_coolify_default_variables' => ['type' => 'boolean', 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Restart the database to apply it.'],
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Send the database logs to the log drain of the server. Log drain must be enabled on the server. Restart the database to apply it.'],
],
),
)
@@ -654,6 +656,8 @@ class DatabasesController extends Controller
'limits_cpu_shares' => 'numeric',
'instant_deploy' => 'boolean',
'redact_all_env_values_in_logs' => 'boolean',
'include_coolify_default_variables' => 'boolean',
'is_log_drain_enabled' => 'boolean',
]);
if ($validator->fails()) {
@@ -665,6 +669,8 @@ class DatabasesController extends Controller
$uuid = $request->uuid;
$instantDeploy = $request->boolean('instant_deploy');
$redactAllEnvValuesInLogs = $request->has('redact_all_env_values_in_logs') ? $request->boolean('redact_all_env_values_in_logs') : null;
$isLogDrainEnabled = $request->has('is_log_drain_enabled') ? $request->boolean('is_log_drain_enabled') : null;
$includeCoolifyDefaultVariables = $request->has('include_coolify_default_variables') ? $request->boolean('include_coolify_default_variables') : null;
removeUnnecessaryFieldsFromRequest($request);
$database = queryDatabaseByUuidWithinTeam($uuid, $teamId);
if (! $database) {
@@ -678,10 +684,17 @@ class DatabasesController extends Controller
$this->authorize('manage', $database);
}
if ($redactAllEnvValuesInLogs !== null) {
if ($redactAllEnvValuesInLogs !== null || $includeCoolifyDefaultVariables !== null) {
$this->authorize('manageEnvironment', $database);
}
if ($isLogDrainEnabled && ! $database->destination->server->isLogDrainEnabled()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['is_log_drain_enabled' => ['Log drain is not enabled on the server. Enable it on the server first.']],
], 422);
}
if ($request->has('is_public') && (bool) $request->is_public !== (bool) $database->is_public) {
$forbidden = $this->deployAbilityForbiddenResponse($request, 'Making a database public or private starts or stops its proxy on the server, so it needs a token with the deploy permission.');
if ($forbidden) {
@@ -935,6 +948,12 @@ class DatabasesController extends Controller
if ($redactAllEnvValuesInLogs !== null) {
$database->redact_all_env_values_in_logs = $redactAllEnvValuesInLogs;
}
if ($isLogDrainEnabled !== null) {
$database->is_log_drain_enabled = $isLogDrainEnabled;
}
if ($includeCoolifyDefaultVariables !== null) {
$database->include_coolify_default_variables = $includeCoolifyDefaultVariables;
}
$database->update($request->only($allowedFields));
// The Redis password is stored in the REDIS_PASSWORD variable, not in a column.
if ($database instanceof StandaloneRedis && filled($request->redis_password)) {
@@ -961,6 +980,8 @@ class DatabasesController extends Controller
'changed_fields' => [
...array_values(array_intersect($allowedFields, array_keys($request->all()))),
...($redactAllEnvValuesInLogs === null ? [] : ['redact_all_env_values_in_logs']),
...($isLogDrainEnabled === null ? [] : ['is_log_drain_enabled']),
...($includeCoolifyDefaultVariables === null ? [] : ['include_coolify_default_variables']),
],
]);
@@ -969,6 +990,14 @@ class DatabasesController extends Controller
]);
}
private function disableLocalBackupWithoutS3Response(): JsonResponse
{
return response()->json([
'message' => 'Validation failed.',
'errors' => ['disable_local_backup' => ['Local backups can only be disabled when save_s3 is true.']],
], 422);
}
#[OA\Post(
summary: 'Create Backup',
description: 'Create a new scheduled backup configuration for a database',
@@ -1001,6 +1030,7 @@ class DatabasesController extends Controller
'frequency' => ['type' => 'string', 'description' => 'Backup frequency (cron expression or: every_minute, hourly, daily, weekly, monthly, yearly)'],
'enabled' => ['type' => 'boolean', 'description' => 'Whether the backup is enabled', 'default' => true],
'save_s3' => ['type' => 'boolean', 'description' => 'Whether to save backups to S3', 'default' => false],
'disable_local_backup' => ['type' => 'boolean', 'description' => 'Delete the local backup file after the upload to S3. Only allowed when save_s3 is true.', 'default' => false],
's3_storage_uuid' => ['type' => 'string', 'description' => 'Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination. Either field is required if save_s3 is true.'],
's3_storage_uuids' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'UUIDs of every S3 storage the backup is uploaded to. Replaces the current destinations. When s3_storage_uuid is also sent, it must be in this list and becomes the primary destination; otherwise the first entry is the primary destination.'],
'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'],
@@ -1054,7 +1084,7 @@ class DatabasesController extends Controller
)]
public function create_backup(Request $request)
{
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 's3_storage_uuids', 'timeout', 'missing_backup_notification_days'];
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 's3_storage_uuids', 'timeout', 'missing_backup_notification_days', 'disable_local_backup'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -1071,6 +1101,7 @@ class DatabasesController extends Controller
'frequency' => 'required|string',
'enabled' => 'boolean',
'save_s3' => 'boolean',
'disable_local_backup' => 'boolean',
'dump_all' => 'boolean',
'backup_now' => 'boolean|nullable',
's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable',
@@ -1138,6 +1169,9 @@ class DatabasesController extends Controller
'errors' => ['s3_storage_uuid' => ['The s3_storage_uuid or s3_storage_uuids field is required when save_s3 is true.']],
], 422);
}
if ($request->boolean('disable_local_backup') && ! $request->boolean('save_s3')) {
return $this->disableLocalBackupWithoutS3Response();
}
// Check for extra fields
$extraFields = array_diff(array_keys($request->all()), $backupConfigFields, ['backup_now']);
@@ -1256,6 +1290,7 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'save_s3' => ['type' => 'boolean', 'description' => 'Whether data is saved in s3 or not'],
'disable_local_backup' => ['type' => 'boolean', 'description' => 'Delete the local backup file after the upload to S3. Only allowed when save_s3 is true. Turning save_s3 off also turns this off.'],
's3_storage_uuid' => ['type' => 'string', 'description' => 'Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination; when it is already a destination, it becomes the primary destination and the other destinations stay.'],
's3_storage_uuids' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'UUIDs of every S3 storage the backup is uploaded to. Replaces the current destinations. When s3_storage_uuid is also sent, it must be in this list and becomes the primary destination; otherwise the first entry is the primary destination.'],
'backup_now' => ['type' => 'boolean', 'description' => 'Whether to take a backup now or not. Needs a token with the deploy permission.'],
@@ -1304,7 +1339,7 @@ class DatabasesController extends Controller
)]
public function update_backup(Request $request)
{
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 's3_storage_uuids', 'timeout', 'missing_backup_notification_days'];
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 's3_storage_uuids', 'timeout', 'missing_backup_notification_days', 'disable_local_backup'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -1317,6 +1352,7 @@ class DatabasesController extends Controller
}
$validator = customApiValidator($request->all(), [
'save_s3' => 'boolean',
'disable_local_backup' => 'boolean',
'backup_now' => 'boolean|nullable',
'enabled' => 'boolean',
'dump_all' => 'boolean',
@@ -1409,11 +1445,18 @@ class DatabasesController extends Controller
'errors' => $errors,
], 422);
}
if ($request->boolean('disable_local_backup') && ! $saveToS3) {
return $this->disableLocalBackupWithoutS3Response();
}
$backupData = Arr::except($request->only($backupConfigFields), ['s3_storage_uuid', 's3_storage_uuids']);
if ($s3Storages !== null) {
$backupData['s3_storage_id'] = $s3Storages->first()?->id;
}
// Without S3 the backup must stay on the server, as in the UI.
if (! $saveToS3) {
$backupData['disable_local_backup'] = false;
}
// Validate databases_to_backup input
if (! empty($backupData['databases_to_backup'])) {
@@ -56,6 +56,7 @@ class ServerProxyController extends Controller
'status' => data_get($server->proxy, 'status'),
'redirect_enabled' => (bool) data_get($server->proxy, 'redirect_enabled', true),
'redirect_url' => data_get($server->proxy, 'redirect_url'),
'proxy_error_page' => $server->proxy_error_page,
'generate_exact_labels' => (bool) ($server->settings->generate_exact_labels ?? false),
'traefik_certificate_resolver' => $server->traefikCertificateResolver(),
];
@@ -91,6 +92,7 @@ class ServerProxyController extends Controller
new OA\Property(property: 'status', type: 'string', nullable: true, example: 'running'),
new OA\Property(property: 'redirect_enabled', type: 'boolean', example: true),
new OA\Property(property: 'redirect_url', type: 'string', nullable: true, example: 'https://example.com'),
new OA\Property(property: 'proxy_error_page', type: 'string', nullable: true),
new OA\Property(property: 'generate_exact_labels', type: 'boolean', example: false),
new OA\Property(property: 'traefik_certificate_resolver', type: 'string', nullable: true, example: 'letsencrypt', description: 'Traefik certificate resolver for generated HTTPS routers. Null when the proxy uses its own certificates.'),
new OA\Property(property: 'configuration', type: 'string', nullable: true, description: 'Docker Compose proxy configuration when stored in the database. Only present with read:sensitive.'),
@@ -136,6 +138,7 @@ class ServerProxyController extends Controller
properties: [
new OA\Property(property: 'redirect_enabled', type: 'boolean'),
new OA\Property(property: 'redirect_url', type: 'string', nullable: true, description: 'Public http(s) redirect URL, or null to clear.'),
new OA\Property(property: 'proxy_error_page', type: 'string', nullable: true, description: 'Custom HTML error page that the proxy shows for unknown hosts (when redirect_url is empty) and for resources in maintenance mode. Null restores the default page.'),
new OA\Property(property: 'generate_exact_labels', type: 'boolean'),
new OA\Property(property: 'traefik_certificate_resolver', type: 'string', nullable: true, description: 'Traefik certificate resolver for generated HTTPS routers (letters, digits, _ and -). Null or empty to use your own certificates. Redeploy resources to apply.'),
new OA\Property(property: 'proxy_type', type: 'string', enum: ['traefik', 'caddy', 'nginx', 'none'], description: 'Proxy type (case-insensitive). Changing it starts the new proxy on the server and needs the deploy permission.'),
@@ -153,6 +156,7 @@ class ServerProxyController extends Controller
new OA\Property(property: 'status', type: 'string', nullable: true),
new OA\Property(property: 'redirect_enabled', type: 'boolean'),
new OA\Property(property: 'redirect_url', type: 'string', nullable: true),
new OA\Property(property: 'proxy_error_page', type: 'string', nullable: true),
new OA\Property(property: 'generate_exact_labels', type: 'boolean'),
new OA\Property(property: 'traefik_certificate_resolver', type: 'string', nullable: true),
new OA\Property(property: 'configuration', type: 'string', nullable: true),
@@ -178,10 +182,11 @@ class ServerProxyController extends Controller
return $return;
}
$allowedFields = ['redirect_enabled', 'redirect_url', 'generate_exact_labels', 'traefik_certificate_resolver', 'proxy_type'];
$allowedFields = ['redirect_enabled', 'redirect_url', 'proxy_error_page', 'generate_exact_labels', 'traefik_certificate_resolver', 'proxy_type'];
$validator = customApiValidator($request->all(), [
'redirect_enabled' => 'boolean',
'redirect_url' => ['nullable', 'string', new SafeExternalUrl],
'proxy_error_page' => ['nullable', 'string', proxyPageSizeRule('custom error page')],
'generate_exact_labels' => 'boolean',
'traefik_certificate_resolver' => ['nullable', 'string', 'max:255', 'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN],
'proxy_type' => 'string|nullable',
@@ -236,6 +241,11 @@ class ServerProxyController extends Controller
$redirectChanged = true;
}
if ($request->exists('proxy_error_page')) {
$server->proxy_error_page = $request->input('proxy_error_page') ?: null;
$redirectChanged = true;
}
if ($redirectChanged) {
$server->save();
}
@@ -314,6 +324,7 @@ class ServerProxyController extends Controller
new OA\Property(property: 'status', type: 'string', nullable: true),
new OA\Property(property: 'redirect_enabled', type: 'boolean'),
new OA\Property(property: 'redirect_url', type: 'string', nullable: true),
new OA\Property(property: 'proxy_error_page', type: 'string', nullable: true),
new OA\Property(property: 'generate_exact_labels', type: 'boolean'),
new OA\Property(property: 'configuration', type: 'string', nullable: true),
]
+15 -2
View File
@@ -705,6 +705,9 @@ class ServersController extends Controller
'server_disk_usage_check_frequency' => ['type' => 'string', 'description' => 'Cron expression for disk usage check frequency.'],
'server_disk_usage_notification_interval_hours' => ['type' => 'integer', 'description' => 'Minimum hours between high disk usage notifications (1-720). Default: 24.'],
'connection_timeout' => ['type' => 'integer', 'description' => 'SSH connection timeout in seconds (1-300). Default: 10.'],
'wildcard_domain' => ['type' => 'string', 'nullable' => true, 'description' => 'Wildcard domain (URL) used to generate domains for new resources on this server. Null removes it.'],
'server_timezone' => ['type' => 'string', 'description' => 'Timezone of the server, for example Europe/Berlin. Scheduled tasks and backups use it.'],
'backup_compression_cpu_percentage' => ['type' => 'integer', 'enum' => [25, 50, 75, 100], 'description' => 'Maximum CPU percentage that backup compression can use. Default: 25.'],
],
),
),
@@ -742,7 +745,7 @@ class ServersController extends Controller
)]
public function update_server(Request $request)
{
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'server_role', 'is_build_server', 'instant_validate', 'proxy_type', 'concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'server_disk_usage_notification_interval_hours', 'connection_timeout', 'is_terminal_enabled'];
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'server_role', 'is_build_server', 'instant_validate', 'proxy_type', 'concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'server_disk_usage_notification_interval_hours', 'connection_timeout', 'is_terminal_enabled', 'wildcard_domain', 'server_timezone', 'backup_compression_cpu_percentage'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -772,6 +775,13 @@ class ServersController extends Controller
'server_disk_usage_notification_interval_hours' => 'integer|min:1|max:720',
'connection_timeout' => 'integer|min:1|max:300',
'is_terminal_enabled' => 'boolean|nullable',
'wildcard_domain' => 'nullable|url',
'server_timezone' => ['string', function (string $attribute, mixed $value, \Closure $fail): void {
if (! validate_timezone($value)) {
$fail('The server_timezone must be a valid timezone.');
}
}],
'backup_compression_cpu_percentage' => 'integer|in:25,50,75,100',
], [
...ValidationPatterns::serverUsernameMessages(),
'name.not_regex' => 'The name must not contain control characters such as new lines.',
@@ -886,10 +896,13 @@ class ServersController extends Controller
]);
}
$advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'server_disk_usage_notification_interval_hours', 'connection_timeout']);
$advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'server_disk_usage_notification_interval_hours', 'connection_timeout', 'server_timezone', 'backup_compression_cpu_percentage']);
if (! empty($advancedSettings)) {
$server->settings()->update(array_filter($advancedSettings, fn ($value) => ! is_null($value)));
}
if ($request->has('wildcard_domain')) {
$server->settings()->update(['wildcard_domain' => $request->input('wildcard_domain')]);
}
if ($proxyTypeChanged) {
$server->changeProxy($request->proxy_type, async: true);
@@ -6,6 +6,7 @@ use App\Actions\Service\DeployServiceApplication;
use App\Actions\Service\RestartServiceApplication;
use App\Actions\Service\StopServiceApplication;
use App\Actions\Service\UpdateServiceApplicationFromApi;
use App\Enums\RedirectTypes;
use App\Http\Controllers\Controller;
use App\Models\EnvironmentVariable;
use App\Models\ServerSetting;
@@ -16,6 +17,7 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use OpenApi\Attributes as OA;
class ServiceApplicationsController extends Controller
@@ -263,6 +265,7 @@ class ServiceApplicationsController extends Controller
'is_stripprefix_enabled' => new OA\Property(property: 'is_stripprefix_enabled', type: 'boolean', nullable: true),
'is_force_https_enabled' => new OA\Property(property: 'is_force_https_enabled', type: 'boolean', nullable: true),
'is_permanent_redirect_enabled' => new OA\Property(property: 'is_permanent_redirect_enabled', type: 'boolean', nullable: true, description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'),
'redirect' => new OA\Property(property: 'redirect', type: 'string', enum: ['both', 'www', 'non-www'], description: 'How to handle www and non-www domains. "www" redirects non-www to www, "non-www" redirects www to non-www. Both need a www and a non-www domain in url.'),
'max_restart_count' => new OA\Property(property: 'max_restart_count', type: 'integer', minimum: 0, nullable: true, description: 'Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.'),
]
)
@@ -339,6 +342,7 @@ class ServiceApplicationsController extends Controller
'is_stripprefix_enabled',
'is_force_https_enabled',
'is_permanent_redirect_enabled',
'redirect',
'max_restart_count',
];
@@ -356,6 +360,7 @@ class ServiceApplicationsController extends Controller
'is_stripprefix_enabled' => 'sometimes|boolean',
'is_force_https_enabled' => 'sometimes|boolean',
'is_permanent_redirect_enabled' => 'sometimes|boolean',
'redirect' => ['sometimes', Rule::enum(RedirectTypes::class)],
'max_restart_count' => 'sometimes|integer|min:0',
];
@@ -1150,6 +1150,7 @@ class ServicesController extends Controller
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'default' => true, 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
],
)
),
@@ -1238,7 +1239,7 @@ class ServicesController extends Controller
$this->authorize('update', $service);
if ($request->has('redact_all_env_values_in_logs')) {
if ($request->has('redact_all_env_values_in_logs') || $request->has('include_coolify_default_variables')) {
$this->authorize('manageEnvironment', $service);
}
@@ -1247,7 +1248,7 @@ class ServicesController extends Controller
$this->authorize('deploy', $service);
}
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override', 'is_container_label_escape_enabled', 'redact_all_env_values_in_logs'];
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override', 'is_container_label_escape_enabled', 'redact_all_env_values_in_logs', 'include_coolify_default_variables'];
$validationRules = [
'name' => 'string|max:255',
@@ -1262,6 +1263,7 @@ class ServicesController extends Controller
'force_domain_override' => 'boolean',
'is_container_label_escape_enabled' => 'boolean',
'redact_all_env_values_in_logs' => 'boolean',
'include_coolify_default_variables' => 'boolean',
];
$validationMessages = [
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
@@ -1332,6 +1334,9 @@ class ServicesController extends Controller
if ($request->has('redact_all_env_values_in_logs')) {
$service->redact_all_env_values_in_logs = $request->boolean('redact_all_env_values_in_logs');
}
if ($request->has('include_coolify_default_variables')) {
$service->include_coolify_default_variables = $request->boolean('include_coolify_default_variables');
}
$previousDnsHostnames = $this->serviceApplicationDnsHostnames($service);
$service->save();
+4
View File
@@ -204,11 +204,13 @@ function sharedDataApplications(?Application $application = null): array
'include_source_commit_in_build' => 'boolean',
'is_env_sorting_enabled' => 'boolean',
'redact_all_env_values_in_logs' => 'boolean',
'include_coolify_default_variables' => 'boolean',
'is_pr_deployments_public_enabled' => 'boolean',
'is_gzip_enabled' => 'boolean',
'is_stripprefix_enabled' => 'boolean',
'is_permanent_redirect_enabled' => 'boolean',
'traefik_certificate_resolver' => ['nullable', 'string', 'max:255', 'regex:'.ServerSetting::TRAEFIK_CERTIFICATE_RESOLVER_PATTERN],
'is_container_label_readonly_enabled' => 'boolean',
'is_raw_compose_deployment_enabled' => 'boolean',
'is_log_drain_enabled' => 'boolean',
'is_gpu_enabled' => 'boolean',
@@ -485,6 +487,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('inject_build_args_to_dockerfile');
$request->offsetUnset('is_env_sorting_enabled');
$request->offsetUnset('redact_all_env_values_in_logs');
$request->offsetUnset('include_coolify_default_variables');
$request->offsetUnset('is_pr_deployments_public_enabled');
$request->offsetUnset('stop_grace_period');
$request->offsetUnset('docker_images_to_keep');
@@ -492,6 +495,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('is_stripprefix_enabled');
$request->offsetUnset('is_permanent_redirect_enabled');
$request->offsetUnset('traefik_certificate_resolver');
$request->offsetUnset('is_container_label_readonly_enabled');
$request->offsetUnset('is_raw_compose_deployment_enabled');
$request->offsetUnset('is_log_drain_enabled');
$request->offsetUnset('is_gpu_enabled');
+155
View File
@@ -808,6 +808,11 @@
"type": "string",
"description": "The Dockerfile location in the repository."
},
"dockerfile_target_build": {
"type": "string",
"nullable": true,
"description": "The Docker build target (stage) in the Dockerfile."
},
"docker_compose_location": {
"type": "string",
"description": "The Docker Compose location."
@@ -893,6 +898,11 @@
"type": "boolean",
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
},
"include_coolify_default_variables": {
"type": "boolean",
"default": true,
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
},
"is_pr_deployments_public_enabled": {
"type": "boolean",
"description": "Make pull request deployments public."
@@ -927,6 +937,11 @@
"nullable": true,
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
},
"is_container_label_readonly_enabled": {
"type": "boolean",
"default": true,
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
@@ -1445,6 +1460,11 @@
"type": "string",
"description": "The Dockerfile location in the repository"
},
"dockerfile_target_build": {
"type": "string",
"nullable": true,
"description": "The Docker build target (stage) in the Dockerfile."
},
"docker_compose_location": {
"type": "string",
"description": "The Docker Compose location."
@@ -1530,6 +1550,11 @@
"type": "boolean",
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
},
"include_coolify_default_variables": {
"type": "boolean",
"default": true,
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
},
"is_pr_deployments_public_enabled": {
"type": "boolean",
"description": "Make pull request deployments public."
@@ -1564,6 +1589,11 @@
"nullable": true,
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
},
"is_container_label_readonly_enabled": {
"type": "boolean",
"default": true,
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
@@ -2082,6 +2112,11 @@
"type": "string",
"description": "The Dockerfile location in the repository."
},
"dockerfile_target_build": {
"type": "string",
"nullable": true,
"description": "The Docker build target (stage) in the Dockerfile."
},
"docker_compose_location": {
"type": "string",
"description": "The Docker Compose location."
@@ -2167,6 +2202,11 @@
"type": "boolean",
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
},
"include_coolify_default_variables": {
"type": "boolean",
"default": true,
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
},
"is_pr_deployments_public_enabled": {
"type": "boolean",
"description": "Make pull request deployments public."
@@ -2201,6 +2241,11 @@
"nullable": true,
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
},
"is_container_label_readonly_enabled": {
"type": "boolean",
"default": true,
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
@@ -2699,6 +2744,11 @@
"type": "boolean",
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
},
"include_coolify_default_variables": {
"type": "boolean",
"default": true,
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
},
"is_pr_deployments_public_enabled": {
"type": "boolean",
"description": "Make pull request deployments public."
@@ -2733,6 +2783,11 @@
"nullable": true,
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
},
"is_container_label_readonly_enabled": {
"type": "boolean",
"default": true,
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
@@ -3211,6 +3266,11 @@
"type": "boolean",
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
},
"include_coolify_default_variables": {
"type": "boolean",
"default": true,
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
},
"is_pr_deployments_public_enabled": {
"type": "boolean",
"description": "Make pull request deployments public."
@@ -3245,6 +3305,11 @@
"nullable": true,
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
},
"is_container_label_readonly_enabled": {
"type": "boolean",
"default": true,
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
@@ -3871,6 +3936,11 @@
"type": "string",
"description": "The Dockerfile location in the repository."
},
"dockerfile_target_build": {
"type": "string",
"nullable": true,
"description": "The Docker build target (stage) in the Dockerfile."
},
"docker_compose_location": {
"type": "string",
"description": "The Docker Compose location."
@@ -3981,6 +4051,11 @@
"type": "boolean",
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
},
"include_coolify_default_variables": {
"type": "boolean",
"default": true,
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
},
"is_pr_deployments_public_enabled": {
"type": "boolean",
"description": "Make pull request deployments public."
@@ -4015,6 +4090,11 @@
"nullable": true,
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
},
"is_container_label_readonly_enabled": {
"type": "boolean",
"default": true,
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
@@ -7025,6 +7105,11 @@
"description": "Whether to save backups to S3",
"default": false
},
"disable_local_backup": {
"type": "boolean",
"description": "Delete the local backup file after the upload to S3. Only allowed when save_s3 is true.",
"default": false
},
"s3_storage_uuid": {
"type": "string",
"description": "Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination. Either field is required if save_s3 is true."
@@ -7502,6 +7587,14 @@
"redact_all_env_values_in_logs": {
"type": "boolean",
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values, database passwords, and remote secrets are redacted. Requires team admin permissions."
},
"include_coolify_default_variables": {
"type": "boolean",
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Restart the database to apply it."
},
"is_log_drain_enabled": {
"type": "boolean",
"description": "Send the database logs to the log drain of the server. Log drain must be enabled on the server. Restart the database to apply it."
}
},
"type": "object"
@@ -7652,6 +7745,10 @@
"type": "boolean",
"description": "Whether data is saved in s3 or not"
},
"disable_local_backup": {
"type": "boolean",
"description": "Delete the local backup file after the upload to S3. Only allowed when save_s3 is true. Turning save_s3 off also turns this off."
},
"s3_storage_uuid": {
"type": "string",
"description": "Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination; when it is already a destination, it becomes the primary destination and the other destinations stay."
@@ -17199,6 +17296,12 @@
],
"example": "https:\/\/example.com"
},
"proxy_error_page": {
"type": [
"string",
"null"
]
},
"generate_exact_labels": {
"type": "boolean",
"example": false
@@ -17274,6 +17377,13 @@
"null"
]
},
"proxy_error_page": {
"description": "Custom HTML error page that the proxy shows for unknown hosts (when redirect_url is empty) and for resources in maintenance mode. Null restores the default page.",
"type": [
"string",
"null"
]
},
"generate_exact_labels": {
"type": "boolean"
},
@@ -17328,6 +17438,12 @@
"null"
]
},
"proxy_error_page": {
"type": [
"string",
"null"
]
},
"generate_exact_labels": {
"type": "boolean"
},
@@ -17442,6 +17558,12 @@
"null"
]
},
"proxy_error_page": {
"type": [
"string",
"null"
]
},
"generate_exact_labels": {
"type": "boolean"
},
@@ -18448,6 +18570,25 @@
"connection_timeout": {
"type": "integer",
"description": "SSH connection timeout in seconds (1-300). Default: 10."
},
"wildcard_domain": {
"type": "string",
"nullable": true,
"description": "Wildcard domain (URL) used to generate domains for new resources on this server. Null removes it."
},
"server_timezone": {
"type": "string",
"description": "Timezone of the server, for example Europe\/Berlin. Scheduled tasks and backups use it."
},
"backup_compression_cpu_percentage": {
"type": "integer",
"enum": [
25,
50,
75,
100
],
"description": "Maximum CPU percentage that backup compression can use. Default: 25."
}
},
"type": "object"
@@ -18913,6 +19054,15 @@
"null"
]
},
"redirect": {
"description": "How to handle www and non-www domains. \"www\" redirects non-www to www, \"non-www\" redirects www to non-www. Both need a www and a non-www domain in url.",
"type": "string",
"enum": [
"both",
"www",
"non-www"
]
},
"max_restart_count": {
"description": "Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.",
"type": [
@@ -20461,6 +20611,11 @@
"type": "boolean",
"default": true,
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
},
"include_coolify_default_variables": {
"type": "boolean",
"default": true,
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
}
},
"type": "object"
+102
View File
@@ -529,6 +529,10 @@ paths:
dockerfile_location:
type: string
description: 'The Dockerfile location in the repository.'
dockerfile_target_build:
type: string
nullable: true
description: 'The Docker build target (stage) in the Dockerfile.'
docker_compose_location:
type: string
description: 'The Docker Compose location.'
@@ -577,6 +581,10 @@ paths:
redact_all_env_values_in_logs:
type: boolean
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
include_coolify_default_variables:
type: boolean
default: true
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
is_pr_deployments_public_enabled:
type: boolean
description: 'Make pull request deployments public.'
@@ -604,6 +612,10 @@ paths:
type: string
nullable: true
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
is_container_label_readonly_enabled:
type: boolean
default: true
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
@@ -950,6 +962,10 @@ paths:
dockerfile_location:
type: string
description: 'The Dockerfile location in the repository'
dockerfile_target_build:
type: string
nullable: true
description: 'The Docker build target (stage) in the Dockerfile.'
docker_compose_location:
type: string
description: 'The Docker Compose location.'
@@ -998,6 +1014,10 @@ paths:
redact_all_env_values_in_logs:
type: boolean
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
include_coolify_default_variables:
type: boolean
default: true
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
is_pr_deployments_public_enabled:
type: boolean
description: 'Make pull request deployments public.'
@@ -1025,6 +1045,10 @@ paths:
type: string
nullable: true
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
is_container_label_readonly_enabled:
type: boolean
default: true
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
@@ -1371,6 +1395,10 @@ paths:
dockerfile_location:
type: string
description: 'The Dockerfile location in the repository.'
dockerfile_target_build:
type: string
nullable: true
description: 'The Docker build target (stage) in the Dockerfile.'
docker_compose_location:
type: string
description: 'The Docker Compose location.'
@@ -1419,6 +1447,10 @@ paths:
redact_all_env_values_in_logs:
type: boolean
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
include_coolify_default_variables:
type: boolean
default: true
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
is_pr_deployments_public_enabled:
type: boolean
description: 'Make pull request deployments public.'
@@ -1446,6 +1478,10 @@ paths:
type: string
nullable: true
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
is_container_label_readonly_enabled:
type: boolean
default: true
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
@@ -1781,6 +1817,10 @@ paths:
redact_all_env_values_in_logs:
type: boolean
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
include_coolify_default_variables:
type: boolean
default: true
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
is_pr_deployments_public_enabled:
type: boolean
description: 'Make pull request deployments public.'
@@ -1808,6 +1848,10 @@ paths:
type: string
nullable: true
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
is_container_label_readonly_enabled:
type: boolean
default: true
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
@@ -2129,6 +2173,10 @@ paths:
redact_all_env_values_in_logs:
type: boolean
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
include_coolify_default_variables:
type: boolean
default: true
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
is_pr_deployments_public_enabled:
type: boolean
description: 'Make pull request deployments public.'
@@ -2156,6 +2204,10 @@ paths:
type: string
nullable: true
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
is_container_label_readonly_enabled:
type: boolean
default: true
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
@@ -2574,6 +2626,10 @@ paths:
dockerfile_location:
type: string
description: 'The Dockerfile location in the repository.'
dockerfile_target_build:
type: string
nullable: true
description: 'The Docker build target (stage) in the Dockerfile.'
docker_compose_location:
type: string
description: 'The Docker Compose location.'
@@ -2640,6 +2696,10 @@ paths:
redact_all_env_values_in_logs:
type: boolean
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
include_coolify_default_variables:
type: boolean
default: true
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
is_pr_deployments_public_enabled:
type: boolean
description: 'Make pull request deployments public.'
@@ -2667,6 +2727,10 @@ paths:
type: string
nullable: true
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
is_container_label_readonly_enabled:
type: boolean
default: true
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
@@ -4519,6 +4583,10 @@ paths:
type: boolean
description: 'Whether to save backups to S3'
default: false
disable_local_backup:
type: boolean
description: 'Delete the local backup file after the upload to S3. Only allowed when save_s3 is true.'
default: false
s3_storage_uuid:
type: string
description: 'Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination. Either field is required if save_s3 is true.'
@@ -4860,6 +4928,12 @@ paths:
redact_all_env_values_in_logs:
type: boolean
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values, database passwords, and remote secrets are redacted. Requires team admin permissions.'
include_coolify_default_variables:
type: boolean
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Restart the database to apply it.'
is_log_drain_enabled:
type: boolean
description: 'Send the database logs to the log drain of the server. Log drain must be enabled on the server. Restart the database to apply it.'
type: object
responses:
'200':
@@ -4958,6 +5032,9 @@ paths:
save_s3:
type: boolean
description: 'Whether data is saved in s3 or not'
disable_local_backup:
type: boolean
description: 'Delete the local backup file after the upload to S3. Only allowed when save_s3 is true. Turning save_s3 off also turns this off.'
s3_storage_uuid:
type: string
description: 'Primary S3 storage UUID. Sent without s3_storage_uuids, it selects this storage as the only destination; when it is already a destination, it becomes the primary destination and the other destinations stay.'
@@ -10926,6 +11003,7 @@ paths:
status: { type: [string, 'null'], example: running }
redirect_enabled: { type: boolean, example: true }
redirect_url: { type: [string, 'null'], example: 'https://example.com' }
proxy_error_page: { type: [string, 'null'] }
generate_exact_labels: { type: boolean, example: false }
traefik_certificate_resolver: { description: 'Traefik certificate resolver for generated HTTPS routers. Null when the proxy uses its own certificates.', type: [string, 'null'], example: letsencrypt }
configuration: { description: 'Docker Compose proxy configuration when stored in the database. Only present with read:sensitive.', type: [string, 'null'] }
@@ -10964,6 +11042,9 @@ paths:
redirect_url:
description: 'Public http(s) redirect URL, or null to clear.'
type: [string, 'null']
proxy_error_page:
description: 'Custom HTML error page that the proxy shows for unknown hosts (when redirect_url is empty) and for resources in maintenance mode. Null restores the default page.'
type: [string, 'null']
generate_exact_labels:
type: boolean
traefik_certificate_resolver:
@@ -10985,6 +11066,7 @@ paths:
status: { type: [string, 'null'] }
redirect_enabled: { type: boolean }
redirect_url: { type: [string, 'null'] }
proxy_error_page: { type: [string, 'null'] }
generate_exact_labels: { type: boolean }
traefik_certificate_resolver: { type: [string, 'null'] }
configuration: { type: [string, 'null'] }
@@ -11041,6 +11123,7 @@ paths:
status: { type: [string, 'null'] }
redirect_enabled: { type: boolean }
redirect_url: { type: [string, 'null'] }
proxy_error_page: { type: [string, 'null'] }
generate_exact_labels: { type: boolean }
configuration: { type: [string, 'null'] }
type: object
@@ -11674,6 +11757,17 @@ paths:
connection_timeout:
type: integer
description: 'SSH connection timeout in seconds (1-300). Default: 10.'
wildcard_domain:
type: string
nullable: true
description: 'Wildcard domain (URL) used to generate domains for new resources on this server. Null removes it.'
server_timezone:
type: string
description: 'Timezone of the server, for example Europe/Berlin. Scheduled tasks and backups use it.'
backup_compression_cpu_percentage:
type: integer
enum: [25, 50, 75, 100]
description: 'Maximum CPU percentage that backup compression can use. Default: 25.'
type: object
responses:
'403':
@@ -11937,6 +12031,10 @@ paths:
is_permanent_redirect_enabled:
description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'
type: [boolean, 'null']
redirect:
description: 'How to handle www and non-www domains. "www" redirects non-www to www, "non-www" redirects www to non-www. Both need a www and a non-www domain in url.'
type: string
enum: [both, www, non-www]
max_restart_count:
description: 'Maximum Docker restart count before Coolify stops the container. Set to 0 to disable the limit.'
type: [integer, 'null']
@@ -12929,6 +13027,10 @@ paths:
type: boolean
default: true
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
include_coolify_default_variables:
type: boolean
default: true
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
type: object
responses:
'200':
@@ -704,3 +704,78 @@ test('application creation rejects an unsafe Traefik certificate resolver', func
expect(Application::query()->where('git_repository', 'like', '%resolver-test%')->exists())->toBeFalse();
});
test('PATCH /api/v1/applications/{uuid} turns off label management and keeps custom labels with new domains', function () {
$this->application->settings->update(['is_container_label_readonly_enabled' => true]);
$this->application->update([
'fqdn' => 'https://app.example.com',
'custom_labels' => base64_encode('sentinel-label=true'),
]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'is_container_label_readonly_enabled' => false,
'domains' => 'https://new.example.com',
])
->assertOk();
$application = $this->application->fresh();
expect($application->settings->is_container_label_readonly_enabled)->toBeFalse()
->and($application->fqdn)->toBe('https://new.example.com')
->and(base64_decode($application->custom_labels))->toBe('sentinel-label=true');
});
test('PATCH /api/v1/applications/{uuid} regenerates labels when label management is turned back on', function () {
$this->application->settings->update(['is_container_label_readonly_enabled' => false]);
$this->application->update([
'fqdn' => 'https://app.example.com',
'custom_labels' => base64_encode('sentinel-label=true'),
]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'is_container_label_readonly_enabled' => true,
])
->assertOk();
$application = $this->application->fresh();
$labels = $application->parseContainerLabels();
expect($application->settings->is_container_label_readonly_enabled)->toBeTrue()
->and($labels)->not->toContain('sentinel-label=true')
->and($labels)->toContain('app.example.com');
});
test('application creation keeps custom labels when label management is off', function () {
Queue::fake();
$response = $this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/public', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/readonly-labels-test',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'domains' => 'https://app.example.com',
'custom_labels' => base64_encode('sentinel-label=true'),
'is_container_label_readonly_enabled' => false,
])
->assertCreated();
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
expect($application->settings->is_container_label_readonly_enabled)->toBeFalse()
->and(base64_decode($application->custom_labels))->toBe('sentinel-label=true');
});
test('rejects an invalid is_container_label_readonly_enabled value', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'is_container_label_readonly_enabled' => 'not-a-boolean',
])
->assertUnprocessable()
->assertJsonValidationErrors('is_container_label_readonly_enabled');
});
@@ -0,0 +1,291 @@
<?php
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
uses(RefreshDatabase::class);
beforeEach(function () {
Queue::fake();
Server::flushIdentityMap();
config()->set('app.maintenance.store', 'array');
config()->set('app.maintenance.driver', 'file');
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
$this->team = Team::factory()->create();
$user = User::factory()->create();
$this->team->members()->attach($user->id, ['role' => 'owner']);
$plainTextToken = Str::random(40);
$token = $user->tokens()->create([
'name' => 'ui-settings-api-parity-test',
'token' => hash('sha256', $plainTextToken),
'abilities' => ['*'],
'team_id' => $this->team->id,
]);
$this->headers = ['Authorization' => 'Bearer '.$token->getKey().'|'.$plainTextToken];
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->firstOrFail();
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
$this->database = StandalonePostgresql::create([
'name' => 'db',
'postgres_user' => 'postgres',
'postgres_password' => 'password',
'postgres_db' => 'app',
'image' => 'postgres:16',
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(),
]);
});
function parityS3Storage(Team $team): S3Storage
{
return S3Storage::create([
'name' => 's3',
'region' => 'us-east-1',
'key' => 'key',
'secret' => 'secret',
'bucket' => 'bucket',
'endpoint' => 'https://s3.example.com',
'team_id' => $team->id,
'is_usable' => true,
]);
}
function parityServiceApplication(object $ctx): ServiceApplication
{
$service = Service::factory()->create([
'environment_id' => $ctx->environment->id,
'server_id' => $ctx->server->id,
'destination_id' => $ctx->destination->id,
'destination_type' => $ctx->destination->getMorphClass(),
'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n",
]);
return ServiceApplication::create([
'uuid' => (string) Str::uuid(),
'name' => 'web',
'service_id' => $service->id,
'image' => 'nginx:alpine',
]);
}
describe('applications', function () {
test('creation accepts dockerfile_target_build', function () {
$response = $this->withHeaders($this->headers)->postJson('/api/v1/applications/public', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/target-build-test',
'git_branch' => 'main',
'build_pack' => 'dockerfile',
'ports_exposes' => '3000',
'dockerfile_target_build' => 'production',
])->assertCreated();
expect(Application::where('uuid', $response->json('uuid'))->firstOrFail()->dockerfile_target_build)->toBe('production');
});
test('PATCH updates include_coolify_default_variables', function () {
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(),
]);
$this->withHeaders($this->headers)
->patchJson("/api/v1/applications/{$application->uuid}", ['include_coolify_default_variables' => false])
->assertOk();
expect($application->fresh()->settings->include_coolify_default_variables)->toBeFalse();
});
});
describe('databases', function () {
test('PATCH saves is_log_drain_enabled when the server has a log drain', function () {
$this->server->settings->update(['is_logdrain_newrelic_enabled' => true]);
$this->withHeaders($this->headers)
->patchJson("/api/v1/databases/{$this->database->uuid}", ['is_log_drain_enabled' => true])
->assertOk();
expect($this->database->fresh()->is_log_drain_enabled)->toBeTruthy();
});
test('PATCH rejects is_log_drain_enabled when the server has no log drain', function () {
$this->withHeaders($this->headers)
->patchJson("/api/v1/databases/{$this->database->uuid}", ['is_log_drain_enabled' => true])
->assertUnprocessable()
->assertJsonValidationErrors('is_log_drain_enabled');
expect($this->database->fresh()->is_log_drain_enabled)->toBeFalsy();
});
test('PATCH updates include_coolify_default_variables', function () {
$this->withHeaders($this->headers)
->patchJson("/api/v1/databases/{$this->database->uuid}", ['include_coolify_default_variables' => false])
->assertOk();
expect($this->database->fresh()->include_coolify_default_variables)->toBeFalse();
});
test('backup creation accepts disable_local_backup with S3', function () {
$s3 = parityS3Storage($this->team);
$response = $this->withHeaders($this->headers)->postJson("/api/v1/databases/{$this->database->uuid}/backups", [
'frequency' => 'daily',
'save_s3' => true,
's3_storage_uuid' => $s3->uuid,
'disable_local_backup' => true,
])->assertCreated();
expect(ScheduledDatabaseBackup::where('uuid', $response->json('uuid'))->firstOrFail()->disable_local_backup)->toBeTruthy();
});
test('backup creation rejects disable_local_backup without S3', function () {
$this->withHeaders($this->headers)->postJson("/api/v1/databases/{$this->database->uuid}/backups", [
'frequency' => 'daily',
'disable_local_backup' => true,
])->assertUnprocessable()
->assertJsonValidationErrors('disable_local_backup');
expect(ScheduledDatabaseBackup::count())->toBe(0);
});
test('backup update turns local backups back on when S3 is turned off', function () {
$s3 = parityS3Storage($this->team);
$backup = ScheduledDatabaseBackup::create([
'frequency' => 'daily',
'save_s3' => true,
's3_storage_id' => $s3->id,
'disable_local_backup' => true,
'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(),
'team_id' => $this->team->id,
]);
$this->withHeaders($this->headers)
->patchJson("/api/v1/databases/{$this->database->uuid}/backups/{$backup->uuid}", ['save_s3' => false])
->assertOk();
expect($backup->fresh()->disable_local_backup)->toBeFalsy();
$this->withHeaders($this->headers)
->patchJson("/api/v1/databases/{$this->database->uuid}/backups/{$backup->uuid}", ['disable_local_backup' => true])
->assertUnprocessable()
->assertJsonValidationErrors('disable_local_backup');
});
});
describe('services', function () {
test('PATCH updates include_coolify_default_variables', function () {
$serviceApplication = parityServiceApplication($this);
$service = $serviceApplication->service;
$this->withHeaders($this->headers)
->patchJson("/api/v1/services/{$service->uuid}", ['include_coolify_default_variables' => false])
->assertOk();
expect($service->fresh()->include_coolify_default_variables)->toBeFalse();
});
test('service application PATCH sets a www redirect when both hosts exist', function () {
$serviceApplication = parityServiceApplication($this);
$this->withHeaders($this->headers)
->patchJson("/api/v1/services/{$serviceApplication->service->uuid}/applications/{$serviceApplication->uuid}", [
'url' => 'https://example.com,https://www.example.com',
'redirect' => 'www',
])->assertOk();
expect($serviceApplication->fresh()->redirect)->toBe('www');
});
test('service application PATCH rejects a www redirect without a www host', function () {
$serviceApplication = parityServiceApplication($this);
$serviceApplication->update(['fqdn' => 'https://example.com']);
$this->withHeaders($this->headers)
->patchJson("/api/v1/services/{$serviceApplication->service->uuid}/applications/{$serviceApplication->uuid}", [
'redirect' => 'www',
])->assertUnprocessable()
->assertJsonValidationErrors('redirect');
expect($serviceApplication->fresh()->redirect)->not->toBe('www');
});
test('service application PATCH rejects an unknown redirect', function () {
$serviceApplication = parityServiceApplication($this);
$this->withHeaders($this->headers)
->patchJson("/api/v1/services/{$serviceApplication->service->uuid}/applications/{$serviceApplication->uuid}", [
'redirect' => 'sometimes',
])->assertUnprocessable()
->assertJsonValidationErrors('redirect');
});
});
describe('servers', function () {
test('PATCH updates wildcard domain, timezone and backup compression', function () {
$this->withHeaders($this->headers)->patchJson("/api/v1/servers/{$this->server->uuid}", [
'wildcard_domain' => 'https://apps.example.com',
'server_timezone' => 'Europe/Berlin',
'backup_compression_cpu_percentage' => 50,
])->assertCreated();
expect($this->server->settings->fresh())
->wildcard_domain->toBe('https://apps.example.com')
->server_timezone->toBe('Europe/Berlin')
->backup_compression_cpu_percentage->toBe(50);
$this->withHeaders($this->headers)
->patchJson("/api/v1/servers/{$this->server->uuid}", ['wildcard_domain' => null])
->assertCreated();
expect($this->server->settings->fresh()->wildcard_domain)->toBeNull();
});
test('PATCH rejects invalid server settings', function (string $field, mixed $value) {
$this->withHeaders($this->headers)
->patchJson("/api/v1/servers/{$this->server->uuid}", [$field => $value])
->assertUnprocessable()
->assertJsonValidationErrors($field);
})->with([
'wildcard domain' => ['wildcard_domain', 'not a url'],
'timezone' => ['server_timezone', 'Mars/Olympus'],
'compression' => ['backup_compression_cpu_percentage', 30],
]);
test('proxy PATCH sets and clears the custom error page', function () {
$this->withHeaders($this->headers)
->patchJson("/api/v1/servers/{$this->server->uuid}/proxy", ['proxy_error_page' => '<h1>Not here</h1>'])
->assertOk()
->assertJsonPath('proxy_error_page', '<h1>Not here</h1>');
expect($this->server->fresh()->proxy_error_page)->toBe('<h1>Not here</h1>');
$this->withHeaders($this->headers)
->patchJson("/api/v1/servers/{$this->server->uuid}/proxy", ['proxy_error_page' => null])
->assertOk();
expect($this->server->fresh()->proxy_error_page)->toBeNull();
});
});