diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4e773f22..f878e244c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,6 +33,26 @@ ### 📚 Documentation
## [4.0.0-beta.400] - 2025-03-27
+### 🚀 Features
+
+- *(database)* Disable MongoDB SSL by default in migration
+
+### 🚜 Refactor
+
+- *(proxy)* Improve port availability checks with multiple methods
+- *(database)* Update MongoDB SSL configuration for improved security
+- *(database)* Enhance SSL configuration handling for various databases
+- *(notifications)* Update Telegram button URL for staging environment
+- *(models)* Remove unnecessary cloud check in isEnabled method
+- *(database)* Streamline event listeners in Redis General component
+- *(database)* Remove redundant database status display in MongoDB view
+- *(database)* Update import statements for Auth in database components
+- *(database)* Require PEM key file for SSL certificate regeneration
+- *(database)* Change MySQL daemon command to MariaDB daemon
+
+## [4.0.0-beta.399] - 2025-03-25
+
+
### 🚀 Features
- *(database)* Disable MongoDB SSL by default in migration
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c055f1009..7b642fdf5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -243,4 +243,4 @@ ### Contributing a New Service
### Contributing to Documentation
To contribute to the Coolify documentation, please refer to this guide:
-[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md)
+[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/readme.md)
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 @@ public function handle(StandaloneDragonfly $database)
$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 @@ public function handle(StandaloneKeydb $database)
$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 @@ public function handle(StandaloneMariadb $database)
$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 @@ public function handle(StandaloneMongodb $database)
$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 @@ public function handle(StandaloneMysql $database)
$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 @@ public function handle(StandalonePostgresql $database)
$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 @@ public function handle(StandaloneRedis $database)
$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/Console/Commands/Dev.php b/app/Console/Commands/Dev.php
index 257de0a92..a4cfde6f8 100644
--- a/app/Console/Commands/Dev.php
+++ b/app/Console/Commands/Dev.php
@@ -5,12 +5,10 @@
use App\Models\InstanceSettings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
-use Illuminate\Support\Facades\Process;
-use Symfony\Component\Yaml\Yaml;
class Dev extends Command
{
- protected $signature = 'dev {--init} {--generate-openapi}';
+ protected $signature = 'dev {--init}';
protected $description = 'Helper commands for development.';
@@ -21,36 +19,6 @@ public function handle()
return;
}
- if ($this->option('generate-openapi')) {
- $this->generateOpenApi();
-
- return;
- }
- }
-
- public function generateOpenApi()
- {
- // Generate OpenAPI documentation
- echo "Generating OpenAPI documentation.\n";
- // https://github.com/OAI/OpenAPI-Specification/releases
- $process = Process::run([
- '/var/www/html/vendor/bin/openapi',
- 'app',
- '-o',
- 'openapi.yaml',
- '--version',
- '3.1.0',
- ]);
- $error = $process->errorOutput();
- $error = preg_replace('/^.*an object literal,.*$/m', '', $error);
- $error = preg_replace('/^\h*\v+/m', '', $error);
- echo $error;
- echo $process->output();
- // Convert YAML to JSON
- $yaml = file_get_contents('openapi.yaml');
- $json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
- file_put_contents('openapi.json', $json);
- echo "Converted OpenAPI YAML to JSON.\n";
}
public function init()
diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/OpenApi.php
index 6cbcb310c..3cef85477 100644
--- a/app/Console/Commands/OpenApi.php
+++ b/app/Console/Commands/OpenApi.php
@@ -4,6 +4,7 @@
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Process;
+use Symfony\Component\Yaml\Yaml;
class OpenApi extends Command
{
@@ -29,5 +30,10 @@ public function handle()
$error = preg_replace('/^\h*\v+/m', '', $error);
echo $error;
echo $process->output();
+
+ $yaml = file_get_contents('openapi.yaml');
+ $json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
+ file_put_contents('openapi.json', $json);
+ echo "Converted OpenAPI YAML to JSON.\n";
}
}
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
index 73b452f86..424c2cc76 100644
--- a/app/Http/Controllers/Api/DeployController.php
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -5,6 +5,7 @@
use App\Actions\Database\StartDatabase;
use App\Actions\Service\StartService;
use App\Http\Controllers\Controller;
+use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\Tag;
@@ -142,6 +143,7 @@ public function deployment_by_uuid(Request $request)
new OA\Parameter(name: 'tag', in: 'query', description: 'Tag name(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')),
+ new OA\Parameter(name: 'pr', in: 'query', description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.', schema: new OA\Schema(type: 'integer')),
],
responses: [
@@ -184,26 +186,32 @@ public function deployment_by_uuid(Request $request)
public function deploy(Request $request)
{
$teamId = getTeamIdFromToken();
+
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
$uuids = $request->query->get('uuid');
$tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false;
+ $pr = $request->query->get('pr') ? max((int) $request->query->get('pr'), 0) : 0;
if ($uuids && $tags) {
return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400);
}
- if (is_null($teamId)) {
- return invalidTokenResponse();
+ if ($tags && $pr) {
+ return response()->json(['message' => 'You can only use tag or pr, not both.'], 400);
}
if ($tags) {
return $this->by_tags($tags, $teamId, $force);
} elseif ($uuids) {
- return $this->by_uuids($uuids, $teamId, $force);
+ return $this->by_uuids($uuids, $teamId, $force, $pr);
}
return response()->json(['message' => 'You must provide uuid or tag.'], 400);
}
- private function by_uuids(string $uuid, int $teamId, bool $force = false)
+ private function by_uuids(string $uuid, int $teamId, bool $force = false, int $pr = 0)
{
$uuids = explode(',', $uuid);
$uuids = collect(array_filter($uuids));
@@ -216,7 +224,7 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false)
foreach ($uuids as $uuid) {
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
- ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
+ ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force, $pr);
if ($deployment_uuid) {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
} else {
@@ -281,7 +289,7 @@ public function by_tags(string $tags, int $team_id, bool $force = false)
return response()->json(['message' => 'No resources found with this tag.'], 404);
}
- public function deploy_resource($resource, bool $force = false): array
+ public function deploy_resource($resource, bool $force = false, int $pr = 0): array
{
$message = null;
$deployment_uuid = null;
@@ -295,6 +303,7 @@ public function deploy_resource($resource, bool $force = false): array
application: $resource,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
+ pull_request_id: $pr,
);
$message = "Application {$resource->name} deployment queued.";
break;
@@ -314,4 +323,68 @@ public function deploy_resource($resource, bool $force = false): array
return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
}
+
+ #[OA\Get(
+ summary: 'List application deployments',
+ description: 'List application deployments by using the app uuid',
+ path: '/deployments/applications/{uuid}',
+ operationId: 'list-deployments-by-app-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Deployments'],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'List application deployments by using the app uuid.',
+ content: [
+
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/Application'),
+ )
+ ),
+ ]),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ ]
+ )]
+ public function get_application_deployments(Request $request)
+ {
+ $request->validate([
+ 'skip' => ['nullable', 'integer', 'min:0'],
+ 'take' => ['nullable', 'integer', 'min:1'],
+ ]);
+
+ $app_uuid = $request->route('uuid', null);
+ $skip = $request->get('skip', 0);
+ $take = $request->get('take', 10);
+
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $servers = Server::whereTeamId($teamId)->get();
+
+ if (is_null($app_uuid)) {
+ return response()->json(['message' => 'Application uuid is required'], 400);
+ }
+
+ $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $app_uuid)->first();
+
+ if (is_null($application)) {
+ return response()->json(['message' => 'Application not found'], 404);
+ }
+ $deployments = $application->deployments($skip, $take);
+
+ return response()->json($deployments);
+ }
}
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 @@ private function build_image()
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 @@ private function build_image()
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 @@ public function mount()
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 @@ public function instantSave()
if ($this->application->settings->is_container_label_readonly_enabled) {
$this->resetDefaultLabels(false);
}
+
}
public function loadComposeFile($isInit = false)
@@ -287,9 +293,9 @@ public function getWildcardDomain()
}
}
- 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 @@ public function regenerateSslCertificate()
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 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false
$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 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false
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\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 @@ private function disableSshMux(): void
$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 14f059d13..b90de4dbc 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -4071,9 +4071,35 @@ function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) {
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;
@@ -4093,7 +4119,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 @@
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 @@ public function up(): void
} 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/lang/no.json b/lang/no.json
new file mode 100644
index 000000000..29d5af124
--- /dev/null
+++ b/lang/no.json
@@ -0,0 +1,40 @@
+{
+ "auth.login": "Logg inn",
+ "auth.login.authentik": "Logg inn med Authentik",
+ "auth.login.azure": "Logg inn med Microsoft",
+ "auth.login.bitbucket": "Logg inn med Bitbucket",
+ "auth.login.github": "Logg inn med GitHub",
+ "auth.login.gitlab": "Logg inn med Gitlab",
+ "auth.login.google": "Logg inn med Google",
+ "auth.login.infomaniak": "Logg inn med Infomaniak",
+ "auth.already_registered": "Allerede registrert?",
+ "auth.confirm_password": "Bekreft passord",
+ "auth.forgot_password": "Glemt passord",
+ "auth.forgot_password_send_email": "Send e-post for tilbakestilling av passord",
+ "auth.register_now": "Registrer deg",
+ "auth.logout": "Logg ut",
+ "auth.register": "Registrer",
+ "auth.registration_disabled": "Registrering er deaktivert. Vennligst kontakt administrator.",
+ "auth.reset_password": "Tilbakestill passord",
+ "auth.failed": "Disse legitimasjonene samsvarer ikke med våre registre.",
+ "auth.failed.callback": "Klarte ikke å behandle tilbakekall fra innloggingsleverandør.",
+ "auth.failed.password": "Det oppgitte passordet er feil.",
+ "auth.failed.email": "Vi finner ingen bruker med den e-postadressen.",
+ "auth.throttle": "For mange innloggingsforsøk. Vennligst prøv igjen om :seconds sekunder.",
+ "input.name": "Navn",
+ "input.email": "E-post",
+ "input.password": "Passord",
+ "input.password.again": "Passord igjen",
+ "input.code": "Engangskode",
+ "input.recovery_code": "Gjenopprettingskode",
+ "button.save": "Lagre",
+ "repository.url": "Eksempler
For offentlige repositorier, bruk https://....
For private repositorier, bruk git@....
https://github.com/coollabsio/coolify-examples main gren vil bli valgt
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify gren vil bli valgt.
https://gitea.com/sedlav/expressjs.git main gren vil bli valgt.
https://gitlab.com/andrasbacsai/nodejs-example.git main gren vil bli valgt.",
+ "service.stop": "Denne tjenesten vil bli stoppet.",
+ "resource.docker_cleanup": "Kjør Docker-opprydding (fjern ubrukte bilder og byggebuffer).",
+ "resource.non_persistent": "Alle ikke-persistente data vil bli slettet.",
+ "resource.delete_volumes": "Slett alle volumer tilknyttet denne ressursen permanent.",
+ "resource.delete_connected_networks": "Slett alle ikke-forhåndsdefinerte nettverk tilknyttet denne ressursen permanent.",
+ "resource.delete_configurations": "Slett alle konfigurasjonsfiler fra serveren permanent.",
+ "database.delete_backups_locally": "Alle sikkerhetskopier vil bli slettet permanent fra lokal lagring.",
+ "warning.sslipdomain": "Konfigurasjonen din er lagret, men sslip-domene med https er IKKE anbefalt, fordi Let's Encrypt-servere med dette offentlige domenet er hastighetsbegrenset (SSL-sertifikatvalidering vil mislykkes).
Bruk ditt eget domene i stedet."
+}
diff --git a/openapi.json b/openapi.json
index 819f229cc..98447067e 100644
--- a/openapi.json
+++ b/openapi.json
@@ -2105,6 +2105,70 @@
]
}
},
+ "\/applications\/{uuid}\/logs": {
+ "get": {
+ "tags": [
+ "Applications"
+ ],
+ "summary": "Get application logs.",
+ "description": "Get application logs by UUID.",
+ "operationId": "get-application-logs-by-uuid",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "description": "UUID of the application.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ {
+ "name": "lines",
+ "in": "query",
+ "description": "Number of lines to show from the end of the logs.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "format": "int32",
+ "default": 100
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Get application logs by UUID.",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "logs": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "401": {
+ "$ref": "#\/components\/responses\/401"
+ },
+ "400": {
+ "$ref": "#\/components\/responses\/400"
+ },
+ "404": {
+ "$ref": "#\/components\/responses\/404"
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ }
+ },
"\/applications\/{uuid}\/envs": {
"get": {
"tags": [
@@ -4477,6 +4541,14 @@
"schema": {
"type": "boolean"
}
+ },
+ {
+ "name": "pr",
+ "in": "query",
+ "description": "Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.",
+ "schema": {
+ "type": "integer"
+ }
}
],
"responses": {
@@ -4523,6 +4595,42 @@
]
}
},
+ "\/deployments\/applications\/{uuid}": {
+ "get": {
+ "tags": [
+ "Deployments"
+ ],
+ "summary": "List application deployments",
+ "description": "List application deployments by using the app uuid",
+ "operationId": "list-deployments-by-app-uuid",
+ "responses": {
+ "200": {
+ "description": "List application deployments by using the app uuid.",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#\/components\/schemas\/Application"
+ }
+ }
+ }
+ }
+ },
+ "401": {
+ "$ref": "#\/components\/responses\/401"
+ },
+ "400": {
+ "$ref": "#\/components\/responses\/400"
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ }
+ },
"\/version": {
"get": {
"summary": "Version",
@@ -5854,8 +5962,8 @@
"tags": [
"Services"
],
- "summary": "Create",
- "description": "Create a one-click service",
+ "summary": "Create service",
+ "description": "Create a one-click \/ custom service",
"operationId": "create-service",
"requestBody": {
"required": true,
@@ -6005,7 +6113,7 @@
},
"responses": {
"201": {
- "description": "Create a service.",
+ "description": "Service created successfully.",
"content": {
"application\/json": {
"schema": {
@@ -6177,6 +6285,114 @@
"bearerAuth": []
}
]
+ },
+ "patch": {
+ "tags": [
+ "Services"
+ ],
+ "summary": "Update",
+ "description": "Update service by UUID.",
+ "operationId": "update-service-by-uuid",
+ "requestBody": {
+ "description": "Service updated.",
+ "required": true,
+ "content": {
+ "application\/json": {
+ "schema": {
+ "required": [
+ "server_uuid",
+ "project_uuid",
+ "environment_name",
+ "environment_uuid",
+ "docker_compose_raw"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The service name."
+ },
+ "description": {
+ "type": "string",
+ "description": "The service description."
+ },
+ "project_uuid": {
+ "type": "string",
+ "description": "The project UUID."
+ },
+ "environment_name": {
+ "type": "string",
+ "description": "The environment name."
+ },
+ "environment_uuid": {
+ "type": "string",
+ "description": "The environment UUID."
+ },
+ "server_uuid": {
+ "type": "string",
+ "description": "The server UUID."
+ },
+ "destination_uuid": {
+ "type": "string",
+ "description": "The destination UUID."
+ },
+ "instant_deploy": {
+ "type": "boolean",
+ "description": "The flag to indicate if the service should be deployed instantly."
+ },
+ "connect_to_docker_network": {
+ "type": "boolean",
+ "default": false,
+ "description": "Connect the service to the predefined docker network."
+ },
+ "docker_compose_raw": {
+ "type": "string",
+ "description": "The Docker Compose raw content."
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Service updated.",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "uuid": {
+ "type": "string",
+ "description": "Service UUID."
+ },
+ "domains": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Service domains."
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "401": {
+ "$ref": "#\/components\/responses\/401"
+ },
+ "400": {
+ "$ref": "#\/components\/responses\/400"
+ },
+ "404": {
+ "$ref": "#\/components\/responses\/404"
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
}
},
"\/services\/{uuid}\/envs": {
diff --git a/openapi.yaml b/openapi.yaml
index c965e9fe2..ba4b7193e 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -3152,6 +3152,12 @@ paths:
description: 'Force rebuild (without cache)'
schema:
type: boolean
+ -
+ name: pr
+ in: query
+ description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.'
+ schema:
+ type: integer
responses:
'200':
description: "Get deployment(s) UUID's"
@@ -3168,6 +3174,29 @@ paths:
security:
-
bearerAuth: []
+ '/deployments/applications/{uuid}':
+ get:
+ tags:
+ - Deployments
+ summary: 'List application deployments'
+ description: 'List application deployments by using the app uuid'
+ operationId: list-deployments-by-app-uuid
+ responses:
+ '200':
+ description: 'List application deployments by using the app uuid.'
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Application'
+ '401':
+ $ref: '#/components/responses/401'
+ '400':
+ $ref: '#/components/responses/400'
+ security:
+ -
+ bearerAuth: []
/version:
get:
summary: Version
@@ -3995,80 +4024,9 @@ paths:
post:
tags:
- Services
- summary: Create
- description: 'Create a service'
+ summary: 'Create service'
+ description: 'Create a one-click / custom service'
operationId: create-service
- requestBody:
- required: true
- content:
- application/json:
- schema:
- required:
- - server_uuid
- - project_uuid
- - environment_name
- - environment_uuid
- - docker_compose_raw
- properties:
- name:
- type: string
- maxLength: 255
- description: 'Name of the service.'
- description:
- type: string
- nullable: true
- description: 'Description of the service.'
- project_uuid:
- type: string
- description: 'Project UUID.'
- environment_name:
- type: string
- description: 'Environment name. You need to provide at least one of environment_name or environment_uuid.'
- environment_uuid:
- type: string
- description: 'Environment UUID. You need to provide at least one of environment_name or environment_uuid.'
- server_uuid:
- type: string
- description: 'Server UUID.'
- destination_uuid:
- type: string
- description: 'Destination UUID. Required if server has multiple destinations.'
- instant_deploy:
- type: boolean
- default: false
- description: 'Start the service immediately after creation.'
- connect_to_docker_network:
- type: boolean
- default: false
- description: 'The flag to connect the service to the predefined Docker network.'
- docker_compose_raw:
- type: string
- description: 'The Docker Compose raw content.'
- type: object
- responses:
- '201':
- description: 'Service created successfully.'
- content:
- application/json:
- schema:
- properties:
- uuid: { type: string, description: 'Service UUID.' }
- domains: { type: array, items: { type: string, nullable: true }, description: 'Service domains.' }
- type: object
- '401':
- $ref: '#/components/responses/401'
- '400':
- $ref: '#/components/responses/400'
- security:
- -
- bearerAuth: []
- /services/one-click:
- post:
- tags:
- - Services
- summary: 'Create one-click'
- description: 'Create a one-click service'
- operationId: create-one-click-service
requestBody:
required: true
content:
@@ -4271,7 +4229,7 @@ paths:
connect_to_docker_network:
type: boolean
default: false
- description: 'The flag to connect the service to the predefined Docker network.'
+ description: 'Connect the service to the predefined docker network.'
docker_compose_raw:
type: string
description: 'The Docker Compose raw content.'
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 @@