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 @@