Compare commits

...

84 Commits

Author SHA1 Message Date
Andras Bacsai
a1d395ff0c Merge pull request #3708 from coollabsio/next
v4.0.0-beta.355
2024-10-03 23:04:54 +02:00
Andras Bacsai
4e9126887f Merge pull request #3707 from coollabsio/custom-traefik-middlewares
fix: parser, espacing container labels
2024-10-03 23:00:17 +02:00
Andras Bacsai
5fa650122d Merge pull request #3637 from MarioZet23/custom-traefik-middlewares
feat: Allow custom traefik middlewares
2024-10-03 22:59:46 +02:00
Andras Bacsai
ee7f8200ac fix: parser, espacing container labels 2024-10-03 22:58:06 +02:00
Andras Bacsai
d2a8f31a1c Merge branch 'next' into custom-traefik-middlewares 2024-10-03 22:44:22 +02:00
Andras Bacsai
2468d0044b chore: Update version to 4.0.0-beta.355 2024-10-03 22:39:44 +02:00
Andras Bacsai
81b8a58415 fix: scheduled backup for services view 2024-10-03 22:38:37 +02:00
Andras Bacsai
7442d19611 Merge pull request #3705 from coollabsio/next
v4.0.0-beta.354
2024-10-03 22:04:36 +02:00
Andras Bacsai
8c024ddb57 chore: Update homarr service template and remove unnecessary code 2024-10-03 22:02:18 +02:00
Andras Bacsai
d990a5691d chore: Update homarr service template and remove unnecessary code 2024-10-03 21:47:06 +02:00
Andras Bacsai
2181ef381b Merge pull request #3697 from danielalves96/add_homarr_template
feat: add homarr service tamplate and logo
2024-10-03 21:39:14 +02:00
Andras Bacsai
c80f5be974 chore: Update it-tools service template and port configuration 2024-10-03 21:38:13 +02:00
Andras Bacsai
358f6575f8 chore: Refactor modal-confirmation component 2024-10-03 21:38:08 +02:00
Andras Bacsai
de0f34734b Merge pull request #3702 from danielalves96/add-it-tools
feat: add it-tools service template and logo
2024-10-03 21:33:33 +02:00
Andras Bacsai
33cb2d150d chore: Fix application deployment queue filter logic 2024-10-03 21:32:02 +02:00
Andras Bacsai
5f07b473e9 fix: parse proxy config and check the set ports usage 2024-10-03 21:29:55 +02:00
Andras Bacsai
5bcd813792 chore: Remove commented code in Server model 2024-10-03 21:29:04 +02:00
Andras Bacsai
a0bb523507 chore: Remove debug statement in Service model 2024-10-03 21:11:14 +02:00
Andras Bacsai
0b4fc38d6b chore: Update version to 4.0.0-beta.354 2024-10-03 21:10:52 +02:00
Andras Bacsai
82c834915d Merge pull request #3703 from coollabsio/next
v4.0.0-beta.353
2024-10-03 21:03:16 +02:00
Andras Bacsai
d637675ce3 chore: Update service application view 2024-10-03 21:02:42 +02:00
Andras Bacsai
e71c04a0c7 chore: Update version to 4.0.0-beta.353 2024-10-03 20:59:10 +02:00
Andras Bacsai
885b3bdea7 Merge pull request #3701 from coollabsio/next
v4.0.0-beta.352
2024-10-03 20:53:17 +02:00
Daniel Alves
9d6757aeb7 feat: add it-tools service template and logo 2024-10-03 15:52:33 -03:00
Andras Bacsai
d84d0a816b chore: Refactor DatabaseBackupJob to handle missing team 2024-10-03 20:51:18 +02:00
Andras Bacsai
0da31c34b5 fix: add new supported database images 2024-10-03 20:47:22 +02:00
Andras Bacsai
ccdaf59ecb fix: service application view 2024-10-03 20:47:02 +02:00
Andras Bacsai
3fc9cf90ab chore: update version to 4.0.0-beta.352 2024-10-03 20:46:45 +02:00
Daniel Alves
c4be720b20 fix: update FQDN 2024-10-03 15:00:52 -03:00
Daniel Alves
aabe27efd1 feat: add homarr service tamplate and logo 2024-10-03 12:58:35 -03:00
Andras Bacsai
a8982379c9 Merge pull request #3683 from coollabsio/next
v4.0.0-beta.351
2024-10-03 15:15:21 +02:00
Andras Bacsai
6dd0bd0742 fix: api useBuildServer 2024-10-03 15:09:56 +02:00
Andras Bacsai
1d3494a6ba fix: network handling
fix: environment variable handling
2024-10-03 15:04:40 +02:00
Andras Bacsai
ee5eb427c9 fix: bitcoin core template 2024-10-03 14:07:58 +02:00
Andras Bacsai
ad7b5e6e1c Merge pull request #3689 from ALsJourney/bitcoin_core_service
Basic Bitcoin Core node service
2024-10-03 14:00:52 +02:00
Andras Bacsai
73bd344147 fix: strapi template 2024-10-03 13:49:50 +02:00
Andras Bacsai
ef448280d8 fix: able to support more database dynamically from Coolify's UI 2024-10-03 13:49:43 +02:00
Andras Bacsai
5282248cb4 Merge pull request #3685 from statickidz/add-strapi-template
Add Strapi template
2024-10-03 13:18:40 +02:00
Andras Bacsai
14c9f25c57 feat: restart service without pulling the latest image 2024-10-03 13:17:35 +02:00
ALsJourney
44b3d08d86 Finished basic bitcoin core service 2024-10-03 13:13:25 +02:00
Andras Bacsai
5da1f48ae1 fix typo 2024-10-03 12:43:41 +02:00
Andras Bacsai
2bc1b9027c Merge pull request #3679 from alepouna/typo-fix
Update docker-registry.yaml
2024-10-03 12:43:46 +02:00
Andras Bacsai
1c7ca56756 feat: backup all databases for mysql,mariadb,postgresql 2024-10-03 12:39:45 +02:00
Adrian Barrio
d70faea845 Merge remote-tracking branch 'origin/next' into add-strapi-template 2024-10-03 11:33:35 +02:00
Adrian Barrio
fdb5cab875 feat: add strapi template 2024-10-03 11:28:27 +02:00
Andras Bacsai
bb6cb8edc9 improvement: show backup button on supported db service stacks 2024-10-03 10:48:25 +02:00
Andras Bacsai
a6ec2b92fb version++ 2024-10-03 10:23:38 +02:00
Andras Bacsai
436a1d945f Merge pull request #3663 from coollabsio/next
v4.0.0-beta.350
2024-10-03 10:09:05 +02:00
Andras Bacsai
a6a3abc273 chore: Update soketi service image to version 1.0.3 2024-10-03 10:05:54 +02:00
Andras Bacsai
c4e702f096 fix: able to select root permission easier 2024-10-03 09:57:37 +02:00
alepouna
31df222798 Update docker-registry.yaml 2024-10-03 04:04:37 +03:00
Andras Bacsai
e91939a4ea refactor: Remove unnecessary watch command from soketi service entrypoint 2024-10-02 21:24:09 +02:00
Andras Bacsai
ceccd093d2 refactor: Improve socket reconnection interval in terminal.js 2024-10-02 21:24:05 +02:00
Andras Bacsai
66bb4e0fc1 refactor: Remove inactivity timer in terminal-server.js 2024-10-02 21:23:59 +02:00
Andras Bacsai
dd3ff38df7 refactor: Encode delimiter in SshMultiplexingHelper 2024-10-02 21:23:46 +02:00
Andras Bacsai
69553ec314 refactor: Improve popup component styling and button behavior 2024-10-02 18:26:51 +02:00
Andras Bacsai
7bb1bf0ae3 refactor: Improve parsing of commands for sudo in parseCommandsByLineForSudo 2024-10-02 18:26:40 +02:00
Andras Bacsai
a1a8f1336a refactor: Fix indentation in modal-confirmation.blade.php 2024-10-02 17:35:48 +02:00
Andras Bacsai
207fe1d709 wtf wtf wtf 2024-10-02 17:27:02 +02:00
Andras Bacsai
059535a676 chore: Remove commented out code for uploading to S3 in DatabaseBackupJob 2024-10-02 16:43:01 +02:00
Andras Bacsai
765a74ca4f handle errors in databasebackupjob 2024-10-02 15:33:14 +02:00
Andras Bacsai
e03e4f2e91 refactor: Improve SSH command generation in Terminal.php and terminal-server.js 2024-10-02 15:16:55 +02:00
Andras Bacsai
0ab432d5e6 chore: Remove unnecessary command from SshMultiplexingHelper 2024-10-02 14:54:48 +02:00
Andras Bacsai
6f25b548c7 fix: realtime watch in development mode 2024-10-02 14:18:52 +02:00
Andras Bacsai
d55e4bf381 feat: Handle HTTPS domain in ConfigureCloudflareTunnels 2024-10-02 13:36:25 +02:00
Andras Bacsai
ec216254b5 chore: Update modal input in server form to prevent closing on outside click 2024-10-02 13:36:09 +02:00
Andras Bacsai
fbb36bfe8e revert modal-confirmation in dev 2024-10-02 12:01:12 +02:00
Andras Bacsai
dd782e75f5 fix: local dev s3 uploads
fix: hetzner s3 uploads (mc alias instead of mc host)
2024-10-02 11:45:30 +02:00
Andras Bacsai
2be2f0ac79 feat: support Hetzner S3 2024-10-02 10:25:45 +02:00
Andras Bacsai
97943db5f4 chore: Add missing import for Attribute class in ApplicationDeploymentQueue model 2024-10-02 09:21:54 +02:00
Andras Bacsai
bbd2748ad7 chore: Update command signature and description for cleanup application deployment queue 2024-10-02 09:21:50 +02:00
Andras Bacsai
a530804a71 feat: Add command to check application deployment queue 2024-10-02 09:21:28 +02:00
Andras Bacsai
8ca8ab82b0 refactor: Remove deployment queue when deleting an application 2024-10-02 09:20:49 +02:00
Andras Bacsai
024ad8e943 fix: cleanup stucked applicationdeploymentqueue 2024-10-02 09:20:08 +02:00
Andras Bacsai
4d86b556a4 fix: ipv6 scp should use -6 flag 2024-10-02 08:15:03 +02:00
Andras Bacsai
73e38ff951 chore: Update version numbers to 4.0.0-beta.350 in configuration files 2024-10-02 08:13:49 +02:00
Andras Bacsai
5354561c39 Merge pull request #3659 from coollabsio/next
v4.0.0-beta.349
2024-10-01 21:36:50 +02:00
Andras Bacsai
223cd37031 fix: remove autofocuses 2024-10-01 21:36:16 +02:00
Andras Bacsai
93a0725a4e Merge pull request #3613 from nfnot/main
refactor: Update search input placeholder in resource index view
2024-10-01 21:32:21 +02:00
Andras Bacsai
4d1d4598b6 chore: Update version numbers to 4.0.0-beta.349 and 4.0.0-beta.350 2024-10-01 21:31:54 +02:00
Norman
888e96448c Merge branch 'main' into main 2024-10-01 13:33:02 +03:00
MarioZet
1b0e2e1257 Feat. Apply all middlewares from labels to coolify router, instead of only basicauth and redirect 2024-09-29 20:08:39 +02:00
Norman
b84576ce87 Merge branch 'main' into main 2024-09-28 18:18:47 +03:00
Norman
f2a9a04461 refactor: Update search input placeholder in resource index view 2024-09-28 01:14:54 +00:00
88 changed files with 973 additions and 392 deletions

View File

@@ -2,14 +2,17 @@
namespace App\Actions\Proxy; namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Models\Server; use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
class CheckProxy class CheckProxy
{ {
use AsAction; use AsAction;
public function handle(Server $server, $fromUI = false) // It should return if the proxy should be started (true) or not (false)
public function handle(Server $server, $fromUI = false): bool
{ {
if (! $server->isFunctional()) { if (! $server->isFunctional()) {
return false; return false;
@@ -62,22 +65,42 @@ class CheckProxy
$ip = 'host.docker.internal'; $ip = 'host.docker.internal';
} }
$connection80 = @fsockopen($ip, '80'); $portsToCheck = ['80', '443'];
$connection443 = @fsockopen($ip, '443');
$port80 = is_resource($connection80) && fclose($connection80); try {
$port443 = is_resource($connection443) && fclose($connection443); if ($server->proxyType() !== ProxyTypes::NONE->value) {
if ($port80) { $proxyCompose = CheckConfiguration::run($server);
if ($fromUI) { if (isset($proxyCompose)) {
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>"); $yaml = Yaml::parse($proxyCompose);
$portsToCheck = [];
if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
$ports = data_get($yaml, 'services.traefik.ports');
} elseif ($server->proxyType() === ProxyTypes::CADDY->value) {
$ports = data_get($yaml, 'services.caddy.ports');
}
if (isset($ports)) {
foreach ($ports as $port) {
$portsToCheck[] = str($port)->before(':')->value();
}
}
}
} else { } else {
return false; $portsToCheck = [];
} }
} catch (\Exception $e) {
ray($e->getMessage());
} }
if ($port443) { if (count($portsToCheck) === 0) {
if ($fromUI) { return false;
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>"); }
} else { foreach ($portsToCheck as $port) {
return false; $connection = @fsockopen($ip, $port);
if (is_resource($connection) && fclose($connection)) {
if ($fromUI) {
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
} }
} }

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Console\Command;
class CheckApplicationDeploymentQueue extends Command
{
protected $signature = 'check:deployment-queue {--force} {--seconds=3600}';
protected $description = 'Check application deployment queue.';
public function handle()
{
$seconds = $this->option('seconds');
$deployments = ApplicationDeploymentQueue::whereIn('status', [
ApplicationDeploymentStatus::IN_PROGRESS,
ApplicationDeploymentStatus::QUEUED,
])->where('created_at', '<=', now()->subSeconds($seconds))->get();
if ($deployments->isEmpty()) {
$this->info('No deployments found in the last '.$seconds.' seconds.');
return;
}
$this->info('Found '.$deployments->count().' deployments created in the last '.$seconds.' seconds.');
foreach ($deployments as $deployment) {
if ($this->option('force')) {
$this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
$this->cancelDeployment($deployment);
} else {
$this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
if ($this->confirm('Do you want to cancel this deployment?', true)) {
$this->cancelDeployment($deployment);
}
}
}
}
private function cancelDeployment(ApplicationDeploymentQueue $deployment)
{
$deployment->update(['status' => ApplicationDeploymentStatus::FAILED]);
if ($deployment->server?->isFunctional()) {
remote_process(['docker rm -f '.$deployment->deployment_uuid], $deployment->server, false);
}
}
}

View File

@@ -7,9 +7,9 @@ use Illuminate\Console\Command;
class CleanupApplicationDeploymentQueue extends Command class CleanupApplicationDeploymentQueue extends Command
{ {
protected $signature = 'cleanup:application-deployment-queue {--team-id=}'; protected $signature = 'cleanup:deployment-queue {--team-id=}';
protected $description = 'CleanupApplicationDeploymentQueue'; protected $description = 'Cleanup application deployment queue.';
public function handle() public function handle()
{ {

View File

@@ -4,6 +4,7 @@ namespace App\Console\Commands;
use App\Jobs\CleanupHelperContainersJob; use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask; use App\Models\ScheduledTask;
@@ -47,6 +48,17 @@ class CleanupStuckedResources extends Command
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stucked resources: {$e->getMessage()}\n"; echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
} }
try {
$applicationsDeploymentQueue = ApplicationDeploymentQueue::get();
foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) {
if (is_null($applicationDeploymentQueue->application)) {
echo "Deleting stuck application deployment queue: {$applicationDeploymentQueue->id}\n";
$applicationDeploymentQueue->delete();
}
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck application deployment queue: {$e->getMessage()}\n";
}
try { try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); $applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) { foreach ($applications as $application) {

View File

@@ -94,7 +94,9 @@ class SshMultiplexingHelper
$muxPersistTime = config('constants.ssh.mux_persist_time'); $muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp "; $scp_command = "timeout $timeout scp ";
if ($server->isIpv6()) {
$scp_command .= '-6 ';
}
if (self::isMultiplexingEnabled()) { if (self::isMultiplexingEnabled()) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server); self::ensureMultiplexedConnection($server);
@@ -136,8 +138,8 @@ class SshMultiplexingHelper
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval')); $ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$delimiter = Hash::make($command); $delimiter = Hash::make($command);
$delimiter = base64_encode($delimiter);
$command = str_replace($delimiter, '', $command); $command = str_replace($delimiter, '', $command);
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL $ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL

View File

@@ -744,8 +744,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer; if (isset($useBuildServer)) {
$application->settings->save(); $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save(); $application->save();
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if (! $application->settings->is_container_label_readonly_enabled) {
@@ -842,8 +844,10 @@ class ApplicationsController extends Controller
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->source_type = $githubApp->getMorphClass(); $application->source_type = $githubApp->getMorphClass();
$application->source_id = $githubApp->id; $application->source_id = $githubApp->id;
$application->settings->is_build_server_enabled = $useBuildServer; if (isset($useBuildServer)) {
$application->settings->save(); $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save(); $application->save();
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if (! $application->settings->is_container_label_readonly_enabled) {
@@ -936,8 +940,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer; if (isset($useBuildServer)) {
$application->settings->save(); $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save(); $application->save();
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if (! $application->settings->is_container_label_readonly_enabled) {
@@ -1017,8 +1023,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer; if (isset($useBuildServer)) {
$application->settings->save(); $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify'; $application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main'; $application->git_branch = 'main';
@@ -1077,8 +1085,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer; if (isset($useBuildServer)) {
$application->settings->save(); $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify'; $application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main'; $application->git_branch = 'main';
@@ -1555,8 +1565,11 @@ class ApplicationsController extends Controller
$instantDeploy = $request->instant_deploy; $instantDeploy = $request->instant_deploy;
$use_build_server = $request->use_build_server; $use_build_server = $request->use_build_server;
$application->settings->is_build_server_enabled = $use_build_server;
$application->settings->save(); if (isset($use_build_server)) {
$application->settings->is_build_server_enabled = $use_build_server;
$application->settings->save();
}
removeUnnecessaryFieldsFromRequest($request); removeUnnecessaryFieldsFromRequest($request);

View File

@@ -2,7 +2,6 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Database\StopDatabase;
use App\Events\BackupCreated; use App\Events\BackupCreated;
use App\Models\S3Storage; use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
@@ -24,7 +23,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{ {
@@ -63,31 +61,32 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct($backup) public function __construct($backup)
{ {
$this->backup = $backup; $this->backup = $backup;
$this->team = Team::find($backup->team_id);
if (is_null($this->team)) {
return;
}
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->service->server;
$this->s3 = $this->backup->s3;
} else {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->s3 = $this->backup->s3;
}
} }
public function handle(): void public function handle(): void
{ {
try { try {
// Check if team is exists $this->team = Team::find($this->backup->team_id);
if (is_null($this->team)) { if (! $this->team) {
StopDatabase::run($this->database); $this->backup->delete();
$this->database->delete();
return; return;
} }
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->service->server;
$this->s3 = $this->backup->s3;
} else {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->s3 = $this->backup->s3;
}
if (is_null($this->server)) {
throw new \Exception('Server not found?!');
}
if (is_null($this->database)) {
throw new \Exception('Database not found?!');
}
BackupCreated::dispatch($this->team->id); BackupCreated::dispatch($this->team->id);
@@ -237,7 +236,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
$this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name; $this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
if ($this->database->name === 'coolify-db') { if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify']; $databasesToBackup = ['coolify'];
$this->directory_name = $this->container_name = 'coolify-db'; $this->directory_name = $this->container_name = 'coolify-db';
@@ -250,6 +248,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
try { try {
if (str($databaseType)->contains('postgres')) { if (str($databaseType)->contains('postgres')) {
$this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp'; $this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([ $this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database, 'database_name' => $database,
@@ -278,6 +279,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->backup_standalone_mongodb($database); $this->backup_standalone_mongodb($database);
} elseif (str($databaseType)->contains('mysql')) { } elseif (str($databaseType)->contains('mysql')) {
$this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp'; $this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([ $this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database, 'database_name' => $database,
@@ -287,6 +291,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->backup_standalone_mysql($database); $this->backup_standalone_mysql($database);
} elseif (str($databaseType)->contains('mariadb')) { } elseif (str($databaseType)->contains('mariadb')) {
$this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp'; $this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([ $this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database, 'database_name' => $database,
@@ -325,7 +332,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage()); send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e; throw $e;
} finally { } finally {
BackupCreated::dispatch($this->team->id); if ($this->team) {
BackupCreated::dispatch($this->team->id);
}
} }
} }
@@ -384,7 +393,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->postgres_password) { if ($this->postgres_password) {
$backupCommand .= " -e PGPASSWORD=$this->postgres_password"; $backupCommand .= " -e PGPASSWORD=$this->postgres_password";
} }
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; if ($this->backup->dump_all) {
$backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
} else {
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
}
$commands[] = $backupCommand; $commands[] = $backupCommand;
ray($commands); ray($commands);
@@ -405,8 +418,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{ {
try { try {
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; if ($this->backup->dump_all) {
ray($commands); $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
}
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') { if ($this->backup_output === '') {
@@ -424,7 +440,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{ {
try { try {
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; if ($this->backup->dump_all) {
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
}
ray($commands); ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
@@ -466,34 +486,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
// private function upload_to_s3(): void
// {
// try {
// if (is_null($this->s3)) {
// return;
// }
// $key = $this->s3->key;
// $secret = $this->s3->secret;
// // $region = $this->s3->region;
// $bucket = $this->s3->bucket;
// $endpoint = $this->s3->endpoint;
// $this->s3->testConnection(shouldSave: true);
// $configName = new Cuid2;
// $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
// $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
// $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
// instant_remote_process($commands, $this->server);
// $this->add_to_backup_output('Uploaded to S3.');
// } catch (\Throwable $e) {
// $this->add_to_backup_output($e->getMessage());
// throw $e;
// } finally {
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
// instant_remote_process($removeConfigCommands, $this->server, false);
// }
// }
private function upload_to_s3(): void private function upload_to_s3(): void
{ {
try { try {
@@ -515,10 +507,27 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->ensureHelperImageAvailable(); $this->ensureHelperImageAvailable();
$fullImageName = $this->getFullImageName(); $fullImageName = $this->getFullImageName();
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; if (isDev()) {
if ($this->database->name === 'coolify-db') {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
} else {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file;
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
}
} else {
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
}
if ($this->s3->isHetzner()) {
$endpointWithoutBucket = 'https://'.str($endpoint)->after('https://')->after('.')->value();
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc alias set --path=off --api=S3v4 temporary {$endpointWithoutBucket} $key $secret";
} else {
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
}
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server); instant_remote_process($commands, $this->server);
$this->add_to_backup_output('Uploaded to S3.'); $this->add_to_backup_output('Uploaded to S3.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage()); $this->add_to_backup_output($e->getMessage());

View File

@@ -30,7 +30,7 @@ class Dashboard extends Component
public function cleanup_queue() public function cleanup_queue()
{ {
Artisan::queue('cleanup:application-deployment-queue', [ Artisan::queue('cleanup:deployment-queue', [
'--team-id' => currentTeam()->id, '--team-id' => currentTeam()->id,
]); ]);
} }

View File

@@ -31,6 +31,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'required|boolean', 'backup.save_s3' => 'required|boolean',
'backup.s3_storage_id' => 'nullable|integer', 'backup.s3_storage_id' => 'nullable|integer',
'backup.databases_to_backup' => 'nullable', 'backup.databases_to_backup' => 'nullable',
'backup.dump_all' => 'required|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@@ -40,6 +41,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'Save to S3', 'backup.save_s3' => 'Save to S3',
'backup.s3_storage_id' => 'S3 Storage', 'backup.s3_storage_id' => 'S3 Storage',
'backup.databases_to_backup' => 'Databases to Backup', 'backup.databases_to_backup' => 'Databases to Backup',
'backup.dump_all' => 'Backup All Databases',
]; ];
protected $messages = [ protected $messages = [

View File

@@ -26,7 +26,7 @@ class ScheduledBackups extends Component
public function mount(): void public function mount(): void
{ {
if ($this->selectedBackupId) { if ($this->selectedBackupId) {
$this->setSelectedBackup($this->selectedBackupId); $this->setSelectedBackup($this->selectedBackupId, true);
} }
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
@@ -37,10 +37,13 @@ class ScheduledBackups extends Component
$this->s3s = currentTeam()->s3s; $this->s3s = currentTeam()->s3s;
} }
public function setSelectedBackup($backupId) public function setSelectedBackup($backupId, $force = false)
{ {
if ($this->selectedBackupId === $backupId && ! $force) {
return;
}
$this->selectedBackupId = $backupId; $this->selectedBackupId = $backupId;
$this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId); $this->selectedBackup = $this->database->scheduledBackups->find($backupId);
if (is_null($this->selectedBackup)) { if (is_null($this->selectedBackup)) {
$this->selectedBackupId = null; $this->selectedBackupId = null;
} }

View File

@@ -108,6 +108,21 @@ class Navbar extends Component
return; return;
} }
StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse();
$this->dispatch('imagePulled');
$activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id);
}
public function pullAndRestartEvent()
{
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
return;
}
PullImage::run($this->service); PullImage::run($this->service);
StopService::run(service: $this->service, dockerCleanup: false); StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse(); $this->service->parse();

View File

@@ -34,9 +34,9 @@ class Terminal extends Component
if ($status !== 'running') { if ($status !== 'running') {
return; return;
} }
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} else { } else {
$command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi');
} }
// ssh command is sent back to frontend then to websocket // ssh command is sent back to frontend then to websocket

View File

@@ -15,6 +15,8 @@ class ApiTokens extends Component
public bool $readOnly = true; public bool $readOnly = true;
public bool $rootAccess = false;
public array $permissions = ['read-only']; public array $permissions = ['read-only'];
public $isApiEnabled; public $isApiEnabled;
@@ -35,12 +37,11 @@ class ApiTokens extends Component
if ($this->viewSensitiveData) { if ($this->viewSensitiveData) {
$this->permissions[] = 'view:sensitive'; $this->permissions[] = 'view:sensitive';
$this->permissions = array_diff($this->permissions, ['*']); $this->permissions = array_diff($this->permissions, ['*']);
$this->rootAccess = false;
} else { } else {
$this->permissions = array_diff($this->permissions, ['view:sensitive']); $this->permissions = array_diff($this->permissions, ['view:sensitive']);
} }
if (count($this->permissions) == 0) { $this->makeSureOneIsSelected();
$this->permissions = ['*'];
}
} }
public function updatedReadOnly() public function updatedReadOnly()
@@ -48,11 +49,30 @@ class ApiTokens extends Component
if ($this->readOnly) { if ($this->readOnly) {
$this->permissions[] = 'read-only'; $this->permissions[] = 'read-only';
$this->permissions = array_diff($this->permissions, ['*']); $this->permissions = array_diff($this->permissions, ['*']);
$this->rootAccess = false;
} else { } else {
$this->permissions = array_diff($this->permissions, ['read-only']); $this->permissions = array_diff($this->permissions, ['read-only']);
} }
if (count($this->permissions) == 0) { $this->makeSureOneIsSelected();
}
public function updatedRootAccess()
{
if ($this->rootAccess) {
$this->permissions = ['*']; $this->permissions = ['*'];
$this->readOnly = false;
$this->viewSensitiveData = false;
} else {
$this->readOnly = true;
$this->permissions = ['read-only'];
}
}
public function makeSureOneIsSelected()
{
if (count($this->permissions) == 0) {
$this->permissions = ['read-only'];
$this->readOnly = true;
} }
} }
@@ -62,12 +82,6 @@ class ApiTokens extends Component
$this->validate([ $this->validate([
'description' => 'required|min:3|max:255', 'description' => 'required|min:3|max:255',
]); ]);
// if ($this->viewSensitiveData) {
// $this->permissions[] = 'view:sensitive';
// }
// if ($this->readOnly) {
// $this->permissions[] = 'read-only';
// }
$token = auth()->user()->createToken($this->description, $this->permissions); $token = auth()->user()->createToken($this->description, $this->permissions);
$this->tokens = auth()->user()->tokens; $this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken); session()->flash('token', $token->plainTextToken);

View File

@@ -30,6 +30,11 @@ class ConfigureCloudflareTunnels extends Component
public function submit() public function submit()
{ {
try { try {
if (str($this->ssh_domain)->contains('https://')) {
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
// remove / from the end
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
}
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail(); $server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::dispatch($server, $this->cloudflare_token); ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true; $server->settings->is_cloudflare_tunnel = true;

View File

@@ -4,7 +4,7 @@ namespace App\Livewire\Server\Proxy;
use App\Actions\Docker\GetContainersStatus; use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\CheckProxy;
use App\Jobs\ContainerStatusJob; use App\Actions\Proxy\StartProxy;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -44,7 +44,10 @@ class Status extends Component
} }
$this->numberOfPolls++; $this->numberOfPolls++;
} }
CheckProxy::run($this->server, true); $shouldStart = CheckProxy::run($this->server, true);
if ($shouldStart) {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated'); $this->dispatch('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') { if ($this->server->proxy->status === 'running') {
$this->polling = false; $this->polling = false;

View File

@@ -43,15 +43,17 @@ class Create extends Component
'endpoint' => 'Endpoint', 'endpoint' => 'Endpoint',
]; ];
public function mount() public function updatedEndpoint($value)
{ {
if (isDev()) { if (! str($value)->startsWith('https://') && ! str($value)->startsWith('http://')) {
$this->name = 'Local MinIO'; $this->endpoint = 'https://'.$value;
$this->description = 'Local MinIO'; $value = $this->endpoint;
$this->key = 'minioadmin'; }
$this->secret = 'minioadmin';
$this->bucket = 'local'; if (str($value)->contains('your-objectstorage.com') && ! isset($this->bucket)) {
$this->endpoint = 'http://coolify-minio:9000'; $this->bucket = str($value)->after('//')->before('.');
} elseif (str($value)->contains('your-objectstorage.com')) {
$this->bucket = $this->bucket ?: str($value)->after('//')->before('.');
} }
} }

View File

@@ -143,6 +143,9 @@ class Application extends BaseModel
} }
$application->tags()->detach(); $application->tags()->detach();
$application->previews()->delete(); $application->previews()->delete();
foreach ($application->deployment_queue as $deployment) {
$deployment->delete();
}
}); });
} }
@@ -710,6 +713,11 @@ class Application extends BaseModel
return $this->hasMany(ApplicationPreview::class); return $this->hasMany(ApplicationPreview::class);
} }
public function deployment_queue()
{
return $this->hasMany(ApplicationDeploymentQueue::class);
}
public function destination() public function destination()
{ {
return $this->morphTo(); return $this->morphTo();

View File

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -39,6 +40,20 @@ class ApplicationDeploymentQueue extends Model
{ {
protected $guarded = []; protected $guarded = [];
public function application(): Attribute
{
return Attribute::make(
get: fn () => Application::find($this->application_id),
);
}
public function server(): Attribute
{
return Attribute::make(
get: fn () => Server::find($this->server_id),
);
}
public function setStatus(string $status) public function setStatus(string $status)
{ {
$this->update([ $this->update([

View File

@@ -40,6 +40,16 @@ class S3Storage extends BaseModel
return "{$this->endpoint}/{$this->bucket}"; return "{$this->endpoint}/{$this->bucket}";
} }
public function isHetzner()
{
return str($this->endpoint)->contains('your-objectstorage.com');
}
public function isDigitalOcean()
{
return str($this->endpoint)->contains('digitaloceanspaces.com');
}
public function testConnection(bool $shouldSave = false) public function testConnection(bool $shouldSave = false)
{ {
try { try {

View File

@@ -460,15 +460,6 @@ $schema://$host {
public function proxyType() public function proxyType()
{ {
// $proxyType = $this->proxy->get('type');
// if ($proxyType === ProxyTypes::NONE->value) {
// return $proxyType;
// }
// if (is_null($proxyType)) {
// $this->proxy->type = ProxyTypes::TRAEFIK->value;
// $this->proxy->status = ProxyStatus::EXITED->value;
// $this->save();
// }
return data_get($this->proxy, 'type'); return data_get($this->proxy, 'type');
} }
@@ -1221,4 +1212,9 @@ $schema://$host {
return instant_remote_process($commands, $this, false); return instant_remote_process($commands, $this, false);
} }
public function isIpv6(): bool
{
return str($this->ip)->contains(':');
}
} }

View File

@@ -770,6 +770,30 @@ class Service extends BaseModel
} }
$fields->put('Code Server', $data->toArray()); $fields->put('Code Server', $data->toArray());
break; break;
case str($image)->contains('elestio/strapi'):
$data = collect([]);
$license = $this->environment_variables()->where('key', 'STRAPI_LICENSE')->first();
if ($license) {
$data = $data->merge([
'License' => [
'key' => data_get($license, 'key'),
'value' => data_get($license, 'value'),
],
]);
}
$nodeEnv = $this->environment_variables()->where('key', 'NODE_ENV')->first();
if ($nodeEnv) {
$data = $data->merge([
'Node Environment' => [
'key' => data_get($nodeEnv, 'key'),
'value' => data_get($nodeEnv, 'value'),
],
]);
}
$fields->put('Strapi', $data->toArray());
break;
} }
} }
$databases = $this->databases()->get(); $databases = $this->databases()->get();
@@ -1108,7 +1132,6 @@ class Service extends BaseModel
$real_value = escapeEnvVariables($env->real_value); $real_value = escapeEnvVariables($env->real_value);
} }
} }
ray("echo \"{$env->key}={$real_value}\" >> .env");
$commands[] = "echo \"{$env->key}={$real_value}\" >> .env"; $commands[] = "echo \"{$env->key}={$real_value}\" >> .env";
} }
} }

View File

@@ -112,4 +112,9 @@ class ServiceApplication extends BaseModel
{ {
getFilesystemVolumesFromServer($this, $isInit); getFilesystemVolumesFromServer($this, $isInit);
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -115,4 +115,13 @@ class ServiceDatabase extends BaseModel
{ {
return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
} }
public function isBackupSolutionAvailable()
{
return str($this->databaseType())->contains('mysql') ||
str($this->databaseType())->contains('postgres') ||
str($this->databaseType())->contains('postgis') ||
str($this->databaseType())->contains('mariadb') ||
str($this->databaseType())->contains('mongodb');
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneClickhouse extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneDragonfly extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneKeydb extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneMariadb extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -314,4 +314,9 @@ class StandaloneMongodb extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -295,4 +295,9 @@ class StandaloneMysql extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -296,4 +296,9 @@ class StandalonePostgresql extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -290,4 +290,9 @@ class StandaloneRedis extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -20,12 +20,16 @@ const RESTART_MODE = 'unless-stopped';
const DATABASE_DOCKER_IMAGES = [ const DATABASE_DOCKER_IMAGES = [
'bitnami/mariadb', 'bitnami/mariadb',
'bitnami/mongodb', 'bitnami/mongodb',
'bitnami/mysql',
'bitnami/postgresql',
'bitnami/redis', 'bitnami/redis',
'mysql', 'mysql',
'bitnami/mysql',
'mysql/mysql-server',
'mariadb', 'mariadb',
'postgis/postgis',
'postgres', 'postgres',
'bitnami/postgresql',
'supabase/postgres',
'elestio/postgres',
'mongo', 'mongo',
'redis', 'redis',
'memcached', 'memcached',
@@ -33,7 +37,6 @@ const DATABASE_DOCKER_IMAGES = [
'neo4j', 'neo4j',
'influxdb', 'influxdb',
'clickhouse/clickhouse-server', 'clickhouse/clickhouse-server',
'supabase/postgres',
]; ];
const SPECIFIC_SERVICES = [ const SPECIFIC_SERVICES = [
'quay.io/minio/minio', 'quay.io/minio/minio',

View File

@@ -325,38 +325,16 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push('traefik.http.middlewares.gzip.compress=true'); $labels->push('traefik.http.middlewares.gzip.compress=true');
$labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'); $labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https');
$basic_auth = false; $middlewares_from_labels = collect([]);
$basic_auth_middleware = null;
$redirect = false;
$redirect_middleware = null;
if ($serviceLabels) { if ($serviceLabels) {
$basic_auth = $serviceLabels->contains(function ($value) { $middlewares_from_labels = $serviceLabels->map(function ($item) {
return str_contains($value, 'basicauth'); if (preg_match('/traefik\.http\.middlewares\.(.*?)(\.|$)/', $item, $matches)) {
}); return $matches[1];
if ($basic_auth) { }
$basic_auth_middleware = $serviceLabels return null;
->map(function ($item) { })->filter()
if (preg_match('/traefik\.http\.middlewares\.(.*?)\.basicauth\.users/', $item, $matches)) { ->unique();
return $matches[1];
}
})
->filter()
->first();
}
$redirect = $serviceLabels->contains(function ($value) {
return str_contains($value, 'redirectregex');
});
if ($redirect) {
$redirect_middleware = $serviceLabels
->map(function ($item) {
if (preg_match('/traefik\.http\.middlewares\.(.*?)\.redirectregex\.regex/', $item, $matches)) {
return $matches[1];
}
})
->filter()
->first();
}
} }
foreach ($domains as $loop => $domain) { foreach ($domains as $loop => $domain) {
try { try {
@@ -404,20 +382,15 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port"); $labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port");
} }
if ($path !== '/') { if ($path !== '/') {
// Middleware handling
$middlewares = collect([]); $middlewares = collect([]);
if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { if ($is_stripprefix_enabled && !str($image)->contains('ghost')) {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares->push("{$https_label}-stripprefix"); $middlewares->push("{$https_label}-stripprefix");
} }
if ($is_gzip_enabled) { if ($is_gzip_enabled) {
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push('redir-ghost');
} }
@@ -425,10 +398,13 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_non_www); $labels = $labels->merge($redirect_to_non_www);
$middlewares->push($to_non_www_name); $middlewares->push($to_non_www_name);
} }
if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_www); $labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name); $middlewares->push($to_www_name);
} }
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
if ($middlewares->isNotEmpty()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
@@ -437,13 +413,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares = collect([]); $middlewares = collect([]);
if ($is_gzip_enabled) { if ($is_gzip_enabled) {
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push('redir-ghost');
} }
@@ -455,6 +425,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www); $labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name); $middlewares->push($to_www_name);
} }
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
if ($middlewares->isNotEmpty()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
@@ -490,12 +463,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_gzip_enabled) { if ($is_gzip_enabled) {
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push('redir-ghost');
} }
@@ -507,6 +474,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www); $labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name); $middlewares->push($to_www_name);
} }
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
if ($middlewares->isNotEmpty()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
@@ -516,12 +486,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_gzip_enabled) { if ($is_gzip_enabled) {
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push('redir-ghost');
} }
@@ -533,6 +497,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www); $labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name); $middlewares->push($to_www_name);
} }
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
if ($middlewares->isNotEmpty()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");

View File

@@ -1,14 +1,11 @@
<?php <?php
use App\Models\S3Storage; use App\Models\S3Storage;
use Illuminate\Support\Str;
function set_s3_target(S3Storage $s3) function set_s3_target(S3Storage $s3)
{ {
$is_digital_ocean = false; $is_digital_ocean = false;
if ($s3->endpoint) {
$is_digital_ocean = Str::contains($s3->endpoint, 'digitaloceanspaces.com');
}
config()->set('filesystems.disks.custom-s3', [ config()->set('filesystems.disks.custom-s3', [
'driver' => 's3', 'driver' => 's3',
'region' => $s3['region'], 'region' => $s3['region'],
@@ -17,7 +14,7 @@ function set_s3_target(S3Storage $s3)
'bucket' => $s3['bucket'], 'bucket' => $s3['bucket'],
'endpoint' => $s3['endpoint'], 'endpoint' => $s3['endpoint'],
'use_path_style_endpoint' => true, 'use_path_style_endpoint' => true,
'bucket_endpoint' => $is_digital_ocean, 'bucket_endpoint' => $s3->isHetzner() || $s3->isDigitalOcean(),
'aws_url' => $s3->awsUrl(), 'aws_url' => $s3->awsUrl(),
]); ]);
} }

View File

@@ -708,7 +708,9 @@ function getTopLevelNetworks(Service|Application $resource)
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -758,7 +760,9 @@ function getTopLevelNetworks(Service|Application $resource)
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -824,6 +828,31 @@ function convertToArray($collection)
return $collection; return $collection;
} }
function parseCommandFromMagicEnvVariable(Str|string $key): Stringable
{
$value = str($key);
$count = substr_count($value->value(), '_');
if ($count === 2) {
if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
// SERVICE_FQDN_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
} else {
// SERVICE_BASE64_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
}
}
if ($count === 3) {
if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
// SERVICE_FQDN_UMAMI_1000
$command = $value->after('SERVICE_')->before('_');
} else {
// SERVICE_BASE64_64_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
}
}
return str($command);
}
function parseEnvVariable(Str|string $value) function parseEnvVariable(Str|string $value)
{ {
$value = str($value); $value = str($value);
@@ -855,6 +884,7 @@ function parseEnvVariable(Str|string $value)
} else { } else {
// SERVICE_BASE64_64_UMAMI // SERVICE_BASE64_64_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_'); $command = $value->after('SERVICE_')->beforeLast('_');
ray($command);
} }
} }
} }
@@ -1184,14 +1214,16 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
function parseCommandsByLineForSudo(Collection $commands, Server $server): array function parseCommandsByLineForSudo(Collection $commands, Server $server): array
{ {
$commands = $commands->map(function ($line) { $commands = $commands->map(function ($line) {
if (! str(trim($line))->startsWith([ if (
'cd', ! str(trim($line))->startsWith([
'command', 'cd',
'echo', 'command',
'true', 'echo',
'if', 'true',
'fi', 'if',
])) { 'fi',
])
) {
return "sudo $line"; return "sudo $line";
} }
@@ -1606,7 +1638,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -2521,7 +2555,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -2982,11 +3018,22 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$predefinedPort = '8000'; $predefinedPort = '8000';
} }
if ($isDatabase) { if ($isDatabase) {
$savedService = ServiceDatabase::firstOrCreate([ $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first();
'name' => $serviceName, if ($applicationFound) {
'image' => $image, $savedService = $applicationFound;
'service_id' => $resource->id, $savedService = ServiceDatabase::firstOrCreate([
]); 'name' => $applicationFound->name,
'image' => $applicationFound->image,
'service_id' => $applicationFound->service_id,
]);
$applicationFound->delete();
} else {
$savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
]);
}
} else { } else {
$savedService = ServiceApplication::firstOrCreate([ $savedService = ServiceApplication::firstOrCreate([
'name' => $serviceName, 'name' => $serviceName,
@@ -3096,7 +3143,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
foreach ($magicEnvironments as $key => $value) { foreach ($magicEnvironments as $key => $value) {
$key = str($key); $key = str($key);
$value = replaceVariables($value); $value = replaceVariables($value);
$command = $key->after('SERVICE_')->before('_'); $command = parseCommandFromMagicEnvVariable($key);
$found = $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->first(); $found = $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->first();
if ($found) { if ($found) {
continue; continue;
@@ -3207,12 +3254,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($serviceName === 'plausible') { if ($serviceName === 'plausible') {
$predefinedPort = '8000'; $predefinedPort = '8000';
} }
if ($isDatabase) { if ($isDatabase) {
$savedService = ServiceDatabase::firstOrCreate([ $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first();
'name' => $serviceName, if ($applicationFound) {
'image' => $image, $savedService = $applicationFound;
'service_id' => $resource->id, $savedService = ServiceDatabase::firstOrCreate([
]); 'name' => $applicationFound->name,
'image' => $applicationFound->image,
'service_id' => $applicationFound->service_id,
]);
$applicationFound->delete();
} else {
$savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
]);
}
} else { } else {
$savedService = ServiceApplication::firstOrCreate([ $savedService = ServiceApplication::firstOrCreate([
'name' => $serviceName, 'name' => $serviceName,
@@ -3643,6 +3702,18 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}); });
} }
$serviceLabels = $labels->merge($defaultLabels); $serviceLabels = $labels->merge($defaultLabels);
if ($serviceLabels->count() > 0) {
if ($isApplication) {
$isContainerLabelEscapeEnabled = data_get($resource, 'settings.is_container_label_escape_enabled');
} else {
$isContainerLabelEscapeEnabled = data_get($resource, 'is_container_label_escape_enabled');
}
if ($isContainerLabelEscapeEnabled) {
$serviceLabels = $serviceLabels->map(function ($value, $key) {
return escapeDollarSign($value);
});
}
}
if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
if ($isApplication) { if ($isApplication) {
$shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels;
@@ -3863,14 +3934,19 @@ function convertComposeEnvironmentToArray($environment)
{ {
$convertedServiceVariables = collect([]); $convertedServiceVariables = collect([]);
if (isAssociativeArray($environment)) { if (isAssociativeArray($environment)) {
// Example: $environment = ['FOO' => 'bar', 'BAZ' => 'qux'];
if ($environment instanceof Collection) { if ($environment instanceof Collection) {
$changedEnvironment = collect([]); $changedEnvironment = collect([]);
$environment->each(function ($value, $key) use ($changedEnvironment) { $environment->each(function ($value, $key) use ($changedEnvironment) {
$parts = explode('=', $value, 2); if (is_numeric($key)) {
if (count($parts) === 2) { $parts = explode('=', $value, 2);
$key = $parts[0]; if (count($parts) === 2) {
$realValue = $parts[1] ?? ''; $key = $parts[0];
$changedEnvironment->put($key, $realValue); $realValue = $parts[1] ?? '';
$changedEnvironment->put($key, $realValue);
} else {
$changedEnvironment->put($key, $value);
}
} else { } else {
$changedEnvironment->put($key, $value); $changedEnvironment->put($key, $value);
} }
@@ -3880,12 +3956,15 @@ function convertComposeEnvironmentToArray($environment)
} }
$convertedServiceVariables = $environment; $convertedServiceVariables = $environment;
} else { } else {
// Example: $environment = ['FOO=bar', 'BAZ=qux'];
foreach ($environment as $value) { foreach ($environment as $value) {
$parts = explode('=', $value, 2); if (is_string($value)) {
$key = $parts[0]; $parts = explode('=', $value, 2);
$realValue = $parts[1] ?? ''; $key = $parts[0];
if ($key) { $realValue = $parts[1] ?? '';
$convertedServiceVariables->put($key, $realValue); if ($key) {
$convertedServiceVariables->put($key, $realValue);
}
} }
} }
} }

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.348', 'release' => '4.0.0-beta.355',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.348'; return '4.0.0-beta.355';

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->boolean('dump_all')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->dropColumn('dump_all');
});
}
};

View File

@@ -58,6 +58,7 @@ services:
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"]
vite: vite:
image: node:20 image: node:20
pull_policy: always pull_policy: always

View File

@@ -113,7 +113,7 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.2' image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3'
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002" - "6002:6002"

View File

@@ -1,9 +1,27 @@
FROM quay.io/soketi/soketi:1.6-16-alpine FROM quay.io/soketi/soketi:1.6-16-alpine
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.4.1
WORKDIR /terminal WORKDIR /terminal
RUN apk add --no-cache openssh-client make g++ python3 RUN apk add --no-cache openssh-client make g++ python3 curl
COPY docker/coolify-realtime/package.json ./ COPY docker/coolify-realtime/package.json ./
RUN npm i RUN npm i
RUN npm rebuild node-pty --update-binary RUN npm rebuild node-pty --update-binary
COPY docker/coolify-realtime/soketi-entrypoint.sh /soketi-entrypoint.sh COPY docker/coolify-realtime/soketi-entrypoint.sh /soketi-entrypoint.sh
COPY docker/coolify-realtime/terminal-server.js /terminal/terminal-server.js COPY docker/coolify-realtime/terminal-server.js /terminal/terminal-server.js
RUN /bin/sh -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
echo 'amd64' && \
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
RUN /bin/sh -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"] ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"]

View File

@@ -1,11 +1,19 @@
#!/bin/sh #!/bin/sh
# Function to timestamp logs # Function to timestamp logs
# Check if the first argument is 'watch'
if [ "$1" = "watch" ]; then
WATCH_MODE="--watch"
else
WATCH_MODE=""
fi
timestamp() { timestamp() {
date "+%Y-%m-%d %H:%M:%S" date "+%Y-%m-%d %H:%M:%S"
} }
# Start the terminal server in the background with logging # Start the terminal server in the background with logging
node /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 & node $WATCH_MODE /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 &
TERMINAL_PID=$! TERMINAL_PID=$!
# Start the Soketi process in the background with logging # Start the Soketi process in the background with logging

View File

@@ -61,9 +61,13 @@ wss.on('connection', (ws) => {
const userSession = { ws, userId, ptyProcess: null, isActive: false }; const userSession = { ws, userId, ptyProcess: null, isActive: false };
userSessions.set(userId, userSession); userSessions.set(userId, userSession);
ws.on('message', (message) => handleMessage(userSession, message)); ws.on('message', (message) => {
handleMessage(userSession, message);
});
ws.on('error', (err) => handleError(err, userId)); ws.on('error', (err) => handleError(err, userId));
ws.on('close', () => handleClose(userId)); ws.on('close', () => handleClose(userId));
}); });
const messageHandlers = { const messageHandlers = {
@@ -108,7 +112,6 @@ function parseMessage(message) {
async function handleCommand(ws, command, userId) { async function handleCommand(ws, command, userId) {
const userSession = userSessions.get(userId); const userSession = userSessions.get(userId);
if (userSession && userSession.isActive) { if (userSession && userSession.isActive) {
const result = await killPtyProcess(userId); const result = await killPtyProcess(userId);
if (!result) { if (!result) {
@@ -127,6 +130,7 @@ async function handleCommand(ws, command, userId) {
cols: 80, cols: 80,
rows: 30, rows: 30,
cwd: process.env.HOME, cwd: process.env.HOME,
env: {},
}; };
// NOTE: - Initiates a process within the Terminal container // NOTE: - Initiates a process within the Terminal container
@@ -139,13 +143,16 @@ async function handleCommand(ws, command, userId) {
ws.send('pty-ready'); ws.send('pty-ready');
ptyProcess.onData((data) => ws.send(data)); ptyProcess.onData((data) => {
ws.send(data);
});
// when parent closes // when parent closes
ptyProcess.onExit(({ exitCode, signal }) => { ptyProcess.onExit(({ exitCode, signal }) => {
console.error(`Process exited with code ${exitCode} and signal ${signal}`); console.error(`Process exited with code ${exitCode} and signal ${signal}`);
ws.send('pty-exited'); ws.send('pty-exited');
userSession.isActive = false; userSession.isActive = false;
}); });
if (timeout) { if (timeout) {
@@ -179,7 +186,7 @@ async function killPtyProcess(userId) {
// session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098 // session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098
// patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947 // patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947
session.ptyProcess.write('kill -TERM -$$ && exit\n'); session.ptyProcess.write('set +o history\nkill -TERM -$$ && exit\nset -o history\n');
setTimeout(() => { setTimeout(() => {
if (!session.isActive || !session.ptyProcess) { if (!session.isActive || !session.ptyProcess) {
@@ -228,5 +235,5 @@ function extractHereDocContent(commandString) {
} }
server.listen(6002, () => { server.listen(6002, () => {
console.log('Server listening on port 6002'); console.log('Coolify realtime terminal server listening on port 6002. Let the hacking begin!');
}); });

View File

@@ -46,6 +46,9 @@ services:
- PUSHER_APP_ID - PUSHER_APP_ID
- PUSHER_APP_KEY - PUSHER_APP_KEY
- PUSHER_APP_SECRET - PUSHER_APP_SECRET
- TERMINAL_PROTOCOL
- TERMINAL_HOST
- TERMINAL_PORT
- AUTOUPDATE - AUTOUPDATE
- SELF_HOSTED - SELF_HOSTED
- SSH_MUX_ENABLED - SSH_MUX_ENABLED
@@ -110,7 +113,7 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.2' image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3'
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002" - "6002:6002"

View File

@@ -1,16 +1,16 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.348" "version": "4.0.0-beta.354"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.349" "version": "4.0.0-beta.355"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.1"
}, },
"realtime": { "realtime": {
"version": "1.0.2" "version": "1.0.3"
} }
} }
} }

15
public/svgs/bitcoin.svg Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"
viewBox="0 0 4091.27 4091.73"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_1421344023328">
<path fill="#F7931A" fill-rule="nonzero" d="M4030.06 2540.77c-273.24,1096.01 -1383.32,1763.02 -2479.46,1489.71 -1095.68,-273.24 -1762.69,-1383.39 -1489.33,-2479.31 273.12,-1096.13 1383.2,-1763.19 2479,-1489.95 1096.06,273.24 1763.03,1383.51 1489.76,2479.57l0.02 -0.02z"/>
<path fill="white" fill-rule="nonzero" d="M2947.77 1754.38c40.72,-272.26 -166.56,-418.61 -450,-516.24l91.95 -368.8 -224.5 -55.94 -89.51 359.09c-59.02,-14.72 -119.63,-28.59 -179.87,-42.34l90.16 -361.46 -224.36 -55.94 -92 368.68c-48.84,-11.12 -96.81,-22.11 -143.35,-33.69l0.26 -1.16 -309.59 -77.31 -59.72 239.78c0,0 166.56,38.18 163.05,40.53 90.91,22.69 107.35,82.87 104.62,130.57l-104.74 420.15c6.26,1.59 14.38,3.89 23.34,7.49 -7.49,-1.86 -15.46,-3.89 -23.73,-5.87l-146.81 588.57c-11.11,27.62 -39.31,69.07 -102.87,53.33 2.25,3.26 -163.17,-40.72 -163.17,-40.72l-111.46 256.98 292.15 72.83c54.35,13.63 107.61,27.89 160.06,41.3l-92.9 373.03 224.24 55.94 92 -369.07c61.26,16.63 120.71,31.97 178.91,46.43l-91.69 367.33 224.51 55.94 92.89 -372.33c382.82,72.45 670.67,43.24 791.83,-303.02 97.63,-278.78 -4.86,-439.58 -206.26,-544.44 146.69,-33.83 257.18,-130.31 286.64,-329.61l-0.07 -0.05zm-512.93 719.26c-69.38,278.78 -538.76,128.08 -690.94,90.29l123.28 -494.2c152.17,37.99 640.17,113.17 567.67,403.91zm69.43 -723.3c-63.29,253.58 -453.96,124.75 -580.69,93.16l111.77 -448.21c126.73,31.59 534.85,90.55 468.94,355.05l-0.02 0z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

11
public/svgs/homarr.svg Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="484px" height="329px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:0.947" fill="#f95251" d="M 157.5,-0.5 C 158.833,-0.5 160.167,-0.5 161.5,-0.5C 200.102,3.59921 222.268,24.9325 228,63.5C 228.667,88.5 228.667,113.5 228,138.5C 222.962,132.265 216.629,130.265 209,132.5C 208.667,109.167 208.333,85.8333 208,62.5C 202.332,36.3319 186.165,21.9986 159.5,19.5C 141.783,20.273 127.949,27.9397 118,42.5C 112.924,38.3791 107.757,34.3791 102.5,30.5C 115.97,11.5969 134.303,1.26361 157.5,-0.5 Z"/></g>
<g><path style="opacity:0.947" fill="#f95251" d="M 321.5,-0.5 C 322.833,-0.5 324.167,-0.5 325.5,-0.5C 348.697,1.26361 367.03,11.5969 380.5,30.5C 375.243,34.3791 370.076,38.3791 365,42.5C 349.005,21.3908 328.505,15.2242 303.5,24C 287.107,31.7273 277.607,44.5606 275,62.5C 274.667,85.8333 274.333,109.167 274,132.5C 266.371,130.265 260.038,132.265 255,138.5C 254.333,113.5 254.333,88.5 255,63.5C 260.732,24.9325 282.898,3.59921 321.5,-0.5 Z"/></g>
<g><path style="opacity:0.984" fill="#f95251" d="M -0.5,139.5 C -0.5,137.167 -0.5,134.833 -0.5,132.5C 19.5,132.5 39.5,132.5 59.5,132.5C 51.7127,98.1844 43.3793,64.0177 34.5,30C 83.3876,36.5512 118.554,62.0512 140,106.5C 159.513,155.397 153.846,201.064 123,243.5C 114.899,253.77 105.399,262.437 94.5,269.5C 90.7735,258.059 87.6068,246.392 85,234.5C 39.0428,218.384 10.5428,186.717 -0.5,139.5 Z"/></g>
<g><path style="opacity:0.984" fill="#f95251" d="M 483.5,132.5 C 483.5,134.833 483.5,137.167 483.5,139.5C 472.457,186.717 443.957,218.384 398,234.5C 395.393,246.392 392.226,258.059 388.5,269.5C 351.514,243.037 332.514,206.871 331.5,161C 333.865,105.236 359.865,64.9024 409.5,40C 421.97,34.3953 434.97,31.0619 448.5,30C 439.621,64.0177 431.287,98.1844 423.5,132.5C 443.5,132.5 463.5,132.5 483.5,132.5 Z"/></g>
<g><path style="opacity:0.95" fill="#f95251" d="M 211.5,170.5 C 225.127,170.958 231.627,177.958 231,191.5C 226.507,203.825 218.007,207.659 205.5,203C 197.535,196.871 195.369,189.037 199,179.5C 201.917,174.637 206.083,171.637 211.5,170.5 Z"/></g>
<g><path style="opacity:0.949" fill="#f95251" d="M 265.5,170.5 C 280.848,170.68 287.348,178.347 285,193.5C 279.06,204.76 270.227,207.593 258.5,202C 249.176,192.625 249.176,183.292 258.5,174C 260.925,172.787 263.259,171.621 265.5,170.5 Z"/></g>
<g><path style="opacity:0.987" fill="#f95251" d="M 388.5,328.5 C 386.5,328.5 384.5,328.5 382.5,328.5C 285.992,327.966 189.325,326.966 92.5,325.5C 120.96,261.579 170.294,227.913 240.5,224.5C 299.804,226.887 345.304,252.887 377,302.5C 381.676,310.846 385.509,319.513 388.5,328.5 Z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

6
public/svgs/it-tools.svg Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="512px" height="512px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:0.995" fill="#18a057" d="M 230.5,-0.5 C 247.5,-0.5 264.5,-0.5 281.5,-0.5C 290.12,3.28885 296.287,9.62218 300,18.5C 299.871,28.5889 300.704,38.4222 302.5,48C 326.717,53.6219 349.217,63.1219 370,76.5C 376.469,71.0323 382.636,65.199 388.5,59C 398.925,52.0844 409.591,51.7511 420.5,58C 432,68.1667 442.833,79 453,90.5C 459,100.5 459,110.5 453,120.5C 447.017,127.317 440.851,133.984 434.5,140.5C 447.998,161.821 457.498,184.821 463,209.5C 474.144,210.648 485.31,211.815 496.5,213C 503.496,216.822 508.496,222.322 511.5,229.5C 511.5,246.5 511.5,263.5 511.5,280.5C 508.841,288.664 503.508,294.497 495.5,298C 484.573,299.444 473.573,300.277 462.5,300.5C 457.369,325.4 448.036,348.566 434.5,370C 441.693,377.526 448.526,385.359 455,393.5C 458.667,402.825 458.001,411.825 453,420.5C 443.167,430.333 433.333,440.167 423.5,450C 414.895,457.565 405.229,459.232 394.5,455C 386.322,448.153 378.155,441.32 370,434.5C 348.501,447.828 325.334,457.161 300.5,462.5C 301.01,473.958 300.177,485.291 298,496.5C 294.006,504.342 287.839,509.342 279.5,511.5C 263.167,511.5 246.833,511.5 230.5,511.5C 222.336,508.841 216.503,503.508 213,495.5C 212,484.833 211,474.167 210,463.5C 198.505,459.749 187.005,455.915 175.5,452C 173.842,451.275 173.342,450.108 174,448.5C 192.195,429.971 210.695,411.805 229.5,394C 293.836,401.915 343.336,378.748 378,324.5C 408.025,263.569 400.691,207.569 356,156.5C 315.267,118.147 267.767,106.314 213.5,121C 164.374,138.458 132.874,172.291 119,222.5C 117.485,228.075 116.485,233.742 116,239.5C 115.261,253.906 115.594,268.239 117,282.5C 98.8333,300.667 80.6667,318.833 62.5,337C 61.552,337.483 60.552,337.649 59.5,337.5C 54.8294,325.486 51.1627,313.153 48.5,300.5C 37.4331,300.188 26.4331,299.355 15.5,298C 7.189,293.843 1.85567,287.343 -0.5,278.5C -0.5,262.833 -0.5,247.167 -0.5,231.5C 2.22646,223.578 7.22646,217.411 14.5,213C 25.7307,211.098 37.0641,210.265 48.5,210.5C 53.718,185.511 63.0513,162.178 76.5,140.5C 71.2189,133.716 65.3855,127.383 59,121.5C 51.8558,110.353 52.1892,99.353 60,88.5C 69.8333,78.6667 79.6667,68.8333 89.5,59C 98.3002,53.1199 107.633,52.1199 117.5,56C 125.629,62.4604 133.463,69.2938 141,76.5C 162.028,62.9274 184.861,53.4274 209.5,48C 210.93,37.2282 212.097,26.3949 213,15.5C 216.503,7.49214 222.336,2.15881 230.5,-0.5 Z"/></g>
<g><path style="opacity:0.99" fill="#1d1d1d" d="M 52.5,511.5 C 46.5,511.5 40.5,511.5 34.5,511.5C 16.5,506.167 4.83333,494.5 -0.5,476.5C -0.5,470.167 -0.5,463.833 -0.5,457.5C 1.14258,451.876 3.64258,446.542 7,441.5C 55.9723,391.528 105.306,341.861 155,292.5C 141.187,245.876 152.02,206.042 187.5,173C 216.657,150.235 248.991,143.902 284.5,154C 289.202,158.922 290.702,164.755 289,171.5C 275,186.167 261,200.833 247,215.5C 234.564,236.922 238.73,254.422 259.5,268C 272.178,272.999 284.178,271.666 295.5,264C 309.596,248.899 324.596,234.899 340.5,222C 353.68,220.169 360.513,226.003 361,239.5C 365.554,290.573 345.387,328.073 300.5,352C 273.591,363.046 246.258,364.379 218.5,356C 169.472,405.361 120.139,454.361 70.5,503C 64.8398,506.712 58.8398,509.545 52.5,511.5 Z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

8
public/svgs/strapi.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 22.1867C0 11.7278 0 6.49832 3.24916 3.24916C6.49832 0 11.7278 0 22.1867 0H41.8133C52.2722 0 57.5017 0 60.7508 3.24916C64 6.49832 64 11.7278 64 22.1867V41.8133C64 52.2722 64 57.5017 60.7508 60.7508C57.5017 64 52.2722 64 41.8133 64H22.1867C11.7278 64 6.49832 64 3.24916 60.7508C0 57.5017 0 52.2722 0 41.8133V22.1867Z" fill="#4945FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.156 19.4131H22.6094V30.4004H33.596V41.3864H44.5827V19.8398C44.5827 19.6041 44.3917 19.4131 44.156 19.4131Z" fill="white"/>
<rect x="33.1719" y="30.4004" width="0.426667" height="0.426667" fill="white"/>
<path d="M22.6172 30.4004H33.1772C33.4128 30.4004 33.6039 30.5914 33.6039 30.8271V41.3871H23.0439C22.8082 41.3871 22.6172 41.196 22.6172 40.9604V30.4004Z" fill="#9593FF"/>
<path d="M33.6016 41.3867H44.5882L33.9657 52.0092C33.8314 52.1436 33.6016 52.0484 33.6016 51.8584V41.3867Z" fill="#9593FF"/>
<path d="M22.6151 30.3998H12.1434C11.9534 30.3998 11.8582 30.17 11.9926 30.0356L22.6151 19.4131V30.3998Z" fill="#9593FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -16,6 +16,7 @@ export function initializeTerminalComponent() {
paused: false, paused: false,
MAX_PENDING_WRITES: 5, MAX_PENDING_WRITES: 5,
keepAliveInterval: null, keepAliveInterval: null,
reconnectInterval: null,
init() { init() {
this.setupTerminal(); this.setupTerminal();
@@ -48,6 +49,9 @@ export function initializeTerminalComponent() {
document.addEventListener(event, () => { document.addEventListener(event, () => {
this.checkIfProcessIsRunningAndKillIt(); this.checkIfProcessIsRunningAndKillIt();
clearInterval(this.keepAliveInterval); clearInterval(this.keepAliveInterval);
if (this.reconnectInterval) {
clearInterval(this.reconnectInterval);
}
}, { once: true }); }, { once: true });
}); });
@@ -103,11 +107,27 @@ export function initializeTerminalComponent() {
}; };
this.socket.onclose = () => { this.socket.onclose = () => {
console.log('WebSocket connection closed'); console.log('WebSocket connection closed');
this.reconnect();
}; };
} }
}, },
reconnect() {
if (this.reconnectInterval) {
clearInterval(this.reconnectInterval);
}
this.reconnectInterval = setInterval(() => {
console.log('Attempting to reconnect...');
this.initializeWebSocket();
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
console.log('Reconnected successfully');
clearInterval(this.reconnectInterval);
this.reconnectInterval = null;
window.location.reload();
}
}, 2000);
},
handleSocketMessage(event) { handleSocketMessage(event) {
this.message = '(connection closed)'; this.message = '(connection closed)';
if (event.data === 'pty-ready') { if (event.data === 'pty-ready') {

View File

@@ -8,7 +8,7 @@
<div class="w-96"> <div class="w-96">
<form action="/user/confirm-password" method="POST" class="flex flex-col gap-2"> <form action="/user/confirm-password" method="POST" class="flex flex-col gap-2">
@csrf @csrf
<x-forms.input required type="password" name="password" label="{{ __('input.password') }}" autofocus /> <x-forms.input required type="password" name="password" label="{{ __('input.password') }}" />
<x-forms.button type="submit">{{ __('auth.confirm_password') }}</x-forms.button> <x-forms.button type="submit">{{ __('auth.confirm_password') }}</x-forms.button>
</form> </form>
@if ($errors->any()) @if ($errors->any())

View File

@@ -12,7 +12,7 @@
@if (is_transactional_emails_active()) @if (is_transactional_emails_active())
<form action="/forgot-password" method="POST" class="flex flex-col gap-2"> <form action="/forgot-password" method="POST" class="flex flex-col gap-2">
@csrf @csrf
<x-forms.input required type="email" name="email" label="{{ __('input.email') }}" autofocus /> <x-forms.input required type="email" name="email" label="{{ __('input.email') }}" />
<x-forms.button type="submit">{{ __('auth.forgot_password_send_email') }}</x-forms.button> <x-forms.button type="submit">{{ __('auth.forgot_password_send_email') }}</x-forms.button>
</form> </form>
@else @else

View File

@@ -10,7 +10,7 @@
@csrf @csrf
@env('local') @env('local')
<x-forms.input value="test@example.com" type="email" autocomplete="email" name="email" <x-forms.input value="test@example.com" type="email" autocomplete="email" name="email"
required label="{{ __('input.email') }}" autofocus /> required label="{{ __('input.email') }}" />
<x-forms.input value="password" type="password" autocomplete="current-password" name="password" <x-forms.input value="password" type="password" autocomplete="current-password" name="password"
required label="{{ __('input.password') }}" /> required label="{{ __('input.password') }}" />
@@ -20,7 +20,7 @@
</a> </a>
@else @else
<x-forms.input type="email" name="email" autocomplete="email" required <x-forms.input type="email" name="email" autocomplete="email" required
label="{{ __('input.email') }}" autofocus /> label="{{ __('input.email') }}" />
<x-forms.input type="password" name="password" autocomplete="current-password" required <x-forms.input type="password" name="password" autocomplete="current-password" required
label="{{ __('input.password') }}" /> label="{{ __('input.password') }}" />
<a href="/forgot-password" class="text-xs"> <a href="/forgot-password" class="text-xs">

View File

@@ -16,7 +16,7 @@
label="{{ __('input.email') }}" /> label="{{ __('input.email') }}" />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<x-forms.input required type="password" id="password" name="password" <x-forms.input required type="password" id="password" name="password"
label="{{ __('input.password') }}" autofocus /> label="{{ __('input.password') }}" />
<x-forms.input required type="password" id="password_confirmation" <x-forms.input required type="password" id="password_confirmation"
name="password_confirmation" label="{{ __('input.password.again') }}" /> name="password_confirmation" label="{{ __('input.password.again') }}" />
</div> </div>

View File

@@ -9,7 +9,7 @@
<form action="/two-factor-challenge" method="POST" class="flex flex-col gap-2"> <form action="/two-factor-challenge" method="POST" class="flex flex-col gap-2">
@csrf @csrf
<div> <div>
<x-forms.input type="number" name="code" autocomplete="one-time-code" label="{{ __('input.code') }}" autofocus /> <x-forms.input type="number" name="code" autocomplete="one-time-code" label="{{ __('input.code') }}" />
<div x-show="!showRecovery" <div x-show="!showRecovery"
class="pt-2 text-xs cursor-pointer hover:underline hover:dark:text-white" class="pt-2 text-xs cursor-pointer hover:underline hover:dark:text-white"
x-on:click="showRecovery = !showRecovery">Enter x-on:click="showRecovery = !showRecovery">Enter

View File

@@ -222,12 +222,12 @@
</template> </template>
@endforeach @endforeach
</ul> </ul>
@if ($confirmWithText && $confirmationText) @if ($confirmWithText)
<div class="mb-4"> <div class="mb-4">
<h4 class="mb-2 text-lg font-semibold">Confirm Actions</h4> <h4 class="mb-2 text-lg font-semibold">Confirm Actions</h4>
<p class="mb-2 text-sm">{{ $confirmationLabel }}</p> <p class="mb-2 text-sm">{{ $confirmationLabel }}</p>
<div class="relative mb-2"> <div class="relative mb-2">
<input autocomplete="off" type="text" x-model="confirmationText" <input type="text" x-model="confirmationText"
class="p-2 pr-10 w-full text-black rounded cursor-text input" readonly> class="p-2 pr-10 w-full text-black rounded cursor-text input" readonly>
<button @click="copyConfirmationText()" <button @click="copyConfirmationText()"
class="absolute right-2 top-1/2 text-gray-500 transform -translate-y-1/2 hover:text-gray-700" class="absolute right-2 top-1/2 text-gray-500 transform -translate-y-1/2 hover:text-gray-700"
@@ -255,7 +255,7 @@
class="block mt-4 text-sm font-medium text-gray-700 dark:text-gray-300"> class="block mt-4 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ $shortConfirmationLabel }} {{ $shortConfirmationLabel }}
</label> </label>
<input autocomplete="off" type="text" x-model="userConfirmationText" <input type="text" x-model="userConfirmationText"
class="p-2 mt-1 w-full text-black rounded input"> class="p-2 mt-1 w-full text-black rounded input">
</div> </div>
@endif @endif
@@ -272,8 +272,10 @@
class="block text-sm font-medium text-gray-700 dark:text-gray-300"> class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Your Password Your Password
</label> </label>
<input autocomplete="off" type="password" id="password-confirm" x-model="password" class="w-full input" <form action="return false">
placeholder="Enter your password"> <input type="password" id="password-confirm" x-model="password" class="w-full input"
placeholder="Enter your password">
</form>
<p x-show="passwordError" x-text="passwordError" class="mt-1 text-sm text-red-500"></p> <p x-show="passwordError" x-text="passwordError" class="mt-1 text-sm text-red-500"></p>
@error('password') @error('password')
<p class="mt-1 text-sm text-red-500">{{ $message }}</p> <p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@@ -296,20 +298,13 @@
</template> </template>
<template x-if="step === 1"> <template x-if="step === 1">
@if(isDev() && $submitAction === 'delete') <x-forms.button @click="step++" class="w-auto" isError>
<x-forms.button class="w-auto" isError <span x-text="step1ButtonText"></span>
@click="$wire.delete('hello')"> </x-forms.button>
<span x-text="step3ButtonText"></span>
</x-forms.button>
@else
<x-forms.button @click="step++" class="w-auto" isError>
<span x-text="step1ButtonText"></span>
</x-forms.button>
@endif
</template> </template>
<template x-if="step === 2"> <template x-if="step === 2">
<x-forms.button x-bind:disabled="confirmationText !== '' && confirmWithText && userConfirmationText !== confirmationText" <x-forms.button x-bind:disabled="confirmWithText && userConfirmationText !== confirmationText"
class="w-auto" isError class="w-auto" isError
@click=" @click="
if (dispatchEvent) { if (dispatchEvent) {

View File

@@ -6,10 +6,10 @@
x-transition:enter-start="translate-y-full" x-transition:enter-end="translate-y-0" x-transition:enter-start="translate-y-full" x-transition:enter-end="translate-y-0"
x-transition:leave="transition ease-in duration-300" x-transition:leave-start="translate-y-0" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="translate-y-0"
x-transition:leave-end="translate-y-full" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);" x-transition:leave-end="translate-y-full" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);"
class="fixed bottom-0 right-0 w-full h-auto duration-300 ease-out sm:px-5 sm:pb-5 sm:w-[26rem] lg:w-full z-[999]" class="fixed bottom-0 right-0 w-full h-auto duration-300 ease-out sm:px-5 sm:pb-5 w-full z-[999]"
x-cloak> x-cloak>
<div <div
class="flex flex-col items-center justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded"> class="flex items-center flex-col justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded">
<div <div
class="flex flex-col items-start h-full pb-6 text-xs lg:items-center lg:flex-row lg:pb-0 lg:pr-6 lg:space-x-5 dark:text-neutral-300"> class="flex flex-col items-start h-full pb-6 text-xs lg:items-center lg:flex-row lg:pb-0 lg:pr-6 lg:space-x-5 dark:text-neutral-300">
@if (isset($icon)) @if (isset($icon))
@@ -23,14 +23,12 @@
<p class="">{{ $description }}</span></p> <p class="">{{ $description }}</span></p>
</div> </div>
</div> </div>
<div class="flex items-end justify-end w-full pl-3 space-x-3 lg:flex-shrink-0 lg:w-auto">
<button <button
@if ($buttonText->attributes->whereStartsWith('@click')->first()) @click="bannerVisible=false;{{ $buttonText->attributes->get('@click') }}" @if ($buttonText->attributes->whereStartsWith('@click')->first()) @click="bannerVisible=false;{{ $buttonText->attributes->get('@click') }}"
@else @else
@click="bannerVisible=false;" @endif @click="bannerVisible=false;" @endif
class="inline-flex items-center justify-center flex-shrink-0 w-1/2 px-4 py-2 text-sm font-medium tracking-wide transition-colors duration-200 rounded-md bg-neutral-100 hover:bg-neutral-200 dark:bg-coolgray-200 lg:w-auto dark:text-neutral-200 dark:hover:bg-coolgray-300 focus:shadow-outline focus:outline-none"> class="w-full px-4 py-2 text-sm font-medium tracking-wide transition-colors duration-200 rounded-md bg-neutral-100 hover:bg-neutral-200 dark:bg-coolgray-200 lg:w-auto dark:text-neutral-200 dark:hover:bg-coolgray-300 focus:shadow-outline focus:outline-none">
{{ $buttonText }} {{ $buttonText }}
</button> </button>
</div>
</div> </div>
</div> </div>

View File

@@ -2,9 +2,12 @@
'status' => 'Restarting', 'status' => 'Restarting',
'lastDeploymentInfo' => null, 'lastDeploymentInfo' => null,
'lastDeploymentLink' => null, 'lastDeploymentLink' => null,
'noLoading' => false,
]) ])
<div class="flex items-center"> <div class="flex items-center">
<x-loading wire:loading.delay.longer /> @if (!$noLoading)
<x-loading wire:loading.delay.longer />
@endif
<span wire:loading.remove.delay.longer class="flex items-center"> <span wire:loading.remove.delay.longer class="flex items-center">
<div class="badge badge-warning "></div> <div class="badge badge-warning "></div>
<div class="pl-2 pr-1 text-xs font-bold tracking-wider dark:text-warning" @if($lastDeploymentInfo) title="{{$lastDeploymentInfo}}" @endif> <div class="pl-2 pr-1 text-xs font-bold tracking-wider dark:text-warning" @if($lastDeploymentInfo) title="{{$lastDeploymentInfo}}" @endif>

View File

@@ -2,9 +2,12 @@
'status' => 'Running', 'status' => 'Running',
'lastDeploymentInfo' => null, 'lastDeploymentInfo' => null,
'lastDeploymentLink' => null, 'lastDeploymentLink' => null,
'noLoading' => false,
]) ])
<div class="flex items-center"> <div class="flex items-center">
<x-loading wire:loading.delay.longer /> @if (!$noLoading)
<x-loading wire:loading.delay.longer />
@endif
<span wire:loading.remove.delay.longer class="flex items-center"> <span wire:loading.remove.delay.longer class="flex items-center">
<div class="badge badge-success "></div> <div class="badge badge-success "></div>
<div class="pl-2 pr-1 text-xs font-bold tracking-wider text-success" @if($lastDeploymentInfo) title="{{$lastDeploymentInfo}}" @endif> <div class="pl-2 pr-1 text-xs font-bold tracking-wider text-success" @if($lastDeploymentInfo) title="{{$lastDeploymentInfo}}" @endif>

View File

@@ -1,8 +1,11 @@
@props([ @props([
'status' => 'Stopped', 'status' => 'Stopped',
'noLoading' => false,
]) ])
<div class="flex items-center"> <div class="flex items-center">
<x-loading wire:loading.delay.longer /> @if (!$noLoading)
<x-loading wire:loading.delay.longer />
@endif
<span wire:loading.remove.delay.longer class="flex items-center"> <span wire:loading.remove.delay.longer class="flex items-center">
<div class="badge badge-error "></div> <div class="badge badge-error "></div>
<div class="pl-2 pr-1 text-xs font-bold tracking-wider text-error">{{ str($status)->before(':')->headline() }}</div> <div class="pl-2 pr-1 text-xs font-bold tracking-wider text-error">{{ str($status)->before(':')->headline() }}</div>

View File

@@ -32,21 +32,16 @@
@if (!isCloud()) @if (!isCloud())
<x-popup> <x-popup>
<x-slot:title> <x-slot:title>
<span class="font-bold text-left text-red-500">WARNING: </span>Realtime Error?! <span class="font-bold text-left text-red-500">WARNING: </span> Cannot connect to real-time service
</x-slot:title> </x-slot:title>
<x-slot:description> <x-slot:description>
<span>Coolify could not connect to its real-time service.<br>This will cause unusual problems on the <div>This will cause unusual problems on the
UI UI! <br><br>
if
not fixed! <br><br>
Please ensure that you have opened the Please ensure that you have opened the
<a class="underline" href='https://coolify.io/docs/knowledge-base/server/firewall' <a class="underline" href='https://coolify.io/docs/knowledge-base/server/firewall'
target='_blank'>required ports</a>, target='_blank'>required ports</a> or get
check the
related <a class="underline" href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels'
target='_blank'>documentation</a> or get
help on <a class="underline" href='https://coollabs.io/discord' target='_blank'>Discord</a>. help on <a class="underline" href='https://coollabs.io/discord' target='_blank'>Discord</a>.
</span> </div>
</x-slot:description> </x-slot:description>
<x-slot:button-text @click="disableRealtime()"> <x-slot:button-text @click="disableRealtime()">
Acknowledge & Disable This Popup Acknowledge & Disable This Popup

View File

@@ -1,5 +1,5 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="Your Cool Project" id="name" label="Name" required /> <x-forms.input placeholder="Your Cool Project" id="name" label="Name" required />
<x-forms.input placeholder="This is my cool project everyone knows about" id="description" label="Description" /> <x-forms.input placeholder="This is my cool project everyone knows about" id="description" label="Description" />
<div class="subtitle">New project will have a default production environment.</div> <div class="subtitle">New project will have a default production environment.</div>
<x-forms.button type="submit" @click="slideOverOpen=false"> <x-forms.button type="submit" @click="slideOverOpen=false">

View File

@@ -1,5 +1,5 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="production" id="name" label="Name" required /> <x-forms.input placeholder="production" id="name" label="Name" required />
<x-forms.button type="submit" @click="slideOverOpen=false"> <x-forms.button type="submit" @click="slideOverOpen=false">
Save Save
</x-forms.button> </x-forms.button>

View File

@@ -8,17 +8,14 @@
<livewire:project.database.backup-now :backup="$backup" /> <livewire:project.database.backup-now :backup="$backup" />
@endif @endif
@if ($backup->database_id !== 0) @if ($backup->database_id !== 0)
<x-modal-confirmation <x-modal-confirmation title="Confirm Backup Schedule Deletion?" buttonTitle="Delete Backups and Schedule"
title="Confirm Backup Schedule Deletion?" isErrorButton submitAction="delete" :checkboxes="$checkboxes" :actions="[
buttonTitle="Delete Backups and Schedule" 'The selected backup schedule will be deleted.',
isErrorButton 'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).',
submitAction="delete" ]"
:checkboxes="$checkboxes" confirmationText="{{ $backup->database->name }}"
:actions="['The selected backup schedule will be deleted.', 'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).']" confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
confirmationText="{{ $backup->database->name }}" shortConfirmationLabel="Database Name" />
confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
shortConfirmationLabel="Database Name"
/>
@endif @endif
</div> </div>
<div class="w-48 pb-2"> <div class="w-48 pb-2">
@@ -36,23 +33,39 @@
</div> </div>
@endif @endif
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2"> <h3>Settings</h3>
<div class="flex gap-2 flex-col ">
@if ($backup->database_type === 'App\Models\StandalonePostgresql') @if ($backup->database_type === 'App\Models\StandalonePostgresql')
<x-forms.input label="Databases To Backup" <div class="w-48">
helper="Comma separated list of databases to backup. Empty will include the default one." <x-forms.checkbox label="Backup All Databases" id="backup.dump_all" instantSave />
id="backup.databases_to_backup" /> </div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="backup.databases_to_backup" />
@endif
@elseif($backup->database_type === 'App\Models\StandaloneMongodb') @elseif($backup->database_type === 'App\Models\StandaloneMongodb')
<x-forms.input label="Databases To Include" <x-forms.input label="Databases To Include"
helper="A list of databases to backup. You can specify which collection(s) per database to exclude from the backup. Empty will include all databases and collections.<br><br>Example:<br><br>database1:collection1,collection2|database2:collection3,collection4<br><br> database1 will include all collections except collection1 and collection2. <br>database2 will include all collections except collection3 and collection4.<br><br>Another Example:<br><br>database1:collection1|database2<br><br> database1 will include all collections except collection1.<br>database2 will include ALL collections." helper="A list of databases to backup. You can specify which collection(s) per database to exclude from the backup. Empty will include all databases and collections.<br><br>Example:<br><br>database1:collection1,collection2|database2:collection3,collection4<br><br> database1 will include all collections except collection1 and collection2. <br>database2 will include all collections except collection3 and collection4.<br><br>Another Example:<br><br>database1:collection1|database2<br><br> database1 will include all collections except collection1.<br>database2 will include ALL collections."
id="backup.databases_to_backup" /> id="backup.databases_to_backup" />
@elseif($backup->database_type === 'App\Models\StandaloneMysql') @elseif($backup->database_type === 'App\Models\StandaloneMysql')
<x-forms.input label="Databases To Backup" <div class="w-48">
helper="Comma separated list of databases to backup. Empty will include the default one." <x-forms.checkbox label="Backup All Databases" id="backup.dump_all" instantSave />
id="backup.databases_to_backup" /> </div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="backup.databases_to_backup" />
@endif
@elseif($backup->database_type === 'App\Models\StandaloneMariadb') @elseif($backup->database_type === 'App\Models\StandaloneMariadb')
<x-forms.input label="Databases To Backup" <div class="w-48">
helper="Comma separated list of databases to backup. Empty will include the default one." <x-forms.checkbox label="Backup All Databases" id="backup.dump_all" instantSave />
id="backup.databases_to_backup" /> </div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="backup.databases_to_backup" />
@endif
@endif @endif
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">

View File

@@ -45,20 +45,11 @@
<x-forms.button class="dark:hover:bg-coolgray-400" <x-forms.button class="dark:hover:bg-coolgray-400"
x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button> x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button>
@endif @endif
<x-modal-confirmation <x-modal-confirmation title="Confirm Backup Deletion?" buttonTitle="Delete" isErrorButton
title="Confirm Backup Deletion?" submitAction="deleteBackup({{ data_get($execution, 'id') }})"
buttonTitle="Delete" :actions="['This backup will be permanently deleted from local storage.']" confirmationText="{{ data_get($execution, 'filename') }}"
isErrorButton confirmationLabel="Please confirm the execution of the actions by entering the Backup Filename below"
submitAction="deleteBackup({{ data_get($execution, 'id') }})" shortConfirmationLabel="Backup Filename" step3ButtonText="Permanently Delete" />
{{-- :checkboxes="$checkboxes" --}}
:actions="[
'This backup will be permanently deleted from local storage.'
]"
confirmationText="{{ data_get($execution, 'filename') }}"
confirmationLabel="Please confirm the execution of the actions by entering the Backup Filename below"
shortConfirmationLabel="Backup Filename"
step3ButtonText="Permanently Delete"
/>
</div> </div>
</div> </div>
@empty @empty

View File

@@ -1,17 +1,19 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="0 0 * * * or daily" id="frequency" <x-forms.input placeholder="0 0 * * * or daily" id="frequency"
helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." label="Frequency" helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." label="Frequency"
required /> required />
<x-forms.checkbox id="save_s3" label="Save to S3" /> @if ($s3s->count() === 0)
<x-forms.select id="selected_storage_id"> <div class="text-red-500">No validated S3 Storages found.</div>
@if ($s3s->count() === 0) @else
<option value="0">No S3 Storages found.</option> <x-forms.checkbox wire:model.live="save_s3" label="Save to S3" />
@else @if ($save_s3)
@foreach ($s3s as $s3) <x-forms.select id="selected_storage_id" label="Select a validated S3 storage">
<option value="{{ $s3->id }}">{{ $s3->name }}</option> @foreach ($s3s as $s3)
@endforeach <option value="{{ $s3->id }}">{{ $s3->name }}</option>
@endforeach
</x-forms.select>
@endif @endif
</x-forms.select> @endif
<x-forms.button type="submit" @click="modalOpen=false"> <x-forms.button type="submit" @click="modalOpen=false">
Save Save
</x-forms.button> </x-forms.button>

View File

@@ -102,7 +102,7 @@
<h3>Initialization scripts</h3> <h3>Initialization scripts</h3>
<x-modal-input buttonTitle="+ Add" title="New Init Script"> <x-modal-input buttonTitle="+ Add" title="New Init Script">
<form class="flex flex-col w-full gap-2 rounded" wire:submit='save_new_init_script'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='save_new_init_script'>
<x-forms.input autofocus placeholder="create_test_db.sql" id="new_filename" label="Filename" <x-forms.input placeholder="create_test_db.sql" id="new_filename" label="Filename"
required /> required />
<x-forms.textarea rows="20" placeholder="CREATE DATABASE test;" id="new_content" <x-forms.textarea rows="20" placeholder="CREATE DATABASE test;" id="new_content"
label="Content" required /> label="Content" required />

View File

@@ -1,37 +1,38 @@
<div> <div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@forelse($database->scheduledBackups as $backup) @forelse($database->scheduledBackups as $backup)
@if ($type == 'database') @if ($type == 'database')
<a class="box" <a class="box"
href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}"> href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
<div class="flex flex-col"> <div class="flex flex-col">
<div>Frequency: {{ $backup->frequency }}</div> <div>Frequency: {{ $backup->frequency }}</div>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div> <div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
<div>Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}</div> <div>Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}</div>
</div> </div>
</a> </a>
@else @else
<div class="box" wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')"> <div class="box" wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')">
<div @class([ 'border-coollabs'=> <div @class([
data_get($backup, 'id') === data_get($selectedBackup, 'id'), 'border-coollabs' =>
'flex flex-col border-l-2 border-transparent', data_get($backup, 'id') === data_get($selectedBackup, 'id'),
])> 'flex flex-col border-l-2 border-transparent',
<div>Frequency: {{ $backup->frequency }}</div> ])>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div> <div>Frequency: {{ $backup->frequency }}</div>
<div>Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}</div> <div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
</div> <div>Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}</div>
</div> </div>
@endif </div>
@endif
@empty @empty
<div>No scheduled backups configured.</div> <div>No scheduled backups configured.</div>
@endforelse @endforelse
</div> </div>
@if ($type === 'service-database' && $selectedBackup) @if ($type === 'service-database' && $selectedBackup)
<div class="pt-10"> <div class="pt-10">
<livewire:project.database.backup-edit wire:key="{{ $selectedBackup->id }}" :backup="$selectedBackup" <livewire:project.database.backup-edit wire:key="{{ $selectedBackup->id }}" :backup="$selectedBackup"
:s3s="$s3s" :status="data_get($database, 'status')" /> :s3s="$s3s" :status="data_get($database, 'status')" />
<h3 class="py-4">Executions</h3> <livewire:project.database.backup-executions wire:key="{{ $selectedBackup->uuid }}" :backup="$selectedBackup"
<livewire:project.database.backup-executions wire:key="{{ $selectedBackup->id }}" :backup="$selectedBackup" :database="$database" /> :database="$database" />
</div> </div>
@endif @endif
</div> </div>

View File

@@ -48,7 +48,7 @@
class="items-center justify-center box">+ Add New Resource</a> class="items-center justify-center box">+ Add New Resource</a>
@else @else
<div x-data="searchComponent()"> <div x-data="searchComponent()">
<x-forms.input autofocus placeholder="Search for name, fqdn..." x-model="search" id="null" /> <x-forms.input placeholder="Search for name, fqdn..." x-model="search" id="null" />
<div class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3"> <div class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in allFilteredItems" :key="item.uuid"> <template x-for="item in allFilteredItems" :key="item.uuid">
<span> <span>

View File

@@ -149,6 +149,12 @@
<div class="text-xs">{{ $database->status }}</div> <div class="text-xs">{{ $database->status }}</div>
</div> </div>
<div class="flex items-center px-4"> <div class="flex items-center px-4">
@if ($database->isBackupSolutionAvailable())
<a class="mx-4 text-xs font-bold hover:underline"
href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $database->uuid]) }}#backups">
Backups
</a>
@endif
<a class="mx-4 text-xs font-bold hover:underline" <a class="mx-4 text-xs font-bold hover:underline"
href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $database->uuid]) }}"> href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $database->uuid]) }}">
Settings Settings

View File

@@ -10,9 +10,7 @@
<a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'" <a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''" @click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''"
href="#">General</a> href="#">General</a>
@if (str($serviceDatabase?->databaseType())->contains('mysql') || @if ($serviceDatabase?->isBackupSolutionAvailable())
str($serviceDatabase?->databaseType())->contains('postgres') ||
str($serviceDatabase?->databaseType())->contains('mariadb'))
<a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item" <a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item"
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#">Backups</a> @click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#">Backups</a>
@endif @endif
@@ -28,22 +26,25 @@
</div> </div>
@endisset @endisset
@isset($serviceDatabase) @isset($serviceDatabase)
<x-slot:title> <x-slot:title>
{{ data_get_str($service, 'name')->limit(10) }} > {{ data_get_str($serviceDatabase, 'name')->limit(10) }} | Coolify {{ data_get_str($service, 'name')->limit(10) }} >
</x-slot> {{ data_get_str($serviceDatabase, 'name')->limit(10) }} | Coolify
</x-slot>
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.database :database="$serviceDatabase" /> <livewire:project.service.database :database="$serviceDatabase" />
</div> </div>
<div x-cloak x-show="activeTab === 'backups'"> @if ($serviceDatabase->isBackupSolutionAvailable())
<div class="flex gap-2 "> <div x-cloak x-show="activeTab === 'backups'">
<h2 class="pb-4">Scheduled Backups</h2> <div class="flex gap-2 ">
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup"> <h2 class="pb-4">Scheduled Backups</h2>
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" :s3s="$s3s" /> <x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
</x-modal-input> <livewire:project.database.create-scheduled-backup :database="$serviceDatabase" :s3s="$s3s" />
</div> </x-modal-input>
<livewire:project.database.scheduled-backups :database="$serviceDatabase" /> </div>
</div> <livewire:project.database.scheduled-backups :database="$serviceDatabase" />
@endisset @endif
</div> </div>
@endisset
</div> </div>
</div> </div>
</div>

View File

@@ -22,7 +22,25 @@
</nav> </nav>
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last"> <div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
@if (str($service->status())->contains('running')) @if (str($service->status())->contains('running'))
<button @click="$wire.dispatch('restartEvent')" class="gap-2 button"> <x-dropdown>
<x-slot:title>
Advanced
</x-slot>
<div class="dropdown-item" @click="$wire.dispatch('pullAndRestartEvent')">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path d="M3 3l18 18" />
</svg>
Pull Latest Images & Restart
</div>
</x-dropdown>
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"> stroke-width="2">
@@ -30,8 +48,8 @@
<path d="M20 4v5h-5" /> <path d="M20 4v5h-5" />
</g> </g>
</svg> </svg>
Pull Latest Images & Restart Restart
</button> </x-forms.button>
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" submitAction="stop" <x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" submitAction="stop"
:checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]" :confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]" :confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue"
step2ButtonText="Stop Service" :dispatchEvent="true" dispatchEventType="stopEvent"> step2ButtonText="Stop Service" :dispatchEvent="true" dispatchEventType="stopEvent">
@@ -135,9 +153,13 @@
$wire.$call('start'); $wire.$call('start');
}); });
$wire.$on('restartEvent', () => { $wire.$on('restartEvent', () => {
$wire.$dispatch('info', 'Pulling new images.'); $wire.$dispatch('info', 'Service restart in progress.');
$wire.$call('restart'); $wire.$call('restart');
}); });
$wire.$on('pullAndRestartEvent', () => {
$wire.$dispatch('info', 'Pulling new images.');
$wire.$call('pullAndRestartEvent');
});
$wire.on('imagePulled', () => { $wire.on('imagePulled', () => {
window.dispatchEvent(new CustomEvent('startservice')); window.dispatchEvent(new CustomEvent('startservice'));
$wire.$dispatch('info', 'Restarting service.'); $wire.$dispatch('info', 'Restarting service.');

View File

@@ -1,5 +1,5 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="NODE_ENV" id="key" label="Name" required /> <x-forms.input placeholder="NODE_ENV" id="key" label="Name" required />
<x-forms.textarea x-show="$wire.is_multiline === true" x-cloak id="value" label="Value" required /> <x-forms.textarea x-show="$wire.is_multiline === true" x-cloak id="value" label="Value" required />
<x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value" <x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value"
x-bind:label="$wire.is_multiline === false && 'Value'" required /> x-bind:label="$wire.is_multiline === false && 'Value'" required />

View File

@@ -1,5 +1,5 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="Run cron" id="name" label="Name" /> <x-forms.input placeholder="Run cron" id="name" label="Name" />
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" /> <x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" />
<x-forms.input placeholder="0 0 * * * or daily" <x-forms.input placeholder="0 0 * * * or daily"
helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." id="frequency" helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." id="frequency"

View File

@@ -6,7 +6,8 @@
<div class="pb-4"> <div class="pb-4">
<h2>API Tokens</h2> <h2>API Tokens</h2>
@if (!$isApiEnabled) @if (!$isApiEnabled)
<div>API is disabled. If you want to use the API, please enable it in the <a href="{{ route('settings.index') }}" class="underline dark:text-white">Settings</a> menu.</div> <div>API is disabled. If you want to use the API, please enable it in the <a
href="{{ route('settings.index') }}" class="underline dark:text-white">Settings</a> menu.</div>
@else @else
<div>Tokens are created with the current team as scope. You will only have access to this team's resources. <div>Tokens are created with the current team as scope. You will only have access to this team's resources.
</div> </div>
@@ -25,7 +26,7 @@
@if ($permissions) @if ($permissions)
@foreach ($permissions as $permission) @foreach ($permissions as $permission)
@if ($permission === '*') @if ($permission === '*')
<div>All (root/admin access), be careful!</div> <div>Root access, be careful!</div>
@else @else
<div>{{ $permission }}</div> <div>{{ $permission }}</div>
@endif @endif
@@ -35,6 +36,7 @@
</div> </div>
<h4>Token Permissions</h4> <h4>Token Permissions</h4>
<div class="w-64"> <div class="w-64">
<x-forms.checkbox label="Root Access" wire:model.live="rootAccess"></x-forms.checkbox>
<x-forms.checkbox label="Read-only" wire:model.live="readOnly"></x-forms.checkbox> <x-forms.checkbox label="Read-only" wire:model.live="readOnly"></x-forms.checkbox>
<x-forms.checkbox label="View Sensitive Data" wire:model.live="viewSensitiveData"></x-forms.checkbox> <x-forms.checkbox label="View Sensitive Data" wire:model.live="viewSensitiveData"></x-forms.checkbox>
</div> </div>

View File

@@ -141,7 +141,7 @@
</div> </div>
@endif @endif
@if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional()) @if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional())
<x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels" class="w-full"> <x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels" class="w-full" :closeOutside="false">
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" /> <livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
</x-modal-input> </x-modal-input>
@endif @endif

View File

@@ -4,7 +4,7 @@
@else @else
<form class="flex flex-col w-full gap-2" wire:submit='submit'> <form class="flex flex-col w-full gap-2" wire:submit='submit'>
<div class="flex w-full gap-2 flex-wrap sm:flex-nowrap"> <div class="flex w-full gap-2 flex-wrap sm:flex-nowrap">
<x-forms.input autofocus id="name" label="Name" required /> <x-forms.input id="name" label="Name" required />
<x-forms.input id="description" label="Description" /> <x-forms.input id="description" label="Description" />
</div> </div>
<div class="flex gap-2 flex-wrap sm:flex-nowrap"> <div class="flex gap-2 flex-wrap sm:flex-nowrap">

View File

@@ -1,5 +1,5 @@
<form wire:submit.prevent="addDynamicConfiguration" class="flex flex-col w-full gap-4"> <form wire:submit.prevent="addDynamicConfiguration" class="flex flex-col w-full gap-4">
<x-forms.input autofocus id="fileName" label="Filename" required /> <x-forms.input id="fileName" label="Filename" required />
<x-forms.textarea allowTab useMonacoEditor id="value" label="Configuration" required rows="20" /> <x-forms.textarea allowTab useMonacoEditor id="value" label="Configuration" required rows="20" />
<x-forms.button type="submit" @click="slideOverOpen=false">Save</x-forms.button> <x-forms.button type="submit" @click="slideOverOpen=false">Save</x-forms.button>
</form> </form>

View File

@@ -1,14 +1,14 @@
<div x-init="$wire.checkProxy()" class="flex gap-2"> <div x-init="$wire.checkProxy()" class="flex gap-2">
@if (data_get($server, 'proxy.status') === 'running') @if (data_get($server, 'proxy.status') === 'running')
<x-status.running status="Proxy Running" /> <x-status.running status="Proxy Running" noLoading />
@elseif (data_get($server, 'proxy.status') === 'restarting') @elseif (data_get($server, 'proxy.status') === 'restarting')
<x-status.restarting status="Proxy Restarting" /> <x-status.restarting status="Proxy Restarting" noLoading />
@elseif (data_get($server, 'proxy.force_stop')) @elseif (data_get($server, 'proxy.force_stop'))
<x-status.stopped status="Proxy Stopped" /> <x-status.stopped status="Proxy Stopped" noLoading />
@elseif (data_get($server, 'proxy.status') === 'exited') @elseif (data_get($server, 'proxy.status') === 'exited')
<x-status.stopped status="Proxy Exited" /> <x-status.stopped status="Proxy Exited" noLoading />
@else @else
<x-status.stopped status="Proxy Not Running" /> <x-status.stopped status="Proxy Not Running" noLoading />
@endif @endif
<x-forms.button wire:click='checkProxy(true)'>Refresh</x-forms.button> <x-forms.button wire:click='checkProxy(true)'>Refresh</x-forms.button>
</div> </div>

View File

@@ -4,7 +4,7 @@
<x-forms.input required label="Name" id="name" /> <x-forms.input required label="Name" id="name" />
<x-forms.input label="Description" id="description" /> <x-forms.input label="Description" id="description" />
</div> </div>
<x-forms.input required type="url" label="Endpoint" id="endpoint" /> <x-forms.input required type="url" label="Endpoint" wire:model.blur="endpoint" />
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input required label="Bucket" id="bucket" /> <x-forms.input required label="Bucket" id="bucket" />
<x-forms.input required label="Region" id="region" /> <x-forms.input required label="Region" id="region" />

View File

@@ -1,5 +1,5 @@
<form class="flex flex-col w-full gap-2" wire:submit='submit'> <form class="flex flex-col w-full gap-2" wire:submit='submit'>
<x-forms.input autofocus id="name" label="Name" required /> <x-forms.input id="name" label="Name" required />
<x-forms.input id="description" label="Description" /> <x-forms.input id="description" label="Description" />
<x-forms.button type="submit"> <x-forms.button type="submit">
Continue Continue

View File

@@ -0,0 +1,17 @@
# documentation: https://hub.docker.com/r/ruimarinho/bitcoin-core/
# slogan: A self-hosted Bitcoin Core full node.
# tags: cryptocurrency,node,blockchain,bitcoin
# logo: svgs/bitcoin.svg
services:
bitcoin-core:
image: ruimarinho/bitcoin-core:latest
environment:
- BITCOIN_RPCUSER=${BITCOIN_RPCUSER:-bitcoinuser}
- BITCOIN_RPCPASSWORD=${SERVICE_PASSWORD_PASSWORD64}
- BITCOIN_NETWORK=${BITCOIN_NETWORK:-mainnet}
- BITCOIN_PRINTTOCONSOLE=${BITCOIN_PRINTTOCONSOLE:-1}
- BITCOIN_TXINDEX=${BITCOIN_TXINDEX:-1}
volumes:
- bitcoin_data:/home/bitcoin/.bitcoin

View File

@@ -1,5 +1,5 @@
# documentation: https://docs.docker.com/registry/ # documentation: https://docs.docker.com/registry/
# slogan: The Docker Registry is lets you distribute Docker images. # slogan: The Docker Registry lets you distribute Docker images.
# tags: registry,images,docker # tags: registry,images,docker
# logo: svgs/docker-registry.png # logo: svgs/docker-registry.png
# port: 5000 # port: 5000

View File

@@ -0,0 +1,21 @@
# documentation: https://homarr.dev
# slogan: Homarr is a self-hosted homepage for your services.
# tags: homarr,self-hosted,homepage
# logo: svgs/homarr.svg
# port: 7575
services:
homarr:
image: ghcr.io/ajnart/homarr:latest
environment:
- SERVICE_FQDN_HOMARR_7575
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./homarr/configs:/app/data/configs
- ./homarr/icons:/app/public/icons
- ./homarr/data:/data
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:7575"]
interval: 5s
timeout: 20s
retries: 10

View File

@@ -0,0 +1,18 @@
# documentation: https://github.com/corentinth/it-tools
# slogan: IT Tools is a self-hosted solution for managing various IT tasks.
# tags: it-tools,management,self-hosted
# logo: svgs/it-tools.svg
# port: 80
services:
it-tools:
image: corentinth/it-tools:latest
environment:
- SERVICE_FQDN_ITTOOLS_80
volumes:
- it-tools-data:/app/data
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:80"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -0,0 +1,60 @@
# documentation: https://docs.strapi.io/
# slogan: Open-source headless CMS to build powerful APIs with built-in content management.
# tags: cms, headless, mysql, api
# logo: svgs/strapi.svg
# port: 1337
services:
strapi:
image: "elestio/strapi-development:latest"
environment:
- SERVICE_FQDN_STRAPI_1337
- DATABASE_CLIENT=postgres
- DATABASE_HOST=postgresql
- DATABASE_PORT=5432
- "DATABASE_NAME=${POSTGRESQL_DATABASE:-strapi}"
- DATABASE_USERNAME=$SERVICE_USER_POSTGRESQL
- DATABASE_PASSWORD=$SERVICE_PASSWORD_POSTGRESQL
- JWT_SECRET=$SERVICE_BASE64_64_SECRET
- ADMIN_JWT_SECRET=$SERVICE_BASE64_64_SECRET
- APP_KEYS=$SERVICE_BASE64_64_KEY
- STRAPI_TELEMETRY_DISABLED=${STRAPI_TELEMETRY_DISABLED:-true}
- STRAPI_LICENSE=${STRAPI_LICENSE}
- NODE_ENV=${NODE_ENV:-development}
- BROWSER=${BROWSER:-true}
- STRAPI_PLUGIN_I18N_INIT_LOCALE_CODE=${STRAPI_PLUGIN_I18N_INIT_LOCALE_CODE:-en}
- STRAPI_ENFORCE_SOURCEMAPS=${STRAPI_ENFORCE_SOURCEMAPS:-false}
- FAST_REFRESH=${FAST_REFRESH:-true}
volumes:
- "strapi-config:/opt/app/config"
- "strapi-src:/opt/app/src"
- "strapi-uploads:/opt/app/public/uploads"
healthcheck:
test:
- CMD
- wget
- "-q"
- "--spider"
- "http://127.0.0.1:1337/"
interval: 5s
timeout: 20s
retries: 10
depends_on:
postgresql:
condition: service_healthy
postgresql:
image: "elestio/postgres:latest"
environment:
- "POSTGRES_DB=${POSTGRESQL_DATABASE:-strapi}"
- POSTGRES_USER=$SERVICE_USER_POSTGRESQL
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRESQL
- PGDATA=/var/lib/postgresql/data
volumes:
- "strapi-postgresql-data:/var/lib/postgresql/data"
healthcheck:
test:
- CMD-SHELL
- "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"
interval: 5s
timeout: 20s
retries: 10

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,16 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.348" "version": "4.0.0-beta.355"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.349" "version": "4.0.0-beta.356"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.1"
}, },
"realtime": { "realtime": {
"version": "1.0.2" "version": "1.0.3"
} }
} }
} }