From d5a5d1c32ad5f36e509d012229300b822fc94ac7 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Wed, 3 Dec 2025 15:45:48 +0100
Subject: [PATCH] feat: Add support for coolify.json configuration import and
schema validation
- Introduced a new schema for coolify.json to validate application configurations.
- Implemented loading and parsing logic for coolify.json in the application.
- Added UI components for importing coolify.json configurations in various project creation flows.
- Enhanced logging for coolify.json processing to aid in debugging.
- Created unit tests to validate coolify.json parsing and magic variable resolution.
- Updated existing forms to include options for importing coolify.json settings.
---
.../Api/ApplicationsController.php | 68 ++
app/Livewire/GlobalSearch.php | 14 +-
.../Project/Application/Configuration.php | 11 +
app/Livewire/Project/Application/General.php | 15 +
.../Project/New/CoolifyJsonImport.php | 154 +++
.../Project/New/GithubPrivateRepository.php | 21 +
.../New/GithubPrivateRepositoryDeployKey.php | 20 +
.../Project/New/PublicGitRepository.php | 22 +-
app/Livewire/Project/New/Select.php | 7 +
app/Models/Application.php | 984 +++++++++++++++++-
bootstrap/helpers/api.php | 2 +
bootstrap/helpers/shared.php | 83 +-
openapi.json | 12 +
openapi.yaml | 9 +
public/schemas/coolify.schema.json | 664 ++++++++++++
.../views/livewire/global-search.blade.php | 1 +
.../project/application/general.blade.php | 9 +
.../project/new/coolify-json-import.blade.php | 57 +
...ub-private-repository-deploy-key.blade.php | 4 +
.../new/github-private-repository.blade.php | 4 +
.../new/public-git-repository.blade.php | 4 +
.../livewire/project/new/select.blade.php | 18 +
.../project/resource/create.blade.php | 2 +
.../project/shared/upload-config.blade.php | 3 +-
tests/Unit/CoolifyJsonConfigTest.php | 206 ++++
25 files changed, 2352 insertions(+), 42 deletions(-)
create mode 100644 app/Livewire/Project/New/CoolifyJsonImport.php
create mode 100644 public/schemas/coolify.schema.json
create mode 100644 resources/views/livewire/project/new/coolify-json-import.blade.php
create mode 100644 tests/Unit/CoolifyJsonConfigTest.php
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index 92c5f04a2..45c867e40 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -193,6 +193,7 @@ class ApplicationsController extends Controller
'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.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
+ 'use_coolify_json' => ['type' => 'boolean', 'description' => 'Check repository for coolify.json and apply configuration if found. Default is true.'],
],
)
),
@@ -344,6 +345,7 @@ class ApplicationsController extends Controller
'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.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
+ 'use_coolify_json' => ['type' => 'boolean', 'description' => 'Check repository for coolify.json and apply configuration if found. Default is true.'],
],
)
),
@@ -495,6 +497,7 @@ class ApplicationsController extends Controller
'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.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
+ 'use_coolify_json' => ['type' => 'boolean', 'description' => 'Check repository for coolify.json and apply configuration if found. Default is true.'],
],
)
),
@@ -933,6 +936,7 @@ class ApplicationsController extends Controller
return $return;
}
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', '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', 'base_directory', 'publish_directory', 'health_check_enabled', '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', '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'];
+ $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', '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', 'base_directory', 'publish_directory', 'health_check_enabled', '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', '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', 'use_coolify_json'];
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
@@ -977,6 +981,7 @@ class ApplicationsController extends Controller
$isStatic = $request->is_static;
$connectToDockerNetwork = $request->connect_to_docker_network;
$customNginxConfiguration = $request->custom_nginx_configuration;
+ $useCoolifyJson = $request->use_coolify_json ?? true;
if (! is_null($customNginxConfiguration)) {
if (! isBase64Encoded($customNginxConfiguration)) {
@@ -1105,6 +1110,28 @@ class ApplicationsController extends Controller
}
$application->isConfigurationChanged(true);
+ // Check for coolify.json configuration if enabled (default: true)
+ if ($useCoolifyJson) {
+ try {
+ $gitUrl = $request->git_repository;
+ if (! str_ends_with($gitUrl, '.git')) {
+ $gitUrl = $gitUrl.'.git';
+ }
+ $config = loadConfigFromGit(
+ $gitUrl,
+ $application->git_branch,
+ $application->base_directory ?? '/',
+ $destination->server->id,
+ $teamId
+ );
+ if ($config) {
+ $application->setConfig($config, fromRepository: true);
+ }
+ } catch (\Exception $e) {
+ \Log::warning('coolify.json: Failed to apply config via API - '.$e->getMessage());
+ }
+ }
+
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
@@ -1265,6 +1292,25 @@ class ApplicationsController extends Controller
}
$application->isConfigurationChanged(true);
+ // Check for coolify.json configuration if enabled (default: true)
+ if ($useCoolifyJson) {
+ try {
+ $gitUrl = 'https://github.com/'.$application->git_repository.'.git';
+ $config = loadConfigFromGit(
+ $gitUrl,
+ $application->git_branch,
+ $application->base_directory ?? '/',
+ $destination->server->id,
+ $teamId
+ );
+ if ($config) {
+ $application->setConfig($config, fromRepository: true);
+ }
+ } catch (\Exception $e) {
+ \Log::warning('coolify.json: Failed to apply config via API - '.$e->getMessage());
+ }
+ }
+
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
@@ -1399,6 +1445,28 @@ class ApplicationsController extends Controller
}
$application->isConfigurationChanged(true);
+ // Check for coolify.json configuration if enabled (default: true)
+ if ($useCoolifyJson) {
+ try {
+ $gitUrl = $application->git_repository;
+ if (! str_ends_with($gitUrl, '.git')) {
+ $gitUrl = $gitUrl.'.git';
+ }
+ $config = loadConfigFromGit(
+ $gitUrl,
+ $application->git_branch,
+ $application->base_directory ?? '/',
+ $destination->server->id,
+ $teamId
+ );
+ if ($config) {
+ $application->setConfig($config, fromRepository: true);
+ }
+ } catch (\Exception $e) {
+ \Log::warning('coolify.json: Failed to apply config via API - '.$e->getMessage());
+ }
+ }
+
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
index 5d3348692..b5f9ab234 100644
--- a/app/Livewire/GlobalSearch.php
+++ b/app/Livewire/GlobalSearch.php
@@ -190,6 +190,9 @@ class GlobalSearch extends Component
'new compose' => 'docker-compose-empty',
'new docker image' => 'docker-image',
'new image' => 'docker-image',
+ 'new coolify json' => 'coolify-json',
+ 'new json' => 'coolify-json',
+ 'new import' => 'coolify-json',
// Databases
'new postgresql' => 'postgresql',
@@ -234,7 +237,7 @@ class GlobalSearch extends Component
'project', 'source',
// Applications
'public', 'private-gh-app', 'private-deploy-key',
- 'dockerfile', 'docker-compose-empty', 'docker-image',
+ 'dockerfile', 'docker-compose-empty', 'docker-image', 'coolify-json',
// Databases
'postgresql', 'mysql', 'mariadb', 'redis', 'keydb',
'dragonfly', 'mongodb', 'clickhouse',
@@ -1028,6 +1031,15 @@ class GlobalSearch extends Component
'category' => 'Applications',
'resourceType' => 'application',
]);
+
+ $items->push([
+ 'name' => 'Import from coolify.json',
+ 'description' => 'Paste a coolify.json configuration to quickly create an application',
+ 'quickcommand' => '(type: new json)',
+ 'type' => 'coolify-json',
+ 'category' => 'Applications',
+ 'resourceType' => 'application',
+ ]);
}
// === Databases Category ===
diff --git a/app/Livewire/Project/Application/Configuration.php b/app/Livewire/Project/Application/Configuration.php
index 5d7f3fd31..109a212b1 100644
--- a/app/Livewire/Project/Application/Configuration.php
+++ b/app/Livewire/Project/Application/Configuration.php
@@ -58,6 +58,17 @@ class Configuration extends Component
if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') {
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
}
+
+ // Check for flash messages from redirect and dispatch as Livewire events
+ if (session('success')) {
+ $this->dispatch('success', session('success'));
+ }
+ if (session('warning')) {
+ $this->dispatch('warning', session('warning'));
+ }
+ if (session('error')) {
+ $this->dispatch('error', session('error'));
+ }
}
public function render()
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index c84de9d8d..10c4af9fb 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -936,6 +936,21 @@ class General extends Component
]);
}
+ public function downloadRepositoryConfig()
+ {
+ $this->authorize('view', $this->application);
+
+ $config = $this->application->generateRepositoryConfig();
+ $json = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+
+ return response()->streamDownload(function () use ($json) {
+ echo $json;
+ }, 'coolify.json', [
+ 'Content-Type' => 'application/json',
+ 'Content-Disposition' => 'attachment; filename=coolify.json',
+ ]);
+ }
+
private function updateServiceEnvironmentVariables()
{
$domains = collect(json_decode($this->application->docker_compose_domains, true)) ?? collect([]);
diff --git a/app/Livewire/Project/New/CoolifyJsonImport.php b/app/Livewire/Project/New/CoolifyJsonImport.php
new file mode 100644
index 000000000..f4572af93
--- /dev/null
+++ b/app/Livewire/Project/New/CoolifyJsonImport.php
@@ -0,0 +1,154 @@
+parameters = get_route_parameters();
+ $this->query = request()->query();
+ if (isDev()) {
+ $this->coolifyJson = json_encode([
+ 'version' => '1.0',
+ 'name' => 'My App',
+ 'source' => [
+ 'repository' => 'https://github.com/coollabsio/coolify-examples',
+ 'branch' => 'main',
+ ],
+ 'build' => [
+ 'type' => 'nixpacks',
+ ],
+ ], JSON_PRETTY_PRINT);
+ }
+ }
+
+ public function updatedCoolifyJson()
+ {
+ $this->parseError = null;
+ $this->parsedConfig = null;
+
+ if (empty(trim($this->coolifyJson))) {
+ return;
+ }
+
+ try {
+ $config = json_decode($this->coolifyJson, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->parseError = 'Invalid JSON: '.json_last_error_msg();
+
+ return;
+ }
+ $this->parsedConfig = $config;
+ } catch (\Exception $e) {
+ $this->parseError = 'Error parsing JSON: '.$e->getMessage();
+ }
+ }
+
+ public function submit()
+ {
+ $this->validate([
+ 'coolifyJson' => 'required',
+ ]);
+
+ // Parse the JSON
+ $config = json_decode($this->coolifyJson, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->dispatch('error', 'Invalid JSON format: '.json_last_error_msg());
+
+ return;
+ }
+
+ // Validate required fields
+ $source = data_get($config, 'source', []);
+ $repository = data_get($source, 'repository');
+ $branch = data_get($source, 'branch', 'main');
+
+ if (empty($repository)) {
+ $this->dispatch('error', 'Git repository URL is required in the source section');
+
+ return;
+ }
+
+ // Get destination
+ $destination_uuid = $this->query['destination'];
+ $destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
+ if (! $destination) {
+ $destination = SwarmDocker::where('uuid', $destination_uuid)->first();
+ }
+ if (! $destination) {
+ throw new \Exception('Destination not found. What?!');
+ }
+ $destination_class = $destination->getMorphClass();
+
+ // Get project and environment
+ $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
+ $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
+
+ // Determine build pack and port
+ $buildPack = data_get($config, 'build.type', 'nixpacks');
+ $portsExposes = data_get($config, 'domains.ports_exposes');
+
+ // If no explicit port in config, use sensible defaults based on build pack
+ // Same logic as other application creation flows (PublicGitRepository, etc.)
+ if ($portsExposes === null) {
+ $portsExposes = ($buildPack === 'static') ? '80' : '3000';
+ }
+
+ // Create the application with basic settings
+ $application = Application::create([
+ 'name' => data_get($config, 'name', 'app-'.new Cuid2),
+ 'description' => data_get($config, 'description'),
+ 'repository_project_id' => 0,
+ 'git_repository' => $repository,
+ 'git_branch' => $branch,
+ 'git_commit_sha' => data_get($source, 'commit_sha', 'HEAD'),
+ 'build_pack' => $buildPack,
+ 'ports_exposes' => $portsExposes,
+ 'environment_id' => $environment->id,
+ 'destination_id' => $destination->id,
+ 'destination_type' => $destination_class,
+ 'health_check_enabled' => false,
+ 'source_id' => 0,
+ 'source_type' => GithubApp::class,
+ ]);
+
+ // Generate FQDN
+ $fqdn = generateUrl(server: $destination->server, random: $application->uuid);
+ $application->update([
+ 'fqdn' => $fqdn,
+ ]);
+
+ // Apply the full configuration using setConfig
+ $application->setConfig($config);
+
+ return redirect()->route('project.application.configuration', [
+ 'application_uuid' => $application->uuid,
+ 'environment_uuid' => $environment->uuid,
+ 'project_uuid' => $project->uuid,
+ ]);
+ }
+
+ public function render()
+ {
+ return view('livewire.project.new.coolify-json-import');
+ }
+}
diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php
index 40d2674e2..eb70a4fdd 100644
--- a/app/Livewire/Project/New/GithubPrivateRepository.php
+++ b/app/Livewire/Project/New/GithubPrivateRepository.php
@@ -66,6 +66,8 @@ class GithubPrivateRepository extends Component
public bool $show_is_static = true;
+ public bool $checkCoolifyConfig = true;
+
public function mount()
{
$this->currentRoute = Route::currentRouteName();
@@ -211,6 +213,25 @@ class GithubPrivateRepository extends Component
$application->name = generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name, $application->uuid);
$application->save();
+ // Check for coolify.json configuration
+ try {
+ $gitRepository = 'https://github.com/'.$this->selected_repository_owner.'/'.$this->selected_repository_repo.'.git';
+ $config = loadConfigFromGit(
+ $gitRepository,
+ $this->selected_branch_name,
+ $this->base_directory ?? '/',
+ $destination->server->id,
+ auth()->user()->currentTeam()->id
+ );
+ if ($config) {
+ $application->setConfig($config, fromRepository: true);
+ session()->flash('success', 'coolify.json configuration detected and applied.');
+ }
+ } catch (\Exception $e) {
+ \Log::warning('coolify.json: Failed to apply config - '.$e->getMessage());
+ session()->flash('warning', 'coolify.json found but failed to apply: '.$e->getMessage());
+ }
+
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_uuid' => $environment->uuid,
diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
index 77b106200..8d77d2107 100644
--- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
+++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
@@ -49,6 +49,8 @@ class GithubPrivateRepositoryDeployKey extends Component
public bool $show_is_static = true;
+ public bool $checkCoolifyConfig = true;
+
private object $repository_url_parsed;
private GithubApp|GitlabApp|string $git_source = 'other';
@@ -199,6 +201,24 @@ class GithubPrivateRepositoryDeployKey extends Component
$application->name = generate_random_name($application->uuid);
$application->save();
+ // Check for coolify.json configuration
+ try {
+ $config = loadConfigFromGit(
+ $this->repository_url,
+ $this->branch,
+ $this->base_directory ?? '/',
+ $destination->server->id,
+ auth()->user()->currentTeam()->id
+ );
+ if ($config) {
+ $application->setConfig($config, fromRepository: true);
+ session()->flash('success', 'coolify.json configuration detected and applied.');
+ }
+ } catch (\Exception $e) {
+ \Log::warning('coolify.json: Failed to apply config - '.$e->getMessage());
+ session()->flash('warning', 'coolify.json found but failed to apply: '.$e->getMessage());
+ }
+
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_uuid' => $environment->uuid,
diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php
index 2fffff6b9..f6a2f5ff4 100644
--- a/app/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Livewire/Project/New/PublicGitRepository.php
@@ -363,10 +363,24 @@ class PublicGitRepository extends Component
$application->fqdn = $fqdn;
$application->save();
if ($this->checkCoolifyConfig) {
- // $config = loadConfigFromGit($this->repository_url, $this->git_branch, $this->base_directory, $this->query['server_id'], auth()->user()->currentTeam()->id);
- // if ($config) {
- // $application->setConfig($config);
- // }
+ try {
+ // Construct clean git URL from host and repository
+ $gitUrl = "https://{$this->git_host}/{$this->git_repository}.git";
+ $config = loadConfigFromGit(
+ $gitUrl,
+ $this->git_branch,
+ $this->base_directory,
+ $destination->server->id,
+ auth()->user()->currentTeam()->id
+ );
+ if ($config) {
+ $application->setConfig($config, fromRepository: true);
+ session()->flash('success', 'coolify.json configuration detected and applied.');
+ }
+ } catch (\Exception $e) {
+ \Log::warning('coolify.json: Failed to apply config - '.$e->getMessage());
+ session()->flash('warning', 'coolify.json found but failed to apply: '.$e->getMessage());
+ }
}
return redirect()->route('project.application.configuration', [
diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index c5dc13987..0cd97b9fb 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -188,6 +188,12 @@ class Select extends Component
'logo' => asset('svgs/docker.svg'),
],
];
+ $coolifyJsonImport = [
+ 'id' => 'coolify-json',
+ 'name' => 'Import from coolify.json',
+ 'description' => 'Paste a coolify.json configuration to quickly create and configure an application.',
+ 'logo' => asset('svgs/coolify-logo.svg'),
+ ];
$databases = [
[
'id' => 'postgresql',
@@ -251,6 +257,7 @@ class Select extends Component
'categories' => $categories,
'gitBasedApplications' => $gitBasedApplications,
'dockerBasedApplications' => $dockerBasedApplications,
+ 'coolifyJsonImport' => $coolifyJsonImport,
'databases' => $databases,
];
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 5006d0ff8..520509e4f 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
use RuntimeException;
@@ -2041,35 +2040,970 @@ class Application extends BaseModel
return $generator->toArray();
}
- public function setConfig($config)
+ /**
+ * Apply configuration from a coolify.json config array or JSON string.
+ *
+ * @param array|string $config The configuration to apply
+ * @param bool $fromRepository If true, the config comes from a repository's coolify.json file.
+ * In this case, the 'source' section (repository, branch, commit_sha)
+ * is ignored since these are already set from the actual git source.
+ * If false (default), the config is from a copy-paste import and
+ * source settings will be applied if present.
+ */
+ public function setConfig(array|string $config, bool $fromRepository = false): void
{
- $validator = Validator::make(['config' => $config], [
- 'config' => 'required|json',
- ]);
- if ($validator->fails()) {
- throw new \Exception('Invalid JSON format');
+ // Accept both JSON string and array
+ if (is_string($config)) {
+ $config = json_decode($config, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new \Exception('Invalid JSON format: '.json_last_error_msg());
+ }
}
- $config = json_decode($config, true);
- $deepValidator = Validator::make(['config' => $config], [
- 'config.build_pack' => 'required|string',
- 'config.base_directory' => 'required|string',
- 'config.publish_directory' => 'required|string',
- 'config.ports_exposes' => 'required|string',
- 'config.settings.is_static' => 'required|boolean',
- ]);
- if ($deepValidator->fails()) {
- throw new \Exception('Invalid data');
+ // Apply git source settings only for copy-paste import (not when loading from repository)
+ // When loading from a repository, the git source is already set from the actual repo
+ if (! $fromRepository && ($source = data_get($config, 'source'))) {
+ if (($value = data_get($source, 'repository')) !== null) {
+ $this->git_repository = $value;
+ }
+ if (($value = data_get($source, 'branch')) !== null) {
+ $this->git_branch = $value;
+ }
+ if (($value = data_get($source, 'commit_sha')) !== null) {
+ $this->git_commit_sha = $value;
+ }
}
- $config = $deepValidator->validated()['config'];
- try {
- $settings = data_get($config, 'settings', []);
- data_forget($config, 'settings');
- $this->update($config);
- $this->settings()->update($settings);
- } catch (\Exception $e) {
- throw new \Exception('Failed to update application settings');
+ // Apply build settings
+ if ($build = data_get($config, 'build')) {
+ $buildMappings = [
+ 'type' => 'build_pack',
+ 'base_directory' => 'base_directory',
+ 'publish_directory' => 'publish_directory',
+ 'dockerfile_location' => 'dockerfile_location',
+ 'docker_compose_location' => 'docker_compose_location',
+ 'install_command' => 'install_command',
+ 'build_command' => 'build_command',
+ 'start_command' => 'start_command',
+ 'watch_paths' => 'watch_paths',
+ 'static_image' => 'static_image',
+ 'dockerfile_target_build' => 'dockerfile_target_build',
+ 'custom_docker_run_options' => 'custom_docker_run_options',
+ 'docker_compose_custom_start_command' => 'docker_compose_custom_start_command',
+ 'docker_compose_custom_build_command' => 'docker_compose_custom_build_command',
+ ];
+
+ foreach ($buildMappings as $configKey => $modelKey) {
+ if (($value = data_get($build, $configKey)) !== null) {
+ $this->{$modelKey} = $value;
+ }
+ }
+ }
+
+ // Apply domain settings
+ if ($domains = data_get($config, 'domains')) {
+ if (($value = data_get($domains, 'ports_exposes')) !== null) {
+ $this->ports_exposes = $value;
+ }
+ if (($value = data_get($domains, 'ports_mappings')) !== null) {
+ $this->ports_mappings = $value;
+ }
+ if (($value = data_get($domains, 'redirect')) !== null) {
+ $this->redirect = $value;
+ }
+ if (($value = data_get($domains, 'custom_nginx_configuration')) !== null) {
+ $this->custom_nginx_configuration = $value;
+ }
+ }
+
+ // Apply network aliases
+ if (($value = data_get($config, 'network_aliases')) !== null) {
+ $this->custom_network_aliases = is_array($value) ? implode(',', $value) : $value;
+ }
+
+ // Apply HTTP Basic Authentication
+ if ($httpAuth = data_get($config, 'http_basic_auth')) {
+ if (($value = data_get($httpAuth, 'enabled')) !== null) {
+ $this->is_http_basic_auth_enabled = $value;
+ }
+ if (($value = data_get($httpAuth, 'username')) !== null) {
+ $this->http_basic_auth_username = $value;
+ }
+ if (($value = data_get($httpAuth, 'password')) !== null) {
+ $this->http_basic_auth_password = $value;
+ }
+ }
+
+ // Apply health check settings
+ if ($healthCheck = data_get($config, 'health_check')) {
+ $healthMappings = [
+ 'enabled' => 'health_check_enabled',
+ 'path' => 'health_check_path',
+ 'port' => 'health_check_port',
+ 'host' => 'health_check_host',
+ 'method' => 'health_check_method',
+ 'return_code' => 'health_check_return_code',
+ 'scheme' => 'health_check_scheme',
+ 'response_text' => 'health_check_response_text',
+ 'interval' => 'health_check_interval',
+ 'timeout' => 'health_check_timeout',
+ 'retries' => 'health_check_retries',
+ 'start_period' => 'health_check_start_period',
+ ];
+
+ foreach ($healthMappings as $configKey => $modelKey) {
+ if (($value = data_get($healthCheck, $configKey)) !== null) {
+ $this->{$modelKey} = $value;
+ }
+ }
+ }
+
+ // Apply resource limits
+ if ($limits = data_get($config, 'limits')) {
+ $limitMappings = [
+ 'memory' => 'limits_memory',
+ 'memory_swap' => 'limits_memory_swap',
+ 'memory_swappiness' => 'limits_memory_swappiness',
+ 'memory_reservation' => 'limits_memory_reservation',
+ 'cpus' => 'limits_cpus',
+ 'cpuset' => 'limits_cpuset',
+ 'cpu_shares' => 'limits_cpu_shares',
+ ];
+
+ foreach ($limitMappings as $configKey => $modelKey) {
+ if (($value = data_get($limits, $configKey)) !== null) {
+ $this->{$modelKey} = $value;
+ }
+ }
+ }
+
+ // Apply name and description
+ if (($value = data_get($config, 'name')) !== null) {
+ $this->name = $value;
+ }
+ if (($value = data_get($config, 'description')) !== null) {
+ $this->description = $value;
+ }
+
+ $this->save();
+
+ // Apply application settings
+ if ($settings = data_get($config, 'settings')) {
+ $settingMappings = [
+ 'is_static' => 'is_static',
+ 'is_spa' => 'is_spa',
+ 'is_auto_deploy_enabled' => 'is_auto_deploy_enabled',
+ 'is_force_https_enabled' => 'is_force_https_enabled',
+ 'is_preview_deployments_enabled' => 'is_preview_deployments_enabled',
+ 'is_pr_deployments_public_enabled' => 'is_pr_deployments_public_enabled',
+ 'is_git_submodules_enabled' => 'is_git_submodules_enabled',
+ 'is_git_lfs_enabled' => 'is_git_lfs_enabled',
+ 'is_git_shallow_clone_enabled' => 'is_git_shallow_clone_enabled',
+ 'is_build_server_enabled' => 'is_build_server_enabled',
+ 'is_preserve_repository_enabled' => 'is_preserve_repository_enabled',
+ 'is_container_label_escape_enabled' => 'is_container_label_escape_enabled',
+ 'is_container_label_readonly_enabled' => 'is_container_label_readonly_enabled',
+ 'use_build_secrets' => 'use_build_secrets',
+ 'inject_build_args_to_dockerfile' => 'inject_build_args_to_dockerfile',
+ 'include_source_commit_in_build' => 'include_source_commit_in_build',
+ 'is_debug_enabled' => 'is_debug_enabled',
+ 'is_consistent_container_name_enabled' => 'is_consistent_container_name_enabled',
+ 'connect_to_docker_network' => 'connect_to_docker_network',
+ 'custom_internal_name' => 'custom_internal_name',
+ 'is_env_sorting_enabled' => 'is_env_sorting_enabled',
+ ];
+
+ $settingsToUpdate = [];
+ foreach ($settingMappings as $configKey => $modelKey) {
+ if (($value = data_get($settings, $configKey)) !== null) {
+ $settingsToUpdate[$modelKey] = $value;
+ }
+ }
+
+ if (! empty($settingsToUpdate)) {
+ $this->settings()->update($settingsToUpdate);
+ }
+ }
+
+ // Apply pre/post deployment commands
+ if ($commands = data_get($config, 'deployment_commands')) {
+ if (($value = data_get($commands, 'pre_deployment_command')) !== null) {
+ $this->pre_deployment_command = $value;
+ }
+ if (($value = data_get($commands, 'pre_deployment_command_container')) !== null) {
+ $this->pre_deployment_command_container = $value;
+ }
+ if (($value = data_get($commands, 'post_deployment_command')) !== null) {
+ $this->post_deployment_command = $value;
+ }
+ if (($value = data_get($commands, 'post_deployment_command_container')) !== null) {
+ $this->post_deployment_command_container = $value;
+ }
+ $this->save();
+ }
+
+ // Apply preview settings
+ if ($preview = data_get($config, 'preview')) {
+ if (($value = data_get($preview, 'preview_url_template')) !== null) {
+ $this->preview_url_template = $value;
+ $this->save();
+ }
+ }
+
+ // Apply swarm settings
+ if ($swarm = data_get($config, 'swarm')) {
+ if (($value = data_get($swarm, 'swarm_replicas')) !== null) {
+ $this->swarm_replicas = $value;
+ }
+ if (($value = data_get($swarm, 'swarm_placement_constraints')) !== null) {
+ $this->swarm_placement_constraints = $value;
+ }
+ $this->save();
+ }
+
+ // Apply docker registry settings
+ if ($dockerRegistry = data_get($config, 'docker_registry')) {
+ if (($value = data_get($dockerRegistry, 'image')) !== null) {
+ $this->docker_registry_image_name = $value;
+ }
+ if (($value = data_get($dockerRegistry, 'tag')) !== null) {
+ $this->docker_registry_image_tag = $value;
+ }
+ $this->save();
+ }
+
+ // Apply persistent storages (Volume Mounts)
+ if ($persistentStorages = data_get($config, 'persistent_storages')) {
+ foreach ($persistentStorages as $storage) {
+ $name = data_get($storage, 'name');
+ $mountPath = data_get($storage, 'mount_path');
+
+ if (empty($name) || empty($mountPath)) {
+ continue;
+ }
+
+ // Check if storage with this name already exists
+ $existingStorage = $this->persistentStorages()
+ ->where('name', $name)
+ ->first();
+
+ if ($existingStorage) {
+ // Update existing storage
+ $existingStorage->update([
+ 'mount_path' => $mountPath,
+ 'host_path' => data_get($storage, 'host_path'),
+ ]);
+ } else {
+ // Create new storage
+ LocalPersistentVolume::create([
+ 'name' => $name,
+ 'mount_path' => $mountPath,
+ 'host_path' => data_get($storage, 'host_path'),
+ 'resource_id' => $this->id,
+ 'resource_type' => $this->getMorphClass(),
+ ]);
+ }
+ }
+ }
+
+ // Apply file mounts
+ if ($fileMounts = data_get($config, 'file_mounts')) {
+ foreach ($fileMounts as $file) {
+ $mountPath = data_get($file, 'mount_path');
+
+ if (empty($mountPath)) {
+ continue;
+ }
+
+ // Ensure mount_path starts with /
+ $mountPath = str($mountPath)->start('/')->value();
+
+ // Determine fs_path - use provided or generate default
+ $fsPath = data_get($file, 'fs_path');
+ if (empty($fsPath)) {
+ $fsPath = application_configuration_dir().'/'.$this->uuid.$mountPath;
+ }
+
+ // Check if file mount already exists
+ $existingFile = $this->fileStorages()
+ ->where('mount_path', $mountPath)
+ ->where('is_directory', false)
+ ->first();
+
+ if ($existingFile) {
+ // Update existing file mount
+ $existingFile->update([
+ 'fs_path' => $fsPath,
+ 'content' => data_get($file, 'content'),
+ ]);
+ } else {
+ // Create new file mount
+ LocalFileVolume::create([
+ 'fs_path' => $fsPath,
+ 'mount_path' => $mountPath,
+ 'content' => data_get($file, 'content'),
+ 'is_directory' => false,
+ 'resource_id' => $this->id,
+ 'resource_type' => $this->getMorphClass(),
+ ]);
+ }
+ }
+ }
+
+ // Apply directory mounts
+ if ($directoryMounts = data_get($config, 'directory_mounts')) {
+ foreach ($directoryMounts as $dir) {
+ $sourcePath = data_get($dir, 'source_path');
+ $mountPath = data_get($dir, 'mount_path');
+
+ if (empty($sourcePath) || empty($mountPath)) {
+ continue;
+ }
+
+ // Ensure paths start with /
+ $sourcePath = str($sourcePath)->start('/')->value();
+ $mountPath = str($mountPath)->start('/')->value();
+
+ // Check if directory mount already exists
+ $existingDir = $this->fileStorages()
+ ->where('mount_path', $mountPath)
+ ->where('is_directory', true)
+ ->first();
+
+ if ($existingDir) {
+ // Update existing directory mount
+ $existingDir->update([
+ 'fs_path' => $sourcePath,
+ ]);
+ } else {
+ // Create new directory mount
+ LocalFileVolume::create([
+ 'fs_path' => $sourcePath,
+ 'mount_path' => $mountPath,
+ 'is_directory' => true,
+ 'resource_id' => $this->id,
+ 'resource_type' => $this->getMorphClass(),
+ ]);
+ }
+ }
+ }
+
+ // Apply scheduled tasks
+ if ($scheduledTasks = data_get($config, 'scheduled_tasks')) {
+ foreach ($scheduledTasks as $task) {
+ $name = data_get($task, 'name');
+ $command = data_get($task, 'command');
+ $frequency = data_get($task, 'frequency');
+
+ if (empty($name) || empty($command) || empty($frequency)) {
+ continue;
+ }
+
+ // Check if scheduled task with this name already exists
+ $existingTask = $this->scheduled_tasks()
+ ->where('name', $name)
+ ->first();
+
+ if ($existingTask) {
+ // Update existing task
+ $existingTask->update([
+ 'command' => $command,
+ 'frequency' => $frequency,
+ 'container' => data_get($task, 'container'),
+ 'enabled' => data_get($task, 'enabled', true),
+ 'timeout' => data_get($task, 'timeout', 300),
+ ]);
+ } else {
+ // Create new scheduled task
+ ScheduledTask::create([
+ 'name' => $name,
+ 'command' => $command,
+ 'frequency' => $frequency,
+ 'container' => data_get($task, 'container'),
+ 'enabled' => data_get($task, 'enabled', true),
+ 'timeout' => data_get($task, 'timeout', 300),
+ 'application_id' => $this->id,
+ 'team_id' => $this->team()->id,
+ ]);
+ }
+ }
+ }
+
+ // Apply environment variables
+ $this->applyEnvironmentVariablesFromConfig($config);
+ }
+
+ protected function applyEnvironmentVariablesFromConfig(array $config): void
+ {
+ $envVars = data_get($config, 'environment_variables', []);
+
+ // Process production environment variables
+ $productionVars = data_get($envVars, 'production', []);
+ foreach ($productionVars as $var) {
+ $this->createEnvironmentVariableFromConfig($var, false);
+ }
+
+ // Process preview environment variables
+ $previewVars = data_get($envVars, 'preview', []);
+ foreach ($previewVars as $var) {
+ $this->createEnvironmentVariableFromConfig($var, true);
}
}
+
+ protected function createEnvironmentVariableFromConfig(array $var, bool $isPreview): void
+ {
+ $key = data_get($var, 'key');
+ $value = data_get($var, 'value', '');
+
+ if (empty($key)) {
+ return;
+ }
+
+ // Skip SERVICE_* variables as they are auto-generated
+ if (str($key)->startsWith('SERVICE_')) {
+ return;
+ }
+
+ // Resolve magic environment variables
+ $value = $this->resolveMagicEnvironmentVariable($value);
+
+ // Check if variable already exists
+ $existingVar = $this->environment_variables()
+ ->where('key', $key)
+ ->where('is_preview', $isPreview)
+ ->first();
+
+ if ($existingVar) {
+ // Update existing variable
+ // Defaults: is_buildtime=true, is_runtime=true (available during build AND runtime)
+ $existingVar->update([
+ 'value' => $value,
+ 'is_multiline' => data_get($var, 'is_multiline', false),
+ 'is_literal' => data_get($var, 'is_literal', false),
+ 'is_buildtime' => data_get($var, 'is_buildtime', true),
+ 'is_runtime' => data_get($var, 'is_runtime', true),
+ ]);
+ } else {
+ // Create new variable
+ // Defaults: is_buildtime=true, is_runtime=true (available during build AND runtime)
+ EnvironmentVariable::create([
+ 'key' => $key,
+ 'value' => $value,
+ 'is_preview' => $isPreview,
+ 'is_multiline' => data_get($var, 'is_multiline', false),
+ 'is_literal' => data_get($var, 'is_literal', false),
+ 'is_buildtime' => data_get($var, 'is_buildtime', true),
+ 'is_runtime' => data_get($var, 'is_runtime', true),
+ 'resourceable_id' => $this->id,
+ 'resourceable_type' => $this->getMorphClass(),
+ ]);
+ }
+ }
+
+ protected function resolveMagicEnvironmentVariable(string $value): string
+ {
+ // Check if this is a magic SERVICE_* value
+ if (! str_starts_with($value, 'SERVICE_')) {
+ return $value;
+ }
+
+ // Extract command from SERVICE_COMMAND format
+ $command = substr($value, strlen('SERVICE_'));
+
+ // Map coolify.json magic values to generateEnvValue() commands
+ // generateEnvValue uses: PASSWORD, PASSWORD_64, USER, BASE64_XX, REALBASE64_XX, HEX_XX, etc.
+ $commandMappings = [
+ 'PASSWORD' => 'PASSWORD_64', // SERVICE_PASSWORD -> 64-char password
+ 'UUID' => null, // SERVICE_UUID -> handled separately (Cuid2)
+ ];
+
+ // Check for direct mapping first
+ if (array_key_exists($command, $commandMappings)) {
+ if ($commandMappings[$command] === null) {
+ // SERVICE_UUID - Generate a Cuid2 (not supported by generateEnvValue)
+ return (string) new Cuid2;
+ }
+ $command = $commandMappings[$command];
+ }
+
+ // SERVICE_USER -> generateEnvValue('USER') returns 16-char random, add prefix
+ if ($command === 'USER') {
+ $generated = generateEnvValue('USER');
+
+ return $generated ? 'user_'.substr($generated, 0, 8) : 'user_'.Str::random(8);
+ }
+
+ // SERVICE_BASE64_XX -> REALBASE64_XX (actual base64 encoding)
+ if (preg_match('/^BASE64_(\d+)$/', $command, $matches)) {
+ $length = (int) $matches[1];
+ // Clamp between 8 and 256
+ $length = max(8, min(256, $length));
+ // Map to REALBASE64_XX if supported, otherwise generate directly
+ $realBase64Command = "REALBASE64_{$length}";
+ $generated = generateEnvValue($realBase64Command);
+ if ($generated) {
+ return $generated;
+ }
+
+ // Fallback: generate base64 directly
+ return base64_encode(Str::random($length));
+ }
+
+ // Try generateEnvValue for PASSWORD_XX and other commands
+ $generated = generateEnvValue($command);
+ if ($generated !== null) {
+ return $generated;
+ }
+
+ // If generateEnvValue returns null, handle PASSWORD_XX directly
+ if (preg_match('/^PASSWORD_(\d+)$/', $command, $matches)) {
+ $length = (int) $matches[1];
+ $length = max(8, min(256, $length));
+
+ return Str::password(length: $length, symbols: false);
+ }
+
+ // Not a recognized magic value, return as-is
+ return $value;
+ }
+
+ public function generateRepositoryConfig(): array
+ {
+ $config = [
+ 'version' => '1.0',
+ 'name' => $this->name,
+ ];
+
+ if ($this->description) {
+ $config['description'] = $this->description;
+ }
+
+ // Git source - optional, allows full app configuration from JSON
+ $gitSource = [];
+ if ($this->git_repository) {
+ $gitSource['repository'] = $this->git_repository;
+ }
+ if ($this->git_branch) {
+ $gitSource['branch'] = $this->git_branch;
+ }
+ if ($this->git_commit_sha && $this->git_commit_sha !== 'HEAD') {
+ $gitSource['commit_sha'] = $this->git_commit_sha;
+ }
+ if (! empty($gitSource)) {
+ $config['source'] = $gitSource;
+ }
+
+ // Build settings - only include non-default values
+ $build = [
+ 'type' => $this->build_pack,
+ ];
+ if ($this->base_directory && $this->base_directory !== '/') {
+ $build['base_directory'] = $this->base_directory;
+ }
+ if ($this->publish_directory) {
+ $build['publish_directory'] = $this->publish_directory;
+ }
+ if ($this->dockerfile_location && $this->dockerfile_location !== '/Dockerfile') {
+ $build['dockerfile_location'] = $this->dockerfile_location;
+ }
+ if ($this->docker_compose_location && $this->docker_compose_location !== '/docker-compose.yml' && $this->docker_compose_location !== '/docker-compose.yaml') {
+ $build['docker_compose_location'] = $this->docker_compose_location;
+ }
+ if ($this->install_command) {
+ $build['install_command'] = $this->install_command;
+ }
+ if ($this->build_command) {
+ $build['build_command'] = $this->build_command;
+ }
+ if ($this->start_command) {
+ $build['start_command'] = $this->start_command;
+ }
+ if ($this->watch_paths) {
+ $build['watch_paths'] = $this->watch_paths;
+ }
+ // Advanced build options - exclude default static_image
+ if ($this->static_image && $this->static_image !== 'nginx:alpine') {
+ $build['static_image'] = $this->static_image;
+ }
+ if ($this->dockerfile_target_build) {
+ $build['dockerfile_target_build'] = $this->dockerfile_target_build;
+ }
+ if ($this->custom_docker_run_options) {
+ $build['custom_docker_run_options'] = $this->custom_docker_run_options;
+ }
+ if ($this->docker_compose_custom_start_command) {
+ $build['docker_compose_custom_start_command'] = $this->docker_compose_custom_start_command;
+ }
+ if ($this->docker_compose_custom_build_command) {
+ $build['docker_compose_custom_build_command'] = $this->docker_compose_custom_build_command;
+ }
+ $config['build'] = $build;
+
+ // Domain settings - only include non-default values
+ $domains = [];
+ if ($this->ports_exposes && $this->ports_exposes !== '80') {
+ $domains['ports_exposes'] = $this->ports_exposes;
+ }
+ if ($this->ports_mappings) {
+ $domains['ports_mappings'] = $this->ports_mappings;
+ }
+ if ($this->redirect && $this->redirect !== 'both') {
+ $domains['redirect'] = $this->redirect;
+ }
+ if ($this->custom_nginx_configuration) {
+ $domains['custom_nginx_configuration'] = $this->custom_nginx_configuration;
+ }
+ if (! empty($domains)) {
+ $config['domains'] = $domains;
+ }
+
+ // Environment variables (with decrypted values for export)
+ // Only include non-default flags to keep the export clean
+ // Filter out SERVICE_* variables as they are auto-generated by Coolify
+ $mapEnvVar = function ($var) {
+ $result = [
+ 'key' => $var->key,
+ 'value' => $var->value,
+ ];
+ // Only include flags if they differ from defaults (true)
+ if (! $var->is_buildtime) {
+ $result['is_buildtime'] = false;
+ }
+ if (! $var->is_runtime) {
+ $result['is_runtime'] = false;
+ }
+ // Only include if true (default is false)
+ if ($var->is_literal) {
+ $result['is_literal'] = true;
+ }
+ if ($var->is_multiline) {
+ $result['is_multiline'] = true;
+ }
+
+ return $result;
+ };
+
+ // Filter out auto-generated SERVICE_* variables (SERVICE_FQDN_*, SERVICE_URL_*, SERVICE_PASSWORD_*, etc.)
+ $filterServiceVars = function ($var) {
+ return ! str($var->key)->startsWith('SERVICE_');
+ };
+
+ $productionVars = $this->environment_variables()
+ ->where('is_preview', false)
+ ->get()
+ ->filter($filterServiceVars)
+ ->values()
+ ->map($mapEnvVar)
+ ->toArray();
+
+ $previewVars = $this->environment_variables()
+ ->where('is_preview', true)
+ ->get()
+ ->filter($filterServiceVars)
+ ->values()
+ ->map($mapEnvVar)
+ ->toArray();
+
+ if (! empty($productionVars) || ! empty($previewVars)) {
+ $config['environment_variables'] = [];
+ if (! empty($productionVars)) {
+ $config['environment_variables']['production'] = $productionVars;
+ }
+ if (! empty($previewVars)) {
+ $config['environment_variables']['preview'] = $previewVars;
+ }
+ }
+
+ // Health check settings - only include non-default values
+ // Default: health_check_enabled=false (changed in 2024_07_19 migration)
+ $healthCheck = [];
+ if ($this->health_check_enabled) {
+ $healthCheck['enabled'] = true;
+ }
+ if ($this->health_check_path && $this->health_check_path !== '/') {
+ $healthCheck['path'] = $this->health_check_path;
+ }
+ if ($this->health_check_port) {
+ $healthCheck['port'] = $this->health_check_port;
+ }
+ if ($this->health_check_host && $this->health_check_host !== 'localhost') {
+ $healthCheck['host'] = $this->health_check_host;
+ }
+ if ($this->health_check_method && $this->health_check_method !== 'GET') {
+ $healthCheck['method'] = $this->health_check_method;
+ }
+ if ($this->health_check_return_code && $this->health_check_return_code !== 200) {
+ $healthCheck['return_code'] = $this->health_check_return_code;
+ }
+ if ($this->health_check_scheme && $this->health_check_scheme !== 'http') {
+ $healthCheck['scheme'] = $this->health_check_scheme;
+ }
+ if ($this->health_check_response_text) {
+ $healthCheck['response_text'] = $this->health_check_response_text;
+ }
+ if ($this->health_check_interval && $this->health_check_interval !== 5) {
+ $healthCheck['interval'] = $this->health_check_interval;
+ }
+ if ($this->health_check_timeout && $this->health_check_timeout !== 5) {
+ $healthCheck['timeout'] = $this->health_check_timeout;
+ }
+ if ($this->health_check_retries && $this->health_check_retries !== 10) {
+ $healthCheck['retries'] = $this->health_check_retries;
+ }
+ if ($this->health_check_start_period && $this->health_check_start_period !== 5) {
+ $healthCheck['start_period'] = $this->health_check_start_period;
+ }
+ if (! empty($healthCheck)) {
+ $config['health_check'] = $healthCheck;
+ }
+
+ // Resource limits (only if non-default)
+ $limits = [];
+ if ($this->limits_memory && $this->limits_memory !== '0') {
+ $limits['memory'] = $this->limits_memory;
+ }
+ if ($this->limits_memory_swap && $this->limits_memory_swap !== '0') {
+ $limits['memory_swap'] = $this->limits_memory_swap;
+ }
+ if ($this->limits_memory_swappiness && $this->limits_memory_swappiness !== 60) {
+ $limits['memory_swappiness'] = $this->limits_memory_swappiness;
+ }
+ if ($this->limits_memory_reservation && $this->limits_memory_reservation !== '0') {
+ $limits['memory_reservation'] = $this->limits_memory_reservation;
+ }
+ if ($this->limits_cpus && $this->limits_cpus !== '0') {
+ $limits['cpus'] = $this->limits_cpus;
+ }
+ if ($this->limits_cpuset && $this->limits_cpuset !== '0') {
+ $limits['cpuset'] = $this->limits_cpuset;
+ }
+ if ($this->limits_cpu_shares && $this->limits_cpu_shares !== 1024) {
+ $limits['cpu_shares'] = $this->limits_cpu_shares;
+ }
+ if (! empty($limits)) {
+ $config['limits'] = $limits;
+ }
+
+ // Application settings - only export non-default values
+ $settings = $this->settings;
+ $configSettings = [];
+
+ // Settings with default=false - only include if true
+ if ($settings->is_static) {
+ $configSettings['is_static'] = true;
+ }
+ if ($settings->is_spa) {
+ $configSettings['is_spa'] = true;
+ }
+ if ($settings->is_preview_deployments_enabled) {
+ $configSettings['is_preview_deployments_enabled'] = true;
+ }
+ if ($settings->is_pr_deployments_public_enabled) {
+ $configSettings['is_pr_deployments_public_enabled'] = true;
+ }
+ if ($settings->is_build_server_enabled) {
+ $configSettings['is_build_server_enabled'] = true;
+ }
+ if ($settings->is_preserve_repository_enabled) {
+ $configSettings['is_preserve_repository_enabled'] = true;
+ }
+ if ($settings->use_build_secrets) {
+ $configSettings['use_build_secrets'] = true;
+ }
+ if ($settings->include_source_commit_in_build) {
+ $configSettings['include_source_commit_in_build'] = true;
+ }
+ if ($settings->is_debug_enabled) {
+ $configSettings['is_debug_enabled'] = true;
+ }
+ if ($settings->is_consistent_container_name_enabled) {
+ $configSettings['is_consistent_container_name_enabled'] = true;
+ }
+ if ($settings->connect_to_docker_network) {
+ $configSettings['connect_to_docker_network'] = true;
+ }
+ if ($settings->is_env_sorting_enabled) {
+ $configSettings['is_env_sorting_enabled'] = true;
+ }
+
+ // Settings with default=true - only include if false
+ if (! $settings->is_auto_deploy_enabled) {
+ $configSettings['is_auto_deploy_enabled'] = false;
+ }
+ if (! $settings->is_force_https_enabled) {
+ $configSettings['is_force_https_enabled'] = false;
+ }
+ if (! $settings->is_git_submodules_enabled) {
+ $configSettings['is_git_submodules_enabled'] = false;
+ }
+ if (! $settings->is_git_lfs_enabled) {
+ $configSettings['is_git_lfs_enabled'] = false;
+ }
+ if (! $settings->is_git_shallow_clone_enabled) {
+ $configSettings['is_git_shallow_clone_enabled'] = false;
+ }
+ if (! $settings->is_container_label_escape_enabled) {
+ $configSettings['is_container_label_escape_enabled'] = false;
+ }
+ if (! $settings->is_container_label_readonly_enabled) {
+ $configSettings['is_container_label_readonly_enabled'] = false;
+ }
+ if (! $settings->inject_build_args_to_dockerfile) {
+ $configSettings['inject_build_args_to_dockerfile'] = false;
+ }
+
+ // String settings - only include if set
+ if ($settings->custom_internal_name) {
+ $configSettings['custom_internal_name'] = $settings->custom_internal_name;
+ }
+
+ if (! empty($configSettings)) {
+ $config['settings'] = $configSettings;
+ }
+
+ // Network aliases
+ if ($this->custom_network_aliases) {
+ $config['network_aliases'] = explode(',', $this->custom_network_aliases);
+ }
+
+ // HTTP Basic Authentication (don't export password for security)
+ if ($this->is_http_basic_auth_enabled) {
+ $config['http_basic_auth'] = [
+ 'enabled' => true,
+ 'username' => $this->http_basic_auth_username,
+ // Password is intentionally not exported for security reasons
+ ];
+ }
+
+ // Pre/Post deployment commands
+ $deploymentCommands = [];
+ if ($this->pre_deployment_command) {
+ $deploymentCommands['pre_deployment_command'] = $this->pre_deployment_command;
+ }
+ if ($this->pre_deployment_command_container) {
+ $deploymentCommands['pre_deployment_command_container'] = $this->pre_deployment_command_container;
+ }
+ if ($this->post_deployment_command) {
+ $deploymentCommands['post_deployment_command'] = $this->post_deployment_command;
+ }
+ if ($this->post_deployment_command_container) {
+ $deploymentCommands['post_deployment_command_container'] = $this->post_deployment_command_container;
+ }
+ if (! empty($deploymentCommands)) {
+ $config['deployment_commands'] = $deploymentCommands;
+ }
+
+ // Preview settings - only include if different from default
+ if ($this->preview_url_template && $this->preview_url_template !== '{{pr_id}}.{{domain}}') {
+ $config['preview'] = [
+ 'preview_url_template' => $this->preview_url_template,
+ ];
+ }
+
+ // Swarm settings
+ $swarm = [];
+ if ($this->swarm_replicas && $this->swarm_replicas !== 1) {
+ $swarm['swarm_replicas'] = $this->swarm_replicas;
+ }
+ if ($this->swarm_placement_constraints) {
+ $swarm['swarm_placement_constraints'] = $this->swarm_placement_constraints;
+ }
+ if (! empty($swarm)) {
+ $config['swarm'] = $swarm;
+ }
+
+ // Docker registry settings
+ $dockerRegistry = [];
+ if ($this->docker_registry_image_name) {
+ $dockerRegistry['image'] = $this->docker_registry_image_name;
+ }
+ if ($this->docker_registry_image_tag) {
+ $dockerRegistry['tag'] = $this->docker_registry_image_tag;
+ }
+ if (! empty($dockerRegistry)) {
+ $config['docker_registry'] = $dockerRegistry;
+ }
+
+ // Persistent storages (Volume Mounts)
+ $persistentStorages = $this->persistentStorages->map(function ($storage) {
+ $storageConfig = [
+ 'name' => $storage->name,
+ 'mount_path' => $storage->mount_path,
+ ];
+ if ($storage->host_path) {
+ $storageConfig['host_path'] = $storage->host_path;
+ }
+
+ return $storageConfig;
+ })->toArray();
+
+ if (! empty($persistentStorages)) {
+ $config['persistent_storages'] = $persistentStorages;
+ }
+
+ // File Mounts (single files with content)
+ $fileMounts = $this->fileStorages()
+ ->where('is_directory', false)
+ ->get()
+ ->map(function ($file) {
+ $fileConfig = [
+ 'mount_path' => $file->mount_path,
+ ];
+ // Include content if not binary
+ if ($file->content && $file->content !== '[binary file]') {
+ $fileConfig['content'] = $file->content;
+ }
+ // Include fs_path if different from mount_path
+ if ($file->fs_path && $file->fs_path !== $file->mount_path) {
+ $fileConfig['fs_path'] = $file->fs_path;
+ }
+
+ return $fileConfig;
+ })->toArray();
+
+ if (! empty($fileMounts)) {
+ $config['file_mounts'] = $fileMounts;
+ }
+
+ // Directory Mounts
+ $directoryMounts = $this->fileStorages()
+ ->where('is_directory', true)
+ ->get()
+ ->map(function ($dir) {
+ return [
+ 'source_path' => $dir->fs_path,
+ 'mount_path' => $dir->mount_path,
+ ];
+ })->toArray();
+
+ if (! empty($directoryMounts)) {
+ $config['directory_mounts'] = $directoryMounts;
+ }
+
+ // Scheduled Tasks (cron jobs)
+ $scheduledTasks = $this->scheduled_tasks->map(function ($task) {
+ $taskConfig = [
+ 'name' => $task->name,
+ 'command' => $task->command,
+ 'frequency' => $task->frequency,
+ ];
+ if ($task->container) {
+ $taskConfig['container'] = $task->container;
+ }
+ if (! $task->enabled) {
+ $taskConfig['enabled'] = false;
+ }
+ if ($task->timeout && $task->timeout !== 300) {
+ $taskConfig['timeout'] = $task->timeout;
+ }
+
+ return $taskConfig;
+ })->toArray();
+
+ if (! empty($scheduledTasks)) {
+ $config['scheduled_tasks'] = $scheduledTasks;
+ }
+
+ return $config;
+ }
}
diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php
index 84bde5393..3728a9657 100644
--- a/bootstrap/helpers/api.php
+++ b/bootstrap/helpers/api.php
@@ -135,6 +135,7 @@ function sharedDataApplications()
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
+ 'use_coolify_json' => 'boolean',
];
}
@@ -179,4 +180,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('is_static');
$request->offsetUnset('force_domain_override');
$request->offsetUnset('autogenerate_domain');
+ $request->offsetUnset('use_coolify_json');
}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 9fc1e6f1c..e81b151de 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -35,6 +35,7 @@ use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Request;
@@ -2963,31 +2964,91 @@ function getHelperVersion(): string
return config('constants.coolify.helper_version');
}
-function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id)
+function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id): ?array
{
- $server = Server::find($server_id)->where('team_id', $team_id)->first();
+ Log::info("coolify.json: Starting detection for {$repository} branch {$branch} base_dir {$base_directory}");
+
+ $server = Server::where('id', $server_id)->where('team_id', $team_id)->first();
if (! $server) {
- return;
+ Log::warning("coolify.json: Server not found - server_id: {$server_id}, team_id: {$team_id}");
+
+ return null;
}
+
$uuid = new Cuid2;
- $cloneCommand = "git clone --no-checkout -b $branch $repository .";
+ $cloneCommand = "git clone --no-checkout -b {$branch} {$repository} .";
$workdir = rtrim($base_directory, '/');
- $fileList = collect([".$workdir/coolify.json"]);
+
+ // Build paths to check: base_directory/coolify.json first, then repo root
+ $pathsToCheck = [];
+ if ($workdir !== '' && $workdir !== '/') {
+ $pathsToCheck[] = ltrim($workdir, '/').'/coolify.json';
+ }
+ $pathsToCheck[] = 'coolify.json';
+
+ Log::info('coolify.json: Checking paths - '.implode(', ', $pathsToCheck));
+
+ // Build sparse-checkout file list
+ $fileList = collect($pathsToCheck)->map(fn ($path) => "./{$path}")->implode(' ');
+
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
- "git sparse-checkout set {$fileList->implode(' ')}",
+ "git sparse-checkout set {$fileList}",
'git read-tree -mu HEAD',
- "cat .$workdir/coolify.json",
- 'rm -rf /tmp/{$uuid}',
]);
+
+ // Add cat commands for each path, trying base_directory first
+ // Use subshell to capture output before cleanup
+ $catCommands = collect($pathsToCheck)->map(fn ($path) => "cat ./{$path} 2>/dev/null")->implode(' || ');
+ $commands->push("({$catCommands}) || true");
+ $commands->push("cd / && rm -rf /tmp/{$uuid}");
+
try {
- return instant_remote_process($commands, $server);
- } catch (\Exception) {
- // continue
+ $output = instant_remote_process($commands, $server);
+
+ Log::info('coolify.json: Raw output length - '.strlen($output ?? ''));
+
+ if (empty($output)) {
+ Log::info('coolify.json: No output from remote process');
+
+ return null;
+ }
+
+ $config = json_decode($output, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ Log::warning('coolify.json: Invalid JSON format - '.json_last_error_msg().' - output was: '.substr($output, 0, 200));
+
+ return null;
+ }
+
+ // Validate schema version
+ $version = data_get($config, 'version', '1.0');
+ $supportedVersions = ['1.0'];
+ if (! in_array($version, $supportedVersions)) {
+ Log::warning('coolify.json: Unsupported schema version - '.$version);
+
+ return null;
+ }
+
+ // Warn about unknown top-level fields
+ $knownFields = ['version', 'name', 'description', 'build', 'domains', 'environment_variables', 'health_check', 'limits', 'settings'];
+ $unknownFields = array_diff(array_keys($config), $knownFields);
+ if (! empty($unknownFields)) {
+ Log::info('coolify.json: Unknown fields detected (will be ignored) - '.implode(', ', $unknownFields));
+ }
+
+ Log::info('coolify.json: Successfully loaded config with keys - '.implode(', ', array_keys($config)));
+
+ return $config;
+ } catch (\Exception $e) {
+ Log::warning('coolify.json: Exception during detection - '.$e->getMessage());
+
+ return null;
}
}
diff --git a/openapi.json b/openapi.json
index fe8ca863e..08dff5443 100644
--- a/openapi.json
+++ b/openapi.json
@@ -366,6 +366,10 @@
"type": "boolean",
"default": true,
"description": "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
+ },
+ "use_coolify_json": {
+ "type": "boolean",
+ "description": "Check repository for coolify.json and apply configuration if found. Default is true."
}
},
"type": "object"
@@ -781,6 +785,10 @@
"type": "boolean",
"default": true,
"description": "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
+ },
+ "use_coolify_json": {
+ "type": "boolean",
+ "description": "Check repository for coolify.json and apply configuration if found. Default is true."
}
},
"type": "object"
@@ -1196,6 +1204,10 @@
"type": "boolean",
"default": true,
"description": "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
+ },
+ "use_coolify_json": {
+ "type": "boolean",
+ "description": "Check repository for coolify.json and apply configuration if found. Default is true."
}
},
"type": "object"
diff --git a/openapi.yaml b/openapi.yaml
index a7faa8c72..be4cceaf6 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -269,6 +269,9 @@ paths:
type: boolean
default: true
description: "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
+ use_coolify_json:
+ type: boolean
+ description: 'Check repository for coolify.json and apply configuration if found. Default is true.'
type: object
responses:
'201':
@@ -539,6 +542,9 @@ paths:
type: boolean
default: true
description: "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
+ use_coolify_json:
+ type: boolean
+ description: 'Check repository for coolify.json and apply configuration if found. Default is true.'
type: object
responses:
'201':
@@ -809,6 +815,9 @@ paths:
type: boolean
default: true
description: "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Default: true."
+ use_coolify_json:
+ type: boolean
+ description: 'Check repository for coolify.json and apply configuration if found. Default is true.'
type: object
responses:
'201':
diff --git a/public/schemas/coolify.schema.json b/public/schemas/coolify.schema.json
new file mode 100644
index 000000000..1f4382631
--- /dev/null
+++ b/public/schemas/coolify.schema.json
@@ -0,0 +1,664 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://coolify.io/schemas/coolify.schema.json",
+ "title": "Coolify Application Configuration",
+ "description": "Schema for coolify.json configuration files used to configure Coolify applications",
+ "type": "object",
+ "properties": {
+ "$schema": {
+ "type": "string",
+ "description": "Reference to this JSON schema for IDE support"
+ },
+ "version": {
+ "type": "string",
+ "description": "Schema version for future compatibility",
+ "enum": ["1.0"],
+ "default": "1.0"
+ },
+ "name": {
+ "type": "string",
+ "description": "Application name displayed in Coolify"
+ },
+ "description": {
+ "type": "string",
+ "description": "Application description"
+ },
+ "source": {
+ "type": "object",
+ "description": "Git source configuration (only used when importing via copy-paste, ignored when coolify.json is in repository)",
+ "properties": {
+ "repository": {
+ "type": "string",
+ "description": "Git repository URL (e.g., https://github.com/user/repo)",
+ "examples": ["https://github.com/coollabsio/coolify-examples"]
+ },
+ "branch": {
+ "type": "string",
+ "description": "Git branch to deploy",
+ "default": "main"
+ },
+ "commit_sha": {
+ "type": "string",
+ "description": "Specific commit SHA to deploy",
+ "default": "HEAD"
+ }
+ }
+ },
+ "build": {
+ "type": "object",
+ "description": "Build configuration settings",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Build pack type",
+ "enum": ["nixpacks", "dockerfile", "dockercompose", "dockerimage", "static"],
+ "default": "nixpacks"
+ },
+ "base_directory": {
+ "type": "string",
+ "description": "Base directory for the build (relative to repository root)",
+ "default": "/",
+ "examples": ["/", "/app", "/frontend"]
+ },
+ "publish_directory": {
+ "type": "string",
+ "description": "Directory containing built files to publish (for static builds)",
+ "examples": ["/dist", "/build", "/public"]
+ },
+ "dockerfile_location": {
+ "type": "string",
+ "description": "Path to Dockerfile (relative to base_directory)",
+ "default": "/Dockerfile",
+ "examples": ["/Dockerfile", "/docker/Dockerfile.prod"]
+ },
+ "docker_compose_location": {
+ "type": "string",
+ "description": "Path to docker-compose file (relative to base_directory)",
+ "default": "/docker-compose.yml",
+ "examples": ["/docker-compose.yml", "/docker-compose.prod.yml"]
+ },
+ "install_command": {
+ "type": "string",
+ "description": "Custom install command (overrides auto-detected)",
+ "examples": ["npm ci", "yarn install --frozen-lockfile", "pnpm install"]
+ },
+ "build_command": {
+ "type": "string",
+ "description": "Custom build command (overrides auto-detected)",
+ "examples": ["npm run build", "yarn build", "cargo build --release"]
+ },
+ "start_command": {
+ "type": "string",
+ "description": "Custom start command (overrides auto-detected)",
+ "examples": ["npm start", "node server.js", "./target/release/app"]
+ },
+ "watch_paths": {
+ "type": "string",
+ "description": "Comma-separated paths to watch for changes (triggers rebuild)",
+ "examples": ["src,package.json", "**/*.rs,Cargo.toml"]
+ },
+ "static_image": {
+ "type": "string",
+ "description": "Docker image to use for static file serving",
+ "default": "nginx:alpine"
+ },
+ "dockerfile_target_build": {
+ "type": "string",
+ "description": "Docker build target stage (for multi-stage builds)",
+ "examples": ["production", "builder"]
+ },
+ "custom_docker_run_options": {
+ "type": "string",
+ "description": "Additional docker run options",
+ "examples": ["--cap-add=SYS_ADMIN", "--privileged"]
+ },
+ "docker_compose_custom_start_command": {
+ "type": "string",
+ "description": "Custom docker compose start command",
+ "examples": ["docker compose up -d --build"]
+ },
+ "docker_compose_custom_build_command": {
+ "type": "string",
+ "description": "Custom docker compose build command",
+ "examples": ["docker compose build --no-cache"]
+ }
+ }
+ },
+ "domains": {
+ "type": "object",
+ "description": "Domain and port configuration",
+ "properties": {
+ "ports_exposes": {
+ "type": "string",
+ "description": "Ports to expose (comma-separated for multiple)",
+ "default": "3000",
+ "examples": ["3000", "80", "3000,8080"]
+ },
+ "ports_mappings": {
+ "type": "string",
+ "description": "Port mappings in format host:container (comma-separated)",
+ "examples": ["8080:3000", "80:3000,443:3001"]
+ },
+ "redirect": {
+ "type": "string",
+ "description": "Redirect configuration",
+ "enum": ["www", "non-www", "both"]
+ },
+ "custom_nginx_configuration": {
+ "type": "string",
+ "description": "Custom Nginx configuration block"
+ }
+ }
+ },
+ "network_aliases": {
+ "type": "array",
+ "description": "Custom network aliases for the container",
+ "items": {
+ "type": "string"
+ },
+ "examples": [["api", "backend"]]
+ },
+ "http_basic_auth": {
+ "type": "object",
+ "description": "HTTP Basic Authentication settings",
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Enable HTTP Basic Authentication",
+ "default": false
+ },
+ "username": {
+ "type": "string",
+ "description": "Basic auth username"
+ },
+ "password": {
+ "type": "string",
+ "description": "Basic auth password"
+ }
+ }
+ },
+ "health_check": {
+ "type": "object",
+ "description": "Container health check configuration",
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Enable health checks",
+ "default": false
+ },
+ "path": {
+ "type": "string",
+ "description": "Health check endpoint path",
+ "default": "/",
+ "examples": ["/health", "/api/health", "/ping"]
+ },
+ "port": {
+ "type": ["integer", "string"],
+ "description": "Port for health check"
+ },
+ "host": {
+ "type": "string",
+ "description": "Host for health check",
+ "default": "localhost"
+ },
+ "method": {
+ "type": "string",
+ "description": "HTTP method for health check",
+ "enum": ["GET", "POST", "HEAD"],
+ "default": "GET"
+ },
+ "return_code": {
+ "type": "integer",
+ "description": "Expected HTTP return code",
+ "default": 200
+ },
+ "scheme": {
+ "type": "string",
+ "description": "Protocol scheme",
+ "enum": ["http", "https"],
+ "default": "http"
+ },
+ "response_text": {
+ "type": "string",
+ "description": "Expected response text to match"
+ },
+ "interval": {
+ "type": "integer",
+ "description": "Health check interval in seconds",
+ "default": 5,
+ "minimum": 1
+ },
+ "timeout": {
+ "type": "integer",
+ "description": "Health check timeout in seconds",
+ "default": 5,
+ "minimum": 1
+ },
+ "retries": {
+ "type": "integer",
+ "description": "Number of retries before marking unhealthy",
+ "default": 10,
+ "minimum": 1
+ },
+ "start_period": {
+ "type": "integer",
+ "description": "Grace period before health checks start (seconds)",
+ "default": 5,
+ "minimum": 0
+ }
+ }
+ },
+ "limits": {
+ "type": "object",
+ "description": "Container resource limits",
+ "properties": {
+ "memory": {
+ "type": "string",
+ "description": "Memory limit (e.g., 512m, 1g)",
+ "examples": ["256m", "512m", "1g", "2g"]
+ },
+ "memory_swap": {
+ "type": "string",
+ "description": "Memory + swap limit",
+ "examples": ["1g", "2g"]
+ },
+ "memory_swappiness": {
+ "type": "integer",
+ "description": "Swappiness value (0-100)",
+ "minimum": 0,
+ "maximum": 100
+ },
+ "memory_reservation": {
+ "type": "string",
+ "description": "Soft memory limit",
+ "examples": ["128m", "256m"]
+ },
+ "cpus": {
+ "type": "string",
+ "description": "CPU limit (e.g., 0.5, 1, 2)",
+ "examples": ["0.5", "1", "2"]
+ },
+ "cpuset": {
+ "type": "string",
+ "description": "CPUs to use (e.g., 0,1 or 0-3)",
+ "examples": ["0", "0,1", "0-3"]
+ },
+ "cpu_shares": {
+ "type": "integer",
+ "description": "CPU shares (relative weight)",
+ "minimum": 0
+ }
+ }
+ },
+ "settings": {
+ "type": "object",
+ "description": "Application behavior settings",
+ "properties": {
+ "is_static": {
+ "type": "boolean",
+ "description": "Treat as static site (serve files with Nginx)",
+ "default": false
+ },
+ "is_spa": {
+ "type": "boolean",
+ "description": "Enable SPA mode (redirect all routes to index.html)",
+ "default": false
+ },
+ "is_auto_deploy_enabled": {
+ "type": "boolean",
+ "description": "Auto-deploy on git push",
+ "default": true
+ },
+ "is_force_https_enabled": {
+ "type": "boolean",
+ "description": "Force HTTPS redirect",
+ "default": true
+ },
+ "is_preview_deployments_enabled": {
+ "type": "boolean",
+ "description": "Enable preview deployments for PRs",
+ "default": false
+ },
+ "is_pr_deployments_public_enabled": {
+ "type": "boolean",
+ "description": "Make PR deployments publicly accessible",
+ "default": false
+ },
+ "is_git_submodules_enabled": {
+ "type": "boolean",
+ "description": "Initialize git submodules",
+ "default": false
+ },
+ "is_git_lfs_enabled": {
+ "type": "boolean",
+ "description": "Enable Git LFS",
+ "default": false
+ },
+ "is_git_shallow_clone_enabled": {
+ "type": "boolean",
+ "description": "Use shallow clone (faster but limited history)",
+ "default": false
+ },
+ "is_build_server_enabled": {
+ "type": "boolean",
+ "description": "Use dedicated build server",
+ "default": false
+ },
+ "is_preserve_repository_enabled": {
+ "type": "boolean",
+ "description": "Preserve repository between builds",
+ "default": false
+ },
+ "is_container_label_escape_enabled": {
+ "type": "boolean",
+ "description": "Escape special characters in container labels",
+ "default": true
+ },
+ "is_container_label_readonly_enabled": {
+ "type": "boolean",
+ "description": "Make container labels read-only",
+ "default": false
+ },
+ "use_build_secrets": {
+ "type": "boolean",
+ "description": "Use Docker build secrets for environment variables",
+ "default": false
+ },
+ "inject_build_args_to_dockerfile": {
+ "type": "boolean",
+ "description": "Inject build args into Dockerfile",
+ "default": false
+ },
+ "include_source_commit_in_build": {
+ "type": "boolean",
+ "description": "Include source commit SHA in build metadata",
+ "default": false
+ },
+ "is_debug_enabled": {
+ "type": "boolean",
+ "description": "Enable debug mode",
+ "default": false
+ },
+ "is_consistent_container_name_enabled": {
+ "type": "boolean",
+ "description": "Use consistent container names across deployments",
+ "default": false
+ },
+ "connect_to_docker_network": {
+ "type": "string",
+ "description": "Additional Docker network to connect to"
+ },
+ "custom_internal_name": {
+ "type": "string",
+ "description": "Custom internal container name"
+ },
+ "is_env_sorting_enabled": {
+ "type": "boolean",
+ "description": "Sort environment variables alphabetically",
+ "default": false
+ }
+ }
+ },
+ "deployment_commands": {
+ "type": "object",
+ "description": "Pre and post deployment commands",
+ "properties": {
+ "pre_deployment_command": {
+ "type": "string",
+ "description": "Command to run before deployment"
+ },
+ "pre_deployment_command_container": {
+ "type": "string",
+ "description": "Container to run pre-deployment command in"
+ },
+ "post_deployment_command": {
+ "type": "string",
+ "description": "Command to run after deployment"
+ },
+ "post_deployment_command_container": {
+ "type": "string",
+ "description": "Container to run post-deployment command in"
+ }
+ }
+ },
+ "preview": {
+ "type": "object",
+ "description": "Preview deployment settings",
+ "properties": {
+ "preview_url_template": {
+ "type": "string",
+ "description": "Template for preview deployment URLs",
+ "examples": ["{{pr_id}}.preview.{{domain}}", "pr-{{pr_id}}.{{domain}}"]
+ }
+ }
+ },
+ "swarm": {
+ "type": "object",
+ "description": "Docker Swarm specific settings",
+ "properties": {
+ "swarm_replicas": {
+ "type": "integer",
+ "description": "Number of replicas in swarm mode",
+ "minimum": 1,
+ "default": 1
+ },
+ "swarm_placement_constraints": {
+ "type": "string",
+ "description": "Swarm placement constraints",
+ "examples": ["node.role==worker", "node.labels.type==compute"]
+ }
+ }
+ },
+ "docker_registry": {
+ "type": "object",
+ "description": "Docker registry settings for image builds",
+ "properties": {
+ "image": {
+ "type": "string",
+ "description": "Docker registry image name",
+ "examples": ["ghcr.io/myorg/myapp", "docker.io/myuser/myapp"]
+ },
+ "tag": {
+ "type": "string",
+ "description": "Docker image tag",
+ "default": "latest"
+ }
+ }
+ },
+ "persistent_storages": {
+ "type": "array",
+ "description": "Persistent volume mounts",
+ "items": {
+ "type": "object",
+ "required": ["name", "mount_path"],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Volume name"
+ },
+ "mount_path": {
+ "type": "string",
+ "description": "Path inside container to mount volume",
+ "examples": ["/data", "/app/uploads", "/var/lib/data"]
+ },
+ "host_path": {
+ "type": "string",
+ "description": "Optional host path for bind mount"
+ }
+ }
+ }
+ },
+ "file_mounts": {
+ "type": "array",
+ "description": "File mounts (inject files into container)",
+ "items": {
+ "type": "object",
+ "required": ["mount_path"],
+ "properties": {
+ "mount_path": {
+ "type": "string",
+ "description": "Path inside container to mount file",
+ "examples": ["/app/config.json", "/etc/nginx/nginx.conf"]
+ },
+ "fs_path": {
+ "type": "string",
+ "description": "Path on host filesystem (auto-generated if not specified)"
+ },
+ "content": {
+ "type": "string",
+ "description": "File content (for inline file creation)"
+ }
+ }
+ }
+ },
+ "directory_mounts": {
+ "type": "array",
+ "description": "Directory mounts (bind mount directories)",
+ "items": {
+ "type": "object",
+ "required": ["source_path", "mount_path"],
+ "properties": {
+ "source_path": {
+ "type": "string",
+ "description": "Source path on host",
+ "examples": ["/opt/data", "/home/user/config"]
+ },
+ "mount_path": {
+ "type": "string",
+ "description": "Path inside container",
+ "examples": ["/data", "/config"]
+ }
+ }
+ }
+ },
+ "scheduled_tasks": {
+ "type": "array",
+ "description": "Scheduled tasks (cron jobs)",
+ "items": {
+ "type": "object",
+ "required": ["name", "command", "frequency"],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Task name"
+ },
+ "command": {
+ "type": "string",
+ "description": "Command to execute",
+ "examples": ["php artisan schedule:run", "node cleanup.js", "/app/backup.sh"]
+ },
+ "frequency": {
+ "type": "string",
+ "description": "Cron expression for scheduling",
+ "examples": ["* * * * *", "0 * * * *", "0 0 * * *", "0 0 * * 0"]
+ },
+ "container": {
+ "type": "string",
+ "description": "Container to run command in (for compose apps)"
+ },
+ "enabled": {
+ "type": "boolean",
+ "description": "Whether the task is enabled",
+ "default": true
+ },
+ "timeout": {
+ "type": "integer",
+ "description": "Task timeout in seconds",
+ "default": 300
+ }
+ }
+ }
+ },
+ "environment_variables": {
+ "type": "object",
+ "description": "Environment variables configuration",
+ "properties": {
+ "production": {
+ "type": "array",
+ "description": "Production environment variables",
+ "items": {
+ "$ref": "#/definitions/environmentVariable"
+ }
+ },
+ "preview": {
+ "type": "array",
+ "description": "Preview deployment environment variables",
+ "items": {
+ "$ref": "#/definitions/environmentVariable"
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "environmentVariable": {
+ "type": "object",
+ "required": ["key"],
+ "properties": {
+ "key": {
+ "type": "string",
+ "description": "Environment variable name",
+ "examples": ["DATABASE_URL", "API_KEY", "NODE_ENV"]
+ },
+ "value": {
+ "type": "string",
+ "description": "Environment variable value. Supports magic variables like $SERVICE_PASSWORD_32, $SERVICE_FQDN_APP, $SERVICE_URL_API",
+ "examples": ["production", "$SERVICE_PASSWORD_32", "$SERVICE_FQDN_API"]
+ },
+ "is_multiline": {
+ "type": "boolean",
+ "description": "Whether the value contains multiple lines",
+ "default": false
+ },
+ "is_literal": {
+ "type": "boolean",
+ "description": "Whether to treat value as literal (no variable expansion)",
+ "default": false
+ },
+ "is_buildtime": {
+ "type": "boolean",
+ "description": "Make available during build",
+ "default": true
+ },
+ "is_runtime": {
+ "type": "boolean",
+ "description": "Make available at runtime",
+ "default": true
+ }
+ }
+ }
+ },
+ "examples": [
+ {
+ "$schema": "https://coolify.io/schemas/coolify.schema.json",
+ "version": "1.0",
+ "name": "My Node.js App",
+ "description": "A simple Node.js application",
+ "build": {
+ "type": "nixpacks",
+ "base_directory": "/",
+ "build_command": "npm run build",
+ "start_command": "npm start"
+ },
+ "domains": {
+ "ports_exposes": "3000"
+ },
+ "settings": {
+ "is_auto_deploy_enabled": true,
+ "is_force_https_enabled": true
+ },
+ "environment_variables": {
+ "production": [
+ {
+ "key": "NODE_ENV",
+ "value": "production"
+ },
+ {
+ "key": "DATABASE_PASSWORD",
+ "value": "$SERVICE_PASSWORD_32"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php
index 8c073ecab..8ad6802fe 100644
--- a/resources/views/livewire/global-search.blade.php
+++ b/resources/views/livewire/global-search.blade.php
@@ -145,6 +145,7 @@
'new public', 'new public git', 'new public repo', 'new public repository',
'new private github', 'new private gh', 'new private deploy', 'new deploy key',
'new dockerfile', 'new docker compose', 'new compose', 'new docker image', 'new image',
+ 'new coolify json', 'new json', 'new import',
'new postgresql', 'new postgres', 'new mysql', 'new mariadb',
'new redis', 'new keydb', 'new dragonfly', 'new mongodb', 'new mongo', 'new clickhouse'
];
diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php
index 254f31ca6..b61b4b72f 100644
--- a/resources/views/livewire/project/application/general.blade.php
+++ b/resources/views/livewire/project/application/general.blade.php
@@ -18,6 +18,15 @@
{{ $application->docker_compose_raw ? 'Reload Compose File' : 'Load Compose File' }}
@endif
+ @can('view', $application)
+