Merge remote-tracking branch 'origin/next' into feat/noindex-domains

This commit is contained in:
Andras Bacsai
2026-07-14 14:04:06 +02:00
29 changed files with 2164 additions and 45 deletions
@@ -35,6 +35,36 @@ class ApplicationsController extends Controller
{
use Concerns\HandlesTagsApi;
private const APPLICATION_SETTING_FIELDS = [
'is_git_submodules_enabled',
'is_git_lfs_enabled',
'is_git_shallow_clone_enabled',
'disable_build_cache',
'inject_build_args_to_dockerfile',
'include_source_commit_in_build',
'is_env_sorting_enabled',
'is_pr_deployments_public_enabled',
'stop_grace_period',
'docker_images_to_keep',
'is_gzip_enabled',
'is_stripprefix_enabled',
'is_raw_compose_deployment_enabled',
];
private const BOOLEAN_APPLICATION_SETTING_FIELDS = [
'is_git_submodules_enabled',
'is_git_lfs_enabled',
'is_git_shallow_clone_enabled',
'disable_build_cache',
'inject_build_args_to_dockerfile',
'include_source_commit_in_build',
'is_env_sorting_enabled',
'is_pr_deployments_public_enabled',
'is_gzip_enabled',
'is_stripprefix_enabled',
'is_raw_compose_deployment_enabled',
];
protected function findTaggableResource(string $uuid, int|string $teamId): mixed
{
return Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $uuid)->first();
@@ -87,9 +117,48 @@ class ApplicationsController extends Controller
$application->makeHidden(['value', 'real_value']);
}
if ($application->relationLoaded('settings')) {
$application->settings?->makeHidden(['id', 'application_id', 'created_at', 'updated_at']);
}
return serializeApiResponse($application);
}
private function applicationSettingsFromRequest(Request $request): array
{
$settings = [];
foreach (self::APPLICATION_SETTING_FIELDS as $field) {
if (! array_key_exists($field, $request->all())) {
continue;
}
$settings[$field] = in_array($field, self::BOOLEAN_APPLICATION_SETTING_FIELDS, true)
? $request->boolean($field)
: $request->input($field);
}
return $settings;
}
private function applyApplicationSettings(Application $application, array $settings): void
{
if ($settings === []) {
return;
}
$regenerateLabels = ! $application->wasRecentlyCreated
&& $application->settings->is_container_label_readonly_enabled
&& (array_key_exists('is_gzip_enabled', $settings) || array_key_exists('is_stripprefix_enabled', $settings));
$application->settings->fill($settings)->save();
if ($regenerateLabels) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
}
}
/**
* Expose sensitive fields on eager-loaded nested Server + ServerSetting
* relations for callers with the `read:sensitive` or `root` token ability.
@@ -286,6 +355,20 @@ class ApplicationsController extends Controller
],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'use_build_secrets' => ['type' => 'boolean', 'default' => false, 'description' => 'Use Docker Build Secrets for build-time environment variables.'],
'is_git_submodules_enabled' => ['type' => 'boolean', 'description' => 'Clone Git submodules.'],
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'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.'],
'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.'],
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
@@ -455,6 +538,20 @@ class ApplicationsController extends Controller
],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'use_build_secrets' => ['type' => 'boolean', 'default' => false, 'description' => 'Use Docker Build Secrets for build-time environment variables.'],
'is_git_submodules_enabled' => ['type' => 'boolean', 'description' => 'Clone Git submodules.'],
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'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.'],
'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.'],
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
@@ -624,6 +721,20 @@ class ApplicationsController extends Controller
],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'use_build_secrets' => ['type' => 'boolean', 'default' => false, 'description' => 'Use Docker Build Secrets for build-time environment variables.'],
'is_git_submodules_enabled' => ['type' => 'boolean', 'description' => 'Clone Git submodules.'],
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'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.'],
'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.'],
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
@@ -765,6 +876,20 @@ class ApplicationsController extends Controller
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'use_build_secrets' => ['type' => 'boolean', 'default' => false, 'description' => 'Use Docker Build Secrets for build-time environment variables.'],
'is_git_submodules_enabled' => ['type' => 'boolean', 'description' => 'Clone Git submodules.'],
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'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.'],
'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.'],
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
@@ -902,6 +1027,20 @@ class ApplicationsController extends Controller
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'use_build_secrets' => ['type' => 'boolean', 'default' => false, 'description' => 'Use Docker Build Secrets for build-time environment variables.'],
'is_git_submodules_enabled' => ['type' => 'boolean', 'description' => 'Clone Git submodules.'],
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'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.'],
'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.'],
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
@@ -986,7 +1125,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', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags', 'is_preserve_repository_enabled'];
$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', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags', 'is_preserve_repository_enabled', ...self::APPLICATION_SETTING_FIELDS];
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
@@ -1041,6 +1180,7 @@ class ApplicationsController extends Controller
$instantDeploy = $request->instant_deploy;
$githubAppUuid = $request->github_app_uuid;
$useBuildServer = $request->use_build_server;
$useBuildSecrets = $request->use_build_secrets;
$isStatic = $request->is_static;
$isSpa = $request->is_spa;
$isAutoDeployEnabled = $request->is_auto_deploy_enabled;
@@ -1050,6 +1190,19 @@ class ApplicationsController extends Controller
$customNginxConfiguration = $request->custom_nginx_configuration;
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true);
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled', false);
$applicationSettings = $this->applicationSettingsFromRequest($request);
$requestedBuildPack = in_array($type, ['public', 'private-gh-app', 'private-deploy-key'], true)
? $request->input('build_pack')
: $type;
if (($applicationSettings['is_raw_compose_deployment_enabled'] ?? false) && $requestedBuildPack !== 'dockercompose') {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'is_raw_compose_deployment_enabled' => 'Raw compose deployment can only be enabled for Docker Compose applications.',
],
], 422);
}
if (! is_null($customNginxConfiguration)) {
if (! isBase64Encoded($customNginxConfiguration)) {
@@ -1237,6 +1390,7 @@ class ApplicationsController extends Controller
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
$application->save();
$this->applyApplicationSettings($application, $applicationSettings);
if (isset($isStatic)) {
$application->settings->is_static = $isStatic;
$application->settings->save();
@@ -1265,6 +1419,10 @@ class ApplicationsController extends Controller
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
if (isset($useBuildSecrets)) {
$application->settings->use_build_secrets = $useBuildSecrets;
$application->settings->save();
}
if (isset($isContainerLabelEscapeEnabled)) {
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
@@ -1483,6 +1641,7 @@ class ApplicationsController extends Controller
$application->repository_project_id = $repository_project_id;
$application->save();
$this->applyApplicationSettings($application, $applicationSettings);
$application->refresh();
// Auto-generate domain if requested and no custom domain provided
if ($autogenerateDomain && blank($fqdn)) {
@@ -1517,6 +1676,10 @@ class ApplicationsController extends Controller
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
if (isset($useBuildSecrets)) {
$application->settings->use_build_secrets = $useBuildSecrets;
$application->settings->save();
}
if (isset($isContainerLabelEscapeEnabled)) {
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
@@ -1699,6 +1862,7 @@ class ApplicationsController extends Controller
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
$application->save();
$this->applyApplicationSettings($application, $applicationSettings);
$application->refresh();
// Auto-generate domain if requested and no custom domain provided
if ($autogenerateDomain && blank($fqdn)) {
@@ -1733,6 +1897,10 @@ class ApplicationsController extends Controller
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
if (isset($useBuildSecrets)) {
$application->settings->use_build_secrets = $useBuildSecrets;
$application->settings->save();
}
if (isset($isContainerLabelEscapeEnabled)) {
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
@@ -1842,6 +2010,7 @@ class ApplicationsController extends Controller
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
$application->save();
$this->applyApplicationSettings($application, $applicationSettings);
$application->refresh();
// Auto-generate domain if requested and no custom domain provided
if ($autogenerateDomain && blank($fqdn)) {
@@ -1864,6 +2033,10 @@ class ApplicationsController extends Controller
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
if (isset($useBuildSecrets)) {
$application->settings->use_build_secrets = $useBuildSecrets;
$application->settings->save();
}
if (isset($isContainerLabelEscapeEnabled)) {
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
@@ -1968,6 +2141,7 @@ class ApplicationsController extends Controller
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
$application->save();
$this->applyApplicationSettings($application, $applicationSettings);
$application->refresh();
// Auto-generate domain if requested and no custom domain provided
if ($autogenerateDomain && blank($fqdn)) {
@@ -1990,6 +2164,10 @@ class ApplicationsController extends Controller
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
if (isset($useBuildSecrets)) {
$application->settings->use_build_secrets = $useBuildSecrets;
$application->settings->save();
}
if (isset($isContainerLabelEscapeEnabled)) {
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
@@ -2095,7 +2273,7 @@ class ApplicationsController extends Controller
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->route('uuid'))->first();
$application = Application::ownedByCurrentTeamAPI($teamId)->with('settings')->where('uuid', $request->route('uuid'))->first();
if (! $application) {
return response()->json(['message' => 'Application not found.'], 404);
}
@@ -2411,11 +2589,24 @@ class ApplicationsController extends Controller
],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'use_build_secrets' => ['type' => 'boolean', 'description' => 'Use Docker Build Secrets for build-time environment variables.'],
'is_git_submodules_enabled' => ['type' => 'boolean', 'description' => 'Clone Git submodules.'],
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'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.'],
'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.'],
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'is_preserve_repository_enabled' => ['type' => 'boolean', 'description' => 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include source commit information in the build. Default is false.'],
],
)
),
@@ -2501,7 +2692,7 @@ class ApplicationsController extends Controller
$this->authorize('update', $application);
$server = $application->destination->server;
$allowedFields = ['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', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', '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', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled', 'include_source_commit_in_build'];
$allowedFields = ['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', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', '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', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'use_build_secrets', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled', ...self::APPLICATION_SETTING_FIELDS];
$validationRules = [
'name' => 'string|max:255',
@@ -2580,6 +2771,17 @@ class ApplicationsController extends Controller
], 422);
}
$applicationSettings = $this->applicationSettingsFromRequest($request);
$requestedBuildPack = $request->input('build_pack', $application->build_pack);
if (($applicationSettings['is_raw_compose_deployment_enabled'] ?? false) && $requestedBuildPack !== 'dockercompose') {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'is_raw_compose_deployment_enabled' => 'Raw compose deployment can only be enabled for Docker Compose applications.',
],
], 422);
}
if ($request->has('is_http_basic_auth_enabled') && $request->is_http_basic_auth_enabled === true) {
if (blank($application->http_basic_auth_username) || blank($application->http_basic_auth_password)) {
$validationErrors = [];
@@ -2734,6 +2936,7 @@ class ApplicationsController extends Controller
$isPreviewDeploymentsEnabled = $request->is_preview_deployments_enabled;
$connectToDockerNetwork = $request->connect_to_docker_network;
$useBuildServer = $request->use_build_server;
$useBuildSecrets = $request->use_build_secrets;
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled');
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled');
$includeSourceCommitInBuild = $request->boolean('include_source_commit_in_build');
@@ -2741,6 +2944,10 @@ class ApplicationsController extends Controller
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
if (isset($useBuildSecrets)) {
$application->settings->use_build_secrets = $useBuildSecrets;
$application->settings->save();
}
if (isset($isStatic)) {
$application->settings->is_static = $isStatic;
@@ -2784,6 +2991,7 @@ class ApplicationsController extends Controller
$application->settings->include_source_commit_in_build = $includeSourceCommitInBuild;
$application->settings->save();
}
$this->applyApplicationSettings($application, $applicationSettings);
removeUnnecessaryFieldsFromRequest($request);
$data = $request->only($allowedFields);
@@ -4035,7 +4243,7 @@ class ApplicationsController extends Controller
),
]
)]
public function move_by_uuid(Request $request): \Illuminate\Http\JsonResponse
public function move_by_uuid(Request $request): JsonResponse
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -286,6 +286,7 @@ class Advanced extends Component
$this->application->settings->save();
$this->dispatch('success', 'Stop grace period updated.');
$this->dispatch('configurationChanged');
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
@@ -496,6 +496,7 @@ class General extends Component
if ($this->isContainerLabelReadonlyEnabled) {
$this->resetDefaultLabels(false);
}
$this->dispatch('configurationChanged');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -147,6 +147,7 @@ class Source extends Component
'source_id' => $source->id,
'source_type' => $sourceType,
]);
$this->dispatch('configurationChanged');
['repository' => $customRepository] = $this->application->customRepository();
$repository = githubApi($this->application->source, "repos/{$customRepository}");
@@ -57,6 +57,7 @@ class Swarm extends Component
$this->authorize('update', $this->application);
$this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.');
$this->dispatch('configurationChanged');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -68,6 +69,7 @@ class Swarm extends Component
$this->authorize('update', $this->application);
$this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.');
$this->dispatch('configurationChanged');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -76,6 +76,7 @@ class All extends Component
$this->resource->settings->save();
$this->getDevView();
$this->dispatch('success', 'Environment variable settings updated.');
$this->dispatch('configurationChanged');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -152,6 +152,7 @@ class HealthChecks extends Component
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
$this->resource->save();
$this->dispatch('success', 'Health check updated.');
$this->dispatch('configurationChanged');
}
public function submit()
@@ -178,6 +179,7 @@ class HealthChecks extends Component
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
$this->resource->save();
$this->dispatch('success', 'Health check updated.');
$this->dispatch('configurationChanged');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -213,6 +215,7 @@ class HealthChecks extends Component
} else {
$this->dispatch('success', 'Health check '.($this->healthCheckEnabled ? 'enabled' : 'disabled').'.');
}
$this->dispatch('configurationChanged');
} catch (\Throwable $e) {
return handleError($e, $this);
}
+1
View File
@@ -113,6 +113,7 @@ use Symfony\Component\Yaml\Yaml;
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
'settings' => new OA\Property(ref: '#/components/schemas/ApplicationSetting'),
]
)]
+53
View File
@@ -4,7 +4,49 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
#[OA\Schema(
description: 'Application settings.',
type: 'object',
properties: [
'is_static' => ['type' => 'boolean'],
'is_git_submodules_enabled' => ['type' => 'boolean'],
'is_git_lfs_enabled' => ['type' => 'boolean'],
'is_auto_deploy_enabled' => ['type' => 'boolean'],
'is_force_https_enabled' => ['type' => 'boolean'],
'is_debug_enabled' => ['type' => 'boolean'],
'is_preview_deployments_enabled' => ['type' => 'boolean'],
'is_log_drain_enabled' => ['type' => 'boolean'],
'is_gpu_enabled' => ['type' => 'boolean'],
'gpu_driver' => ['type' => 'string', 'nullable' => true],
'gpu_count' => ['type' => 'string', 'nullable' => true],
'gpu_device_ids' => ['type' => 'string', 'nullable' => true],
'gpu_options' => ['type' => 'string', 'nullable' => true],
'is_include_timestamps' => ['type' => 'boolean'],
'is_swarm_only_worker_nodes' => ['type' => 'boolean'],
'is_raw_compose_deployment_enabled' => ['type' => 'boolean'],
'is_build_server_enabled' => ['type' => 'boolean'],
'is_consistent_container_name_enabled' => ['type' => 'boolean'],
'is_gzip_enabled' => ['type' => 'boolean'],
'is_stripprefix_enabled' => ['type' => 'boolean'],
'connect_to_docker_network' => ['type' => 'boolean'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true],
'is_container_label_escape_enabled' => ['type' => 'boolean'],
'is_env_sorting_enabled' => ['type' => 'boolean'],
'is_container_label_readonly_enabled' => ['type' => 'boolean'],
'is_preserve_repository_enabled' => ['type' => 'boolean'],
'disable_build_cache' => ['type' => 'boolean'],
'is_spa' => ['type' => 'boolean'],
'is_git_shallow_clone_enabled' => ['type' => 'boolean'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean'],
'use_build_secrets' => ['type' => 'boolean'],
'inject_build_args_to_dockerfile' => ['type' => 'boolean'],
'include_source_commit_in_build' => ['type' => 'boolean'],
'docker_images_to_keep' => ['type' => 'integer'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true],
]
)]
class ApplicationSetting extends Model
{
protected $casts = [
@@ -27,6 +69,17 @@ class ApplicationSetting extends Model
'is_git_shallow_clone_enabled' => 'boolean',
'docker_images_to_keep' => 'integer',
'stop_grace_period' => 'integer',
'is_log_drain_enabled' => 'boolean',
'is_gpu_enabled' => 'boolean',
'is_include_timestamps' => 'boolean',
'is_swarm_only_worker_nodes' => 'boolean',
'is_raw_compose_deployment_enabled' => 'boolean',
'is_consistent_container_name_enabled' => 'boolean',
'is_gzip_enabled' => 'boolean',
'is_stripprefix_enabled' => 'boolean',
'connect_to_docker_network' => 'boolean',
'is_env_sorting_enabled' => 'boolean',
'disable_build_cache' => 'boolean',
];
protected $fillable = [
@@ -102,6 +102,8 @@ class ApplicationConfigurationSnapshot
$this->item('git_repository', 'Repository', $this->application->git_repository, 'build'),
$this->item('git_branch', 'Branch', $this->application->git_branch, 'build'),
$this->item('git_commit_sha', 'Commit SHA', $this->application->git_commit_sha, 'build'),
$this->item('source_id', 'Source ID', $this->application->source_id, 'build'),
$this->item('source_type', 'Source type', $this->application->source_type, 'build'),
$this->item('private_key_id', 'Private key', $this->application->private_key_id, 'build'),
];
}
@@ -113,6 +115,8 @@ class ApplicationConfigurationSnapshot
{
return [
$this->item('build_pack', 'Build pack', $this->application->build_pack, 'build'),
$this->item('is_static', 'Static site', data_get($this->application, 'settings.is_static'), 'build'),
$this->item('is_spa', 'Single-page application', data_get($this->application, 'settings.is_spa'), 'build'),
$this->item('static_image', 'Static image', $this->application->static_image, 'build'),
$this->item('base_directory', 'Base directory', $this->application->base_directory, 'build'),
$this->item('publish_directory', 'Publish directory', $this->application->publish_directory, 'build'),
@@ -127,7 +131,11 @@ class ApplicationConfigurationSnapshot
// so comparing it would flag a permanent change for git-based compose apps.
$this->item('docker_compose_raw', 'Docker Compose', $this->application->docker_compose_raw, 'build', displayValue: $this->summarizeText($this->application->docker_compose_raw), displayFull: $this->application->docker_compose_raw, diffMode: 'lines'),
$this->item('docker_compose_custom_build_command', 'Docker Compose custom build command', $this->application->docker_compose_custom_build_command, 'build'),
$this->item('custom_docker_run_options', 'Custom Docker run options', $this->application->custom_docker_run_options, 'build'),
$this->item('is_git_submodules_enabled', 'Git submodules', data_get($this->application, 'settings.is_git_submodules_enabled'), 'build'),
$this->item('is_git_lfs_enabled', 'Git LFS', data_get($this->application, 'settings.is_git_lfs_enabled'), 'build'),
$this->item('is_git_shallow_clone_enabled', 'Shallow clone', data_get($this->application, 'settings.is_git_shallow_clone_enabled'), 'build'),
$this->item('is_env_sorting_enabled', 'Sort environment variables', data_get($this->application, 'settings.is_env_sorting_enabled'), 'build'),
$this->item('custom_docker_run_options', 'Custom Docker run options', $this->application->custom_docker_run_options, 'redeploy'),
$this->item('use_build_secrets', 'Use build secrets', data_get($this->application, 'settings.use_build_secrets'), 'build'),
$this->item('inject_build_args_to_dockerfile', 'Inject build args to Dockerfile', data_get($this->application, 'settings.inject_build_args_to_dockerfile'), 'build'),
$this->item('include_source_commit_in_build', 'Include source commit in build', data_get($this->application, 'settings.include_source_commit_in_build'), 'build'),
@@ -142,13 +150,26 @@ class ApplicationConfigurationSnapshot
private function runtimeItems(): array
{
return [
$this->item('docker_registry_image_name', 'Docker image', $this->application->docker_registry_image_name, 'redeploy'),
$this->item('docker_registry_image_tag', 'Docker image tag or hash', $this->application->docker_registry_image_tag, 'redeploy'),
$this->item('start_command', 'Start command', $this->application->start_command, 'redeploy'),
$this->item('pre_deployment_command', 'Pre-deployment command', $this->application->pre_deployment_command, 'redeploy'),
$this->item('pre_deployment_command_container', 'Pre-deployment command container', $this->application->pre_deployment_command_container, 'redeploy'),
$this->item('post_deployment_command', 'Post-deployment command', $this->application->post_deployment_command, 'redeploy'),
$this->item('post_deployment_command_container', 'Post-deployment command container', $this->application->post_deployment_command_container, 'redeploy'),
$this->item('docker_compose_custom_start_command', 'Docker Compose custom start command', $this->application->docker_compose_custom_start_command, 'redeploy'),
$this->item('ports_exposes', 'Exposed ports', $this->application->ports_exposes, 'redeploy'),
$this->item('ports_mappings', 'Port mappings', $this->application->ports_mappings, 'redeploy'),
$this->item('custom_network_aliases', 'Network aliases', $this->application->custom_network_aliases, 'redeploy'),
$this->item('connect_to_docker_network', 'Connect to Docker network', data_get($this->application, 'settings.connect_to_docker_network'), 'redeploy'),
$this->item('custom_internal_name', 'Custom container name', data_get($this->application, 'settings.custom_internal_name'), 'redeploy'),
$this->item('is_consistent_container_name_enabled', 'Consistent container name', data_get($this->application, 'settings.is_consistent_container_name_enabled'), 'redeploy'),
$this->item('is_container_label_escape_enabled', 'Escape container labels', data_get($this->application, 'settings.is_container_label_escape_enabled'), 'redeploy'),
$this->item('is_container_label_readonly_enabled', 'Read-only container labels', data_get($this->application, 'settings.is_container_label_readonly_enabled'), 'redeploy'),
$this->item('is_log_drain_enabled', 'Log drain', data_get($this->application, 'settings.is_log_drain_enabled'), 'redeploy'),
$this->item('is_swarm_only_worker_nodes', 'Swarm worker nodes only', data_get($this->application, 'settings.is_swarm_only_worker_nodes'), 'redeploy'),
$this->item('stop_grace_period', 'Stop grace period', $this->normalizedStopGracePeriod(), 'redeploy'),
$this->item('is_preserve_repository_enabled', 'Preserve repository', data_get($this->application, 'settings.is_preserve_repository_enabled'), 'redeploy'),
$this->item('is_raw_compose_deployment_enabled', 'Raw Compose deployment', data_get($this->application, 'settings.is_raw_compose_deployment_enabled'), 'redeploy'),
$this->item('is_gpu_enabled', 'GPU enabled', data_get($this->application, 'settings.is_gpu_enabled'), 'redeploy'),
$this->item('gpu_driver', 'GPU driver', data_get($this->application, 'settings.gpu_driver'), 'redeploy'),
@@ -170,7 +191,7 @@ class ApplicationConfigurationSnapshot
$this->item('docker_compose_domains', 'Service domains', $this->decodedComposeDomains(), 'redeploy', displayValue: $this->summarizeText($this->composeDomainsText()), displayFull: $this->composeDomainsText(), diffMode: 'lines'),
$this->item('redirect', 'Redirect', $this->application->redirect, 'redeploy'),
$this->item('custom_labels', 'Container labels', $this->application->custom_labels, 'redeploy', displayValue: $this->summarizeText($this->decodeCustomLabels($this->application->custom_labels)), displayFull: $this->decodeCustomLabels($this->application->custom_labels), diffMode: 'lines'),
$this->item('custom_nginx_configuration', 'Custom Nginx configuration', $this->application->custom_nginx_configuration, 'redeploy', displayValue: $this->summarizeText($this->application->custom_nginx_configuration), displayFull: $this->application->custom_nginx_configuration),
$this->item('custom_nginx_configuration', 'Custom Nginx configuration', $this->application->custom_nginx_configuration, 'build', displayValue: $this->summarizeText($this->application->custom_nginx_configuration), displayFull: $this->application->custom_nginx_configuration),
$this->item('is_force_https_enabled', 'Force HTTPS', data_get($this->application, 'settings.is_force_https_enabled'), 'redeploy'),
$this->item('is_gzip_enabled', 'Gzip', data_get($this->application, 'settings.is_gzip_enabled'), 'redeploy'),
$this->item('is_stripprefix_enabled', 'Strip prefix', data_get($this->application, 'settings.is_stripprefix_enabled'), 'redeploy'),
@@ -327,6 +348,17 @@ class ApplicationConfigurationSnapshot
return $flags ? "Hidden ({$flags})" : 'Hidden';
}
private function normalizedStopGracePeriod(): ?int
{
$stopGracePeriod = data_get($this->application, 'settings.stop_grace_period');
if ($stopGracePeriod === null || (int) $stopGracePeriod === DEFAULT_STOP_GRACE_PERIOD_SECONDS) {
return null;
}
return (int) $stopGracePeriod;
}
private function environmentFlags(EnvironmentVariable $environmentVariable): string
{
return collect([
@@ -17,6 +17,28 @@ class ConfigurationDiffer
*/
private const IGNORED_KEYS = ['build.docker_compose'];
/**
* Defaults for fields introduced after configuration snapshots were first
* stored. Older snapshots omitted these keys, which should not make an
* unchanged default look like a pending configuration change.
*
* @var array<string, bool|array<int, bool>>
*/
private const INTRODUCED_DEFAULTS = [
'build.is_static' => false,
'build.is_spa' => false,
'build.is_git_submodules_enabled' => true,
'build.is_git_lfs_enabled' => true,
'build.is_git_shallow_clone_enabled' => true,
'build.is_env_sorting_enabled' => [false, true],
'runtime.is_consistent_container_name_enabled' => false,
'runtime.is_container_label_escape_enabled' => true,
'runtime.is_container_label_readonly_enabled' => true,
'runtime.is_log_drain_enabled' => false,
'runtime.is_swarm_only_worker_nodes' => true,
'runtime.is_preserve_repository_enabled' => false,
];
/**
* @param array<string, mixed> $previousSnapshot
* @param array<string, mixed> $currentSnapshot
@@ -36,6 +58,14 @@ class ConfigurationDiffer
$previous = $previousItems[$key] ?? null;
$current = $currentItems[$key] ?? null;
if (
$previous === null
&& array_key_exists($key, self::INTRODUCED_DEFAULTS)
&& in_array((bool) data_get($current, 'compare_value'), (array) self::INTRODUCED_DEFAULTS[$key], true)
) {
continue;
}
if (($previous['compare_value'] ?? null) === ($current['compare_value'] ?? null)) {
continue;
}
+27
View File
@@ -117,6 +117,20 @@ function sharedDataApplications()
'is_auto_deploy_enabled' => 'boolean',
'is_force_https_enabled' => 'boolean',
'is_preview_deployments_enabled' => 'boolean',
'use_build_secrets' => 'boolean',
'is_git_submodules_enabled' => 'boolean',
'is_git_lfs_enabled' => 'boolean',
'is_git_shallow_clone_enabled' => 'boolean',
'disable_build_cache' => 'boolean',
'inject_build_args_to_dockerfile' => 'boolean',
'include_source_commit_in_build' => 'boolean',
'is_env_sorting_enabled' => 'boolean',
'is_pr_deployments_public_enabled' => 'boolean',
'is_gzip_enabled' => 'boolean',
'is_stripprefix_enabled' => 'boolean',
'is_raw_compose_deployment_enabled' => 'boolean',
'stop_grace_period' => 'nullable|integer|min:'.MIN_STOP_GRACE_PERIOD_SECONDS.'|max:'.MAX_STOP_GRACE_PERIOD_SECONDS,
'docker_images_to_keep' => 'integer|min:0|max:100',
'static_image' => Rule::enum(StaticImageTypes::class),
'domains' => ValidationPatterns::applicationDomainRules(),
'noindex_domains' => 'array|nullable',
@@ -274,6 +288,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('github_app_uuid');
$request->offsetUnset('private_key_uuid');
$request->offsetUnset('use_build_server');
$request->offsetUnset('use_build_secrets');
$request->offsetUnset('is_static');
$request->offsetUnset('is_spa');
$request->offsetUnset('is_auto_deploy_enabled');
@@ -285,6 +300,18 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('is_container_label_escape_enabled');
$request->offsetUnset('is_preserve_repository_enabled');
$request->offsetUnset('include_source_commit_in_build');
$request->offsetUnset('is_git_submodules_enabled');
$request->offsetUnset('is_git_lfs_enabled');
$request->offsetUnset('is_git_shallow_clone_enabled');
$request->offsetUnset('disable_build_cache');
$request->offsetUnset('inject_build_args_to_dockerfile');
$request->offsetUnset('is_env_sorting_enabled');
$request->offsetUnset('is_pr_deployments_public_enabled');
$request->offsetUnset('stop_grace_period');
$request->offsetUnset('docker_images_to_keep');
$request->offsetUnset('is_gzip_enabled');
$request->offsetUnset('is_stripprefix_enabled');
$request->offsetUnset('is_raw_compose_deployment_enabled');
$request->offsetUnset('docker_compose_raw');
$request->offsetUnset('tags');
}
+1 -1
View File
@@ -9,7 +9,7 @@ ARG CLOUDFLARED_VERSION=2025.7.0
# Note: We are using version 18 of the postgres client (while still using postgres 15 for the postgres server) as version 15 has been removed from Alpine 3.23+ https://pkgs.alpinelinux.org/packages?name=postgresql*-client&branch=v3.23&repo=&arch=x86_64&origin=&flagged=&maintainer=
ARG POSTGRES_VERSION=18
# https://nginx.org/en/linux_packages.html
ARG NGINX_VERSION=1.31.0-r1
ARG NGINX_VERSION=1.31.2-r1
# =================================================================
# Get MinIO client
+1 -1
View File
@@ -9,7 +9,7 @@ ARG CLOUDFLARED_VERSION=2025.7.0
# Note: We are using version 18 of the postgres client (while still using postgres 15 for the postgres server) as version 15 has been removed from Alpine 3.23+ https://pkgs.alpinelinux.org/packages?name=postgresql*-client&branch=v3.23&repo=&arch=x86_64&origin=&flagged=&maintainer=
ARG POSTGRES_VERSION=18
# https://nginx.org/en/linux_packages.html
ARG NGINX_VERSION=1.31.0-r1
ARG NGINX_VERSION=1.31.2-r1
# Add user/group
ARG USER_ID=9999
+551 -4
View File
@@ -137,6 +137,13 @@
"type": "string",
"description": "The application URLs in a comma-separated list."
},
"noindex_domains": {
"type": "array",
"items": {
"type": "string"
},
"description": "The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored."
},
"git_commit_sha": {
"type": "string",
"description": "The git commit SHA."
@@ -380,6 +387,68 @@
"nullable": true,
"description": "Use build server."
},
"use_build_secrets": {
"type": "boolean",
"default": false,
"description": "Use Docker Build Secrets for build-time environment variables."
},
"is_git_submodules_enabled": {
"type": "boolean",
"description": "Clone Git submodules."
},
"is_git_lfs_enabled": {
"type": "boolean",
"description": "Enable Git LFS."
},
"is_git_shallow_clone_enabled": {
"type": "boolean",
"description": "Use a shallow Git clone."
},
"disable_build_cache": {
"type": "boolean",
"description": "Disable the build cache."
},
"inject_build_args_to_dockerfile": {
"type": "boolean",
"description": "Inject build arguments into the Dockerfile build."
},
"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."
},
"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."
},
"is_gzip_enabled": {
"type": "boolean",
"description": "Enable gzip compression."
},
"is_stripprefix_enabled": {
"type": "boolean",
"description": "Enable path prefix stripping."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
},
"is_http_basic_auth_enabled": {
"type": "boolean",
"description": "HTTP Basic Authentication enabled."
@@ -598,6 +667,13 @@
"type": "string",
"description": "The application URLs in a comma-separated list."
},
"noindex_domains": {
"type": "array",
"items": {
"type": "string"
},
"description": "The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored."
},
"git_commit_sha": {
"type": "string",
"description": "The git commit SHA."
@@ -841,6 +917,68 @@
"nullable": true,
"description": "Use build server."
},
"use_build_secrets": {
"type": "boolean",
"default": false,
"description": "Use Docker Build Secrets for build-time environment variables."
},
"is_git_submodules_enabled": {
"type": "boolean",
"description": "Clone Git submodules."
},
"is_git_lfs_enabled": {
"type": "boolean",
"description": "Enable Git LFS."
},
"is_git_shallow_clone_enabled": {
"type": "boolean",
"description": "Use a shallow Git clone."
},
"disable_build_cache": {
"type": "boolean",
"description": "Disable the build cache."
},
"inject_build_args_to_dockerfile": {
"type": "boolean",
"description": "Inject build arguments into the Dockerfile build."
},
"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."
},
"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."
},
"is_gzip_enabled": {
"type": "boolean",
"description": "Enable gzip compression."
},
"is_stripprefix_enabled": {
"type": "boolean",
"description": "Enable path prefix stripping."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
},
"is_http_basic_auth_enabled": {
"type": "boolean",
"description": "HTTP Basic Authentication enabled."
@@ -1059,6 +1197,13 @@
"type": "string",
"description": "The application URLs in a comma-separated list."
},
"noindex_domains": {
"type": "array",
"items": {
"type": "string"
},
"description": "The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored."
},
"git_commit_sha": {
"type": "string",
"description": "The git commit SHA."
@@ -1302,6 +1447,68 @@
"nullable": true,
"description": "Use build server."
},
"use_build_secrets": {
"type": "boolean",
"default": false,
"description": "Use Docker Build Secrets for build-time environment variables."
},
"is_git_submodules_enabled": {
"type": "boolean",
"description": "Clone Git submodules."
},
"is_git_lfs_enabled": {
"type": "boolean",
"description": "Enable Git LFS."
},
"is_git_shallow_clone_enabled": {
"type": "boolean",
"description": "Use a shallow Git clone."
},
"disable_build_cache": {
"type": "boolean",
"description": "Disable the build cache."
},
"inject_build_args_to_dockerfile": {
"type": "boolean",
"description": "Inject build arguments into the Dockerfile build."
},
"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."
},
"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."
},
"is_gzip_enabled": {
"type": "boolean",
"description": "Enable gzip compression."
},
"is_stripprefix_enabled": {
"type": "boolean",
"description": "Enable path prefix stripping."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
},
"is_http_basic_auth_enabled": {
"type": "boolean",
"description": "HTTP Basic Authentication enabled."
@@ -1505,6 +1712,13 @@
"type": "string",
"description": "The application URLs in a comma-separated list."
},
"noindex_domains": {
"type": "array",
"items": {
"type": "string"
},
"description": "The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored."
},
"docker_registry_image_name": {
"type": "string",
"description": "The docker registry image name."
@@ -1668,6 +1882,68 @@
"nullable": true,
"description": "Use build server."
},
"use_build_secrets": {
"type": "boolean",
"default": false,
"description": "Use Docker Build Secrets for build-time environment variables."
},
"is_git_submodules_enabled": {
"type": "boolean",
"description": "Clone Git submodules."
},
"is_git_lfs_enabled": {
"type": "boolean",
"description": "Enable Git LFS."
},
"is_git_shallow_clone_enabled": {
"type": "boolean",
"description": "Use a shallow Git clone."
},
"disable_build_cache": {
"type": "boolean",
"description": "Disable the build cache."
},
"inject_build_args_to_dockerfile": {
"type": "boolean",
"description": "Inject build arguments into the Dockerfile build."
},
"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."
},
"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."
},
"is_gzip_enabled": {
"type": "boolean",
"description": "Enable gzip compression."
},
"is_stripprefix_enabled": {
"type": "boolean",
"description": "Enable path prefix stripping."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
},
"is_http_basic_auth_enabled": {
"type": "boolean",
"description": "HTTP Basic Authentication enabled."
@@ -1863,6 +2139,13 @@
"type": "string",
"description": "The application URLs in a comma-separated list."
},
"noindex_domains": {
"type": "array",
"items": {
"type": "string"
},
"description": "The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored."
},
"ports_mappings": {
"type": "string",
"description": "The ports mappings."
@@ -2014,6 +2297,68 @@
"nullable": true,
"description": "Use build server."
},
"use_build_secrets": {
"type": "boolean",
"default": false,
"description": "Use Docker Build Secrets for build-time environment variables."
},
"is_git_submodules_enabled": {
"type": "boolean",
"description": "Clone Git submodules."
},
"is_git_lfs_enabled": {
"type": "boolean",
"description": "Enable Git LFS."
},
"is_git_shallow_clone_enabled": {
"type": "boolean",
"description": "Use a shallow Git clone."
},
"disable_build_cache": {
"type": "boolean",
"description": "Disable the build cache."
},
"inject_build_args_to_dockerfile": {
"type": "boolean",
"description": "Inject build arguments into the Dockerfile build."
},
"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."
},
"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."
},
"is_gzip_enabled": {
"type": "boolean",
"description": "Enable gzip compression."
},
"is_stripprefix_enabled": {
"type": "boolean",
"description": "Enable path prefix stripping."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
},
"is_http_basic_auth_enabled": {
"type": "boolean",
"description": "HTTP Basic Authentication enabled."
@@ -2360,6 +2705,13 @@
"type": "string",
"description": "The application URLs in a comma-separated list."
},
"noindex_domains": {
"type": "array",
"items": {
"type": "string"
},
"description": "The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored."
},
"git_commit_sha": {
"type": "string",
"description": "The git commit SHA."
@@ -2596,6 +2948,67 @@
"nullable": true,
"description": "Use build server."
},
"use_build_secrets": {
"type": "boolean",
"description": "Use Docker Build Secrets for build-time environment variables."
},
"is_git_submodules_enabled": {
"type": "boolean",
"description": "Clone Git submodules."
},
"is_git_lfs_enabled": {
"type": "boolean",
"description": "Enable Git LFS."
},
"is_git_shallow_clone_enabled": {
"type": "boolean",
"description": "Use a shallow Git clone."
},
"disable_build_cache": {
"type": "boolean",
"description": "Disable the build cache."
},
"inject_build_args_to_dockerfile": {
"type": "boolean",
"description": "Inject build arguments into the Dockerfile build."
},
"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."
},
"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."
},
"is_gzip_enabled": {
"type": "boolean",
"description": "Enable gzip compression."
},
"is_stripprefix_enabled": {
"type": "boolean",
"description": "Enable path prefix stripping."
},
"is_raw_compose_deployment_enabled": {
"type": "boolean",
"description": "Deploy the raw Docker Compose definition."
},
"connect_to_docker_network": {
"type": "boolean",
"description": "The flag to connect the service to the predefined Docker network."
@@ -2612,10 +3025,6 @@
"is_preserve_repository_enabled": {
"type": "boolean",
"description": "Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false."
},
"include_source_commit_in_build": {
"type": "boolean",
"description": "Include source commit information in the build. Default is false."
}
},
"type": "object"
@@ -12212,6 +12621,16 @@
"null"
]
},
"noindex_domains": {
"description": "The subset of the service application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the domains are ignored.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
},
"human_name": {
"type": [
"string",
@@ -14783,6 +15202,14 @@
"nullable": true,
"description": "The application domains."
},
"noindex_domains": {
"type": "array",
"items": {
"type": "string"
},
"nullable": true,
"description": "The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header."
},
"config_hash": {
"type": "string",
"description": "Configuration hash."
@@ -15146,6 +15573,9 @@
"type": "string",
"nullable": true,
"description": "Password for HTTP Basic Authentication"
},
"": {
"$ref": "#\/components\/schemas\/ApplicationSetting"
}
},
"type": "object"
@@ -15241,6 +15671,123 @@
},
"type": "object"
},
"ApplicationSetting": {
"description": "Application settings.",
"properties": {
"is_static": {
"type": "boolean"
},
"is_git_submodules_enabled": {
"type": "boolean"
},
"is_git_lfs_enabled": {
"type": "boolean"
},
"is_auto_deploy_enabled": {
"type": "boolean"
},
"is_force_https_enabled": {
"type": "boolean"
},
"is_debug_enabled": {
"type": "boolean"
},
"is_preview_deployments_enabled": {
"type": "boolean"
},
"is_log_drain_enabled": {
"type": "boolean"
},
"is_gpu_enabled": {
"type": "boolean"
},
"gpu_driver": {
"type": "string",
"nullable": true
},
"gpu_count": {
"type": "string",
"nullable": true
},
"gpu_device_ids": {
"type": "string",
"nullable": true
},
"gpu_options": {
"type": "string",
"nullable": true
},
"is_include_timestamps": {
"type": "boolean"
},
"is_swarm_only_worker_nodes": {
"type": "boolean"
},
"is_raw_compose_deployment_enabled": {
"type": "boolean"
},
"is_build_server_enabled": {
"type": "boolean"
},
"is_consistent_container_name_enabled": {
"type": "boolean"
},
"is_gzip_enabled": {
"type": "boolean"
},
"is_stripprefix_enabled": {
"type": "boolean"
},
"connect_to_docker_network": {
"type": "boolean"
},
"custom_internal_name": {
"type": "string",
"nullable": true
},
"is_container_label_escape_enabled": {
"type": "boolean"
},
"is_env_sorting_enabled": {
"type": "boolean"
},
"is_container_label_readonly_enabled": {
"type": "boolean"
},
"is_preserve_repository_enabled": {
"type": "boolean"
},
"disable_build_cache": {
"type": "boolean"
},
"is_spa": {
"type": "boolean"
},
"is_git_shallow_clone_enabled": {
"type": "boolean"
},
"is_pr_deployments_public_enabled": {
"type": "boolean"
},
"use_build_secrets": {
"type": "boolean"
},
"inject_build_args_to_dockerfile": {
"type": "boolean"
},
"include_source_commit_in_build": {
"type": "boolean"
},
"docker_images_to_keep": {
"type": "integer"
},
"stop_grace_period": {
"type": "integer",
"nullable": true
}
},
"type": "object"
},
"Environment": {
"description": "Environment model",
"properties": {
+403 -3
View File
@@ -97,6 +97,10 @@ paths:
domains:
type: string
description: 'The application URLs in a comma-separated list.'
noindex_domains:
type: array
items: { type: string }
description: 'The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored.'
git_commit_sha:
type: string
description: 'The git commit SHA.'
@@ -268,6 +272,54 @@ paths:
type: boolean
nullable: true
description: 'Use build server.'
use_build_secrets:
type: boolean
default: false
description: 'Use Docker Build Secrets for build-time environment variables.'
is_git_submodules_enabled:
type: boolean
description: 'Clone Git submodules.'
is_git_lfs_enabled:
type: boolean
description: 'Enable Git LFS.'
is_git_shallow_clone_enabled:
type: boolean
description: 'Use a shallow Git clone.'
disable_build_cache:
type: boolean
description: 'Disable the build cache.'
inject_build_args_to_dockerfile:
type: boolean
description: 'Inject build arguments into the Dockerfile build.'
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.'
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.'
is_gzip_enabled:
type: boolean
description: 'Enable gzip compression.'
is_stripprefix_enabled:
type: boolean
description: 'Enable path prefix stripping.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
is_http_basic_auth_enabled:
type: boolean
description: 'HTTP Basic Authentication enabled.'
@@ -391,6 +443,10 @@ paths:
domains:
type: string
description: 'The application URLs in a comma-separated list.'
noindex_domains:
type: array
items: { type: string }
description: 'The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored.'
git_commit_sha:
type: string
description: 'The git commit SHA.'
@@ -562,6 +618,54 @@ paths:
type: boolean
nullable: true
description: 'Use build server.'
use_build_secrets:
type: boolean
default: false
description: 'Use Docker Build Secrets for build-time environment variables.'
is_git_submodules_enabled:
type: boolean
description: 'Clone Git submodules.'
is_git_lfs_enabled:
type: boolean
description: 'Enable Git LFS.'
is_git_shallow_clone_enabled:
type: boolean
description: 'Use a shallow Git clone.'
disable_build_cache:
type: boolean
description: 'Disable the build cache.'
inject_build_args_to_dockerfile:
type: boolean
description: 'Inject build arguments into the Dockerfile build.'
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.'
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.'
is_gzip_enabled:
type: boolean
description: 'Enable gzip compression.'
is_stripprefix_enabled:
type: boolean
description: 'Enable path prefix stripping.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
is_http_basic_auth_enabled:
type: boolean
description: 'HTTP Basic Authentication enabled.'
@@ -685,6 +789,10 @@ paths:
domains:
type: string
description: 'The application URLs in a comma-separated list.'
noindex_domains:
type: array
items: { type: string }
description: 'The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored.'
git_commit_sha:
type: string
description: 'The git commit SHA.'
@@ -856,6 +964,54 @@ paths:
type: boolean
nullable: true
description: 'Use build server.'
use_build_secrets:
type: boolean
default: false
description: 'Use Docker Build Secrets for build-time environment variables.'
is_git_submodules_enabled:
type: boolean
description: 'Clone Git submodules.'
is_git_lfs_enabled:
type: boolean
description: 'Enable Git LFS.'
is_git_shallow_clone_enabled:
type: boolean
description: 'Use a shallow Git clone.'
disable_build_cache:
type: boolean
description: 'Disable the build cache.'
inject_build_args_to_dockerfile:
type: boolean
description: 'Inject build arguments into the Dockerfile build.'
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.'
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.'
is_gzip_enabled:
type: boolean
description: 'Enable gzip compression.'
is_stripprefix_enabled:
type: boolean
description: 'Enable path prefix stripping.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
is_http_basic_auth_enabled:
type: boolean
description: 'HTTP Basic Authentication enabled.'
@@ -970,6 +1126,10 @@ paths:
domains:
type: string
description: 'The application URLs in a comma-separated list.'
noindex_domains:
type: array
items: { type: string }
description: 'The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored.'
docker_registry_image_name:
type: string
description: 'The docker registry image name.'
@@ -1091,6 +1251,54 @@ paths:
type: boolean
nullable: true
description: 'Use build server.'
use_build_secrets:
type: boolean
default: false
description: 'Use Docker Build Secrets for build-time environment variables.'
is_git_submodules_enabled:
type: boolean
description: 'Clone Git submodules.'
is_git_lfs_enabled:
type: boolean
description: 'Enable Git LFS.'
is_git_shallow_clone_enabled:
type: boolean
description: 'Use a shallow Git clone.'
disable_build_cache:
type: boolean
description: 'Disable the build cache.'
inject_build_args_to_dockerfile:
type: boolean
description: 'Inject build arguments into the Dockerfile build.'
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.'
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.'
is_gzip_enabled:
type: boolean
description: 'Enable gzip compression.'
is_stripprefix_enabled:
type: boolean
description: 'Enable path prefix stripping.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
is_http_basic_auth_enabled:
type: boolean
description: 'HTTP Basic Authentication enabled.'
@@ -1200,6 +1408,10 @@ paths:
domains:
type: string
description: 'The application URLs in a comma-separated list.'
noindex_domains:
type: array
items: { type: string }
description: 'The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored.'
ports_mappings:
type: string
description: 'The ports mappings.'
@@ -1312,6 +1524,54 @@ paths:
type: boolean
nullable: true
description: 'Use build server.'
use_build_secrets:
type: boolean
default: false
description: 'Use Docker Build Secrets for build-time environment variables.'
is_git_submodules_enabled:
type: boolean
description: 'Clone Git submodules.'
is_git_lfs_enabled:
type: boolean
description: 'Enable Git LFS.'
is_git_shallow_clone_enabled:
type: boolean
description: 'Use a shallow Git clone.'
disable_build_cache:
type: boolean
description: 'Disable the build cache.'
inject_build_args_to_dockerfile:
type: boolean
description: 'Inject build arguments into the Dockerfile build.'
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.'
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.'
is_gzip_enabled:
type: boolean
description: 'Enable gzip compression.'
is_stripprefix_enabled:
type: boolean
description: 'Enable path prefix stripping.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
is_http_basic_auth_enabled:
type: boolean
description: 'HTTP Basic Authentication enabled.'
@@ -1521,6 +1781,10 @@ paths:
domains:
type: string
description: 'The application URLs in a comma-separated list.'
noindex_domains:
type: array
items: { type: string }
description: 'The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored.'
git_commit_sha:
type: string
description: 'The git commit SHA.'
@@ -1688,6 +1952,53 @@ paths:
type: boolean
nullable: true
description: 'Use build server.'
use_build_secrets:
type: boolean
description: 'Use Docker Build Secrets for build-time environment variables.'
is_git_submodules_enabled:
type: boolean
description: 'Clone Git submodules.'
is_git_lfs_enabled:
type: boolean
description: 'Enable Git LFS.'
is_git_shallow_clone_enabled:
type: boolean
description: 'Use a shallow Git clone.'
disable_build_cache:
type: boolean
description: 'Disable the build cache.'
inject_build_args_to_dockerfile:
type: boolean
description: 'Inject build arguments into the Dockerfile build.'
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.'
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.'
is_gzip_enabled:
type: boolean
description: 'Enable gzip compression.'
is_stripprefix_enabled:
type: boolean
description: 'Enable path prefix stripping.'
is_raw_compose_deployment_enabled:
type: boolean
description: 'Deploy the raw Docker Compose definition.'
connect_to_docker_network:
type: boolean
description: 'The flag to connect the service to the predefined Docker network.'
@@ -1701,9 +2012,6 @@ paths:
is_preserve_repository_enabled:
type: boolean
description: 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'
include_source_commit_in_build:
type: boolean
description: 'Include source commit information in the build. Default is false.'
type: object
responses:
'200':
@@ -7805,6 +8113,10 @@ paths:
url:
description: 'Comma-separated list of URLs (e.g. "http://app.example.com:8080,https://app2.example.com"). Stored as fqdn.'
type: [string, 'null']
noindex_domains:
description: 'The subset of the service application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the domains are ignored.'
type: [array, 'null']
items: { type: string }
human_name:
type: [string, 'null']
description:
@@ -9399,6 +9711,12 @@ components:
type: string
nullable: true
description: 'The application domains.'
noindex_domains:
type: array
items:
type: string
nullable: true
description: 'The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header.'
config_hash:
type: string
description: 'Configuration hash.'
@@ -9683,6 +10001,8 @@ components:
type: string
nullable: true
description: 'Password for HTTP Basic Authentication'
'':
$ref: '#/components/schemas/ApplicationSetting'
type: object
ApplicationDeploymentQueue:
description: 'Project model'
@@ -9746,6 +10066,86 @@ components:
commit_message:
type: string
type: object
ApplicationSetting:
description: 'Application settings.'
properties:
is_static:
type: boolean
is_git_submodules_enabled:
type: boolean
is_git_lfs_enabled:
type: boolean
is_auto_deploy_enabled:
type: boolean
is_force_https_enabled:
type: boolean
is_debug_enabled:
type: boolean
is_preview_deployments_enabled:
type: boolean
is_log_drain_enabled:
type: boolean
is_gpu_enabled:
type: boolean
gpu_driver:
type: string
nullable: true
gpu_count:
type: string
nullable: true
gpu_device_ids:
type: string
nullable: true
gpu_options:
type: string
nullable: true
is_include_timestamps:
type: boolean
is_swarm_only_worker_nodes:
type: boolean
is_raw_compose_deployment_enabled:
type: boolean
is_build_server_enabled:
type: boolean
is_consistent_container_name_enabled:
type: boolean
is_gzip_enabled:
type: boolean
is_stripprefix_enabled:
type: boolean
connect_to_docker_network:
type: boolean
custom_internal_name:
type: string
nullable: true
is_container_label_escape_enabled:
type: boolean
is_env_sorting_enabled:
type: boolean
is_container_label_readonly_enabled:
type: boolean
is_preserve_repository_enabled:
type: boolean
disable_build_cache:
type: boolean
is_spa:
type: boolean
is_git_shallow_clone_enabled:
type: boolean
is_pr_deployments_public_enabled:
type: boolean
use_build_secrets:
type: boolean
inject_build_args_to_dockerfile:
type: boolean
include_source_commit_in_build:
type: boolean
docker_images_to_keep:
type: integer
stop_grace_period:
type: integer
nullable: true
type: object
Environment:
description: 'Environment model'
properties:
+3 -3
View File
@@ -7,7 +7,7 @@
services:
frontend:
image: ghcr.io/smaug6739/alexandrie-frontend:v8.7.2
image: ghcr.io/smaug6739/alexandrie-frontend:v8.10.0
environment:
- SERVICE_URL_FRONTEND_8200
- PORT=8200
@@ -21,7 +21,7 @@ services:
- backend
backend:
image: ghcr.io/smaug6739/alexandrie-backend:v8.7.2
image: ghcr.io/smaug6739/alexandrie-backend:v8.10.0
environment:
- SERVICE_URL_BACKEND_8201
- BACKEND_PORT=8201
@@ -74,7 +74,7 @@ services:
retries: 5
rustfs:
image: rustfs/rustfs:1.0.0-alpha.90
image: rustfs/rustfs:1.0.0-beta.8
environment:
- SERVICE_URL_RUSTFS_9000
- RUSTFS_ACCESS_KEY=${SERVICE_USER_RUSTFS}
+35 -17
View File
@@ -7,7 +7,8 @@
services:
espocrm:
image: espocrm/espocrm:9
image: espocrm/espocrm:10
container_name: espocrm
environment:
- SERVICE_URL_ESPOCRM
- ESPOCRM_ADMIN_USERNAME=${ESPOCRM_ADMIN_USERNAME:-admin}
@@ -19,30 +20,39 @@ services:
- ESPOCRM_DATABASE_PASSWORD=${SERVICE_PASSWORD_MARIADB}
- ESPOCRM_SITE_URL=${SERVICE_URL_ESPOCRM}
volumes:
- espocrm:/var/www/html
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:80"]
interval: 2s
start_period: 60s
timeout: 10s
retries: 15
- espocrm-data:/var/www/html/data
- espocrm-custom:/var/www/html/custom
- espocrm-custom-client:/var/www/html/client/custom
restart: unless-stopped
depends_on:
espocrm-db:
condition: service_healthy
healthcheck:
test: ["CMD", "bin/command", "app-check"]
start_period: 20s
interval: 60s
timeout: 20s
retries: 3
espocrm-daemon:
image: espocrm/espocrm:9
image: espocrm/espocrm:10
container_name: espocrm-daemon
volumes:
- espocrm:/var/www/html
restart: always
volumes_from:
- espocrm
restart: unless-stopped
entrypoint: docker-daemon.sh
depends_on:
espocrm:
condition: service_healthy
healthcheck:
test: ["CMD", "bin/command", "app-check"]
start_period: 20s
interval: 180s
timeout: 20s
retries: 3
espocrm-websocket:
image: espocrm/espocrm:9
image: espocrm/espocrm:10
container_name: espocrm-websocket
environment:
- SERVICE_URL_ESPOCRM_WEBSOCKET_8080
@@ -50,16 +60,23 @@ services:
- ESPOCRM_CONFIG_WEB_SOCKET_URL=$SERVICE_URL_ESPOCRM_WEBSOCKET
- ESPOCRM_CONFIG_WEB_SOCKET_ZERO_M_Q_SUBSCRIBER_DSN=tcp://*:7777
- ESPOCRM_CONFIG_WEB_SOCKET_ZERO_M_Q_SUBMISSION_DSN=tcp://espocrm-websocket:7777
volumes:
- espocrm:/var/www/html
restart: always
volumes_from:
- espocrm
restart: unless-stopped
entrypoint: docker-websocket.sh
depends_on:
espocrm:
condition: service_healthy
healthcheck:
test: ["CMD", "bin/command", "app-check"]
start_period: 20s
interval: 180s
timeout: 20s
retries: 3
espocrm-db:
image: mariadb:11.8
image: mariadb:12.3
container_name: espocrm-db
environment:
- MARIADB_DATABASE=${MARIADB_DATABASE:-espocrm}
- MARIADB_USER=${SERVICE_USER_MARIADB}
@@ -67,6 +84,7 @@ services:
- MARIADB_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT}
volumes:
- espocrm-db:/var/lib/mysql
restart: unless-stopped
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 20s
+4 -4
View File
@@ -53,7 +53,7 @@
"alexandrie": {
"documentation": "https://github.com/Smaug6739/Alexandrie/tree/main/docs?utm_source=coolify.io",
"slogan": "A powerful Markdown workspace designed for speed, clarity, and creativity.",
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ2doY3IuaW8vc21hdWc2NzM5L2FsZXhhbmRyaWUtZnJvbnRlbmQ6djguNy4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfRlJPTlRFTkRfODIwMAogICAgICAtIFBPUlQ9ODIwMAogICAgICAtICdOVVhUX1BVQkxJQ19DT05GSUdfRElTQUJMRV9TSUdOVVBfUEFHRT0ke0NPTkZJR19ESVNBQkxFX1NJR05VUDotZmFsc2V9JwogICAgICAtICdOVVhUX1BVQkxJQ19DT05GSUdfRElTQUJMRV9MQU5ESU5HX1BBR0U9JHtDT05GSUdfRElTQUJMRV9MQU5ESU5HOi1mYWxzZX0nCiAgICAgIC0gJ05VWFRfUFVCTElDX0JBU0VfQVBJPSR7U0VSVklDRV9VUkxfQkFDS0VORH0nCiAgICAgIC0gJ05VWFRfUFVCTElDX0JBU0VfQ0ROPSR7U0VSVklDRV9VUkxfUlVTVEZTfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQ0ROX0VORFBPSU5UPSR7Q0ROX0VORFBPSU5UOi0vYWxleGFuZHJpZS99JwogICAgICAtICdOVVhUX1BVQkxJQ19CQVNFX1VSTD0ke1NFUlZJQ0VfVVJMX0ZST05URU5EfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYmFja2VuZAogIGJhY2tlbmQ6CiAgICBpbWFnZTogJ2doY3IuaW8vc21hdWc2NzM5L2FsZXhhbmRyaWUtYmFja2VuZDp2OC43LjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9CQUNLRU5EXzgyMDEKICAgICAgLSBCQUNLRU5EX1BPUlQ9ODIwMQogICAgICAtIEdJTl9NT0RFPXJlbGVhc2UKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnQ09PS0lFX0RPTUFJTj0ke1NFUlZJQ0VfVVJMX0ZST05URU5EfScKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9VUkxfRlJPTlRFTkR9JwogICAgICAtICdBTExPV19VTlNFQ1VSRT0ke0FMTE9XX1VOU0VDVVJFOi1mYWxzZX0nCiAgICAgIC0gREFUQUJBU0VfSE9TVD1teXNxbAogICAgICAtIERBVEFCQVNFX1BPUlQ9MzMwNgogICAgICAtICdEQVRBQkFTRV9OQU1FPSR7TVlTUUxfREFUQUJBU0U6LWFsZXhhbmRyaWUtZGJ9JwogICAgICAtICdEQVRBQkFTRV9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnREFUQUJBU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTUlOSU9fRU5EUE9JTlQ9cnVzdGZzOjkwMDAnCiAgICAgIC0gJ01JTklPX1BVQkxJQ19VUkw9JHtTRVJWSUNFX1VSTF9SVVNURlN9JwogICAgICAtICdNSU5JT19TRUNVUkU9JHtNSU5JT19TRUNVUkU6LWZhbHNlfScKICAgICAgLSAnTUlOSU9fQUNDRVNTS0VZPSR7U0VSVklDRV9VU0VSX1JVU1RGU30nCiAgICAgIC0gJ01JTklPX1NFQ1JFVEtFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfUlVTVEZTfScKICAgICAgLSAnTUlOSU9fQlVDS0VUPSR7TUlOSU9fQlVDS0VUOi1hbGV4YW5kcmllfScKICAgICAgLSAnU01UUF9IT1NUPSR7U01UUF9IT1NUOi19JwogICAgICAtICdTTVRQX01BSUw9JHtTTVRQX01BSUw6LX0nCiAgICAgIC0gJ1NNVFBfUEFTU1dPUkQ9JHtTTVRQX1BBU1NXT1JEOi19JwogICAgZGVwZW5kc19vbjoKICAgICAgbXlzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnVzdGZzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOjguMCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTFJPT1R9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnTVlTUUxfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtNWVNRTF9EQVRBQkFTRTotYWxleGFuZHJpZS1kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdteXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIGxvY2FsaG9zdAogICAgICAgIC0gJy11JwogICAgICAgIC0gcm9vdAogICAgICAgIC0gJy1wJHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgcmV0cmllczogNQogIHJ1c3RmczoKICAgIGltYWdlOiAncnVzdGZzL3J1c3RmczoxLjAuMC1hbHBoYS45MCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1JVU1RGU185MDAwCiAgICAgIC0gJ1JVU1RGU19BQ0NFU1NfS0VZPSR7U0VSVklDRV9VU0VSX1JVU1RGU30nCiAgICAgIC0gJ1JVU1RGU19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdSVVNURlNfQ09OU09MRV9FTkFCTEU9JHtSVVNURlNfQ09OU09MRV9FTkFCTEU6LWZhbHNlfScKICAgICAgLSAnUlVTVEZTX0xPR19MRVZFTD0ke1JVU1RGU19MT0dfTEVWRUw6LWluZm99JwogICAgdm9sdW1lczoKICAgICAgLSAncnVzdGZzLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3J1c3Rmcy1sb2dzOi9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiBsb2NhbGhvc3QgOTAwMCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ2doY3IuaW8vc21hdWc2NzM5L2FsZXhhbmRyaWUtZnJvbnRlbmQ6djguMTAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0ZST05URU5EXzgyMDAKICAgICAgLSBQT1JUPTgyMDAKICAgICAgLSAnTlVYVF9QVUJMSUNfQ09ORklHX0RJU0FCTEVfU0lHTlVQX1BBR0U9JHtDT05GSUdfRElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQ09ORklHX0RJU0FCTEVfTEFORElOR19QQUdFPSR7Q09ORklHX0RJU0FCTEVfTEFORElORzotZmFsc2V9JwogICAgICAtICdOVVhUX1BVQkxJQ19CQVNFX0FQST0ke1NFUlZJQ0VfVVJMX0JBQ0tFTkR9JwogICAgICAtICdOVVhUX1BVQkxJQ19CQVNFX0NETj0ke1NFUlZJQ0VfVVJMX1JVU1RGU30nCiAgICAgIC0gJ05VWFRfUFVCTElDX0NETl9FTkRQT0lOVD0ke0NETl9FTkRQT0lOVDotL2FsZXhhbmRyaWUvfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQkFTRV9VUkw9JHtTRVJWSUNFX1VSTF9GUk9OVEVORH0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGJhY2tlbmQKICBiYWNrZW5kOgogICAgaW1hZ2U6ICdnaGNyLmlvL3NtYXVnNjczOS9hbGV4YW5kcmllLWJhY2tlbmQ6djguMTAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0JBQ0tFTkRfODIwMQogICAgICAtIEJBQ0tFTkRfUE9SVD04MjAxCiAgICAgIC0gR0lOX01PREU9cmVsZWFzZQogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdDT09LSUVfRE9NQUlOPSR7U0VSVklDRV9VUkxfRlJPTlRFTkR9JwogICAgICAtICdGUk9OVEVORF9VUkw9JHtTRVJWSUNFX1VSTF9GUk9OVEVORH0nCiAgICAgIC0gJ0FMTE9XX1VOU0VDVVJFPSR7QUxMT1dfVU5TRUNVUkU6LWZhbHNlfScKICAgICAgLSBEQVRBQkFTRV9IT1NUPW15c3FsCiAgICAgIC0gREFUQUJBU0VfUE9SVD0zMzA2CiAgICAgIC0gJ0RBVEFCQVNFX05BTUU9JHtNWVNRTF9EQVRBQkFTRTotYWxleGFuZHJpZS1kYn0nCiAgICAgIC0gJ0RBVEFCQVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdEQVRBQkFTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNSU5JT19FTkRQT0lOVD1ydXN0ZnM6OTAwMCcKICAgICAgLSAnTUlOSU9fUFVCTElDX1VSTD0ke1NFUlZJQ0VfVVJMX1JVU1RGU30nCiAgICAgIC0gJ01JTklPX1NFQ1VSRT0ke01JTklPX1NFQ1VSRTotZmFsc2V9JwogICAgICAtICdNSU5JT19BQ0NFU1NLRVk9JHtTRVJWSUNFX1VTRVJfUlVTVEZTfScKICAgICAgLSAnTUlOSU9fU0VDUkVUS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdNSU5JT19CVUNLRVQ9JHtNSU5JT19CVUNLRVQ6LWFsZXhhbmRyaWV9JwogICAgICAtICdTTVRQX0hPU1Q9JHtTTVRQX0hPU1Q6LX0nCiAgICAgIC0gJ1NNVFBfTUFJTD0ke1NNVFBfTUFJTDotfScKICAgICAgLSAnU01UUF9QQVNTV09SRD0ke1NNVFBfUEFTU1dPUkQ6LX0nCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBydXN0ZnM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBteXNxbDoKICAgIGltYWdlOiAnbXlzcWw6OC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1hbGV4YW5kcmllLWRifScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ215c3FsLWRhdGE6L3Zhci9saWIvbXlzcWwnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbXlzcWxhZG1pbgogICAgICAgIC0gcGluZwogICAgICAgIC0gJy1oJwogICAgICAgIC0gbG9jYWxob3N0CiAgICAgICAgLSAnLXUnCiAgICAgICAgLSByb290CiAgICAgICAgLSAnLXAke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICByZXRyaWVzOiA1CiAgcnVzdGZzOgogICAgaW1hZ2U6ICdydXN0ZnMvcnVzdGZzOjEuMC4wLWJldGEuOCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1JVU1RGU185MDAwCiAgICAgIC0gJ1JVU1RGU19BQ0NFU1NfS0VZPSR7U0VSVklDRV9VU0VSX1JVU1RGU30nCiAgICAgIC0gJ1JVU1RGU19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdSVVNURlNfQ09OU09MRV9FTkFCTEU9JHtSVVNURlNfQ09OU09MRV9FTkFCTEU6LWZhbHNlfScKICAgICAgLSAnUlVTVEZTX0xPR19MRVZFTD0ke1JVU1RGU19MT0dfTEVWRUw6LWluZm99JwogICAgdm9sdW1lczoKICAgICAgLSAncnVzdGZzLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3J1c3Rmcy1sb2dzOi9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiBsb2NhbGhvc3QgOTAwMCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
"tags": [
"note-taking",
"markdown",
@@ -64,7 +64,7 @@
"category": "productivity",
"logo": "svgs/alexandrie.svg",
"minversion": "0.0.0",
"template_last_updated_at": "2026-04-05T13:36:24+02:00",
"template_last_updated_at": "2026-07-07T13:24:35+02:00",
"port": "8200"
},
"anythingllm": {
@@ -1310,7 +1310,7 @@
"espocrm": {
"documentation": "https://docs.espocrm.com?utm_source=coolify.io",
"slogan": "EspoCRM is a free and open-source CRM platform.",
"compose": "c2VydmljZXM6CiAgZXNwb2NybToKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjknCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9FU1BPQ1JNCiAgICAgIC0gJ0VTUE9DUk1fQURNSU5fVVNFUk5BTUU9JHtFU1BPQ1JNX0FETUlOX1VTRVJOQU1FOi1hZG1pbn0nCiAgICAgIC0gJ0VTUE9DUk1fQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSBFU1BPQ1JNX0RBVEFCQVNFX1BMQVRGT1JNPU15c3FsCiAgICAgIC0gRVNQT0NSTV9EQVRBQkFTRV9IT1NUPWVzcG9jcm0tZGIKICAgICAgLSAnRVNQT0NSTV9EQVRBQkFTRV9OQU1FPSR7TUFSSUFEQl9EQVRBQkFTRTotZXNwb2NybX0nCiAgICAgIC0gJ0VTUE9DUk1fREFUQUJBU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NQVJJQURCfScKICAgICAgLSAnRVNQT0NSTV9EQVRBQkFTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQn0nCiAgICAgIC0gJ0VTUE9DUk1fU0lURV9VUkw9JHtTRVJWSUNFX1VSTF9FU1BPQ1JNfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VzcG9jcm06L3Zhci93d3cvaHRtbCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHN0YXJ0X3BlcmlvZDogNjBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgZGVwZW5kc19vbjoKICAgICAgZXNwb2NybS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIGVzcG9jcm0tZGFlbW9uOgogICAgaW1hZ2U6ICdlc3BvY3JtL2VzcG9jcm06OScKICAgIGNvbnRhaW5lcl9uYW1lOiBlc3BvY3JtLWRhZW1vbgogICAgdm9sdW1lczoKICAgICAgLSAnZXNwb2NybTovdmFyL3d3dy9odG1sJwogICAgcmVzdGFydDogYWx3YXlzCiAgICBlbnRyeXBvaW50OiBkb2NrZXItZGFlbW9uLnNoCiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgZXNwb2NybS13ZWJzb2NrZXQ6CiAgICBpbWFnZTogJ2VzcG9jcm0vZXNwb2NybTo5JwogICAgY29udGFpbmVyX25hbWU6IGVzcG9jcm0td2Vic29ja2V0CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9FU1BPQ1JNX1dFQlNPQ0tFVF84MDgwCiAgICAgIC0gRVNQT0NSTV9DT05GSUdfVVNFX1dFQl9TT0NLRVQ9dHJ1ZQogICAgICAtIEVTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfVVJMPSRTRVJWSUNFX1VSTF9FU1BPQ1JNX1dFQlNPQ0tFVAogICAgICAtICdFU1BPQ1JNX0NPTkZJR19XRUJfU09DS0VUX1pFUk9fTV9RX1NVQlNDUklCRVJfRFNOPXRjcDovLyo6Nzc3NycKICAgICAgLSAnRVNQT0NSTV9DT05GSUdfV0VCX1NPQ0tFVF9aRVJPX01fUV9TVUJNSVNTSU9OX0RTTj10Y3A6Ly9lc3BvY3JtLXdlYnNvY2tldDo3Nzc3JwogICAgdm9sdW1lczoKICAgICAgLSAnZXNwb2NybTovdmFyL3d3dy9odG1sJwogICAgcmVzdGFydDogYWx3YXlzCiAgICBlbnRyeXBvaW50OiBkb2NrZXItd2Vic29ja2V0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgZXNwb2NybS1kYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01BUklBREJfREFUQUJBU0U9JHtNQVJJQURCX0RBVEFCQVNFOi1lc3BvY3JtfScKICAgICAgLSAnTUFSSUFEQl9VU0VSPSR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICAtICdNQVJJQURCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCfScKICAgICAgLSAnTUFSSUFEQl9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9ST09UfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VzcG9jcm0tZGI6L3Zhci9saWIvbXlzcWwnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaGVhbHRoY2hlY2suc2gKICAgICAgICAtICctLWNvbm5lY3QnCiAgICAgICAgLSAnLS1pbm5vZGJfaW5pdGlhbGl6ZWQnCiAgICAgIGludGVydmFsOiAyMHMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMK",
"compose": "c2VydmljZXM6CiAgZXNwb2NybToKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjEwJwogICAgY29udGFpbmVyX25hbWU6IGVzcG9jcm0KICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0VTUE9DUk0KICAgICAgLSAnRVNQT0NSTV9BRE1JTl9VU0VSTkFNRT0ke0VTUE9DUk1fQURNSU5fVVNFUk5BTUU6LWFkbWlufScKICAgICAgLSAnRVNQT0NSTV9BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgICAtIEVTUE9DUk1fREFUQUJBU0VfUExBVEZPUk09TXlzcWwKICAgICAgLSBFU1BPQ1JNX0RBVEFCQVNFX0hPU1Q9ZXNwb2NybS1kYgogICAgICAtICdFU1BPQ1JNX0RBVEFCQVNFX05BTUU9JHtNQVJJQURCX0RBVEFCQVNFOi1lc3BvY3JtfScKICAgICAgLSAnRVNQT0NSTV9EQVRBQkFTRV9VU0VSPSR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICAtICdFU1BPQ1JNX0RBVEFCQVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCfScKICAgICAgLSAnRVNQT0NSTV9TSVRFX1VSTD0ke1NFUlZJQ0VfVVJMX0VTUE9DUk19JwogICAgdm9sdW1lczoKICAgICAgLSAnZXNwb2NybS1kYXRhOi92YXIvd3d3L2h0bWwvZGF0YScKICAgICAgLSAnZXNwb2NybS1jdXN0b206L3Zhci93d3cvaHRtbC9jdXN0b20nCiAgICAgIC0gJ2VzcG9jcm0tY3VzdG9tLWNsaWVudDovdmFyL3d3dy9odG1sL2NsaWVudC9jdXN0b20nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgZGVwZW5kc19vbjoKICAgICAgZXNwb2NybS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGJpbi9jb21tYW5kCiAgICAgICAgLSBhcHAtY2hlY2sKICAgICAgc3RhcnRfcGVyaW9kOiAyMHMKICAgICAgaW50ZXJ2YWw6IDYwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwogIGVzcG9jcm0tZGFlbW9uOgogICAgaW1hZ2U6ICdlc3BvY3JtL2VzcG9jcm06MTAnCiAgICBjb250YWluZXJfbmFtZTogZXNwb2NybS1kYWVtb24KICAgIHZvbHVtZXNfZnJvbToKICAgICAgLSBlc3BvY3JtCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgZW50cnlwb2ludDogZG9ja2VyLWRhZW1vbi5zaAogICAgZGVwZW5kc19vbjoKICAgICAgZXNwb2NybToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGJpbi9jb21tYW5kCiAgICAgICAgLSBhcHAtY2hlY2sKICAgICAgc3RhcnRfcGVyaW9kOiAyMHMKICAgICAgaW50ZXJ2YWw6IDE4MHMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDMKICBlc3BvY3JtLXdlYnNvY2tldDoKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjEwJwogICAgY29udGFpbmVyX25hbWU6IGVzcG9jcm0td2Vic29ja2V0CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9FU1BPQ1JNX1dFQlNPQ0tFVF84MDgwCiAgICAgIC0gRVNQT0NSTV9DT05GSUdfVVNFX1dFQl9TT0NLRVQ9dHJ1ZQogICAgICAtIEVTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfVVJMPSRTRVJWSUNFX1VSTF9FU1BPQ1JNX1dFQlNPQ0tFVAogICAgICAtICdFU1BPQ1JNX0NPTkZJR19XRUJfU09DS0VUX1pFUk9fTV9RX1NVQlNDUklCRVJfRFNOPXRjcDovLyo6Nzc3NycKICAgICAgLSAnRVNQT0NSTV9DT05GSUdfV0VCX1NPQ0tFVF9aRVJPX01fUV9TVUJNSVNTSU9OX0RTTj10Y3A6Ly9lc3BvY3JtLXdlYnNvY2tldDo3Nzc3JwogICAgdm9sdW1lc19mcm9tOgogICAgICAtIGVzcG9jcm0KICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnRyeXBvaW50OiBkb2NrZXItd2Vic29ja2V0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gYmluL2NvbW1hbmQKICAgICAgICAtIGFwcC1jaGVjawogICAgICBzdGFydF9wZXJpb2Q6IDIwcwogICAgICBpbnRlcnZhbDogMTgwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwogIGVzcG9jcm0tZGI6CiAgICBpbWFnZTogJ21hcmlhZGI6MTIuMycKICAgIGNvbnRhaW5lcl9uYW1lOiBlc3BvY3JtLWRiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUFSSUFEQl9EQVRBQkFTRT0ke01BUklBREJfREFUQUJBU0U6LWVzcG9jcm19JwogICAgICAtICdNQVJJQURCX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUFSSUFEQn0nCiAgICAgIC0gJ01BUklBREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01BUklBREJ9JwogICAgICAtICdNQVJJQURCX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JPT1R9JwogICAgdm9sdW1lczoKICAgICAgLSAnZXNwb2NybS1kYjovdmFyL2xpYi9teXNxbCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaGVhbHRoY2hlY2suc2gKICAgICAgICAtICctLWNvbm5lY3QnCiAgICAgICAgLSAnLS1pbm5vZGJfaW5pdGlhbGl6ZWQnCiAgICAgIGludGVydmFsOiAyMHMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMK",
"tags": [
"crm",
"self-hosted",
@@ -1322,7 +1322,7 @@
"category": "productivity",
"logo": "svgs/espocrm.svg",
"minversion": "0.0.0",
"template_last_updated_at": "2026-04-06T11:35:16-05:00",
"template_last_updated_at": "2026-07-03T15:15:43+03:00",
"port": "80"
},
"evolution-api": {
+4 -4
View File
@@ -53,7 +53,7 @@
"alexandrie": {
"documentation": "https://github.com/Smaug6739/Alexandrie/tree/main/docs?utm_source=coolify.io",
"slogan": "A powerful Markdown workspace designed for speed, clarity, and creativity.",
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ2doY3IuaW8vc21hdWc2NzM5L2FsZXhhbmRyaWUtZnJvbnRlbmQ6djguNy4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZST05URU5EXzgyMDAKICAgICAgLSBQT1JUPTgyMDAKICAgICAgLSAnTlVYVF9QVUJMSUNfQ09ORklHX0RJU0FCTEVfU0lHTlVQX1BBR0U9JHtDT05GSUdfRElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQ09ORklHX0RJU0FCTEVfTEFORElOR19QQUdFPSR7Q09ORklHX0RJU0FCTEVfTEFORElORzotZmFsc2V9JwogICAgICAtICdOVVhUX1BVQkxJQ19CQVNFX0FQST0ke1NFUlZJQ0VfRlFETl9CQUNLRU5EfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQkFTRV9DRE49JHtTRVJWSUNFX0ZRRE5fUlVTVEZTfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQ0ROX0VORFBPSU5UPSR7Q0ROX0VORFBPSU5UOi0vYWxleGFuZHJpZS99JwogICAgICAtICdOVVhUX1BVQkxJQ19CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9GUk9OVEVORH0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGJhY2tlbmQKICBiYWNrZW5kOgogICAgaW1hZ2U6ICdnaGNyLmlvL3NtYXVnNjczOS9hbGV4YW5kcmllLWJhY2tlbmQ6djguNy4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0JBQ0tFTkRfODIwMQogICAgICAtIEJBQ0tFTkRfUE9SVD04MjAxCiAgICAgIC0gR0lOX01PREU9cmVsZWFzZQogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdDT09LSUVfRE9NQUlOPSR7U0VSVklDRV9GUUROX0ZST05URU5EfScKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0ZST05URU5EfScKICAgICAgLSAnQUxMT1dfVU5TRUNVUkU9JHtBTExPV19VTlNFQ1VSRTotZmFsc2V9JwogICAgICAtIERBVEFCQVNFX0hPU1Q9bXlzcWwKICAgICAgLSBEQVRBQkFTRV9QT1JUPTMzMDYKICAgICAgLSAnREFUQUJBU0VfTkFNRT0ke01ZU1FMX0RBVEFCQVNFOi1hbGV4YW5kcmllLWRifScKICAgICAgLSAnREFUQUJBU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NWVNRTH0nCiAgICAgIC0gJ0RBVEFCQVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTH0nCiAgICAgIC0gJ01JTklPX0VORFBPSU5UPXJ1c3Rmczo5MDAwJwogICAgICAtICdNSU5JT19QVUJMSUNfVVJMPSR7U0VSVklDRV9GUUROX1JVU1RGU30nCiAgICAgIC0gJ01JTklPX1NFQ1VSRT0ke01JTklPX1NFQ1VSRTotZmFsc2V9JwogICAgICAtICdNSU5JT19BQ0NFU1NLRVk9JHtTRVJWSUNFX1VTRVJfUlVTVEZTfScKICAgICAgLSAnTUlOSU9fU0VDUkVUS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdNSU5JT19CVUNLRVQ9JHtNSU5JT19CVUNLRVQ6LWFsZXhhbmRyaWV9JwogICAgICAtICdTTVRQX0hPU1Q9JHtTTVRQX0hPU1Q6LX0nCiAgICAgIC0gJ1NNVFBfTUFJTD0ke1NNVFBfTUFJTDotfScKICAgICAgLSAnU01UUF9QQVNTV09SRD0ke1NNVFBfUEFTU1dPUkQ6LX0nCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBydXN0ZnM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBteXNxbDoKICAgIGltYWdlOiAnbXlzcWw6OC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1hbGV4YW5kcmllLWRifScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ215c3FsLWRhdGE6L3Zhci9saWIvbXlzcWwnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbXlzcWxhZG1pbgogICAgICAgIC0gcGluZwogICAgICAgIC0gJy1oJwogICAgICAgIC0gbG9jYWxob3N0CiAgICAgICAgLSAnLXUnCiAgICAgICAgLSByb290CiAgICAgICAgLSAnLXAke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICByZXRyaWVzOiA1CiAgcnVzdGZzOgogICAgaW1hZ2U6ICdydXN0ZnMvcnVzdGZzOjEuMC4wLWFscGhhLjkwJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1JVU1RGU185MDAwCiAgICAgIC0gJ1JVU1RGU19BQ0NFU1NfS0VZPSR7U0VSVklDRV9VU0VSX1JVU1RGU30nCiAgICAgIC0gJ1JVU1RGU19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdSVVNURlNfQ09OU09MRV9FTkFCTEU9JHtSVVNURlNfQ09OU09MRV9FTkFCTEU6LWZhbHNlfScKICAgICAgLSAnUlVTVEZTX0xPR19MRVZFTD0ke1JVU1RGU19MT0dfTEVWRUw6LWluZm99JwogICAgdm9sdW1lczoKICAgICAgLSAncnVzdGZzLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3J1c3Rmcy1sb2dzOi9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiBsb2NhbGhvc3QgOTAwMCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
"compose": "c2VydmljZXM6CiAgZnJvbnRlbmQ6CiAgICBpbWFnZTogJ2doY3IuaW8vc21hdWc2NzM5L2FsZXhhbmRyaWUtZnJvbnRlbmQ6djguMTAuMCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GUk9OVEVORF84MjAwCiAgICAgIC0gUE9SVD04MjAwCiAgICAgIC0gJ05VWFRfUFVCTElDX0NPTkZJR19ESVNBQkxFX1NJR05VUF9QQUdFPSR7Q09ORklHX0RJU0FCTEVfU0lHTlVQOi1mYWxzZX0nCiAgICAgIC0gJ05VWFRfUFVCTElDX0NPTkZJR19ESVNBQkxFX0xBTkRJTkdfUEFHRT0ke0NPTkZJR19ESVNBQkxFX0xBTkRJTkc6LWZhbHNlfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQkFTRV9BUEk9JHtTRVJWSUNFX0ZRRE5fQkFDS0VORH0nCiAgICAgIC0gJ05VWFRfUFVCTElDX0JBU0VfQ0ROPSR7U0VSVklDRV9GUUROX1JVU1RGU30nCiAgICAgIC0gJ05VWFRfUFVCTElDX0NETl9FTkRQT0lOVD0ke0NETl9FTkRQT0lOVDotL2FsZXhhbmRyaWUvfScKICAgICAgLSAnTlVYVF9QVUJMSUNfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fRlJPTlRFTkR9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBiYWNrZW5kCiAgYmFja2VuZDoKICAgIGltYWdlOiAnZ2hjci5pby9zbWF1ZzY3MzkvYWxleGFuZHJpZS1iYWNrZW5kOnY4LjEwLjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkFDS0VORF84MjAxCiAgICAgIC0gQkFDS0VORF9QT1JUPTgyMDEKICAgICAgLSBHSU5fTU9ERT1yZWxlYXNlCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0NPT0tJRV9ET01BSU49JHtTRVJWSUNFX0ZRRE5fRlJPTlRFTkR9JwogICAgICAtICdGUk9OVEVORF9VUkw9JHtTRVJWSUNFX0ZRRE5fRlJPTlRFTkR9JwogICAgICAtICdBTExPV19VTlNFQ1VSRT0ke0FMTE9XX1VOU0VDVVJFOi1mYWxzZX0nCiAgICAgIC0gREFUQUJBU0VfSE9TVD1teXNxbAogICAgICAtIERBVEFCQVNFX1BPUlQ9MzMwNgogICAgICAtICdEQVRBQkFTRV9OQU1FPSR7TVlTUUxfREFUQUJBU0U6LWFsZXhhbmRyaWUtZGJ9JwogICAgICAtICdEQVRBQkFTRV9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnREFUQUJBU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTUlOSU9fRU5EUE9JTlQ9cnVzdGZzOjkwMDAnCiAgICAgIC0gJ01JTklPX1BVQkxJQ19VUkw9JHtTRVJWSUNFX0ZRRE5fUlVTVEZTfScKICAgICAgLSAnTUlOSU9fU0VDVVJFPSR7TUlOSU9fU0VDVVJFOi1mYWxzZX0nCiAgICAgIC0gJ01JTklPX0FDQ0VTU0tFWT0ke1NFUlZJQ0VfVVNFUl9SVVNURlN9JwogICAgICAtICdNSU5JT19TRUNSRVRLRVk9JHtTRVJWSUNFX1BBU1NXT1JEX1JVU1RGU30nCiAgICAgIC0gJ01JTklPX0JVQ0tFVD0ke01JTklPX0JVQ0tFVDotYWxleGFuZHJpZX0nCiAgICAgIC0gJ1NNVFBfSE9TVD0ke1NNVFBfSE9TVDotfScKICAgICAgLSAnU01UUF9NQUlMPSR7U01UUF9NQUlMOi19JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7U01UUF9QQVNTV09SRDotfScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJ1c3RmczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo4LjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgICAgLSAnTVlTUUxfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NWVNRTH0nCiAgICAgIC0gJ01ZU1FMX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWFsZXhhbmRyaWUtZGJ9JwogICAgdm9sdW1lczoKICAgICAgLSAnbXlzcWwtZGF0YTovdmFyL2xpYi9teXNxbCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSBsb2NhbGhvc3QKICAgICAgICAtICctdScKICAgICAgICAtIHJvb3QKICAgICAgICAtICctcCR7U0VSVklDRV9QQVNTV09SRF9NWVNRTFJPT1R9JwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHJldHJpZXM6IDUKICBydXN0ZnM6CiAgICBpbWFnZTogJ3J1c3Rmcy9ydXN0ZnM6MS4wLjAtYmV0YS44JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1JVU1RGU185MDAwCiAgICAgIC0gJ1JVU1RGU19BQ0NFU1NfS0VZPSR7U0VSVklDRV9VU0VSX1JVU1RGU30nCiAgICAgIC0gJ1JVU1RGU19TRUNSRVRfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9SVVNURlN9JwogICAgICAtICdSVVNURlNfQ09OU09MRV9FTkFCTEU9JHtSVVNURlNfQ09OU09MRV9FTkFCTEU6LWZhbHNlfScKICAgICAgLSAnUlVTVEZTX0xPR19MRVZFTD0ke1JVU1RGU19MT0dfTEVWRUw6LWluZm99JwogICAgdm9sdW1lczoKICAgICAgLSAncnVzdGZzLWRhdGE6L2RhdGEnCiAgICAgIC0gJ3J1c3Rmcy1sb2dzOi9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiBsb2NhbGhvc3QgOTAwMCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=",
"tags": [
"note-taking",
"markdown",
@@ -64,7 +64,7 @@
"category": "productivity",
"logo": "svgs/alexandrie.svg",
"minversion": "0.0.0",
"template_last_updated_at": "2026-04-05T13:36:24+02:00",
"template_last_updated_at": "2026-07-07T13:24:35+02:00",
"port": "8200"
},
"anythingllm": {
@@ -1310,7 +1310,7 @@
"espocrm": {
"documentation": "https://docs.espocrm.com?utm_source=coolify.io",
"slogan": "EspoCRM is a free and open-source CRM platform.",
"compose": "c2VydmljZXM6CiAgZXNwb2NybToKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjknCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRVNQT0NSTQogICAgICAtICdFU1BPQ1JNX0FETUlOX1VTRVJOQU1FPSR7RVNQT0NSTV9BRE1JTl9VU0VSTkFNRTotYWRtaW59JwogICAgICAtICdFU1BPQ1JNX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICAgIC0gRVNQT0NSTV9EQVRBQkFTRV9QTEFURk9STT1NeXNxbAogICAgICAtIEVTUE9DUk1fREFUQUJBU0VfSE9TVD1lc3BvY3JtLWRiCiAgICAgIC0gJ0VTUE9DUk1fREFUQUJBU0VfTkFNRT0ke01BUklBREJfREFUQUJBU0U6LWVzcG9jcm19JwogICAgICAtICdFU1BPQ1JNX0RBVEFCQVNFX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUFSSUFEQn0nCiAgICAgIC0gJ0VTUE9DUk1fREFUQUJBU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01BUklBREJ9JwogICAgICAtICdFU1BPQ1JNX1NJVEVfVVJMPSR7U0VSVklDRV9GUUROX0VTUE9DUk19JwogICAgdm9sdW1lczoKICAgICAgLSAnZXNwb2NybTovdmFyL3d3dy9odG1sJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgc3RhcnRfcGVyaW9kOiA2MHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgZXNwb2NybS1kYWVtb246CiAgICBpbWFnZTogJ2VzcG9jcm0vZXNwb2NybTo5JwogICAgY29udGFpbmVyX25hbWU6IGVzcG9jcm0tZGFlbW9uCiAgICB2b2x1bWVzOgogICAgICAtICdlc3BvY3JtOi92YXIvd3d3L2h0bWwnCiAgICByZXN0YXJ0OiBhbHdheXMKICAgIGVudHJ5cG9pbnQ6IGRvY2tlci1kYWVtb24uc2gKICAgIGRlcGVuZHNfb246CiAgICAgIGVzcG9jcm06CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBlc3BvY3JtLXdlYnNvY2tldDoKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjknCiAgICBjb250YWluZXJfbmFtZTogZXNwb2NybS13ZWJzb2NrZXQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9FU1BPQ1JNX1dFQlNPQ0tFVF84MDgwCiAgICAgIC0gRVNQT0NSTV9DT05GSUdfVVNFX1dFQl9TT0NLRVQ9dHJ1ZQogICAgICAtIEVTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfVVJMPSRTRVJWSUNFX0ZRRE5fRVNQT0NSTV9XRUJTT0NLRVQKICAgICAgLSAnRVNQT0NSTV9DT05GSUdfV0VCX1NPQ0tFVF9aRVJPX01fUV9TVUJTQ1JJQkVSX0RTTj10Y3A6Ly8qOjc3NzcnCiAgICAgIC0gJ0VTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfWkVST19NX1FfU1VCTUlTU0lPTl9EU049dGNwOi8vZXNwb2NybS13ZWJzb2NrZXQ6Nzc3NycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VzcG9jcm06L3Zhci93d3cvaHRtbCcKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgZW50cnlwb2ludDogZG9ja2VyLXdlYnNvY2tldC5zaAogICAgZGVwZW5kc19vbjoKICAgICAgZXNwb2NybToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIGVzcG9jcm0tZGI6CiAgICBpbWFnZTogJ21hcmlhZGI6MTEuOCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdNQVJJQURCX0RBVEFCQVNFPSR7TUFSSUFEQl9EQVRBQkFTRTotZXNwb2NybX0nCiAgICAgIC0gJ01BUklBREJfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NQVJJQURCfScKICAgICAgLSAnTUFSSUFEQl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQn0nCiAgICAgIC0gJ01BUklBREJfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICB2b2x1bWVzOgogICAgICAtICdlc3BvY3JtLWRiOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGhlYWx0aGNoZWNrLnNoCiAgICAgICAgLSAnLS1jb25uZWN0JwogICAgICAgIC0gJy0taW5ub2RiX2luaXRpYWxpemVkJwogICAgICBpbnRlcnZhbDogMjBzCiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCg==",
"compose": "c2VydmljZXM6CiAgZXNwb2NybToKICAgIGltYWdlOiAnZXNwb2NybS9lc3BvY3JtOjEwJwogICAgY29udGFpbmVyX25hbWU6IGVzcG9jcm0KICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9FU1BPQ1JNCiAgICAgIC0gJ0VTUE9DUk1fQURNSU5fVVNFUk5BTUU9JHtFU1BPQ1JNX0FETUlOX1VTRVJOQU1FOi1hZG1pbn0nCiAgICAgIC0gJ0VTUE9DUk1fQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSBFU1BPQ1JNX0RBVEFCQVNFX1BMQVRGT1JNPU15c3FsCiAgICAgIC0gRVNQT0NSTV9EQVRBQkFTRV9IT1NUPWVzcG9jcm0tZGIKICAgICAgLSAnRVNQT0NSTV9EQVRBQkFTRV9OQU1FPSR7TUFSSUFEQl9EQVRBQkFTRTotZXNwb2NybX0nCiAgICAgIC0gJ0VTUE9DUk1fREFUQUJBU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NQVJJQURCfScKICAgICAgLSAnRVNQT0NSTV9EQVRBQkFTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQn0nCiAgICAgIC0gJ0VTUE9DUk1fU0lURV9VUkw9JHtTRVJWSUNFX0ZRRE5fRVNQT0NSTX0nCiAgICB2b2x1bWVzOgogICAgICAtICdlc3BvY3JtLWRhdGE6L3Zhci93d3cvaHRtbC9kYXRhJwogICAgICAtICdlc3BvY3JtLWN1c3RvbTovdmFyL3d3dy9odG1sL2N1c3RvbScKICAgICAgLSAnZXNwb2NybS1jdXN0b20tY2xpZW50Oi92YXIvd3d3L2h0bWwvY2xpZW50L2N1c3RvbScKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gYmluL2NvbW1hbmQKICAgICAgICAtIGFwcC1jaGVjawogICAgICBzdGFydF9wZXJpb2Q6IDIwcwogICAgICBpbnRlcnZhbDogNjBzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAzCiAgZXNwb2NybS1kYWVtb246CiAgICBpbWFnZTogJ2VzcG9jcm0vZXNwb2NybToxMCcKICAgIGNvbnRhaW5lcl9uYW1lOiBlc3BvY3JtLWRhZW1vbgogICAgdm9sdW1lc19mcm9tOgogICAgICAtIGVzcG9jcm0KICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnRyeXBvaW50OiBkb2NrZXItZGFlbW9uLnNoCiAgICBkZXBlbmRzX29uOgogICAgICBlc3BvY3JtOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gYmluL2NvbW1hbmQKICAgICAgICAtIGFwcC1jaGVjawogICAgICBzdGFydF9wZXJpb2Q6IDIwcwogICAgICBpbnRlcnZhbDogMTgwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwogIGVzcG9jcm0td2Vic29ja2V0OgogICAgaW1hZ2U6ICdlc3BvY3JtL2VzcG9jcm06MTAnCiAgICBjb250YWluZXJfbmFtZTogZXNwb2NybS13ZWJzb2NrZXQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9FU1BPQ1JNX1dFQlNPQ0tFVF84MDgwCiAgICAgIC0gRVNQT0NSTV9DT05GSUdfVVNFX1dFQl9TT0NLRVQ9dHJ1ZQogICAgICAtIEVTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfVVJMPSRTRVJWSUNFX0ZRRE5fRVNQT0NSTV9XRUJTT0NLRVQKICAgICAgLSAnRVNQT0NSTV9DT05GSUdfV0VCX1NPQ0tFVF9aRVJPX01fUV9TVUJTQ1JJQkVSX0RTTj10Y3A6Ly8qOjc3NzcnCiAgICAgIC0gJ0VTUE9DUk1fQ09ORklHX1dFQl9TT0NLRVRfWkVST19NX1FfU1VCTUlTU0lPTl9EU049dGNwOi8vZXNwb2NybS13ZWJzb2NrZXQ6Nzc3NycKICAgIHZvbHVtZXNfZnJvbToKICAgICAgLSBlc3BvY3JtCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgZW50cnlwb2ludDogZG9ja2VyLXdlYnNvY2tldC5zaAogICAgZGVwZW5kc19vbjoKICAgICAgZXNwb2NybToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGJpbi9jb21tYW5kCiAgICAgICAgLSBhcHAtY2hlY2sKICAgICAgc3RhcnRfcGVyaW9kOiAyMHMKICAgICAgaW50ZXJ2YWw6IDE4MHMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDMKICBlc3BvY3JtLWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjEyLjMnCiAgICBjb250YWluZXJfbmFtZTogZXNwb2NybS1kYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01BUklBREJfREFUQUJBU0U9JHtNQVJJQURCX0RBVEFCQVNFOi1lc3BvY3JtfScKICAgICAgLSAnTUFSSUFEQl9VU0VSPSR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICAtICdNQVJJQURCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCfScKICAgICAgLSAnTUFSSUFEQl9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9ST09UfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VzcG9jcm0tZGI6L3Zhci9saWIvbXlzcWwnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGhlYWx0aGNoZWNrLnNoCiAgICAgICAgLSAnLS1jb25uZWN0JwogICAgICAgIC0gJy0taW5ub2RiX2luaXRpYWxpemVkJwogICAgICBpbnRlcnZhbDogMjBzCiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCg==",
"tags": [
"crm",
"self-hosted",
@@ -1322,7 +1322,7 @@
"category": "productivity",
"logo": "svgs/espocrm.svg",
"minversion": "0.0.0",
"template_last_updated_at": "2026-04-06T11:35:16-05:00",
"template_last_updated_at": "2026-07-03T15:15:43+03:00",
"port": "80"
},
"evolution-api": {
@@ -0,0 +1,257 @@
<?php
use App\Models\Application;
use App\Models\Environment;
use App\Models\GithubApp;
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
uses(RefreshDatabase::class);
beforeEach(function () {
Storage::fake('ssh-keys');
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
session(['currentTeam' => $this->team]);
$this->bearerToken = $this->user->createToken('build-secrets-api-test', ['*'])->plainTextToken;
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
$this->application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(),
]);
});
function buildSecretsApiHeaders(string $bearerToken): array
{
return [
'Authorization' => 'Bearer '.$bearerToken,
'Content-Type' => 'application/json',
];
}
function buildSecretsGithubPrivateKey(): string
{
$key = openssl_pkey_new([
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
openssl_pkey_export($key, $privateKey);
return $privateKey;
}
describe('PATCH /api/v1/applications/{uuid} use_build_secrets', function () {
test('updates the application setting', function () {
expect($this->application->settings->use_build_secrets)->toBeFalse();
$this->withHeaders(buildSecretsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'use_build_secrets' => true,
])
->assertOk();
expect($this->application->fresh()->settings->use_build_secrets)->toBeTrue();
$this->withHeaders(buildSecretsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'use_build_secrets' => false,
])
->assertOk();
expect($this->application->fresh()->settings->use_build_secrets)->toBeFalse();
});
test('rejects non boolean values', function () {
$this->withHeaders(buildSecretsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'use_build_secrets' => 'not-a-boolean',
])
->assertUnprocessable()
->assertJsonValidationErrors('use_build_secrets');
});
test('does not change the setting when omitted', function () {
$this->application->settings->update(['use_build_secrets' => true]);
$this->withHeaders(buildSecretsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'name' => 'updated-name',
])
->assertOk();
expect($this->application->fresh()->settings->use_build_secrets)->toBeTrue();
});
});
describe('POST /api/v1/applications/public use_build_secrets', function () {
test('creates an application with the requested build secrets setting', function (bool $useBuildSecrets) {
$response = $this->withHeaders(buildSecretsApiHeaders($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/build-secrets-test',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'use_build_secrets' => $useBuildSecrets,
'autogenerate_domain' => false,
])
->assertCreated();
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
expect($application->settings->use_build_secrets)->toBe($useBuildSecrets);
})->with([
'enabled' => true,
'disabled' => false,
]);
test('rejects non boolean values', function () {
$this->withHeaders(buildSecretsApiHeaders($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/build-secrets-test',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'use_build_secrets' => 'not-a-boolean',
'autogenerate_domain' => false,
])
->assertUnprocessable()
->assertJsonValidationErrors('use_build_secrets');
});
});
describe('other application creation endpoints use_build_secrets', function () {
test('creates a Dockerfile application with build secrets enabled', function () {
$response = $this->withHeaders(buildSecretsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/dockerfile', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'dockerfile' => base64_encode("FROM nginx:alpine\nEXPOSE 80"),
'use_build_secrets' => true,
'autogenerate_domain' => false,
])
->assertCreated();
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
expect($application->settings->use_build_secrets)->toBeTrue();
});
test('creates a Docker image application with build secrets enabled', function () {
$response = $this->withHeaders(buildSecretsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/dockerimage', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'docker_registry_image_name' => 'nginx',
'docker_registry_image_tag' => 'alpine',
'ports_exposes' => '80',
'use_build_secrets' => true,
'autogenerate_domain' => false,
])
->assertCreated();
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
expect($application->settings->use_build_secrets)->toBeTrue();
});
test('creates a private deploy key application with build secrets enabled', function () {
$privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
$response = $this->withHeaders(buildSecretsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/private-deploy-key', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'private_key_uuid' => $privateKey->uuid,
'git_repository' => 'git@gitlab.com:coolify/build-secrets-test.git',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'use_build_secrets' => true,
'autogenerate_domain' => false,
])
->assertCreated();
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
expect($application->settings->use_build_secrets)->toBeTrue();
});
test('creates a private GitHub App application with build secrets enabled', function () {
$privateKey = PrivateKey::create([
'name' => 'GitHub App Key',
'private_key' => buildSecretsGithubPrivateKey(),
'team_id' => $this->team->id,
]);
$githubApp = GithubApp::create([
'name' => 'Build Secrets GitHub App',
'api_url' => 'https://api.github.com',
'html_url' => 'https://github.com',
'app_id' => 12345,
'installation_id' => 67890,
'client_id' => 'build-secrets-client-id',
'client_secret' => 'build-secrets-client-secret',
'webhook_secret' => 'build-secrets-webhook-secret',
'private_key_id' => $privateKey->id,
'team_id' => $this->team->id,
'is_system_wide' => false,
'is_public' => false,
]);
Http::fake([
'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, [
'Date' => now()->toRfc7231String(),
]),
'https://api.github.com/app/installations/67890/access_tokens' => Http::response([
'token' => 'github-installation-token',
], 201),
'https://api.github.com/repos/coolify/build-secrets-test' => Http::response([
'id' => 123456,
]),
]);
$response = $this->withHeaders(buildSecretsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/private-github-app', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'github_app_uuid' => $githubApp->uuid,
'git_repository' => 'coolify/build-secrets-test',
'git_branch' => 'main',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'use_build_secrets' => true,
'autogenerate_domain' => false,
])
->assertCreated();
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
expect($application->settings->use_build_secrets)->toBeTrue();
});
});
@@ -0,0 +1,183 @@
<?php
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
session(['currentTeam' => $this->team]);
$this->bearerToken = $this->user->createToken('application-settings-api-test', ['*'])->plainTextToken;
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
$this->application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(),
]);
});
function applicationSettingsApiHeaders(string $bearerToken): array
{
return [
'Authorization' => 'Bearer '.$bearerToken,
'Content-Type' => 'application/json',
];
}
function recommendedApplicationSettingsPayload(): array
{
return [
'is_git_submodules_enabled' => false,
'is_git_lfs_enabled' => false,
'is_git_shallow_clone_enabled' => false,
'disable_build_cache' => true,
'inject_build_args_to_dockerfile' => false,
'include_source_commit_in_build' => true,
'is_env_sorting_enabled' => true,
'is_pr_deployments_public_enabled' => true,
'stop_grace_period' => 45,
'docker_images_to_keep' => 7,
'is_gzip_enabled' => false,
'is_stripprefix_enabled' => false,
'is_raw_compose_deployment_enabled' => true,
];
}
test('GET /api/v1/applications/{uuid} includes settings without internal metadata', function () {
$this->application->settings->update(recommendedApplicationSettingsPayload());
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->getJson("/api/v1/applications/{$this->application->uuid}")
->assertOk()
->assertJsonPath('settings.disable_build_cache', true)
->assertJsonPath('settings.stop_grace_period', 45)
->assertJsonMissingPath('settings.id')
->assertJsonMissingPath('settings.application_id')
->assertJsonMissingPath('settings.created_at')
->assertJsonMissingPath('settings.updated_at');
});
test('PATCH /api/v1/applications/{uuid} updates application settings', function () {
$this->application->update(['build_pack' => 'dockercompose']);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", recommendedApplicationSettingsPayload())
->assertOk();
$settings = $this->application->fresh()->settings;
foreach (recommendedApplicationSettingsPayload() as $field => $value) {
expect($settings->{$field})->toBe($value);
}
});
test('application creation accepts application settings', function () {
Queue::fake();
$response = $this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/public', array_merge([
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/application-settings-test',
'git_branch' => 'main',
'build_pack' => 'dockercompose',
'autogenerate_domain' => false,
], recommendedApplicationSettingsPayload()))
->assertCreated();
$settings = Application::where('uuid', $response->json('uuid'))->firstOrFail()->settings;
foreach (recommendedApplicationSettingsPayload() as $field => $value) {
expect($settings->{$field})->toBe($value);
}
});
test('proxy settings regenerate managed labels', function () {
$this->application->settings->update([
'is_container_label_readonly_enabled' => true,
'is_gzip_enabled' => true,
'is_stripprefix_enabled' => true,
]);
$this->application->update(['custom_labels' => base64_encode('sentinel-label=true')]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'is_gzip_enabled' => false,
'is_stripprefix_enabled' => false,
])
->assertOk();
expect(base64_decode($this->application->fresh()->custom_labels))->not->toContain('sentinel-label=true');
});
test('rejects invalid boolean application settings', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'disable_build_cache' => 'not-a-boolean',
])
->assertUnprocessable()
->assertJsonValidationErrors('disable_build_cache');
});
test('validates stop grace period bounds', function (int $stopGracePeriod) {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'stop_grace_period' => $stopGracePeriod,
])
->assertUnprocessable()
->assertJsonValidationErrors('stop_grace_period');
})->with([
'below minimum' => 0,
'above maximum' => 3601,
]);
test('validates Docker image retention bounds', function (int $dockerImagesToKeep) {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'docker_images_to_keep' => $dockerImagesToKeep,
])
->assertUnprocessable()
->assertJsonValidationErrors('docker_images_to_keep');
})->with([
'below minimum' => -1,
'above maximum' => 101,
]);
test('stop grace period can be reset to null', function () {
$this->application->settings->update(['stop_grace_period' => 45]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'stop_grace_period' => null,
])
->assertOk();
expect($this->application->fresh()->settings->stop_grace_period)->toBeNull();
});
test('raw compose deployment can only be enabled for Docker Compose applications', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'is_raw_compose_deployment_enabled' => true,
])
->assertUnprocessable()
->assertJsonValidationErrors('is_raw_compose_deployment_enabled');
});
@@ -3,7 +3,11 @@
use App\Livewire\Project\Application\General;
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -81,9 +85,26 @@ describe('Application noindex domains', function () {
});
test('the Livewire toggle persists the flag', function () {
InstanceSettings::unguarded(function () {
InstanceSettings::updateOrCreate(['id' => 0], []);
});
$privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $privateKey->id,
]);
$destination = StandaloneDocker::where('server_id', $server->id)->first()
?? StandaloneDocker::factory()->create(['server_id' => $server->id]);
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $destination->id,
'destination_type' => StandaloneDocker::class,
'fqdn' => 'https://prod.example.com,https://staging.example.com',
'static_image' => 'nginx:alpine',
'base_directory' => '/',
'is_http_basic_auth_enabled' => false,
'redirect' => 'no',
]);
Livewire::test(General::class, ['application' => $application])
@@ -11,6 +11,7 @@ use App\Models\Server;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
use Livewire\Livewire;
use Visus\Cuid2\Cuid2;
@@ -117,6 +118,48 @@ test('changeSource rejects an arbitrary class as source_type', function () {
expect($this->applicationA->source_type)->not->toBe(Server::class);
});
test('changeSource dispatches configuration changed for an owned source', function () {
Http::fake([
'https://api.github.com/repos/*' => Http::response(['id' => 123]),
]);
$source = GithubApp::create([
'name' => 'own-github-app',
'team_id' => $this->teamA->id,
'api_url' => 'https://api.github.com',
'html_url' => 'https://github.com',
'is_public' => true,
]);
$this->applicationA->update(['git_repository' => 'coollabsio/coolify']);
Livewire::test(Source::class, ['application' => $this->applicationA->fresh()])
->call('changeSource', $source->id, GithubApp::class)
->assertDispatched('configurationChanged');
});
test('changeSource dispatches configuration changed when repository metadata lookup fails after persistence', function () {
Http::fake([
'https://api.github.com/repos/*' => Http::response(
['message' => 'Unavailable'],
503,
['X-RateLimit-Reset' => now()->addMinute()->timestamp],
),
]);
$source = GithubApp::create([
'name' => 'own-unavailable-github-app',
'team_id' => $this->teamA->id,
'api_url' => 'https://api.github.com',
'html_url' => 'https://github.com',
'is_public' => true,
]);
$this->applicationA->update(['git_repository' => 'coollabsio/coolify']);
Livewire::test(Source::class, ['application' => $this->applicationA->fresh()])
->call('changeSource', $source->id, GithubApp::class)
->assertDispatched('configurationChanged');
expect($this->applicationA->refresh()->source_id)->toBe($source->id);
});
test('privateKeyId is locked so submit() cannot persist a client-supplied foreign id', function () {
// Without #[Locked], an attacker could POST {"updates": {"privateKeyId": <foreign_id>},
// "calls": [{"method": "submit"}]} and have syncData(true) write the foreign id through
@@ -16,6 +16,8 @@ uses(RefreshDatabase::class);
function createApplicationForAdvancedStopGracePeriodTest(): Application
{
$team = Team::factory()->create();
$team->members()->attach(auth()->id(), ['role' => 'owner']);
session(['currentTeam' => $team]);
$server = Server::factory()->create(['team_id' => $team->id]);
$project = Project::factory()->create(['team_id' => $team->id]);
$environment = Environment::factory()->create(['project_id' => $project->id]);
@@ -43,7 +45,8 @@ it('saves a valid stop grace period', function () {
->set('stopGracePeriod', '300')
->call('saveStopGracePeriod')
->assertHasNoErrors()
->assertDispatched('success');
->assertDispatched('success')
->assertDispatched('configurationChanged');
expect($application->settings()->first()->stop_grace_period)->toBe(300);
});
@@ -0,0 +1,44 @@
<?php
use App\Livewire\Project\Shared\EnvironmentVariable\All as EnvironmentVariableAll;
use App\Livewire\Project\Shared\HealthChecks;
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::unguarded(function () {
InstanceSettings::updateOrCreate(['id' => 0], []);
});
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
$this->actingAs($this->user);
session(['currentTeam' => $this->team]);
$project = Project::factory()->create(['team_id' => $this->team->id]);
$environment = Environment::factory()->create(['project_id' => $project->id]);
$this->application = Application::factory()->create(['environment_id' => $environment->id])->refresh();
});
it('refreshes configuration changes after health check saves', function (string $method) {
Livewire::test(HealthChecks::class, ['resource' => $this->application])
->call($method)
->assertHasNoErrors()
->assertDispatched('configurationChanged');
})->with(['instantSave', 'submit', 'toggleHealthcheck']);
it('refreshes configuration changes after environment variable settings are saved', function () {
Livewire::test(EnvironmentVariableAll::class, ['resource' => $this->application])
->set('use_build_secrets', true)
->call('instantSave')
->assertHasNoErrors()
->assertDispatched('configurationChanged');
});
@@ -0,0 +1,36 @@
<?php
use App\Livewire\Project\Application\Swarm;
use App\Models\Application;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
it('dispatches configuration changed when Swarm settings are saved', function (string $method) {
$team = Team::factory()->create();
$user = User::factory()->create();
$team->members()->attach($user->id, ['role' => 'owner']);
$server = Server::factory()->create(['team_id' => $team->id]);
$project = Project::factory()->create(['team_id' => $team->id]);
$environment = Environment::factory()->create(['project_id' => $project->id]);
$application = Application::factory()->create([
'environment_id' => $environment->id,
'destination_id' => $server->standaloneDockers()->firstOrFail()->id,
'destination_type' => $server->standaloneDockers()->firstOrFail()->getMorphClass(),
'swarm_replicas' => 1,
]);
$this->actingAs($user);
Livewire::test(Swarm::class, ['application' => $application])
->set('isSwarmOnlyWorkerNodes', false)
->call($method)
->assertHasNoErrors()
->assertDispatched('configurationChanged');
})->with(['instantSave', 'submit']);
@@ -118,4 +118,24 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
->assertSee('nixpacks.toml')
->assertDontSee('railpack.json');
});
test('saving general settings refreshes configuration changes when label reset is disabled', function () {
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => StandaloneDocker::class,
'build_pack' => 'nixpacks',
'static_image' => 'nginx:alpine',
'base_directory' => '/',
'is_http_basic_auth_enabled' => false,
'redirect' => 'no',
]);
$application->settings->update(['is_container_label_readonly_enabled' => false]);
Livewire::test(General::class, ['application' => $application->refresh()])
->set('isPreserveRepositoryEnabled', true)
->call('instantSave')
->assertHasNoErrors()
->assertDispatched('configurationChanged');
});
});
@@ -6,6 +6,7 @@ use App\Models\Environment;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Team;
use App\Services\DeploymentConfiguration\ConfigurationDiffer;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Tests\TestCase;
@@ -78,6 +79,192 @@ it('detects redeploy-only domain changes', function () {
->and($change['new_full_value'])->toBe($domains);
});
it('detects Docker image reference changes as redeploy-only changes', function (string $field, string $label, string $newValue) {
$application = snapshotTestApplication([
'build_pack' => 'dockerimage',
'docker_registry_image_name' => 'ghcr.io/coollabsio/shoutrrr',
'docker_registry_image_tag' => '1.3.0-rc.4',
]);
markSnapshotTestApplicationDeployed($application);
$application->update([$field => $newValue]);
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
$change = collect($diff->changes())->firstWhere('label', $label);
expect($diff->isChanged())->toBeTrue()
->and($diff->requiresBuild())->toBeFalse()
->and($change)->not->toBeNull()
->and($change['old_display_value'])->not->toBe($change['new_display_value'])
->and($change['new_display_value'])->toBe($newValue);
})->with([
'image name' => ['docker_registry_image_name', 'Docker image', 'ghcr.io/coollabsio/coolify'],
'image tag' => ['docker_registry_image_tag', 'Docker image tag or hash', '1.3.0-rc.5'],
]);
it('detects deployment hook changes as redeploy-only changes', function (string $field, string $label, string $newValue) {
$application = snapshotTestApplication();
markSnapshotTestApplicationDeployed($application);
$application->update([$field => $newValue]);
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
expect($diff->requiresBuild())->toBeFalse()
->and(collect($diff->changes())->pluck('label'))->toContain($label);
})->with([
'pre-deployment command' => ['pre_deployment_command', 'Pre-deployment command', 'php artisan migrate --force'],
'pre-deployment container' => ['pre_deployment_command_container', 'Pre-deployment command container', 'web'],
'post-deployment command' => ['post_deployment_command', 'Post-deployment command', 'php artisan cache:clear'],
'post-deployment container' => ['post_deployment_command_container', 'Post-deployment command container', 'worker'],
]);
it('detects source integration changes as build changes', function (string $field, string $label, mixed $newValue) {
$application = snapshotTestApplication([
'source_id' => 1,
'source_type' => 'App\\Models\\GithubApp',
]);
markSnapshotTestApplicationDeployed($application);
$application->forceFill([$field => $newValue])->save();
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
expect($diff->requiresBuild())->toBeTrue()
->and(collect($diff->changes())->pluck('label'))->toContain($label);
})->with([
'source ID' => ['source_id', 'Source ID', 2],
'source type' => ['source_type', 'Source type', 'App\\Models\\GitlabApp'],
]);
it('detects build-affecting application settings', function (string $field, string $label) {
$application = snapshotTestApplication();
$application->settings->update([$field => false]);
markSnapshotTestApplicationDeployed($application);
$application->settings->update([$field => true]);
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
expect($diff->requiresBuild())->toBeTrue()
->and(collect($diff->changes())->pluck('label'))->toContain($label);
})->with([
'static site' => ['is_static', 'Static site'],
'single-page application' => ['is_spa', 'Single-page application'],
'Git submodules' => ['is_git_submodules_enabled', 'Git submodules'],
'Git LFS' => ['is_git_lfs_enabled', 'Git LFS'],
'shallow clone' => ['is_git_shallow_clone_enabled', 'Shallow clone'],
'environment variable sorting' => ['is_env_sorting_enabled', 'Sort environment variables'],
]);
it('detects runtime-affecting application settings as redeploy-only changes', function (string $field, string $label, mixed $newValue) {
$application = snapshotTestApplication();
$application->settings->update([$field => is_bool($newValue) ? ! $newValue : null]);
markSnapshotTestApplicationDeployed($application);
$application->settings->update([$field => $newValue]);
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
expect($diff->requiresBuild())->toBeFalse()
->and(collect($diff->changes())->pluck('label'))->toContain($label);
})->with([
'consistent container name' => ['is_consistent_container_name_enabled', 'Consistent container name', true],
'container label escaping' => ['is_container_label_escape_enabled', 'Escape container labels', false],
'container labels read-only' => ['is_container_label_readonly_enabled', 'Read-only container labels', false],
'log drain' => ['is_log_drain_enabled', 'Log drain', true],
'Swarm worker nodes' => ['is_swarm_only_worker_nodes', 'Swarm worker nodes only', true],
'stop grace period' => ['stop_grace_period', 'Stop grace period', 45],
'preserve repository' => ['is_preserve_repository_enabled', 'Preserve repository', true],
]);
it('classifies runtime options as redeploy-only and custom nginx configuration as build-affecting', function () {
$application = snapshotTestApplication();
markSnapshotTestApplicationDeployed($application);
$application->update(['custom_docker_run_options' => '--init']);
expect($application->refresh()->pendingDeploymentConfigurationDiff()->requiresBuild())->toBeFalse();
markSnapshotTestApplicationDeployed($application->refresh());
$application->update(['custom_nginx_configuration' => 'server { listen 80; }']);
expect($application->refresh()->pendingDeploymentConfigurationDiff()->requiresBuild())->toBeTrue();
});
it('keeps Docker run options compatible with older build-section snapshots', function () {
$application = snapshotTestApplication(['custom_docker_run_options' => '--init'])->refresh();
$previousSnapshot = $application->deploymentConfigurationSnapshot();
$buildItems = collect(data_get($previousSnapshot, 'sections.build.items'));
expect($buildItems->pluck('key'))->toContain('custom_docker_run_options');
data_set(
$previousSnapshot,
'sections.build.items',
$buildItems->map(function (array $item): array {
if ($item['key'] === 'custom_docker_run_options') {
$item['impact'] = 'build';
}
return $item;
})->all(),
);
expect(app(ConfigurationDiffer::class)->diff($previousSnapshot, $application->deploymentConfigurationSnapshot())->isChanged())->toBeFalse();
$application->update(['custom_docker_run_options' => '--rm']);
expect(app(ConfigurationDiffer::class)->diff($previousSnapshot, $application->refresh()->deploymentConfigurationSnapshot())->requiresBuild())->toBeFalse();
});
it('does not report newly tracked settings when an older snapshot omitted their default values', function () {
$application = snapshotTestApplication();
$application->settings->update(['stop_grace_period' => DEFAULT_STOP_GRACE_PERIOD_SECONDS]);
$currentSnapshot = $application->deploymentConfigurationSnapshot();
$introducedKeys = [
'is_static',
'is_spa',
'is_git_submodules_enabled',
'is_git_lfs_enabled',
'is_git_shallow_clone_enabled',
'is_env_sorting_enabled',
'is_consistent_container_name_enabled',
'is_container_label_escape_enabled',
'is_container_label_readonly_enabled',
'is_log_drain_enabled',
'is_swarm_only_worker_nodes',
'is_preserve_repository_enabled',
'stop_grace_period',
];
$previousSnapshot = $currentSnapshot;
foreach (['build', 'runtime'] as $section) {
data_set(
$previousSnapshot,
"sections.{$section}.items",
collect(data_get($previousSnapshot, "sections.{$section}.items"))
->reject(fn (array $item): bool => in_array($item['key'], $introducedKeys, true))
->values()
->all(),
);
}
expect(app(ConfigurationDiffer::class)->diff($previousSnapshot, $currentSnapshot)->isChanged())->toBeFalse();
});
it('accepts the historical environment sorting default in older snapshots', function () {
$application = snapshotTestApplication();
$application->settings->update(['is_env_sorting_enabled' => true]);
$currentSnapshot = $application->deploymentConfigurationSnapshot();
$previousSnapshot = $currentSnapshot;
data_set(
$previousSnapshot,
'sections.build.items',
collect(data_get($previousSnapshot, 'sections.build.items'))
->reject(fn (array $item): bool => $item['key'] === 'is_env_sorting_enabled')
->values()
->all(),
);
expect(app(ConfigurationDiffer::class)->diff($previousSnapshot, $currentSnapshot)->isChanged())->toBeFalse();
});
it('detects environment variable value changes without exposing secret values', function () {
$application = snapshotTestApplication();
EnvironmentVariable::create([