diff --git a/app/Actions/Server/InstallLogDrain.php b/app/Actions/Server/InstallLogDrain.php index 9b6741211..43376efe5 100644 --- a/app/Actions/Server/InstallLogDrain.php +++ b/app/Actions/Server/InstallLogDrain.php @@ -209,6 +209,8 @@ public function handle(Server $server) ]; $command = array_merge($command, $add_envs_command, $restart_command); + StopLogDrain::run($server); + return instant_remote_process($command, $server); } catch (\Throwable $e) { return handleError($e); diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index e2af0c3ba..aef7282e3 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -9,25 +9,21 @@ class StartSentinel { use AsAction; - public function handle(Server $server, bool $restart = false, bool $is_dev = false) + public function handle(Server $server, bool $restart = false, ?string $latestVersion = null) { - // TODO: Sentinel is not available in this version (soon). - if (! $is_dev) { - return; - } - $version = get_latest_sentinel_version(); if ($server->isSwarm() || $server->isBuildServer()) { return; } if ($restart) { StopSentinel::run($server); } - $metrics_history = data_get($server, 'settings.sentinel_metrics_history_days'); - $refresh_rate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds'); - $push_interval = data_get($server, 'settings.sentinel_push_interval_seconds'); + $version = $latestVersion ?? get_latest_sentinel_version(); + $metricsHistory = data_get($server, 'settings.sentinel_metrics_history_days'); + $refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds'); + $pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds'); $token = data_get($server, 'settings.sentinel_token'); $endpoint = data_get($server, 'settings.sentinel_custom_url'); - $mount_dir = '/data/coolify/sentinel'; + $mountDir = '/data/coolify/sentinel'; $image = "ghcr.io/coollabsio/sentinel:$version"; if (! $endpoint) { throw new \Exception('You should set FQDN in Instance Settings.'); @@ -35,26 +31,29 @@ public function handle(Server $server, bool $restart = false, bool $is_dev = fal $environments = [ 'TOKEN' => $token, 'PUSH_ENDPOINT' => $endpoint, - 'PUSH_INTERVAL_SECONDS' => $push_interval, + 'PUSH_INTERVAL_SECONDS' => $pushInterval, 'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false', - 'COLLECTOR_REFRESH_RATE_SECONDS' => $refresh_rate, - 'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history, + 'COLLECTOR_REFRESH_RATE_SECONDS' => $refreshRate, + 'COLLECTOR_RETENTION_PERIOD_DAYS' => $metricsHistory, + ]; + $labels = [ + 'coolify.managed' => 'true', ]; if (isDev()) { // data_set($environments, 'DEBUG', 'true'); - $mount_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel'; // $image = 'sentinel'; + $mountDir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel'; } - $docker_environments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"'; - - $docker_command = "docker run -d $docker_environments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mount_dir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway $image"; + $dockerEnvironments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"'; + $dockerLabels = implode(' ', array_map(fn ($key, $value) => "$key=$value", array_keys($labels), $labels)); + $dockerCommand = "docker run -d $dockerEnvironments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mountDir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway --label $dockerLabels $image"; instant_remote_process([ 'docker rm -f coolify-sentinel || true', - "mkdir -p $mount_dir", - $docker_command, - "chown -R 9999:root $mount_dir", - "chmod -R 700 $mount_dir", + "mkdir -p $mountDir", + $dockerCommand, + "chown -R 9999:root $mountDir", + "chmod -R 700 $mountDir", ], $server); $server->settings->is_sentinel_enabled = true; diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 01e480af5..1e55aa57f 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,13 +2,13 @@ namespace App\Console; +use App\Jobs\CheckAndStartSentinelJob; use App\Jobs\CheckForUpdatesJob; use App\Jobs\CheckHelperImageJob; use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\CleanupStaleMultiplexedConnections; use App\Jobs\DatabaseBackupJob; use App\Jobs\DockerCleanupJob; -use App\Jobs\PullSentinelImageJob; use App\Jobs\PullTemplatesFromCDN; use App\Jobs\ScheduledTaskJob; use App\Jobs\ServerCheckJob; @@ -23,11 +23,11 @@ class Kernel extends ConsoleKernel { - private $all_servers; + private $allServers; protected function schedule(Schedule $schedule): void { - $this->all_servers = Server::all(); + $this->allServers = Server::all(); $settings = instanceSettings(); $schedule->job(new CleanupStaleMultiplexedConnections)->hourly(); @@ -37,9 +37,9 @@ protected function schedule(Schedule $schedule): void $schedule->command('horizon:snapshot')->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); // Server Jobs - $this->check_scheduled_backups($schedule); - $this->check_resources($schedule); - $this->check_scheduled_tasks($schedule); + $this->checkScheduledBackups($schedule); + $this->checkResources($schedule); + $this->checkScheduledTasks($schedule); $schedule->command('uploads:clear')->everyTwoMinutes(); $schedule->command('telescope:prune')->daily(); @@ -51,32 +51,27 @@ protected function schedule(Schedule $schedule): void $schedule->command('cleanup:unreachable-servers')->daily()->onOneServer(); $schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); - $this->schedule_updates($schedule); + $this->scheduleUpdates($schedule); // Server Jobs - $this->check_scheduled_backups($schedule); - $this->check_resources($schedule); - $this->pull_images($schedule); - $this->check_scheduled_tasks($schedule); + $this->checkScheduledBackups($schedule); + $this->checkResources($schedule); + $this->pullImages($schedule); + $this->checkScheduledTasks($schedule); $schedule->command('cleanup:database --yes')->daily(); $schedule->command('uploads:clear')->everyTwoMinutes(); } } - private function pull_images($schedule) + private function pullImages($schedule): void { $settings = instanceSettings(); - $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); + $servers = $this->allServers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); foreach ($servers as $server) { if ($server->isSentinelEnabled()) { $schedule->job(function () use ($server) { - $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $server, false); - $sentinel_found = json_decode($sentinel_found, true); - $status = data_get($sentinel_found, '0.State.Status', 'exited'); - if ($status !== 'running') { - PullSentinelImageJob::dispatch($server); - } + CheckAndStartSentinelJob::dispatch($server); })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); } } @@ -86,7 +81,7 @@ private function pull_images($schedule) ->onOneServer(); } - private function schedule_updates($schedule) + private function scheduleUpdates($schedule): void { $settings = instanceSettings(); @@ -105,21 +100,21 @@ private function schedule_updates($schedule) } } - private function check_resources($schedule) + private function checkResources($schedule): void { if (isCloud()) { - $servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4'); + $servers = $this->allServers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4'); $own = Team::find(0)->servers; $servers = $servers->merge($own); } else { - $servers = $this->all_servers->where('ip', '!=', '1.2.3.4'); + $servers = $this->allServers->where('ip', '!=', '1.2.3.4'); } foreach ($servers as $server) { - $last_sentinel_update = $server->sentinel_updated_at; - if (Carbon::parse($last_sentinel_update)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { + $lastSentinelUpdate = $server->sentinel_updated_at; + $serverTimezone = $server->settings->server_timezone; + if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); } - $serverTimezone = $server->settings->server_timezone; if ($server->settings->force_docker_cleanup) { $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer(); } else { @@ -128,7 +123,7 @@ private function check_resources($schedule) } } - private function check_scheduled_backups($schedule) + private function checkScheduledBackups($schedule): void { $scheduled_backups = ScheduledDatabaseBackup::all(); if ($scheduled_backups->isEmpty()) { @@ -161,7 +156,7 @@ private function check_scheduled_backups($schedule) } } - private function check_scheduled_tasks($schedule) + private function checkScheduledTasks($schedule): void { $scheduled_tasks = ScheduledTask::all(); if ($scheduled_tasks->isEmpty()) { diff --git a/app/Jobs/CheckAndStartSentinelJob.php b/app/Jobs/CheckAndStartSentinelJob.php new file mode 100644 index 000000000..8fbeee663 --- /dev/null +++ b/app/Jobs/CheckAndStartSentinelJob.php @@ -0,0 +1,56 @@ +server, false); + $sentinelFoundJson = json_decode($sentinelFound, true); + $sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited'); + if ($sentinelStatus !== 'running') { + StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion); + + return; + } + // If sentinel is running, check if it needs an update + $runningVersion = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false); + if (empty($runningVersion)) { + $runningVersion = '0.0.0'; + } + if ($latestVersion === '0.0.0' && $runningVersion === '0.0.0') { + StartSentinel::run(server: $this->server, restart: true, latestVersion: 'latest'); + + return; + } else { + if (version_compare($runningVersion, $latestVersion, '<')) { + StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion); + + return; + } + } + } catch (\Throwable $e) { + throw $e; + } + } +} diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php deleted file mode 100644 index 054f81d99..000000000 --- a/app/Jobs/PullSentinelImageJob.php +++ /dev/null @@ -1,47 +0,0 @@ -server, false); - if (empty($local_version)) { - $local_version = '0.0.0'; - } - if (version_compare($local_version, $version, '<')) { - StartSentinel::run($this->server, true); - - return; - } - ray('Sentinel image is up to date'); - } catch (\Throwable $e) { - // send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - throw $e; - } - } -} diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index 384deb80e..62f059129 100644 --- a/app/Jobs/PushServerUpdateJob.php +++ b/app/Jobs/PushServerUpdateJob.php @@ -97,8 +97,6 @@ public function handle() } $data = collect($this->data); - $this->serverStatus(); - $this->server->sentinelHeartbeat(); $this->containers = collect(data_get($data, 'containers')); @@ -212,16 +210,6 @@ public function handle() } - private function serverStatus() - { - if ($this->server->isFunctional() === false) { - throw new \Exception('Server is not ready.'); - } - if ($this->server->status() === false) { - throw new \Exception('Server is not reachable.'); - } - } - private function updateApplicationStatus(string $applicationId, string $containerStatus) { $application = $this->applications->where('id', $applicationId)->first(); diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index 0013ab784..5ac98e954 100644 --- a/app/Jobs/ServerCheckJob.php +++ b/app/Jobs/ServerCheckJob.php @@ -43,22 +43,15 @@ public function __construct(public Server $server) {} public function handle() { try { + if ($this->server->serverStatus() === false) { + return 'Server is not reachable or not ready.'; + } + $this->applications = $this->server->applications(); $this->databases = $this->server->databases(); $this->services = $this->server->services()->get(); $this->previews = $this->server->previews(); - $up = $this->serverStatus(); - if (! $up) { - ray('Server is not reachable.'); - - return 'Server is not reachable.'; - } - if (! $this->server->isFunctional()) { - ray('Server is not ready.'); - - return 'Server is not ready.'; - } if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) { ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers(); if (is_null($this->containers)) { @@ -67,9 +60,14 @@ public function handle() ServerStorageCheckJob::dispatch($this->server); GetContainersStatus::run($this->server, $this->containers, $containerReplicates); + if ($this->server->isSentinelEnabled()) { + CheckAndStartSentinelJob::dispatch($this->server); + } + if ($this->server->isLogDrainEnabled()) { $this->checkLogDrainContainer(); } + if ($this->server->proxySet() && ! $this->server->proxy->force_stop) { $this->server->proxyType(); $foundProxyContainer = $this->containers->filter(function ($value, $key) { @@ -106,39 +104,6 @@ public function handle() } - private function serverStatus() - { - ['uptime' => $uptime] = $this->server->validateConnection(false); - if ($uptime) { - if ($this->server->unreachable_notification_sent === true) { - $this->server->update(['unreachable_notification_sent' => false]); - } - } else { - // $this->server->team?->notify(new Unreachable($this->server)); - foreach ($this->applications as $application) { - $application->update(['status' => 'exited']); - } - foreach ($this->databases as $database) { - $database->update(['status' => 'exited']); - } - foreach ($this->services as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - $app->update(['status' => 'exited']); - } - foreach ($dbs as $db) { - $db->update(['status' => 'exited']); - } - } - - return false; - } - - return true; - - } - private function checkLogDrainContainer() { $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php deleted file mode 100644 index fcc33c859..000000000 --- a/app/Jobs/ServerStatusJob.php +++ /dev/null @@ -1,60 +0,0 @@ -server->isServerReady($this->tries)) { - throw new \RuntimeException('Server is not ready.'); - } - try { - if ($this->server->isFunctional()) { - $this->remove_unnecessary_coolify_yaml(); - if ($this->server->isSentinelEnabled()) { - $this->server->checkSentinel(); - } - } - } catch (\Throwable $e) { - // send_internal_notification('ServerStatusJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - - return handleError($e); - } - - } - - private function remove_unnecessary_coolify_yaml() - { - // This will remote the coolify.yaml file from the server as it is not needed on cloud servers - if (isCloud() && $this->server->id !== 0) { - $file = $this->server->proxyPath().'/dynamic/coolify.yaml'; - - return instant_remote_process([ - "rm -f $file", - ], $this->server, false); - } - } -} diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php index 7c3916954..c646f77eb 100644 --- a/app/Jobs/ServerStorageCheckJob.php +++ b/app/Jobs/ServerStorageCheckJob.php @@ -10,7 +10,6 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\RateLimiter; class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue @@ -39,7 +38,7 @@ public function handle() if (is_null($this->percentage)) { $this->percentage = $this->server->storageCheck(); - Log::info('Server storage check percentage: '.$this->percentage); + loggy('Server storage check percentage: '.$this->percentage); } if (! $this->percentage) { return 'No percentage could be retrieved.'; diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php index 26b31e515..16cd9152e 100644 --- a/app/Livewire/Admin/Index.php +++ b/app/Livewire/Admin/Index.php @@ -14,6 +14,18 @@ class Index extends Component public $search = ''; + public function mount() + { + if (! isCloud()) { + return redirect()->route('dashboard'); + } + + if (auth()->user()->id !== 0) { + return redirect()->route('dashboard'); + } + $this->getSubscribers(); + } + public function submitSearch() { if ($this->search !== '') { @@ -38,17 +50,6 @@ public function submitSearch() } } - public function mount() - { - if (! isCloud()) { - return redirect()->route('dashboard'); - } - if (auth()->user()->id !== 0) { - return redirect()->route('dashboard'); - } - $this->getSubscribers(); - } - public function getSubscribers() { $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php index 2c45de269..10ba0c86a 100644 --- a/app/Livewire/NavbarDeleteTeam.php +++ b/app/Livewire/NavbarDeleteTeam.php @@ -19,7 +19,7 @@ public function mount() public function delete($password) { - if (! InstanceSettings::get('disable_two_step_confirmation')) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index d356d739c..016218b78 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -59,7 +59,7 @@ public function mount() public function delete($password) { - if (! InstanceSettings::get('disable_two_step_confirmation')) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index 30c01fc3d..d7ac18097 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -42,7 +42,7 @@ public function cleanupFailed() public function deleteBackup($executionId, $password) { - if (! InstanceSettings::get('disable_two_step_confirmation')) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 2b6f18ec2..4d070bc0c 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -88,7 +88,7 @@ public function convertToFile() public function delete($password) { - if (! InstanceSettings::get('disable_two_step_confirmation')) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 22d4cf4cb..8324ee645 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -50,7 +50,7 @@ public function instantSaveAdvanced() public function delete($password) { - if (! InstanceSettings::get('disable_two_step_confirmation')) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index c7aad89ef..2a21532ce 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -92,7 +92,7 @@ public function mount() public function delete($password) { - if (! InstanceSettings::get('disable_two_step_confirmation')) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 1e872c60a..c305e817c 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -119,7 +119,7 @@ public function addServer(int $network_id, int $server_id) public function removeServer(int $network_id, int $server_id, $password) { - if (! InstanceSettings::get('disable_two_step_confirmation')) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index 0e140b8c1..f3d9da07d 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -39,7 +39,7 @@ class GetLogs extends Component public ?bool $showTimeStamps = true; - public int $numberOfLines = 100; + public ?int $numberOfLines = 100; public function mount() { @@ -98,7 +98,7 @@ public function getLogs($refresh = false) if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) { return; } - if ($this->numberOfLines <= 0) { + if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) { $this->numberOfLines = 1000; } if ($this->container) { diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index 4c9303033..54b1be3af 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -41,7 +41,7 @@ public function submit() public function delete($password) { - if (! InstanceSettings::get('disable_two_step_confirmation')) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php index 606977b20..cc2b335e1 100644 --- a/app/Livewire/Server/Delete.php +++ b/app/Livewire/Server/Delete.php @@ -17,7 +17,7 @@ class Delete extends Component public function delete($password) { - if (! InstanceSettings::get('disable_two_step_confirmation')) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index 0e4263e53..82d9f5d8e 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -174,7 +174,21 @@ public function instantSave() $this->server->settings->refresh(); return handleError($e, $this); - } finally {} + } finally { + } + } + + public function saveSentinel() + { + try { + $this->validate(); + $this->server->settings->save(); + $this->dispatch('success', 'Sentinel updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } finally { + $this->checkSyncStatus(); + } } public function restartSentinel($notification = true) @@ -184,7 +198,8 @@ public function restartSentinel($notification = true) $this->validate([ 'server.settings.sentinel_custom_url' => 'required|url', ]); - $this->server->restartSentinel(); + $this->server->settings->save(); + $this->server->restartSentinel(async: false); if ($notification) { $this->dispatch('success', 'Sentinel restarted.'); } diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 9747329f6..938d0e548 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -5,6 +5,8 @@ use App\Jobs\CheckForUpdatesJob; use App\Models\InstanceSettings; use App\Models\Server; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Livewire\Component; class Index extends Component @@ -185,8 +187,14 @@ public function render() return view('livewire.settings.index'); } - public function toggleTwoStepConfirmation() + public function toggleTwoStepConfirmation($password) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); + + return; + } + $this->settings->disable_two_step_confirmation = true; $this->settings->save(); $this->disable_two_step_confirmation = true; diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php index ca0c9c1ae..8cb7aad8f 100644 --- a/app/Livewire/Settings/License.php +++ b/app/Livewire/Settings/License.php @@ -28,6 +28,9 @@ public function mount() if (! isCloud()) { abort(404); } + if (! isInstanceAdmin()) { + return redirect()->route('home'); + } $this->instance_id = config('app.id'); $this->settings = instanceSettings(); } diff --git a/app/Livewire/SettingsOauth.php b/app/Livewire/SettingsOauth.php index c3884589f..17b3b89a3 100644 --- a/app/Livewire/SettingsOauth.php +++ b/app/Livewire/SettingsOauth.php @@ -24,6 +24,9 @@ protected function rules() public function mount() { + if (! isInstanceAdmin()) { + return redirect()->route('home'); + } $this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) { $carry[$setting->provider] = $setting; diff --git a/app/Livewire/Tags/Deployments.php b/app/Livewire/Tags/Deployments.php index 270aa176a..e4afa5b60 100644 --- a/app/Livewire/Tags/Deployments.php +++ b/app/Livewire/Tags/Deployments.php @@ -7,19 +7,19 @@ class Deployments extends Component { - public $deployments_per_tag_per_server = []; + public $deploymentsPerTagPerServer = []; - public $resource_ids = []; + public $resourceIds = []; public function render() { return view('livewire.tags.deployments'); } - public function get_deployments() + public function getDeployments() { try { - $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resource_ids)->get([ + $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resourceIds)->get([ 'id', 'application_id', 'application_name', @@ -29,7 +29,7 @@ public function get_deployments() 'server_id', 'status', ])->sortBy('id')->groupBy('server_name')->toArray(); - $this->dispatch('deployments', $this->deployments_per_tag_per_server); + $this->dispatch('deployments', $this->deploymentsPerTagPerServer); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php index a01d00a70..642b2bded 100644 --- a/app/Livewire/Tags/Index.php +++ b/app/Livewire/Tags/Index.php @@ -5,9 +5,11 @@ use App\Http\Controllers\Api\DeployController; use App\Models\Tag; use Illuminate\Support\Collection; +use Livewire\Attributes\Title; use Livewire\Attributes\Url; use Livewire\Component; +#[Title('Tags | Coolify')] class Index extends Component { #[Url()] @@ -21,33 +23,47 @@ class Index extends Component public $webhook = null; - public $deployments_per_tag_per_server = []; + public $deploymentsPerTagPerServer = []; - protected $listeners = ['deployments' => 'update_deployments']; + protected $listeners = ['deployments' => 'updateDeployments']; - public function update_deployments($deployments) + public function render() { - $this->deployments_per_tag_per_server = $deployments; + return view('livewire.tags.index'); } - public function tag_updated() + public function mount() + { + $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); + if ($this->tag) { + $this->tagUpdated(); + } + } + + public function updateDeployments($deployments) + { + $this->deploymentsPerTagPerServer = $deployments; + } + + public function tagUpdated() { if ($this->tag == '') { return; } - $tag = $this->tags->where('name', $this->tag)->first(); + $sanitizedTag = htmlspecialchars($this->tag, ENT_QUOTES, 'UTF-8'); + $tag = $this->tags->where('name', $sanitizedTag)->first(); if (! $tag) { - $this->dispatch('error', "Tag ({$this->tag}) not found."); + $this->dispatch('error', 'Tag ('.e($sanitizedTag).') not found.'); $this->tag = ''; return; } - $this->webhook = generatTagDeployWebhook($tag->name); + $this->webhook = generateTagDeployWebhook($tag->name); $this->applications = $tag->applications()->get(); $this->services = $tag->services()->get(); } - public function redeploy_all() + public function redeployAll() { try { $this->applications->each(function ($resource) { @@ -63,17 +79,4 @@ public function redeploy_all() return handleError($e, $this); } } - - public function mount() - { - $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); - if ($this->tag) { - $this->tag_updated(); - } - } - - public function render() - { - return view('livewire.tags.index'); - } } diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php index 668101edb..0dffcce57 100644 --- a/app/Livewire/Tags/Show.php +++ b/app/Livewire/Tags/Show.php @@ -5,8 +5,10 @@ use App\Http\Controllers\Api\DeployController; use App\Models\ApplicationDeploymentQueue; use App\Models\Tag; +use Livewire\Attributes\Title; use Livewire\Component; +#[Title('Tags | Coolify')] class Show extends Component { public $tags; @@ -28,7 +30,7 @@ public function mount() if (! $tag) { return redirect()->route('tags.index'); } - $this->webhook = generatTagDeployWebhook($tag->name); + $this->webhook = generateTagDeployWebhook($tag->name); $this->applications = $tag->applications()->get(); $this->services = $tag->services()->get(); $this->tag = $tag; diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php index 25af875bb..700c30f0a 100644 --- a/app/Livewire/Team/AdminView.php +++ b/app/Livewire/Team/AdminView.php @@ -78,7 +78,7 @@ private function finalizeDeletion(User $user, Team $team) public function delete($id, $password) { - if (! InstanceSettings::get('disable_two_step_confirmation')) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php index 6c9e405fc..25f8a1ff5 100644 --- a/app/Livewire/Team/InviteLink.php +++ b/app/Livewire/Team/InviteLink.php @@ -41,6 +41,9 @@ private function generate_invite_link(bool $sendEmail = false) { try { $this->validate(); + if (auth()->user()->role() === 'admin' && $this->role === 'owner') { + throw new \Exception('Admins cannot invite owners.'); + } $member_emails = currentTeam()->members()->get()->pluck('email'); if ($member_emails->contains($this->email)) { return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.'); diff --git a/app/Livewire/Team/Member.php b/app/Livewire/Team/Member.php index 680cb901b..fa0cf6122 100644 --- a/app/Livewire/Team/Member.php +++ b/app/Livewire/Team/Member.php @@ -4,10 +4,12 @@ use App\Models\User; use Illuminate\Support\Facades\Cache; +use Livewire\Attributes\Locked; use Livewire\Component; class Member extends Component { + #[Locked] public User $member; public function makeAdmin() diff --git a/app/Models/Server.php b/app/Models/Server.php index ca4e20746..229f34620 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -5,7 +5,9 @@ use App\Actions\Server\InstallDocker; use App\Actions\Server\StartSentinel; use App\Enums\ProxyTypes; -use App\Jobs\PullSentinelImageJob; +use App\Jobs\CheckAndStartSentinelJob; +use App\Notifications\Server\Reachable; +use App\Notifications\Server\Unreachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\SoftDeletes; @@ -61,6 +63,7 @@ protected static function booted() $payload['ip'] = str($server->ip)->trim(); } $server->forceFill($payload); + }); static::created(function ($server) { ServerSetting::create([ @@ -115,12 +118,15 @@ protected static function booted() }); } - public $casts = [ + protected $casts = [ 'proxy' => SchemalessAttributes::class, 'logdrain_axiom_api_key' => 'encrypted', 'logdrain_newrelic_license_key' => 'encrypted', 'delete_unused_volumes' => 'boolean', 'delete_unused_networks' => 'boolean', + 'unreachable_notification_sent' => 'boolean', + 'is_build_server' => 'boolean', + 'force_disabled' => 'boolean', ]; protected $schemalessAttributes = [ @@ -139,11 +145,11 @@ protected static function booted() protected $guarded = []; - public function type() { return 'server'; } + public static function isReachable() { return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true); @@ -514,16 +520,14 @@ public function isForceDisabled() public function forceEnableServer() { - $this->settings->update([ - 'force_disabled' => false, - ]); + $this->settings->force_disabled = false; + $this->settings->save(); } public function forceDisableServer() { - $this->settings->update([ - 'force_disabled' => true, - ]); + $this->settings->force_disabled = true; + $this->settings->save(); $sshKeyFileLocation = "id.root@{$this->uuid}"; Storage::disk('ssh-keys')->delete($sshKeyFileLocation); Storage::disk('ssh-mux')->delete($this->muxFilename()); @@ -570,21 +574,9 @@ public function isServerApiEnabled() return $this->settings->is_sentinel_enabled; } - public function checkSentinel() { - // ray("Checking sentinel on server: {$this->name}"); - if ($this->isSentinelEnabled()) { - $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); - $sentinel_found = json_decode($sentinel_found, true); - $status = data_get($sentinel_found, '0.State.Status', 'exited'); - if ($status !== 'running') { - // ray('Sentinel is not running, starting it...'); - PullSentinelImageJob::dispatch($this); - } else { - // ray('Sentinel is running'); - } - } + CheckAndStartSentinelJob::dispatch($this); } public function getCpuMetrics(int $mins = 5) @@ -631,72 +623,6 @@ public function getMemoryMetrics(int $mins = 5) } } - public function isServerReady(int $tries = 3) - { - if ($this->skipServer()) { - return false; - } - $serverUptimeCheckNumber = $this->unreachable_count; - if ($this->unreachable_count < $tries) { - $serverUptimeCheckNumber = $this->unreachable_count + 1; - } - if ($this->unreachable_count > $tries) { - $serverUptimeCheckNumber = $tries; - } - - $serverUptimeCheckNumberMax = $tries; - - // ray('server: ' . $this->name); - // ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber); - // ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax); - - ['uptime' => $uptime] = $this->validateConnection(); - if ($uptime) { - if ($this->unreachable_notification_sent === true) { - $this->update(['unreachable_notification_sent' => false]); - } - - return true; - } else { - if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) { - // Reached max number of retries - if ($this->unreachable_notification_sent === false) { - ray('Server unreachable, sending notification...'); - // $this->team?->notify(new Unreachable($this)); - $this->update(['unreachable_notification_sent' => true]); - } - if ($this->settings->is_reachable === true) { - $this->settings()->update([ - 'is_reachable' => false, - ]); - } - - foreach ($this->applications() as $application) { - $application->update(['status' => 'exited']); - } - foreach ($this->databases() as $database) { - $database->update(['status' => 'exited']); - } - foreach ($this->services()->get() as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - $app->update(['status' => 'exited']); - } - foreach ($dbs as $db) { - $db->update(['status' => 'exited']); - } - } - } else { - $this->update([ - 'unreachable_count' => $this->unreachable_count + 1, - ]); - } - - return false; - } - } - public function getDiskUsage(): ?string { return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false); @@ -1045,29 +971,43 @@ public function isSwarmWorker() return data_get($this, 'settings.is_swarm_worker'); } + public function serverStatus(): bool + { + if ($this->status() === false) { + return false; + } + if ($this->isFunctional() === false) { + return false; + } + + return true; + } + public function status(): bool { + if ($this->skipServer()) { + return false; + } ['uptime' => $uptime] = $this->validateConnection(false); - if ($uptime) { - if ($this->unreachable_notification_sent === true) { - $this->update(['unreachable_notification_sent' => false]); + if ($uptime === false) { + foreach ($this->applications() as $application) { + $application->status = 'exited'; + $application->save(); } - } else { - // $this->server->team?->notify(new Unreachable($this->server)); - foreach ($this->applications as $application) { - $application->update(['status' => 'exited']); + foreach ($this->databases() as $database) { + $database->status = 'exited'; + $database->save(); } - foreach ($this->databases as $database) { - $database->update(['status' => 'exited']); - } - foreach ($this->services as $service) { + foreach ($this->services() as $service) { $apps = $service->applications()->get(); $dbs = $service->databases()->get(); foreach ($apps as $app) { - $app->update(['status' => 'exited']); + $app->status = 'exited'; + $app->save(); } foreach ($dbs as $db) { - $db->update(['status' => 'exited']); + $db->status = 'exited'; + $db->save(); } } @@ -1077,39 +1017,65 @@ public function status(): bool return true; } + public function isReachableChanged() + { + $this->refresh(); + $unreachableNotificationSent = (bool) $this->unreachable_notification_sent; + $isReachable = (bool) $this->settings->is_reachable; + loggy('Server setting is_reachable changed to '.$isReachable.' for server '.$this->id.'. Unreachable notification sent: '.$unreachableNotificationSent); + // If the server is reachable, send the reachable notification if it was sent before + if ($isReachable === true) { + if ($unreachableNotificationSent === true) { + $this->sendReachableNotification(); + } + } else { + // If the server is unreachable, send the unreachable notification if it was not sent before + if ($unreachableNotificationSent === false) { + $this->sendUnreachableNotification(); + } + } + } + + public function sendReachableNotification() + { + $this->unreachable_notification_sent = false; + $this->save(); + $this->refresh(); + $this->team->notify(new Reachable($this)); + } + + public function sendUnreachableNotification() + { + $this->unreachable_notification_sent = true; + $this->save(); + $this->refresh(); + $this->team->notify(new Unreachable($this)); + } + public function validateConnection($isManualCheck = true) { config()->set('constants.ssh.mux_enabled', ! $isManualCheck); - // ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false')); - $server = Server::find($this->id); - if (! $server) { - return ['uptime' => false, 'error' => 'Server not found.']; - } - if ($server->skipServer()) { + if ($this->skipServer()) { return ['uptime' => false, 'error' => 'Server skipped.']; } try { // Make sure the private key is stored - if ($server->privateKey) { - $server->privateKey->storeInFileSystem(); + if ($this->privateKey) { + $this->privateKey->storeInFileSystem(); } - instant_remote_process(['ls /'], $server); - $server->settings()->update([ - 'is_reachable' => true, - ]); - $server->update([ - 'unreachable_count' => 0, - ]); - if (data_get($server, 'unreachable_notification_sent') === true) { - $server->update(['unreachable_notification_sent' => false]); + instant_remote_process(['ls /'], $this); + if ($this->settings->is_reachable === false) { + $this->settings->is_reachable = true; + $this->settings->save(); } return ['uptime' => true, 'error' => null]; } catch (\Throwable $e) { - $server->settings()->update([ - 'is_reachable' => false, - ]); + if ($this->settings->is_reachable === true) { + $this->settings->is_reachable = false; + $this->settings->save(); + } return ['uptime' => false, 'error' => $e->getMessage()]; } @@ -1265,14 +1231,19 @@ public function isIpv6(): bool return str($this->ip)->contains(':'); } - public function restartSentinel() + public function restartSentinel(bool $async = true): void { try { - StartSentinel::dispatch($this,true); + if ($async) { + StartSentinel::dispatch($this, true); + } else { + StartSentinel::run($this, true); + } } catch (\Throwable $e) { loggy('Error restarting Sentinel: '.$e->getMessage()); } } + public function url() { return base_url().'/server/'.$this->uuid; diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 5011009e6..7a8e7b8ed 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -54,6 +54,8 @@ class ServerSetting extends Model 'force_docker_cleanup' => 'boolean', 'docker_cleanup_threshold' => 'integer', 'sentinel_token' => 'encrypted', + 'is_reachable' => 'boolean', + 'is_usable' => 'boolean', ]; protected static function booted() @@ -70,15 +72,18 @@ protected static function booted() loggy('Error creating server setting: '.$e->getMessage()); } }); - static::updated(function ($setting) { + static::updated(function ($settings) { if ( - $setting->isDirty('sentinel_token') || - $setting->isDirty('sentinel_custom_url') || - $setting->isDirty('sentinel_metrics_refresh_rate_seconds') || - $setting->isDirty('sentinel_metrics_history_days') || - $setting->isDirty('sentinel_push_interval_seconds') + $settings->isDirty('sentinel_token') || + $settings->isDirty('sentinel_custom_url') || + $settings->isDirty('sentinel_metrics_refresh_rate_seconds') || + $settings->isDirty('sentinel_metrics_history_days') || + $settings->isDirty('sentinel_push_interval_seconds') ) { - $setting->server->restartSentinel(); + $settings->server->restartSentinel(); + } + if ($settings->isDirty('is_reachable')) { + $settings->server->isReachableChanged(); } }); } @@ -106,12 +111,13 @@ public function generateSentinelUrl(bool $save = true) $domain = 'http://host.docker.internal:8000'; } elseif ($settings->fqdn) { $domain = $settings->fqdn; - } elseif ($settings->ipv4) { - $domain = $settings->ipv4.':8000'; - } elseif ($settings->ipv6) { - $domain = $settings->ipv6.':8000'; + } elseif ($settings->public_ipv4) { + $domain = 'http://'.$settings->public_ipv4.':8000'; + } elseif ($settings->public_ipv6) { + $domain = 'http://'.$settings->public_ipv6.':8000'; } $this->sentinel_custom_url = $domain; + loggy('Sentinel URL: '.$domain); if ($save) { $this->save(); } diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index 3a01aabe4..e373abc03 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -3,9 +3,6 @@ namespace App\Notifications\Server; use App\Models\Server; -use App\Notifications\Channels\DiscordChannel; -use App\Notifications\Channels\EmailChannel; -use App\Notifications\Channels\TelegramChannel; use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Reachable.php similarity index 74% rename from app/Notifications/Server/Revived.php rename to app/Notifications/Server/Reachable.php index 8d1ceeaef..9b54501d9 100644 --- a/app/Notifications/Server/Revived.php +++ b/app/Notifications/Server/Reachable.php @@ -2,8 +2,6 @@ namespace App\Notifications\Server; -use App\Actions\Docker\GetContainersStatus; -use App\Jobs\ContainerStatusJob; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; @@ -13,25 +11,28 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Facades\RateLimiter; -class Revived extends Notification implements ShouldQueue +class Reachable extends Notification implements ShouldQueue { use Queueable; public $tries = 1; + protected bool $isRateLimited = false; + public function __construct(public Server $server) { - if ($this->server->unreachable_notification_sent === false) { - return; - } - GetContainersStatus::dispatch($server)->onQueue('high'); - // dispatch(new ContainerStatusJob($server)); + $this->isRateLimited = isEmailRateLimited( + limiterKey: 'server-reachable:'.$this->server->id, + ); } public function via(object $notifiable): array { + if ($this->isRateLimited) { + return []; + } + $channels = []; $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); @@ -46,20 +47,8 @@ public function via(object $notifiable): array if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } - $executed = RateLimiter::attempt( - 'notification-server-revived-'.$this->server->uuid, - 1, - function () use ($channels) { - return $channels; - }, - 7200, - ); - if (! $executed) { - return []; - } - - return $executed; + return $channels; } public function toMail(): MailMessage diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index 65ea6a2ff..5bc568e82 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -11,7 +11,6 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Facades\RateLimiter; class Unreachable extends Notification implements ShouldQueue { @@ -19,10 +18,21 @@ class Unreachable extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server) {} + protected bool $isRateLimited = false; + + public function __construct(public Server $server) + { + $this->isRateLimited = isEmailRateLimited( + limiterKey: 'server-unreachable:'.$this->server->id, + ); + } public function via(object $notifiable): array { + if ($this->isRateLimited) { + return []; + } + $channels = []; $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); @@ -37,23 +47,11 @@ public function via(object $notifiable): array if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } - $executed = RateLimiter::attempt( - 'notification-server-unreachable-'.$this->server->uuid, - 1, - function () use ($channels) { - return $channels; - }, - 7200, - ); - if (! $executed) { - return []; - } - - return $executed; + return $channels; } - public function toMail(): MailMessage + public function toMail(): ?MailMessage { $mail = new MailMessage; $mail->subject("Coolify: Your server ({$this->server->name}) is unreachable."); @@ -64,7 +62,7 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): DiscordMessage + public function toDiscord(): ?DiscordMessage { $message = new DiscordMessage( title: ':cross_mark: Server unreachable', @@ -77,7 +75,7 @@ public function toDiscord(): DiscordMessage return $message; } - public function toTelegram(): array + public function toTelegram(): ?array { return [ 'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.", diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index cd0eb709a..dbab6861d 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -39,6 +39,7 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Process; +use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Validator; @@ -173,9 +174,6 @@ function get_latest_sentinel_version(): string return data_get($versions, 'coolify.sentinel.version'); } catch (\Throwable $e) { - //throw $e; - ray($e->getMessage()); - return '0.0.0'; } } @@ -647,7 +645,7 @@ function queryResourcesByUuid(string $uuid) return $resource; } -function generatTagDeployWebhook($tag_name) +function generateTagDeployWebhook($tag_name) { $baseUrl = base_url(); $api = Url::fromString($baseUrl).'/api/v1'; @@ -4041,3 +4039,30 @@ function sslipDomainWarning(string $domains) return $showSslipHttpsWarning; } + +function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?callable $callbackOnSuccess = null): bool +{ + if (isDev()) { + $decaySeconds = 120; + } + $rateLimited = false; + $executed = RateLimiter::attempt( + $limiterKey, + $maxAttempts = 0, + function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) { + isDev() && loggy('Rate limit not reached for '.$limiterKey); + $rateLimited = false; + + if ($callbackOnSuccess) { + $callbackOnSuccess(); + } + }, + $decaySeconds, + ); + if (! $executed) { + isDev() && loggy('Rate limit reached for '.$limiterKey.'. Rate limiter will be disabled for '.$decaySeconds.' seconds.'); + $rateLimited = true; + } + + return $rateLimited; +} diff --git a/public/svgs/foundryvtt.png b/public/svgs/foundryvtt.png new file mode 100644 index 000000000..c6a04508f Binary files /dev/null and b/public/svgs/foundryvtt.png differ diff --git a/public/svgs/homebox.svg b/public/svgs/homebox.svg deleted file mode 100644 index 08670bbb9..000000000 --- a/public/svgs/homebox.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/public/svgs/jupyterlab.svg b/public/svgs/jupyterlab.svg deleted file mode 100644 index ab2550874..000000000 --- a/public/svgs/jupyterlab.svg +++ /dev/null @@ -1,90 +0,0 @@ - -Group.svg -Created using Figma 0.90 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/svgs/martin.png b/public/svgs/martin.png new file mode 100644 index 000000000..d1a99e148 Binary files /dev/null and b/public/svgs/martin.png differ diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index b30078237..34c71d3a1 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -5,7 +5,7 @@ Save - {{-- + {{-- Download Config @@ -238,9 +238,9 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @if ($application->build_pack !== 'dockercompose')
+ label="Use a Build Server?" />
@endif @if ($application->could_set_build_commands()) diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index bfcd42d90..385620ecb 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -158,68 +158,65 @@ class="px-4 py-2 text-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-co @endif - @if (!$server->isSwarm() && !$server->isBuildServer()) - @if (isDev()) -
-

Sentinel

- @if ($server->isSentinelEnabled()) -
settings->sentinel_push_interval_seconds }}s="checkSyncStatus"> - @if ($server->isSentinelLive()) - - Restart - @else - - Sync - @endif -
- @endif -
- @else + + @if ($server->isFunctional() && !$server->isSwarm() && !$server->isBuildServer()) +
+

Sentinel

-
Sentinel is not available in this version (soon).
- @endif - @if (isDev()) -
-
- - @if ($server->isSentinelEnabled()) - + @if ($server->isSentinelEnabled()) +
+ @if ($server->isSentinelLive()) + + Save + Restart @else - + + Save + Sync @endif
+ @endif +
+
+
Experimental feature
+
+ @if ($server->isSentinelEnabled()) -
- - Regenerate -
- - - -
-
- - - -
-
+ + @else + @endif
- @endif - @endif - + @if ($server->isSentinelEnabled()) +
+ + Regenerate +
+ + + +
+
+ + + +
+
+ @endif +
+ + @endif +
diff --git a/resources/views/livewire/server/show-private-key.blade.php b/resources/views/livewire/server/show-private-key.blade.php index f84086bff..7c24a3a9b 100644 --- a/resources/views/livewire/server/show-private-key.blade.php +++ b/resources/views/livewire/server/show-private-key.blade.php @@ -14,17 +14,17 @@
@forelse ($privateKeys as $private_key) -
-
+
+
{{ $private_key->name }}
{{ $private_key->description }}
@if (data_get($server, 'privateKey.uuid') !== $private_key->uuid) - - Use this key + + Use this key @else - + Currently used @endif diff --git a/resources/views/livewire/settings/index.blade.php b/resources/views/livewire/settings/index.blade.php index 91084bff9..8eba94681 100644 --- a/resources/views/livewire/settings/index.blade.php +++ b/resources/views/livewire/settings/index.blade.php @@ -142,22 +142,23 @@ class="px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-coolgray-300 tex helper="When disabled, you will not need to confirm actions with a text and user password. This significantly reduces security and may lead to accidental deletions or unwanted changes. Use with extreme caution, especially on production servers." />
@else -
- -
+
+ +
+
+

Warning!

+

Disabling two step confirmation reduces security (as anyone can easily delete anything) and increases + the + risk of accidental actions. This is not recommended for production servers.

+
@endif -
-

Warning!

-

Disabling two step confirmation reduces security (as anyone can easily delete anything) and increases the - risk of accidental actions. This is not recommended for production servers.

-
diff --git a/resources/views/livewire/tags/deployments.blade.php b/resources/views/livewire/tags/deployments.blade.php index 03da021f9..8f23d994d 100644 --- a/resources/views/livewire/tags/deployments.blade.php +++ b/resources/views/livewire/tags/deployments.blade.php @@ -1,5 +1,5 @@ -
- @forelse ($deployments_per_tag_per_server as $server_name => $deployments) +
+ @forelse ($deploymentsPerTagPerServer as $server_name => $deployments)

{{ $server_name }}

@foreach ($deployments as $deployment) diff --git a/resources/views/livewire/tags/index.blade.php b/resources/views/livewire/tags/index.blade.php index 287e1da55..3a98519a7 100644 --- a/resources/views/livewire/tags/index.blade.php +++ b/resources/views/livewire/tags/index.blade.php @@ -1,15 +1,12 @@
- - Tags | Coolify -

Tags

-
+
Tags help you to perform actions on multiple resources.
@if ($tags->count() === 0)
No tags yet defined yet. Go to a resource and add a tag there.
@else - + @foreach ($tags as $oneTag) @endforeach @@ -21,7 +18,7 @@

Deployments

- @if (count($deployments_per_tag_per_server) > 0) + @if (count($deploymentsPerTagPerServer) > 0) @endif
- +
@endif @endif diff --git a/resources/views/livewire/tags/show.blade.php b/resources/views/livewire/tags/show.blade.php index 869b56dae..f135a8246 100644 --- a/resources/views/livewire/tags/show.blade.php +++ b/resources/views/livewire/tags/show.blade.php @@ -1,7 +1,4 @@
- - Tag | Coolify -

Tags

diff --git a/resources/views/livewire/team/invite-link.blade.php b/resources/views/livewire/team/invite-link.blade.php index 739c06267..2e0f02078 100644 --- a/resources/views/livewire/team/invite-link.blade.php +++ b/resources/views/livewire/team/invite-link.blade.php @@ -1,8 +1,10 @@
- + - + @if (auth()->user()->role() === 'owner') + + @endif diff --git a/routes/web.php b/routes/web.php index eb1480b9f..d4cdff7e5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -83,9 +83,9 @@ Route::get('/admin', AdminIndex::class)->name('admin.index'); -Route::post('/forgot-password', [Controller::class, 'forgot_password'])->name('password.forgot'); +Route::post('/forgot-password', [Controller::class, 'forgot_password'])->name('password.forgot')->middleware('throttle:forgot-password'); Route::get('/realtime', [Controller::class, 'realtime_test'])->middleware('auth'); -Route::get('/waitlist', WaitlistIndex::class)->name('waitlist.index'); +// Route::get('/waitlist', WaitlistIndex::class)->name('waitlist.index'); Route::get('/verify', [Controller::class, 'verify'])->middleware('auth')->name('verify.email'); Route::get('/email/verify/{id}/{hash}', [Controller::class, 'email_verify'])->middleware(['auth'])->name('verify.verify'); Route::middleware(['throttle:login'])->group(function () { diff --git a/scripts/run b/scripts/run index f8ac0f97d..f7e7b5264 100755 --- a/scripts/run +++ b/scripts/run @@ -20,6 +20,9 @@ function help { compgen -A function | cat -n } +function logs { + docker exec -t coolify tail -f storage/logs/laravel.log +} function test { docker exec -t coolify php artisan test --testsuite=Feature } @@ -35,11 +38,6 @@ function db:reset-prod { bash spin exec -u webuser coolify php artisan migrate:fresh --force --seed --seeder=ProductionSeeder || php artisan migrate:fresh --force --seed --seeder=ProductionSeeder } - -function mfs { - db:reset -} - function coolify { bash spin exec -u webuser coolify bash } diff --git a/templates/compose/authentik.yaml b/templates/compose/authentik.yaml index 85281e175..87128f6c4 100644 --- a/templates/compose/authentik.yaml +++ b/templates/compose/authentik.yaml @@ -72,7 +72,7 @@ services: redis: condition: service_healthy postgresql: - image: docker.io/library/postgres:16-alpine + image: postgres:16-alpine restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] @@ -86,7 +86,7 @@ services: - POSTGRES_USER=${SERVICE_USER_POSTGRESQL} - POSTGRES_DB=authentik redis: - image: docker.io/library/redis:alpine + image: redis:alpine command: --save 60 1 --loglevel warning restart: unless-stopped healthcheck: diff --git a/templates/compose/dashboard.yaml b/templates/compose/dashboard.yaml deleted file mode 100644 index f977e3876..000000000 --- a/templates/compose/dashboard.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# documentation: https://github.com/phntxx/dashboard?tab=readme-ov-file#dashboard -# slogan: A dashboard, inspired by SUI. -# tags: dashboard, web, search, bookmarks -# port: 8080 - -services: - dashboard: - image: phntxx/dashboard:latest - environment: - - SERVICE_FQDN_DASHBOARD_8080 - volumes: - - dashboard-data:/app/data - healthcheck: - test: ["CMD", "curl", "-f", "http://127.0.0.1:8080"] - interval: 2s - timeout: 10s - retries: 15 diff --git a/templates/compose/dozzle-with-auth.yaml b/templates/compose/dozzle-with-auth.yaml index 0b0e5b9a3..8521f824b 100644 --- a/templates/compose/dozzle-with-auth.yaml +++ b/templates/compose/dozzle-with-auth.yaml @@ -1,4 +1,3 @@ -# ignore: true # documentation: https://dozzle.dev/ # slogan: Dozzle is a simple and lightweight web UI for Docker logs. # tags: dozzle,docker,logs,web-ui @@ -14,19 +13,19 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock - type: bind - source: /data/users.yml - target: /data/users.yml + source: ./data/users.yml + target: /data/users.yml:ro content: | users: - # "admin" here is username + # "admin" is the username admin: - name: "Admin" - # Just sha-256 which can be computed with "echo -n password | shasum -a 256" - password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" - email: me@email.net + email: test@email.com + name: Admin + # A sha-256 hash of the password you want to use. Can be computed with "echo -n password | shasum -a 256". Default password is "Test". + password: $2a$11$viucCvFLlHWvBNOOI6uypuVU.D09UWb.zswRxEg0MkDPi1q/bKbdG + healthcheck: test: ["CMD", "/dozzle", "healthcheck"] interval: 3s timeout: 30s retries: 5 - start_period: 30s diff --git a/templates/compose/firefly.yaml b/templates/compose/firefly.yaml index 4dd8dda96..1b1c6bf65 100644 --- a/templates/compose/firefly.yaml +++ b/templates/compose/firefly.yaml @@ -29,7 +29,7 @@ services: mysql: condition: service_healthy mysql: - image: mariadb:lts + image: mariadb:11 environment: - MYSQL_USER=${SERVICE_USER_MYSQL} - MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} diff --git a/templates/compose/foundryvtt.yaml b/templates/compose/foundryvtt.yaml new file mode 100644 index 000000000..5cf961a37 --- /dev/null +++ b/templates/compose/foundryvtt.yaml @@ -0,0 +1,52 @@ +# documentation: https://foundryvtt.com/kb/ +# slogan: Foundry Virtual Tabletop is a self-hosted & modern roleplaying platform +# tags: foundryvtt,foundry,vtt,ttrpg,roleplaying +# logo: svgs/foundryvtt.png +# port: 30000 + +services: + foundryvtt: + image: felddy/foundryvtt:release + expose: + - 30000 + environment: + - SERVICE_FQDN_FOUNDRY_30000 + # Account username or email address for foundryvtt.com. Required for downloading an application distribution. + - FOUNDRY_USERNAME=${FOUNDRY_USERNAME} + # Account password for foundryvtt.com. Required for downloading an application distribution. + - FOUNDRY_PASSWORD=${FOUNDRY_PASSWORD} + # The presigned URL generate from the user's profile. Required for downloading an application distribution if username/password are not provided. + - FOUNDRY_RELEASE_URL=${FOUNDRY_RELEASE_URL} + # The license key to install. e.g.; AAAA-BBBB-CCCC-DDDD-EEEE-FFFF If left unset, a license key will be fetched when using account authentication. + - FOUNDRY_LICENSE_KEY=${FOUNDRY_LICENSE_KEY} + # Admin password to be applied at startup. If omitted the admin password will be cleared. + - FOUNDRY_ADMIN_KEY=${FOUNDRY_ADMIN:-atropos} + # A custom hostname to use in place of the host machine's public IP address when displaying the address of the game session. This allows for reverse proxies or DNS servers to modify the public address. Example: foundry.example.com + - FOUNDRY_HOSTNAME=${FOUNDRY_HOSTNAME} + # A string path which is appended to the base hostname to serve Foundry VTT content from a specific namespace. For example setting this to demo will result in data being served from http://x.x.x.x/demo/. + - FOUNDRY_ROUTE_PREFIX=${FOUNDRY_ROUTE_PREFIX} + # Inform the Foundry server that the software is running behind a reverse proxy on some other port. This allows the invitation links created to the game to include the correct external port. + - FOUNDRY_PROXY_PORT=${FOUNDRY_PROXY_PORT:-80} + # Indicates whether the software is running behind a reverse proxy that uses SSL. This allows invitation links and A/V functionality to work as if the Foundry server had SSL configured directly. + - FOUNDRY_PROXY_SSL=${FOUNDRY_PROXY_SSL:-true} + # An absolute or relative path that points to the awsConfig.json⁠ or true for AWS environment variable credentials evaluation⁠ usage. + - FOUNDRY_AWS_CONFIG=${FOUNDRY_AWS_CONFIG} + # The default application language and module which provides the core translation files. + - FOUNDRY_LANGUAGE=${FOUNDRY_LANGUAGE:-en.core} + # Choose the CSS theme for the setup page. Choose from foundry, fantasy, or scifi. + - FOUNDRY_CSS_THEME=${FOUNDRY_CSS_THEME:-foundry} + # Set to true to reduce network traffic by serving minified static JavaScript and CSS files. Enabling this setting is recommended for most users, but module developers may wish to disable it. + - FOUNDRY_MINIFY_STATIC_FILES=${FOUNDRY_MINIFY_STATIC_FILES:-true} + # The world ID to startup at system start. + - FOUNDRY_WORLD=${FOUNDRY_WORLD} + - FOUNDRY_TELEMETRY=${FOUNDRY_TELEMETRY:-false} + - TIMEZONE=${TIMEZONE:-UTC} + # Set a path to cache downloads of the Foundry distribution archive and speed up subsequent container startups. + - CONTAINER_CACHE=/data/container_cache + volumes: + - foundryvtt-data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:30000"] + timeout: 5s + interval: 30s + retries: 3 diff --git a/templates/compose/homebox.yaml b/templates/compose/homebox.yaml deleted file mode 100644 index f7c09ed31..000000000 --- a/templates/compose/homebox.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# documentation: https://github.com/hay-kot/homebox -# slogan: Homebox is a self-hosted file management solution. -# tags: homebox,file-management,self-hosted -# logo: svgs/homebox.svg -# port: 7745 - -services: - homebox: - image: ghcr.io/hay-kot/homebox:latest - environment: - - SERVICE_FQDN_HOMEBOX_7745 - - HBOX_LOG_LEVEL=${HBOX_LOG_LEVEL:-info} - - HBOX_LOG_FORMAT=${HBOX_LOG_FORMAT:-text} - - HBOX_WEB_MAX_UPLOAD_SIZE=${HBOX_WEB_MAX_UPLOAD_SIZE:-10} - volumes: - - homebox-data:/data/ - healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:7745"] - interval: 5s - timeout: 20s - retries: 10 diff --git a/templates/compose/jupyterlab.yaml b/templates/compose/jupyterlab.yaml deleted file mode 100644 index 765e8cc06..000000000 --- a/templates/compose/jupyterlab.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# documentation: https://jupyterlab.readthedocs.io/en/latest/ -# slogan: JupyterLab Notebook with C++ (xeus-cling) and Javascript (Deno) Kernel -# tags: jupyter,notebook,python,cpp,deno,jupyterlab -# logo: svgs/jupyterlab.svg -# port: 8008 - -services: - jupyterlab: - image: yokowasis/jupyterlab - platform: linux/amd64 - expose: - - 8008 - environment: - - SERVICE_FQDN_JUPYTERLAB_8008 - - PORT=${PORT:-8008} - - TOKEN=${SERVICE_PASSWORD_TOKEN} - - CONDA_PACKAGES=${CONDA_PACKAGES:-pandas numpy matplotlib seaborn scikit-learn pytorch nltk openpyxl category_encoders scikit-learn tensorflow spacy} - - PIP_PACKAGES=${PIP_PACKAGES:-sastrawi} - volumes: - - jupyterlab-data:/home/mambauser/data - healthcheck: - test: - [ - "CMD", - "curl", - "-f", - "http://127.0.0.1:8008/login/", - ] - timeout: 5s - interval: 5s - retries: 5 diff --git a/templates/compose/martin.yaml b/templates/compose/martin.yaml new file mode 100644 index 000000000..a56ebe12c --- /dev/null +++ b/templates/compose/martin.yaml @@ -0,0 +1,36 @@ +# documentation: https://maplibre.org/martin/introduction.html/ +# slogan: Martin is a tile server able to generate and serve vector tiles on the fly from large PostGIS databases, PMTiles (local or remote), and MBTiles files, allowing multiple tile sources to be dynamically combined into one. +# tags: postgis, vector, tiles +# logo: svgs/martin.png +# port: 3000 + +services: + martin: + image: ghcr.io/maplibre/martin:v0.13.0 + environment: + - SERVICE_FQDN_MARTIN_3000 + - HOST=${SERVICE_FQDN_MARTIN} + - DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgresql:5432/${POSTGRES_DB:-martin-db} + depends_on: + postgresql: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000"] + interval: 5s + timeout: 20s + retries: 10 + + postgresql: + image: postgis/postgis:16-3.4-alpine + platform: linux/amd64 + volumes: + - martin-postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=$SERVICE_USER_POSTGRES + - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - POSTGRES_DB=${POSTGRES_DB:-martin-db} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/mosquitto.yaml b/templates/compose/mosquitto.yaml index 1ec6f0cdc..475d7cf39 100644 --- a/templates/compose/mosquitto.yaml +++ b/templates/compose/mosquitto.yaml @@ -32,7 +32,7 @@ services: echo ''listener 1883'' > /mosquitto/config/mosquitto.conf; fi && echo ''require_certificate ''$REQUIRE_CERTIFICATE >> /mosquitto/config/mosquitto.conf && - echo ''allow_anonymous ''$ALLOW_ANONYMOUS >> /mosquitto/config/mosquitto.conf && + echo ''allow_anonymous ''$ALLOW_ANONYMOUS >> /mosquitto/config/mosquitto.conf; if [ -n ''$SERVICE_USER_MOSQUITTO''] && [ -n ''$SERVICE_PASSWORD_MOSQUITTO'' ]; then echo ''password_file /mosquitto/config/passwords'' >> /mosquitto/config/mosquitto.conf && touch /mosquitto/config/passwords && diff --git a/templates/compose/paperless.yaml b/templates/compose/paperless.yaml index af74b5ea1..76c7c5b55 100644 --- a/templates/compose/paperless.yaml +++ b/templates/compose/paperless.yaml @@ -5,7 +5,7 @@ services: redis: - image: docker.io/library/redis:7.4 + image: redis:7.4 volumes: - paperless-redis:/data healthcheck: diff --git a/templates/compose/reactive-resume.yaml b/templates/compose/reactive-resume.yaml index e2d18e8e4..0cf8ed6b9 100644 --- a/templates/compose/reactive-resume.yaml +++ b/templates/compose/reactive-resume.yaml @@ -45,10 +45,11 @@ services: retries: 10 minio: - image: minio/minio + image: quay.io/minio/minio:latest command: server /data --console-address ":9001" environment: - - SERVICE_FQDN_MINIO_9000 + - MINIO_SERVER_URL=$MINIO_SERVER_URL + - MINIO_BROWSER_REDIRECT_URL=$MINIO_BROWSER_REDIRECT_URL - MINIO_ROOT_USER=$SERVICE_USER_MINIO - MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO volumes: @@ -61,6 +62,7 @@ services: chrome: image: ghcr.io/browserless/chrome:latest + platform: linux/amd64 environment: - HEALTH=true - TIMEOUT=10000 @@ -68,7 +70,7 @@ services: - TOKEN=$SERVICE_PASSWORD_CHROMETOKEN redis: - image: redis:alpine + image: redis:7-alpine command: redis-server volumes: - redis_data:/data diff --git a/templates/compose/weird.yaml b/templates/compose/weird.yaml deleted file mode 100644 index 85a3afe19..000000000 --- a/templates/compose/weird.yaml +++ /dev/null @@ -1,77 +0,0 @@ -# ignore: true -services: - ghost: - image: ghost:5 - volumes: - - ~/configs:/etc/configs/:ro - - ./var/lib/ghost/content:/tmp/ghost2/content:ro - - /var/lib/ghost/content:/tmp/ghost/content:rw - - ghost-content-data:/var/lib/ghost/content - - type: volume - source: mydata - target: /data - volume: - nocopy: true - - type: bind - source: ./var/lib/ghost/data - target: /data - - type: bind - source: /tmp - target: /tmp - labels: - - "test.label=true" - ports: - - "3000" - - "3000-3005" - - "8000:8000" - - "9090-9091:8080-8081" - - "49100:22" - - "127.0.0.1:8001:8001" - - "127.0.0.1:5000-5010:5000-5010" - - "127.0.0.1::5000" - - "6060:6060/udp" - - "12400-12500:1240" - - target: 80 - published: 8080 - protocol: tcp - mode: host - networks: - - some-network - - other-network - environment: - - database__client=${DATABASE_CLIENT:-mysql} - - database__connection__database=${MYSQL_DATABASE:-ghost} - - database__connection__host=${DATABASE_CONNECTION_HOST:-mysql} - - test=${TEST:?true} - - url=$SERVICE_FQDN_GHOST - - database__connection__user=$SERVICE_USER_MYSQL - - database__connection__password=$SERVICE_PASSWORD_MYSQL - depends_on: - - mysql - mysql: - image: mysql:8.0 - volumes: - - ghost-mysql-data:/var/lib/mysql - environment: - - MYSQL_USER=${SERVICE_USER_MYSQL} - - MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} - - MYSQL_DATABASE=$MYSQL_DATABASE - - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT} - - SESSION_SECRET - minio: - image: minio/minio - environment: - RACK_ENV: development - A: $A - SHOW: ${SHOW} - SHOW1: ${SHOW2-show1} - SHOW2: ${SHOW3:-show2} - SHOW3: ${SHOW4?show3} - SHOW4: ${SHOW5:?show4} - SHOW5: ${SERVICE_USER_MINIO} - SHOW6: ${SERVICE_PASSWORD_MINIO} - SHOW7: ${SERVICE_PASSWORD_64_MINIO} - SHOW8: ${SERVICE_BASE64_64_MINIO} - SHOW9: ${SERVICE_BASE64_128_MINIO} - SHOW10: ${SERVICE_BASE64_MINIO} - SHOW11: diff --git a/templates/service-templates.json b/templates/service-templates.json index 67aff4d1b..ab7677bca 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -115,7 +115,7 @@ "authentik": { "documentation": "https://docs.goauthentik.io/docs/installation/docker-compose?utm_source=coolify.io", "slogan": "An open-source Identity Provider, focused on flexibility and versatility.", - "compose": "c2VydmljZXM6CiAgYXV0aGVudGlrLXNlcnZlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI0LjguMH0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogc2VydmVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVVUSEVOVElLU0VSVkVSXzkwMDAKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEOi10cnVlfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL21lZGlhOi9tZWRpYScKICAgICAgLSAnLi9jdXN0b20tdGVtcGxhdGVzOi90ZW1wbGF0ZXMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgYXV0aGVudGlrLXdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI0LjguMH0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogd29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB1c2VyOiByb290CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnLi9tZWRpYTovbWVkaWEnCiAgICAgIC0gJy4vY2VydHM6L2NlcnRzJwogICAgICAtICcuL2N1c3RvbS10ZW1wbGF0ZXM6L3RlbXBsYXRlcycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vbGlicmFyeS9wb3N0Z3JlczoxNi1hbHBpbmUnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1kICQke1BPU1RHUkVTX0RCfSAtVSAkJHtQT1NUR1JFU19VU0VSfScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAnYXV0aGVudGlrLWRiOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gUE9TVEdSRVNfREI9YXV0aGVudGlrCiAgcmVkaXM6CiAgICBpbWFnZTogJ2RvY2tlci5pby9saWJyYXJ5L3JlZGlzOmFscGluZScKICAgIGNvbW1hbmQ6ICctLXNhdmUgNjAgMSAtLWxvZ2xldmVsIHdhcm5pbmcnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdyZWRpcy1jbGkgcGluZyB8IGdyZXAgUE9ORycKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXM6L2RhdGEnCg==", + "compose": "c2VydmljZXM6CiAgYXV0aGVudGlrLXNlcnZlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI0LjguMH0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogc2VydmVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVVUSEVOVElLU0VSVkVSXzkwMDAKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEOi10cnVlfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL21lZGlhOi9tZWRpYScKICAgICAgLSAnLi9jdXN0b20tdGVtcGxhdGVzOi90ZW1wbGF0ZXMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgYXV0aGVudGlrLXdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI0LjguMH0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogd29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB1c2VyOiByb290CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnLi9tZWRpYTovbWVkaWEnCiAgICAgIC0gJy4vY2VydHM6L2NlcnRzJwogICAgICAtICcuL2N1c3RvbS10ZW1wbGF0ZXM6L3RlbXBsYXRlcycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1kICQke1BPU1RHUkVTX0RCfSAtVSAkJHtQT1NUR1JFU19VU0VSfScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAnYXV0aGVudGlrLWRiOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gUE9TVEdSRVNfREI9YXV0aGVudGlrCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGNvbW1hbmQ6ICctLXNhdmUgNjAgMSAtLWxvZ2xldmVsIHdhcm5pbmcnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdyZWRpcy1jbGkgcGluZyB8IGdyZXAgUE9ORycKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXM6L2RhdGEnCg==", "tags": [ "identity", "login", @@ -408,20 +408,6 @@ "minversion": "0.0.0", "port": "8000" }, - "dashboard": { - "documentation": "https://github.com/phntxx/dashboard?tab=readme-ov-file#dashboard?utm_source=coolify.io", - "slogan": "A dashboard, inspired by SUI.", - "compose": "c2VydmljZXM6CiAgZGFzaGJvYXJkOgogICAgaW1hZ2U6ICdwaG50eHgvZGFzaGJvYXJkOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9EQVNIQk9BUkRfODA4MAogICAgdm9sdW1lczoKICAgICAgLSAnZGFzaGJvYXJkLWRhdGE6L2FwcC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", - "tags": [ - "dashboard", - "web", - "search", - "bookmarks" - ], - "logo": "svgs/coolify.png", - "minversion": "0.0.0", - "port": "8080" - }, "directus-with-postgresql": { "documentation": "https://directus.io?utm_source=coolify.io", "slogan": "Directus wraps databases with a dynamic API, and provides an intuitive app for managing its content.", @@ -516,6 +502,20 @@ "logo": "svgs/dokuwiki.png", "minversion": "0.0.0" }, + "dozzle-with-auth": { + "documentation": "https://dozzle.dev/?utm_source=coolify.io", + "slogan": "Dozzle is a simple and lightweight web UI for Docker logs.", + "compose": "c2VydmljZXM6CiAgZG96emxlOgogICAgaW1hZ2U6ICdhbWlyMjAvZG96emxlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET1paTEVfODA4MAogICAgICAtIERPWlpMRV9BVVRIX1BST1ZJREVSPXNpbXBsZQogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2RhdGEvdXNlcnMueW1sCiAgICAgICAgdGFyZ2V0OiAnL2RhdGEvdXNlcnMueW1sOnJvJwogICAgICAgIGNvbnRlbnQ6ICJ1c2VyczpcbiAgIyBcImFkbWluXCIgaXMgdGhlIHVzZXJuYW1lXG4gIGFkbWluOlxuICAgIGVtYWlsOiB0ZXN0QGVtYWlsLmNvbVxuICAgIG5hbWU6IEFkbWluXG4gICAgIyBBIHNoYS0yNTYgaGFzaCBvZiB0aGUgcGFzc3dvcmQgeW91IHdhbnQgdG8gdXNlLiBDYW4gYmUgY29tcHV0ZWQgd2l0aCBcImVjaG8gLW4gcGFzc3dvcmQgfCBzaGFzdW0gLWEgMjU2XCIuIERlZmF1bHQgcGFzc3dvcmQgaXMgXCJUZXN0XCIuXG4gICAgcGFzc3dvcmQ6ICQyYSQxMSR2aXVjQ3ZGTGxIV3ZCTk9PSTZ1eXB1VlUuRDA5VVdiLnpzd1J4RWcwTWtEUGkxcS9iS2JkR1xuIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9kb3p6bGUKICAgICAgICAtIGhlYWx0aGNoZWNrCiAgICAgIGludGVydmFsOiAzcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogNQo=", + "tags": [ + "dozzle", + "docker", + "logs", + "web-ui" + ], + "logo": "svgs/dozzle.svg", + "minversion": "0.0.0", + "port": "8080" + }, "dozzle": { "documentation": "https://dozzle.dev/guide/getting-started#running-with-docker?utm_source=coolify.io", "slogan": "Dozzle is a simple and lightweight web UI for Docker logs.", @@ -629,7 +629,7 @@ "firefly": { "documentation": "https://firefly-iii.org?utm_source=coolify.io", "slogan": "A personal finances manager that can help you save money.", - "compose": "c2VydmljZXM6CiAgZmlyZWZseToKICAgIGltYWdlOiAnZmlyZWZseWlpaS9jb3JlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSVJFRkxZXzgwODAKICAgICAgLSBBUFBfS0VZPSRTRVJWSUNFX0JBU0U2NF9BUFBLRVkKICAgICAgLSBEQl9IT1NUPW15c3FsCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfQ09OTkVDVElPTj1teXNxbAogICAgICAtICdEQl9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1maXJlZmx5fScKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBTVEFUSUNfQ1JPTl9UT0tFTj0kU0VSVklDRV9CQVNFNjRfQ1JPTlRPS0VOCiAgICAgIC0gJ1RSVVNURURfUFJPWElFUz0qJwogICAgdm9sdW1lczoKICAgICAgLSAnZmlyZWZseS11cGxvYWQ6L3Zhci93d3cvaHRtbC9zdG9yYWdlL3VwbG9hZCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdtYXJpYWRiOmx0cycKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnTVlTUUxfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtNWVNRTF9EQVRBQkFTRTotZmlyZWZseX0nCiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbWFyaWFkYi1hZG1pbgogICAgICAgIC0gcGluZwogICAgICAgIC0gJy1oJwogICAgICAgIC0gMTI3LjAuMC4xCiAgICAgICAgLSAnLXVyb290JwogICAgICAgIC0gJy1wJHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ZpcmVmbHktbXlzcWwtZGF0YTovdmFyL2xpYi9teXNxbCcKICBjcm9uOgogICAgaW1hZ2U6IGFscGluZQogICAgZW50cnlwb2ludDoKICAgICAgLSAvZW50cnlwb2ludC5zaAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZW50cnlwb2ludC5zaAogICAgICAgIHRhcmdldDogL2VudHJ5cG9pbnQuc2gKICAgICAgICBjb250ZW50OiAiIyEvYmluL3NoXG4jIFN1YnN0aXR1dGUgdGhlIGVudmlyb25tZW50IHZhcmlhYmxlIGludG8gdGhlIGNyb24gY29tbWFuZFxuQ1JPTl9DT01NQU5EPVwiMCAzICogKiAqIHdnZXQgLXFPLSBodHRwOi8vZmlyZWZseTo4MDgwL2FwaS92MS9jcm9uLyR7U1RBVElDX0NST05fVE9LRU59XCJcbiMgQWRkIHRoZSBjcm9uIGNvbW1hbmQgdG8gdGhlIGNyb250YWJcbmVjaG8gXCIkQ1JPTl9DT01NQU5EXCIgfCBjcm9udGFiIC1cbiMgU3RhcnQgdGhlIGNyb24gZGFlbW9uIGluIHRoZSBmb3JlZ3JvdW5kIHdpdGggbG9nZ2luZyB0byBzdGRvdXRcbmNyb25kIC1mIC1MIC9kZXYvc3Rkb3V0IgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU1RBVElDX0NST05fVE9LRU49JFNFUlZJQ0VfQkFTRTY0X0NST05UT0tFTgo=", + "compose": "c2VydmljZXM6CiAgZmlyZWZseToKICAgIGltYWdlOiAnZmlyZWZseWlpaS9jb3JlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSVJFRkxZXzgwODAKICAgICAgLSBBUFBfS0VZPSRTRVJWSUNFX0JBU0U2NF9BUFBLRVkKICAgICAgLSBEQl9IT1NUPW15c3FsCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfQ09OTkVDVElPTj1teXNxbAogICAgICAtICdEQl9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1maXJlZmx5fScKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBTVEFUSUNfQ1JPTl9UT0tFTj0kU0VSVklDRV9CQVNFNjRfQ1JPTlRPS0VOCiAgICAgIC0gJ1RSVVNURURfUFJPWElFUz0qJwogICAgdm9sdW1lczoKICAgICAgLSAnZmlyZWZseS11cGxvYWQ6L3Zhci93d3cvaHRtbC9zdG9yYWdlL3VwbG9hZCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1maXJlZmx5fScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYXJpYWRiLWFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSAxMjcuMC4wLjEKICAgICAgICAtICctdXJvb3QnCiAgICAgICAgLSAnLXAke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgdm9sdW1lczoKICAgICAgLSAnZmlyZWZseS1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogIGNyb246CiAgICBpbWFnZTogYWxwaW5lCiAgICBlbnRyeXBvaW50OgogICAgICAtIC9lbnRyeXBvaW50LnNoCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9lbnRyeXBvaW50LnNoCiAgICAgICAgdGFyZ2V0OiAvZW50cnlwb2ludC5zaAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vc2hcbiMgU3Vic3RpdHV0ZSB0aGUgZW52aXJvbm1lbnQgdmFyaWFibGUgaW50byB0aGUgY3JvbiBjb21tYW5kXG5DUk9OX0NPTU1BTkQ9XCIwIDMgKiAqICogd2dldCAtcU8tIGh0dHA6Ly9maXJlZmx5OjgwODAvYXBpL3YxL2Nyb24vJHtTVEFUSUNfQ1JPTl9UT0tFTn1cIlxuIyBBZGQgdGhlIGNyb24gY29tbWFuZCB0byB0aGUgY3JvbnRhYlxuZWNobyBcIiRDUk9OX0NPTU1BTkRcIiB8IGNyb250YWIgLVxuIyBTdGFydCB0aGUgY3JvbiBkYWVtb24gaW4gdGhlIGZvcmVncm91bmQgd2l0aCBsb2dnaW5nIHRvIHN0ZG91dFxuY3JvbmQgLWYgLUwgL2Rldi9zdGRvdXQiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTVEFUSUNfQ1JPTl9UT0tFTj0kU0VSVklDRV9CQVNFNjRfQ1JPTlRPS0VOCg==", "tags": [ "finance", "money", @@ -770,6 +770,21 @@ "minversion": "0.0.0", "port": "3000" }, + "foundryvtt": { + "documentation": "https://foundryvtt.com/kb/?utm_source=coolify.io", + "slogan": "Foundry Virtual Tabletop is a self-hosted & modern roleplaying platform", + "compose": "c2VydmljZXM6CiAgZm91bmRyeXZ0dDoKICAgIGltYWdlOiAnZmVsZGR5L2ZvdW5kcnl2dHQ6cmVsZWFzZScKICAgIGV4cG9zZToKICAgICAgLSAzMDAwMAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZPVU5EUllfMzAwMDAKICAgICAgLSAnRk9VTkRSWV9VU0VSTkFNRT0ke0ZPVU5EUllfVVNFUk5BTUV9JwogICAgICAtICdGT1VORFJZX1BBU1NXT1JEPSR7Rk9VTkRSWV9QQVNTV09SRH0nCiAgICAgIC0gJ0ZPVU5EUllfUkVMRUFTRV9VUkw9JHtGT1VORFJZX1JFTEVBU0VfVVJMfScKICAgICAgLSAnRk9VTkRSWV9MSUNFTlNFX0tFWT0ke0ZPVU5EUllfTElDRU5TRV9LRVl9JwogICAgICAtICdGT1VORFJZX0FETUlOX0tFWT0ke0ZPVU5EUllfQURNSU46LWF0cm9wb3N9JwogICAgICAtICdGT1VORFJZX0hPU1ROQU1FPSR7Rk9VTkRSWV9IT1NUTkFNRX0nCiAgICAgIC0gJ0ZPVU5EUllfUk9VVEVfUFJFRklYPSR7Rk9VTkRSWV9ST1VURV9QUkVGSVh9JwogICAgICAtICdGT1VORFJZX1BST1hZX1BPUlQ9JHtGT1VORFJZX1BST1hZX1BPUlQ6LTgwfScKICAgICAgLSAnRk9VTkRSWV9QUk9YWV9TU0w9JHtGT1VORFJZX1BST1hZX1NTTDotdHJ1ZX0nCiAgICAgIC0gJ0ZPVU5EUllfQVdTX0NPTkZJRz0ke0ZPVU5EUllfQVdTX0NPTkZJR30nCiAgICAgIC0gJ0ZPVU5EUllfTEFOR1VBR0U9JHtGT1VORFJZX0xBTkdVQUdFOi1lbi5jb3JlfScKICAgICAgLSAnRk9VTkRSWV9DU1NfVEhFTUU9JHtGT1VORFJZX0NTU19USEVNRTotZm91bmRyeX0nCiAgICAgIC0gJ0ZPVU5EUllfTUlOSUZZX1NUQVRJQ19GSUxFUz0ke0ZPVU5EUllfTUlOSUZZX1NUQVRJQ19GSUxFUzotdHJ1ZX0nCiAgICAgIC0gJ0ZPVU5EUllfV09STEQ9JHtGT1VORFJZX1dPUkxEfScKICAgICAgLSAnRk9VTkRSWV9URUxFTUVUUlk9JHtGT1VORFJZX1RFTEVNRVRSWTotZmFsc2V9JwogICAgICAtICdUSU1FWk9ORT0ke1RJTUVaT05FOi1VVEN9JwogICAgICAtIENPTlRBSU5FUl9DQUNIRT0vZGF0YS9jb250YWluZXJfY2FjaGUKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ZvdW5kcnl2dHQtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwMCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICByZXRyaWVzOiAzCg==", + "tags": [ + "foundryvtt", + "foundry", + "vtt", + "ttrpg", + "roleplaying" + ], + "logo": "svgs/foundryvtt.png", + "minversion": "0.0.0", + "port": "30000" + }, "freshrss-with-mariadb": { "documentation": "https://freshrss.org/index.html?utm_source=coolify.io", "slogan": "A free, self-hostable feed aggregator.", @@ -1062,19 +1077,6 @@ "minversion": "0.0.0", "port": "7575" }, - "homebox": { - "documentation": "https://github.com/hay-kot/homebox?utm_source=coolify.io", - "slogan": "Homebox is a self-hosted file management solution.", - "compose": "c2VydmljZXM6CiAgaG9tZWJveDoKICAgIGltYWdlOiAnZ2hjci5pby9oYXkta290L2hvbWVib3g6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0hPTUVCT1hfNzc0NQogICAgICAtICdIQk9YX0xPR19MRVZFTD0ke0hCT1hfTE9HX0xFVkVMOi1pbmZvfScKICAgICAgLSAnSEJPWF9MT0dfRk9STUFUPSR7SEJPWF9MT0dfRk9STUFUOi10ZXh0fScKICAgICAgLSAnSEJPWF9XRUJfTUFYX1VQTE9BRF9TSVpFPSR7SEJPWF9XRUJfTUFYX1VQTE9BRF9TSVpFOi0xMH0nCiAgICB2b2x1bWVzOgogICAgICAtICdob21lYm94LWRhdGE6L2RhdGEvJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjc3NDUnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", - "tags": [ - "homebox", - "file-management", - "self-hosted" - ], - "logo": "svgs/homebox.svg", - "minversion": "0.0.0", - "port": "7745" - }, "homepage": { "documentation": "https://gethomepage.dev/latest/?utm_source=coolify.io", "slogan": "A modern, fully static, fast, secure fully proxied, highly customizable application dashboard", @@ -1182,22 +1184,6 @@ "minversion": "0.0.0", "port": "22300" }, - "jupyterlab": { - "documentation": "https://jupyterlab.readthedocs.io/en/latest/?utm_source=coolify.io", - "slogan": "JupyterLab Notebook with C++ (xeus-cling) and Javascript (Deno) Kernel", - "compose": "c2VydmljZXM6CiAganVweXRlcmxhYjoKICAgIGltYWdlOiB5b2tvd2FzaXMvanVweXRlcmxhYgogICAgcGxhdGZvcm06IGxpbnV4L2FtZDY0CiAgICBleHBvc2U6CiAgICAgIC0gODAwOAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0pVUFlURVJMQUJfODAwOAogICAgICAtICdQT1JUPSR7UE9SVDotODAwOH0nCiAgICAgIC0gJ1RPS0VOPSR7U0VSVklDRV9QQVNTV09SRF9UT0tFTn0nCiAgICAgIC0gJ0NPTkRBX1BBQ0tBR0VTPSR7Q09OREFfUEFDS0FHRVM6LXBhbmRhcyBudW1weSBtYXRwbG90bGliIHNlYWJvcm4gc2Npa2l0LWxlYXJuIHB5dG9yY2ggbmx0ayBvcGVucHl4bCBjYXRlZ29yeV9lbmNvZGVycyBzY2lraXQtbGVhcm4gdGVuc29yZmxvdyBzcGFjeX0nCiAgICAgIC0gJ1BJUF9QQUNLQUdFUz0ke1BJUF9QQUNLQUdFUzotc2FzdHJhd2l9JwogICAgdm9sdW1lczoKICAgICAgLSAnanVweXRlcmxhYi1kYXRhOi9ob21lL21hbWJhdXNlci9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwMDgvbG9naW4vJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogNQo=", - "tags": [ - "jupyter", - "notebook", - "python", - "cpp", - "deno", - "jupyterlab" - ], - "logo": "svgs/jupyterlab.svg", - "minversion": "0.0.0", - "port": "8008" - }, "keycloak-with-postgres": { "documentation": "https://www.keycloak.org?utm_source=coolify.io", "slogan": "Keycloak is an open-source Identity and Access Management tool.", @@ -1442,6 +1428,19 @@ "minversion": "0.0.0", "port": "8025" }, + "martin": { + "documentation": "https://maplibre.org/martin/introduction.html/?utm_source=coolify.io", + "slogan": "Martin is a tile server able to generate and serve vector tiles on the fly from large PostGIS databases, PMTiles (local or remote), and MBTiles files, allowing multiple tile sources to be dynamically combined into one.", + "compose": "c2VydmljZXM6CiAgbWFydGluOgogICAgaW1hZ2U6ICdnaGNyLmlvL21hcGxpYnJlL21hcnRpbjp2MC4xMy4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01BUlRJTl8zMDAwCiAgICAgIC0gJ0hPU1Q9JHtTRVJWSUNFX0ZRRE5fTUFSVElOfScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlc3FsOjU0MzIvJHtQT1NUR1JFU19EQjotbWFydGluLWRifScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdpcy9wb3N0Z2lzOjE2LTMuNC1hbHBpbmUnCiAgICBwbGF0Zm9ybTogbGludXgvYW1kNjQKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21hcnRpbi1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbWFydGluLWRifScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "postgis", + "vector", + "tiles" + ], + "logo": "svgs/martin.png", + "minversion": "0.0.0", + "port": "3000" + }, "mattermost": { "documentation": "https://docs.mattermost.com?utm_source=coolify.io", "slogan": "Mattermost is an open source, self-hosted Slack-alternative.", @@ -1609,7 +1608,7 @@ "mosquitto": { "documentation": "https://mosquitto.org/documentation/?utm_source=coolify.io", "slogan": "Mosquitto is lightweight and suitable for use on all devices, from low-power single-board computers to full servers.", - "compose": "c2VydmljZXM6CiAgbW9zcXVpdHRvOgogICAgaW1hZ2U6IGVjbGlwc2UtbW9zcXVpdHRvCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTU9TUVVJVFRPXzE4ODMKICAgICAgLSAnTVFUVF9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9NT1NRVUlUVE99JwogICAgICAtICdNUVRUX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NT1NRVUlUVE99JwogICAgICAtICdSRVFVSVJFX0NFUlRJRklDQVRFPSR7UkVRVUlSRV9DRVJUSUZJQ0FURTotZmFsc2V9JwogICAgICAtICdBTExPV19BTk9OWU1PVVM9JHtBTExPV19BTk9OWU1PVVM6LXRydWV9JwogICAgdm9sdW1lczoKICAgICAgLSAnbW9zcXVpdHRvLWNvbmZpZzovbW9zcXVpdHRvL2NvbmZpZycKICAgICAgLSAnbW9zcXVpdHRvLWNlcnRzOi9jZXJ0cycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnZXhpdCAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgICBlbnRyeXBvaW50OiAic2ggLWMgXCIgaWYgWyAnJFJFUVVJUkVfQ0VSVElGSUNBVEUnID0gJ3RydWUnIF07IHRoZW4gZWNobyAnbGlzdGVuZXIgODg4MycgPiAvbW9zcXVpdHRvL2NvbmZpZy9tb3NxdWl0dG8uY29uZiAmJiBlY2hvICdjYWZpbGUgL2NlcnRzL2NhLmNydCcgPj4gL21vc3F1aXR0by9jb25maWcvbW9zcXVpdHRvLmNvbmYgJiYgZWNobyAnY2VydGZpbGUgL2NlcnRzL3NlcnZlci5jcnQnID4+IC9tb3NxdWl0dG8vY29uZmlnL21vc3F1aXR0by5jb25mICYmIGVjaG8gJ2tleWZpbGUgIC9jZXJ0cy9zZXJ2ZXIua2V5JyA+PiAvbW9zcXVpdHRvL2NvbmZpZy9tb3NxdWl0dG8uY29uZjsgZWxzZSBlY2hvICdsaXN0ZW5lciAxODgzJyA+IC9tb3NxdWl0dG8vY29uZmlnL21vc3F1aXR0by5jb25mOyBmaSAmJiBlY2hvICdyZXF1aXJlX2NlcnRpZmljYXRlICckUkVRVUlSRV9DRVJUSUZJQ0FURSA+PiAvbW9zcXVpdHRvL2NvbmZpZy9tb3NxdWl0dG8uY29uZiAmJiBlY2hvICdhbGxvd19hbm9ueW1vdXMgJyRBTExPV19BTk9OWU1PVVMgPj4gL21vc3F1aXR0by9jb25maWcvbW9zcXVpdHRvLmNvbmYgJiYgaWYgWyAtbiAnJFNFUlZJQ0VfVVNFUl9NT1NRVUlUVE8nXSAmJiBbIC1uICckU0VSVklDRV9QQVNTV09SRF9NT1NRVUlUVE8nIF07IHRoZW4gZWNobyAncGFzc3dvcmRfZmlsZSAvbW9zcXVpdHRvL2NvbmZpZy9wYXNzd29yZHMnID4+IC9tb3NxdWl0dG8vY29uZmlnL21vc3F1aXR0by5jb25mICYmIHRvdWNoIC9tb3NxdWl0dG8vY29uZmlnL3Bhc3N3b3JkcyAmJiBjaG1vZCAwNzAwIC9tb3NxdWl0dG8vY29uZmlnL3Bhc3N3b3JkcyAmJiBjaG93biByb290OnJvb3QgL21vc3F1aXR0by9jb25maWcvcGFzc3dvcmRzICYmIG1vc3F1aXR0b19wYXNzd2QgLWIgLWMgL21vc3F1aXR0by9jb25maWcvcGFzc3dvcmRzICRTRVJWSUNFX1VTRVJfTU9TUVVJVFRPICRTRVJWSUNFX1BBU1NXT1JEX01PU1FVSVRUTyAmJiBjaG93biBtb3NxdWl0dG86bW9zcXVpdHRvIC9tb3NxdWl0dG8vY29uZmlnL3Bhc3N3b3JkczsgZmkgJiYgZXhlYyBtb3NxdWl0dG8gLWMgL21vc3F1aXR0by9jb25maWcvbW9zcXVpdHRvLmNvbmYgXCIiCiAgICBsYWJlbHM6CiAgICAgIC0gdHJhZWZpay50Y3Aucm91dGVycy5tcXR0LmVudHJ5cG9pbnRzPW1xdHQKICAgICAgLSB0cmFlZmlrLnRjcC5yb3V0ZXJzLm1xdHRzLmVudHJ5cG9pbnRzPW1xdHRzCg==", + "compose": "c2VydmljZXM6CiAgbW9zcXVpdHRvOgogICAgaW1hZ2U6IGVjbGlwc2UtbW9zcXVpdHRvCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTU9TUVVJVFRPXzE4ODMKICAgICAgLSAnTVFUVF9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9NT1NRVUlUVE99JwogICAgICAtICdNUVRUX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NT1NRVUlUVE99JwogICAgICAtICdSRVFVSVJFX0NFUlRJRklDQVRFPSR7UkVRVUlSRV9DRVJUSUZJQ0FURTotZmFsc2V9JwogICAgICAtICdBTExPV19BTk9OWU1PVVM9JHtBTExPV19BTk9OWU1PVVM6LXRydWV9JwogICAgdm9sdW1lczoKICAgICAgLSAnbW9zcXVpdHRvLWNvbmZpZzovbW9zcXVpdHRvL2NvbmZpZycKICAgICAgLSAnbW9zcXVpdHRvLWNlcnRzOi9jZXJ0cycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnZXhpdCAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgICBlbnRyeXBvaW50OiAic2ggLWMgXCIgaWYgWyAnJFJFUVVJUkVfQ0VSVElGSUNBVEUnID0gJ3RydWUnIF07IHRoZW4gZWNobyAnbGlzdGVuZXIgODg4MycgPiAvbW9zcXVpdHRvL2NvbmZpZy9tb3NxdWl0dG8uY29uZiAmJiBlY2hvICdjYWZpbGUgL2NlcnRzL2NhLmNydCcgPj4gL21vc3F1aXR0by9jb25maWcvbW9zcXVpdHRvLmNvbmYgJiYgZWNobyAnY2VydGZpbGUgL2NlcnRzL3NlcnZlci5jcnQnID4+IC9tb3NxdWl0dG8vY29uZmlnL21vc3F1aXR0by5jb25mICYmIGVjaG8gJ2tleWZpbGUgIC9jZXJ0cy9zZXJ2ZXIua2V5JyA+PiAvbW9zcXVpdHRvL2NvbmZpZy9tb3NxdWl0dG8uY29uZjsgZWxzZSBlY2hvICdsaXN0ZW5lciAxODgzJyA+IC9tb3NxdWl0dG8vY29uZmlnL21vc3F1aXR0by5jb25mOyBmaSAmJiBlY2hvICdyZXF1aXJlX2NlcnRpZmljYXRlICckUkVRVUlSRV9DRVJUSUZJQ0FURSA+PiAvbW9zcXVpdHRvL2NvbmZpZy9tb3NxdWl0dG8uY29uZiAmJiBlY2hvICdhbGxvd19hbm9ueW1vdXMgJyRBTExPV19BTk9OWU1PVVMgPj4gL21vc3F1aXR0by9jb25maWcvbW9zcXVpdHRvLmNvbmY7IGlmIFsgLW4gJyRTRVJWSUNFX1VTRVJfTU9TUVVJVFRPJ10gJiYgWyAtbiAnJFNFUlZJQ0VfUEFTU1dPUkRfTU9TUVVJVFRPJyBdOyB0aGVuIGVjaG8gJ3Bhc3N3b3JkX2ZpbGUgL21vc3F1aXR0by9jb25maWcvcGFzc3dvcmRzJyA+PiAvbW9zcXVpdHRvL2NvbmZpZy9tb3NxdWl0dG8uY29uZiAmJiB0b3VjaCAvbW9zcXVpdHRvL2NvbmZpZy9wYXNzd29yZHMgJiYgY2htb2QgMDcwMCAvbW9zcXVpdHRvL2NvbmZpZy9wYXNzd29yZHMgJiYgY2hvd24gcm9vdDpyb290IC9tb3NxdWl0dG8vY29uZmlnL3Bhc3N3b3JkcyAmJiBtb3NxdWl0dG9fcGFzc3dkIC1iIC1jIC9tb3NxdWl0dG8vY29uZmlnL3Bhc3N3b3JkcyAkU0VSVklDRV9VU0VSX01PU1FVSVRUTyAkU0VSVklDRV9QQVNTV09SRF9NT1NRVUlUVE8gJiYgY2hvd24gbW9zcXVpdHRvOm1vc3F1aXR0byAvbW9zcXVpdHRvL2NvbmZpZy9wYXNzd29yZHM7IGZpICYmIGV4ZWMgbW9zcXVpdHRvIC1jIC9tb3NxdWl0dG8vY29uZmlnL21vc3F1aXR0by5jb25mIFwiIgogICAgbGFiZWxzOgogICAgICAtIHRyYWVmaWsudGNwLnJvdXRlcnMubXF0dC5lbnRyeXBvaW50cz1tcXR0CiAgICAgIC0gdHJhZWZpay50Y3Aucm91dGVycy5tcXR0cy5lbnRyeXBvaW50cz1tcXR0cwo=", "tags": [ "mosquitto", "mqtt", @@ -1923,7 +1922,7 @@ "paperless": { "documentation": "https://docs.paperless-ngx.com/configuration/?utm_source=coolify.io", "slogan": "Paperless-ngx is a community-supported open-source document management system that transforms your physical documents into a searchable online archive so you can keep, well, less paper.", - "compose": "c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ2RvY2tlci5pby9saWJyYXJ5L3JlZGlzOjcuNCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhcGVybGVzcy1yZWRpczovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICBwYXBlcmxlc3M6CiAgICBpbWFnZTogJ3BhcGVybGVzc25neC9wYXBlcmxlc3Mtbmd4OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mcycKICAgICAgICAtICctUycKICAgICAgICAtICctLW1heC10aW1lJwogICAgICAgIC0gJzInCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MDAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICdwYXBlcmxlc3MtZGF0YTovdXNyL3NyYy9wYXBlcmxlc3MvZGF0YScKICAgICAgLSAncGFwZXJsZXNzLW1lZGlhOi91c3Ivc3JjL3BhcGVybGVzcy9tZWRpYScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXhwb3J0CiAgICAgICAgdGFyZ2V0OiAvdXNyL3NyYy9wYXBlcmxlc3MvZXhwb3J0CiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NvbnN1bWUKICAgICAgICB0YXJnZXQ6IC91c3Ivc3JjL3BhcGVybGVzcy9jb25zdW1lCiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTXzgwMDAKICAgICAgLSBQQVBFUkxFU1NfVVJMPSRTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTXzgwMDAKICAgICAgLSAnUEFQRVJMRVNTX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QQVBFUkxFU1N9JwogICAgICAtICdQQVBFUkxFU1NfQURNSU5fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QQVBFUkxFU1N9JwogICAgICAtICdQQVBFUkxFU1NfUkVESVM9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtICdQQVBFUkxFU1NfU0VDUkVUX0tFWT0ke1NFUlZJQ0VfUkVBTEJBU0U2NF82NF9QQVBFUkxFU1N9Jwo=", + "compose": "c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuNCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhcGVybGVzcy1yZWRpczovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICBwYXBlcmxlc3M6CiAgICBpbWFnZTogJ3BhcGVybGVzc25neC9wYXBlcmxlc3Mtbmd4OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mcycKICAgICAgICAtICctUycKICAgICAgICAtICctLW1heC10aW1lJwogICAgICAgIC0gJzInCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MDAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICdwYXBlcmxlc3MtZGF0YTovdXNyL3NyYy9wYXBlcmxlc3MvZGF0YScKICAgICAgLSAncGFwZXJsZXNzLW1lZGlhOi91c3Ivc3JjL3BhcGVybGVzcy9tZWRpYScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXhwb3J0CiAgICAgICAgdGFyZ2V0OiAvdXNyL3NyYy9wYXBlcmxlc3MvZXhwb3J0CiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NvbnN1bWUKICAgICAgICB0YXJnZXQ6IC91c3Ivc3JjL3BhcGVybGVzcy9jb25zdW1lCiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTXzgwMDAKICAgICAgLSBQQVBFUkxFU1NfVVJMPSRTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTXzgwMDAKICAgICAgLSAnUEFQRVJMRVNTX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QQVBFUkxFU1N9JwogICAgICAtICdQQVBFUkxFU1NfQURNSU5fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QQVBFUkxFU1N9JwogICAgICAtICdQQVBFUkxFU1NfUkVESVM9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtICdQQVBFUkxFU1NfU0VDUkVUX0tFWT0ke1NFUlZJQ0VfUkVBTEJBU0U2NF82NF9QQVBFUkxFU1N9Jwo=", "tags": null, "logo": "svgs/paperless.svg", "minversion": "0.0.0", @@ -2087,7 +2086,7 @@ "reactive-resume": { "documentation": "https://rxresu.me/?utm_source=coolify.io", "slogan": "A one-of-a-kind resume builder that keeps your privacy in mind.", - "compose": "c2VydmljZXM6CiAgcmVhY3RpdmUtcmVzdW1lOgogICAgaW1hZ2U6ICdhbXJ1dGhwaWxsYWkvcmVhY3RpdmUtcmVzdW1lOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9SRUFDVElWRVJFU1VNRV8zMDAwCiAgICAgIC0gUFVCTElDX1VSTD0kU0VSVklDRV9GUUROX1JFQUNUSVZFUkVTVU1FCiAgICAgIC0gJ1NUT1JBR0VfVVJMPSR7U0VSVklDRV9GUUROX01JTklPfS9kZWZhdWx0JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gQUNDRVNTX1RPS0VOX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF9BQ0NFU1NUT0tFTgogICAgICAtIFJFRlJFU0hfVE9LRU5fU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEX1JFRlJFU0hUT0tFTgogICAgICAtIENIUk9NRV9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9DSFJPTUVUT0tFTgogICAgICAtICdDSFJPTUVfVVJMPXdzOi8vY2hyb21lOjMwMDAvY2hyb21lJwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtIFNUT1JBR0VfRU5EUE9JTlQ9bWluaW8KICAgICAgLSBTVE9SQUdFX1BPUlQ9OTAwMAogICAgICAtIFNUT1JBR0VfUkVHSU9OPXVzLWVhc3QtMQogICAgICAtIFNUT1JBR0VfQlVDS0VUPWRlZmF1bHQKICAgICAgLSBTVE9SQUdFX0FDQ0VTU19LRVk9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIFNUT1JBR0VfU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtIFNUT1JBR0VfVVNFX1NTTD1mYWxzZQogICAgICAtICdESVNBQkxFX1NJR05VUFM9JHtTRVJWSUNFX0RJU0FCTEVfU0lHTlVQUzotZmFsc2V9JwogICAgICAtICdESVNBQkxFX0VNQUlMX0FVVEg9JHtTRVJWSUNFX0RJU0FCTEVfRU1BSUxfQVVUSDotZmFsc2V9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIG1pbmlvCiAgICAgIC0gY2hyb21lCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvOgogICAgaW1hZ2U6IG1pbmlvL21pbmlvCiAgICBjb21tYW5kOiAnc2VydmVyIC9kYXRhIC0tY29uc29sZS1hZGRyZXNzICI6OTAwMSInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTUlOSU9fOTAwMAogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBjaHJvbWU6CiAgICBpbWFnZTogJ2doY3IuaW8vYnJvd3Nlcmxlc3MvY2hyb21lOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEhFQUxUSD10cnVlCiAgICAgIC0gVElNRU9VVD0xMDAwMAogICAgICAtIENPTkNVUlJFTlQ9MTAKICAgICAgLSBUT0tFTj0kU0VSVklDRV9QQVNTV09SRF9DSFJPTUVUT0tFTgogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczphbHBpbmUnCiAgICBjb21tYW5kOiByZWRpcy1zZXJ2ZXIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzX2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "c2VydmljZXM6CiAgcmVhY3RpdmUtcmVzdW1lOgogICAgaW1hZ2U6ICdhbXJ1dGhwaWxsYWkvcmVhY3RpdmUtcmVzdW1lOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9SRUFDVElWRVJFU1VNRV8zMDAwCiAgICAgIC0gUFVCTElDX1VSTD0kU0VSVklDRV9GUUROX1JFQUNUSVZFUkVTVU1FCiAgICAgIC0gJ1NUT1JBR0VfVVJMPSR7U0VSVklDRV9GUUROX01JTklPfS9kZWZhdWx0JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gQUNDRVNTX1RPS0VOX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF9BQ0NFU1NUT0tFTgogICAgICAtIFJFRlJFU0hfVE9LRU5fU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEX1JFRlJFU0hUT0tFTgogICAgICAtIENIUk9NRV9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9DSFJPTUVUT0tFTgogICAgICAtICdDSFJPTUVfVVJMPXdzOi8vY2hyb21lOjMwMDAvY2hyb21lJwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtIFNUT1JBR0VfRU5EUE9JTlQ9bWluaW8KICAgICAgLSBTVE9SQUdFX1BPUlQ9OTAwMAogICAgICAtIFNUT1JBR0VfUkVHSU9OPXVzLWVhc3QtMQogICAgICAtIFNUT1JBR0VfQlVDS0VUPWRlZmF1bHQKICAgICAgLSBTVE9SQUdFX0FDQ0VTU19LRVk9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIFNUT1JBR0VfU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtIFNUT1JBR0VfVVNFX1NTTD1mYWxzZQogICAgICAtICdESVNBQkxFX1NJR05VUFM9JHtTRVJWSUNFX0RJU0FCTEVfU0lHTlVQUzotZmFsc2V9JwogICAgICAtICdESVNBQkxFX0VNQUlMX0FVVEg9JHtTRVJWSUNFX0RJU0FCTEVfRU1BSUxfQVVUSDotZmFsc2V9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIG1pbmlvCiAgICAgIC0gY2hyb21lCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvOgogICAgaW1hZ2U6ICdxdWF5LmlvL21pbmlvL21pbmlvOmxhdGVzdCcKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgL2RhdGEgLS1jb25zb2xlLWFkZHJlc3MgIjo5MDAxIicKICAgIGVudmlyb25tZW50OgogICAgICAtIE1JTklPX1NFUlZFUl9VUkw9JE1JTklPX1NFUlZFUl9VUkwKICAgICAgLSBNSU5JT19CUk9XU0VSX1JFRElSRUNUX1VSTD0kTUlOSU9fQlJPV1NFUl9SRURJUkVDVF9VUkwKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgIHZvbHVtZXM6CiAgICAgIC0gJ21pbmlvLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbWMKICAgICAgICAtIHJlYWR5CiAgICAgICAgLSBsb2NhbAogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgY2hyb21lOgogICAgaW1hZ2U6ICdnaGNyLmlvL2Jyb3dzZXJsZXNzL2Nocm9tZTpsYXRlc3QnCiAgICBwbGF0Zm9ybTogbGludXgvYW1kNjQKICAgIGVudmlyb25tZW50OgogICAgICAtIEhFQUxUSD10cnVlCiAgICAgIC0gVElNRU9VVD0xMDAwMAogICAgICAtIENPTkNVUlJFTlQ9MTAKICAgICAgLSBUT0tFTj0kU0VSVklDRV9QQVNTV09SRF9DSFJPTUVUT0tFTgogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LWFscGluZScKICAgIGNvbW1hbmQ6IHJlZGlzLXNlcnZlcgogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXNfZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "reactive-resume", "resume-builder",