diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 882ed3c2e..38ad99d2e 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -57,6 +57,17 @@ class StartDragonfly $server = $this->database->destination->server; $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + if (! $caCert) { + $server->generateCaCertificate(); + $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + } + + if (! $caCert) { + $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.'); + + return; + } + $this->ssl_certificate = $this->database->sslCertificates()->first(); if (! $this->ssl_certificate) { diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index 311b5094a..59bcd4123 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -58,6 +58,17 @@ class StartKeydb $server = $this->database->destination->server; $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + if (! $caCert) { + $server->generateCaCertificate(); + $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + } + + if (! $caCert) { + $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.'); + + return; + } + $this->ssl_certificate = $this->database->sslCertificates()->first(); if (! $this->ssl_certificate) { diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index c29273a66..13dba4b43 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -59,6 +59,17 @@ class StartMariadb $server = $this->database->destination->server; $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + if (! $caCert) { + $server->generateCaCertificate(); + $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + } + + if (! $caCert) { + $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.'); + + return; + } + $this->ssl_certificate = $this->database->sslCertificates()->first(); if (! $this->ssl_certificate) { diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 3ea8287ac..ff0233e62 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -63,6 +63,16 @@ class StartMongodb $server = $this->database->destination->server; $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + if (! $caCert) { + $server->generateCaCertificate(); + $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + } + + if (! $caCert) { + $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.'); + + return; + } $this->ssl_certificate = $this->database->sslCertificates()->first(); if (! $this->ssl_certificate) { diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index a2e08c316..5d5611e07 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -59,6 +59,17 @@ class StartMysql $server = $this->database->destination->server; $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + if (! $caCert) { + $server->generateCaCertificate(); + $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + } + + if (! $caCert) { + $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.'); + + return; + } + $this->ssl_certificate = $this->database->sslCertificates()->first(); if (! $this->ssl_certificate) { diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 97e565ec8..a40eac17b 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -64,6 +64,17 @@ class StartPostgresql $server = $this->database->destination->server; $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + if (! $caCert) { + $server->generateCaCertificate(); + $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + } + + if (! $caCert) { + $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.'); + + return; + } + $this->ssl_certificate = $this->database->sslCertificates()->first(); if (! $this->ssl_certificate) { diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index 9e7a2a084..68a1f3fe3 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -58,6 +58,17 @@ class StartRedis $server = $this->database->destination->server; $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + if (! $caCert) { + $server->generateCaCertificate(); + $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + } + + if (! $caCert) { + $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.'); + + return; + } + $this->ssl_certificate = $this->database->sslCertificates()->first(); if (! $this->ssl_certificate) { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 92186953b..b72d2ade3 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2027,7 +2027,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { $nginx_config = base64_encode($this->application->custom_nginx_configuration); } else { - $nginx_config = base64_encode(defaultNginxConfiguration()); + if ($this->application->settings->is_spa) { + $nginx_config = base64_encode(defaultNginxConfiguration('spa')); + } else { + $nginx_config = base64_encode(defaultNginxConfiguration()); + } } } else { if ($this->application->build_pack === 'nixpacks') { @@ -2094,7 +2098,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { $nginx_config = base64_encode($this->application->custom_nginx_configuration); } else { - $nginx_config = base64_encode(defaultNginxConfiguration()); + if ($this->application->settings->is_spa) { + $nginx_config = base64_encode(defaultNginxConfiguration('spa')); + } else { + $nginx_config = base64_encode(defaultNginxConfiguration()); + } } } $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 1d58ed33a..b85023a0c 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -86,6 +86,7 @@ class General extends Component 'application.post_deployment_command_container' => 'nullable', 'application.custom_nginx_configuration' => 'nullable', 'application.settings.is_static' => 'boolean|required', + 'application.settings.is_spa' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_container_label_escape_enabled' => 'boolean|required', 'application.settings.is_container_label_readonly_enabled' => 'boolean|required', @@ -124,6 +125,7 @@ class General extends Component 'application.docker_compose_custom_build_command' => 'Docker compose custom build command', 'application.custom_nginx_configuration' => 'Custom Nginx configuration', 'application.settings.is_static' => 'Is static', + 'application.settings.is_spa' => 'Is SPA', 'application.settings.is_build_server_enabled' => 'Is build server enabled', 'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled', 'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly', @@ -171,6 +173,9 @@ class General extends Component public function instantSave() { + if ($this->application->settings->isDirty('is_spa')) { + $this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static'); + } $this->application->settings->save(); $this->dispatch('success', 'Settings saved.'); $this->application->refresh(); @@ -190,6 +195,7 @@ class General extends Component if ($this->application->settings->is_container_label_readonly_enabled) { $this->resetDefaultLabels(false); } + } public function loadComposeFile($isInit = false) @@ -287,9 +293,9 @@ class General extends Component } } - public function generateNginxConfiguration() + public function generateNginxConfiguration($type = 'static') { - $this->application->custom_nginx_configuration = defaultNginxConfiguration(); + $this->application->custom_nginx_configuration = defaultNginxConfiguration($type); $this->application->save(); $this->dispatch('success', 'Nginx configuration generated.'); } diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index 9aef91ac4..0fffbef31 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -214,10 +214,23 @@ class General extends Component return; } - $caCert = SslCertificate::where('server_id', $existingCert->server_id) + $server = $this->database->destination->server; + + $caCert = SslCertificate::where('server_id', $server->id) ->where('is_ca_certificate', true) ->first(); + if (! $caCert) { + $server->generateCaCertificate(); + $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + } + + if (! $caCert) { + $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.'); + + return; + } + SslHelper::generateSslCertificate( commonName: $existingCert->commonName, subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [], diff --git a/app/Models/Application.php b/app/Models/Application.php index 57a69423d..d07577cc7 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1530,7 +1530,6 @@ class Application extends BaseModel $interval = str($healthcheckCommand)->match('/--interval=([0-9]+[a-zµ]*)/'); $timeout = str($healthcheckCommand)->match('/--timeout=([0-9]+[a-zµ]*)/'); $start_period = str($healthcheckCommand)->match('/--start-period=([0-9]+[a-zµ]*)/'); - $start_interval = str($healthcheckCommand)->match('/--start-interval=([0-9]+[a-zµ]*)/'); $retries = str($healthcheckCommand)->match('/--retries=(\d+)/'); if ($interval->isNotEmpty()) { @@ -1542,13 +1541,10 @@ class Application extends BaseModel if ($start_period->isNotEmpty()) { $this->health_check_start_period = parseDockerfileInterval($start_period); } - if ($start_interval->isNotEmpty()) { - $this->health_check_start_interval = parseDockerfileInterval($start_interval); - } if ($retries->isNotEmpty()) { $this->health_check_retries = $retries->toInteger(); } - if ($interval || $timeout || $start_period || $start_interval || $retries) { + if ($interval || $timeout || $start_period || $retries) { $this->custom_healthcheck_found = true; $this->save(); } diff --git a/app/Models/Server.php b/app/Models/Server.php index fedb95697..56aa58e87 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -7,7 +7,9 @@ use App\Actions\Server\InstallDocker; use App\Actions\Server\StartSentinel; use App\Enums\ProxyTypes; use App\Events\ServerReachabilityChanged; +use App\Helpers\SslHelper; use App\Jobs\CheckAndStartSentinelJob; +use App\Jobs\RegenerateSslCertJob; use App\Notifications\Server\Reachable; use App\Notifications\Server\Unreachable; use App\Services\ConfigurationRepository; @@ -1337,4 +1339,41 @@ $schema://$host { $configRepository = app(ConfigurationRepository::class); $configRepository->disableSshMux(); } + + public function generateCaCertificate() + { + try { + ray('Generating CA certificate for server', $this->id); + SslHelper::generateSslCertificate( + commonName: 'Coolify CA Certificate', + serverId: $this->id, + isCaCertificate: true, + validityDays: 10 * 365 + ); + $caCertificate = SslCertificate::where('server_id', $this->id)->where('is_ca_certificate', true)->first(); + ray('CA certificate generated', $caCertificate); + if ($caCertificate) { + $certificateContent = $caCertificate->ssl_certificate; + $caCertPath = config('constants.coolify.base_config_path').'/ssl/'; + + $commands = collect([ + "mkdir -p $caCertPath", + "chown -R 9999:root $caCertPath", + "chmod -R 700 $caCertPath", + "rm -rf $caCertPath/coolify-ca.crt", + "echo '{$certificateContent}' > $caCertPath/coolify-ca.crt", + "chmod 644 $caCertPath/coolify-ca.crt", + ]); + + instant_remote_process($commands, $this, false); + + dispatch(new RegenerateSslCertJob( + server_id: $this->id, + force_regeneration: true + )); + } + } catch (\Throwable $e) { + return handleError($e); + } + } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index a020e7558..12bfccfaf 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -4061,9 +4061,35 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla return $rateLimited; } -function defaultNginxConfiguration(): string +function defaultNginxConfiguration(string $type = 'static'): string { - return 'server { + if ($type === 'spa') { + return <<<'NGINX' +server { + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + + # Handle 404 errors + error_page 404 /404.html; + location = /404.html { + root /usr/share/nginx/html; + internal; + } + + # Handle server errors (50x) + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + internal; + } +} +NGINX; + } else { + return <<<'NGINX' +server { location / { root /usr/share/nginx/html; index index.html index.htm; @@ -4083,7 +4109,9 @@ function defaultNginxConfiguration(): string root /usr/share/nginx/html; internal; } -}'; +} +NGINX; + } } function convertGitUrl(string $gitRepository, string $deploymentType, ?GithubApp $source = null): array diff --git a/database/migrations/2025_03_29_204400_revert_some_local_volume_encryption.php b/database/migrations/2025_03_29_204400_revert_some_local_volume_encryption.php index 683f1be3d..fe3e51318 100644 --- a/database/migrations/2025_03_29_204400_revert_some_local_volume_encryption.php +++ b/database/migrations/2025_03_29_204400_revert_some_local_volume_encryption.php @@ -13,15 +13,17 @@ return new class extends Migration public function up(): void { if (DB::table('local_file_volumes')->exists()) { + // First, get all volumes and decrypt their values + $decryptedVolumes = collect(); + DB::table('local_file_volumes') ->orderBy('id') - ->chunk(100, function ($volumes) { + ->chunk(100, function ($volumes) use (&$decryptedVolumes) { foreach ($volumes as $volume) { - DB::beginTransaction(); - try { $fs_path = $volume->fs_path; $mount_path = $volume->mount_path; + try { if ($fs_path) { $fs_path = Crypt::decryptString($fs_path); @@ -36,18 +38,59 @@ return new class extends Migration } catch (\Exception $e) { } - DB::table('local_file_volumes')->where('id', $volume->id)->update([ + $decryptedVolumes->push([ + 'id' => $volume->id, 'fs_path' => $fs_path, 'mount_path' => $mount_path, + 'resource_id' => $volume->resource_id, + 'resource_type' => $volume->resource_type, ]); - echo "Updated volume {$volume->id}\n"; + } catch (\Exception $e) { - echo "Error encrypting local file volume fields: {$e->getMessage()}\n"; - Log::error('Error encrypting local file volume fields: '.$e->getMessage()); + echo "Error decrypting volume {$volume->id}: {$e->getMessage()}\n"; + Log::error("Error decrypting volume {$volume->id}: ".$e->getMessage()); } - DB::commit(); } }); + + // Group by the unique constraint fields and keep only the first occurrence + $uniqueVolumes = $decryptedVolumes->groupBy(function ($volume) { + return $volume['mount_path'].'|'.$volume['resource_id'].'|'.$volume['resource_type']; + })->map(function ($group) { + return $group->first(); + }); + + // Get IDs to delete (all except the ones we're keeping) + $idsToKeep = $uniqueVolumes->pluck('id')->toArray(); + $idsToDelete = $decryptedVolumes->pluck('id')->diff($idsToKeep)->toArray(); + + // Delete duplicate records + if (! empty($idsToDelete)) { + // Show details of volumes being deleted + $volumesToDelete = $decryptedVolumes->whereIn('id', $idsToDelete); + echo "\nVolumes to be deleted:\n"; + foreach ($volumesToDelete as $volume) { + echo "ID: {$volume['id']}, Mount Path: {$volume['mount_path']}, Resource ID: {$volume['resource_id']}, Resource Type: {$volume['resource_type']}\n"; + echo "FS Path: {$volume['fs_path']}\n"; + echo "-------------------\n"; + } + + DB::table('local_file_volumes')->whereIn('id', $idsToDelete)->delete(); + echo 'Deleted '.count($idsToDelete)." duplicate volume(s)\n"; + } + + // Update the remaining records with decrypted values + foreach ($uniqueVolumes as $volume) { + try { + DB::table('local_file_volumes')->where('id', $volume['id'])->update([ + 'fs_path' => $volume['fs_path'], + 'mount_path' => $volume['mount_path'], + ]); + } catch (\Exception $e) { + echo "Error updating volume {$volume['id']}: {$e->getMessage()}\n"; + Log::error("Error updating volume {$volume['id']}: ".$e->getMessage()); + } + } } } diff --git a/database/migrations/2025_03_31_124212_add_specific_spa_configuration.php b/database/migrations/2025_03_31_124212_add_specific_spa_configuration.php new file mode 100644 index 000000000..1ec0d722b --- /dev/null +++ b/database/migrations/2025_03_31_124212_add_specific_spa_configuration.php @@ -0,0 +1,28 @@ +boolean('is_spa')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('is_spa'); + }); + } +}; diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 1cc71d063..8c12d1d62 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -69,6 +69,17 @@ Generate Default Nginx Configuration @endif +
+ @if ($application->could_set_build_commands()) + + @endif + @if ($application->settings->is_static && $application->build_pack !== 'static') + + @endif +
@if ($application->build_pack !== 'dockercompose')
@if ($application->settings->is_container_label_readonly_enabled == false) @@ -274,13 +285,6 @@ label="Use a Build Server?" />
@endif - @if ($application->could_set_build_commands()) -
- -
- @endif @endif @endif