Optimize PushServerUpdateJob performance with batch updates and async jobs
- Eager load service applications and databases to eliminate N+1 queries
- Replace individual model updates with batch database updates for applications, previews, and services
- Move connectProxyToNetworks to async ConnectProxyToNetworksJob to avoid blocking status updates
- Optimize Server.databases() and applications() methods with efficient database queries
- Use flatMap for cleaner collection transformations
🤖 Generated with Claude Code
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
383572edac
commit
0efa4af5c3
8 changed files with 184 additions and 111 deletions
55
app/Jobs/ConnectProxyToNetworksJob.php
Normal file
55
app/Jobs/ConnectProxyToNetworksJob.php
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Laravel\Horizon\Contracts\Silenced;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously connects the coolify-proxy to Docker networks.
|
||||||
|
*
|
||||||
|
* This job is dispatched from PushServerUpdateJob when the proxy is found running
|
||||||
|
* to ensure it's connected to all required networks without blocking the status update.
|
||||||
|
*/
|
||||||
|
class ConnectProxyToNetworksJob implements ShouldBeEncrypted, ShouldQueue, Silenced
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $tries = 1;
|
||||||
|
|
||||||
|
public $timeout = 60;
|
||||||
|
|
||||||
|
public function middleware(): array
|
||||||
|
{
|
||||||
|
// Prevent overlapping executions for the same server and throttle to max once per 10 seconds
|
||||||
|
return [
|
||||||
|
(new WithoutOverlapping('connect-proxy-networks-'.$this->server->uuid))
|
||||||
|
->expireAfter(60)
|
||||||
|
->dontRelease(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct(public Server $server) {}
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if (! $this->server->isFunctional()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||||
|
|
||||||
|
if (empty($connectProxyToDockerNetworks)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
use App\Actions\Server\StartLogDrain;
|
use App\Actions\Server\StartLogDrain;
|
||||||
use App\Actions\Shared\ComplexStatusCheck;
|
use App\Actions\Shared\ComplexStatusCheck;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
|
use App\Models\ApplicationPreview;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\ServiceApplication;
|
use App\Models\ServiceApplication;
|
||||||
use App\Models\ServiceDatabase;
|
use App\Models\ServiceDatabase;
|
||||||
|
|
@ -134,29 +135,29 @@ public function handle()
|
||||||
if ($this->containers->isEmpty()) {
|
if ($this->containers->isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->applications = $this->server->applications();
|
$this->applications = $this->server->applications();
|
||||||
$this->databases = $this->server->databases();
|
$this->databases = $this->server->databases();
|
||||||
$this->previews = $this->server->previews();
|
$this->previews = $this->server->previews();
|
||||||
$this->services = $this->server->services()->get();
|
// Eager load service applications and databases to avoid N+1 queries
|
||||||
|
$this->services = $this->server->services()
|
||||||
|
->with(['applications:id,service_id', 'databases:id,service_id'])
|
||||||
|
->get();
|
||||||
|
|
||||||
$this->allApplicationIds = $this->applications->filter(function ($application) {
|
$this->allApplicationIds = $this->applications->filter(function ($application) {
|
||||||
return $application->additional_servers->count() === 0;
|
return $application->additional_servers_count === 0;
|
||||||
})->pluck('id');
|
})->pluck('id');
|
||||||
$this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) {
|
$this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) {
|
||||||
return $application->additional_servers->count() > 0;
|
return $application->additional_servers_count > 0;
|
||||||
});
|
});
|
||||||
$this->allApplicationPreviewsIds = $this->previews->map(function ($preview) {
|
$this->allApplicationPreviewsIds = $this->previews->map(function ($preview) {
|
||||||
return $preview->application_id.':'.$preview->pull_request_id;
|
return $preview->application_id.':'.$preview->pull_request_id;
|
||||||
});
|
});
|
||||||
$this->allDatabaseUuids = $this->databases->pluck('uuid');
|
$this->allDatabaseUuids = $this->databases->pluck('uuid');
|
||||||
$this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid');
|
$this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid');
|
||||||
$this->services->each(function ($service) {
|
// Use eager-loaded relationships instead of querying in loop
|
||||||
$service->applications()->pluck('id')->each(function ($applicationId) {
|
$this->allServiceApplicationIds = $this->services->flatMap(fn ($service) => $service->applications->pluck('id'));
|
||||||
$this->allServiceApplicationIds->push($applicationId);
|
$this->allServiceDatabaseIds = $this->services->flatMap(fn ($service) => $service->databases->pluck('id'));
|
||||||
});
|
|
||||||
$service->databases()->pluck('id')->each(function ($databaseId) {
|
|
||||||
$this->allServiceDatabaseIds->push($databaseId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach ($this->containers as $container) {
|
foreach ($this->containers as $container) {
|
||||||
$containerStatus = data_get($container, 'state', 'exited');
|
$containerStatus = data_get($container, 'state', 'exited');
|
||||||
|
|
@ -402,66 +403,59 @@ private function updateApplicationPreviewStatus(string $applicationId, string $p
|
||||||
private function updateNotFoundApplicationStatus()
|
private function updateNotFoundApplicationStatus()
|
||||||
{
|
{
|
||||||
$notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds);
|
$notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds);
|
||||||
if ($notFoundApplicationIds->isNotEmpty()) {
|
if ($notFoundApplicationIds->isEmpty()) {
|
||||||
$notFoundApplicationIds->each(function ($applicationId) {
|
return;
|
||||||
$application = Application::find($applicationId);
|
|
||||||
if ($application) {
|
|
||||||
// Don't mark as exited if already exited
|
|
||||||
if (str($application->status)->startsWith('exited')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only protection: Verify we received any container data at all
|
|
||||||
// If containers collection is completely empty, Sentinel might have failed
|
|
||||||
if ($this->containers->isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($application->status !== 'exited') {
|
|
||||||
$application->status = 'exited';
|
|
||||||
$application->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only protection: Verify we received any container data at all
|
||||||
|
// If containers collection is completely empty, Sentinel might have failed
|
||||||
|
if ($this->containers->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch update: mark all not-found applications as exited (excluding already exited ones)
|
||||||
|
Application::whereIn('id', $notFoundApplicationIds)
|
||||||
|
->where('status', 'not like', 'exited%')
|
||||||
|
->update(['status' => 'exited']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateNotFoundApplicationPreviewStatus()
|
private function updateNotFoundApplicationPreviewStatus()
|
||||||
{
|
{
|
||||||
$notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds);
|
$notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds);
|
||||||
if ($notFoundApplicationPreviewsIds->isNotEmpty()) {
|
if ($notFoundApplicationPreviewsIds->isEmpty()) {
|
||||||
$notFoundApplicationPreviewsIds->each(function ($previewKey) {
|
return;
|
||||||
// Parse the previewKey format "application_id:pull_request_id"
|
}
|
||||||
$parts = explode(':', $previewKey);
|
|
||||||
if (count($parts) !== 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$applicationId = $parts[0];
|
// Only protection: Verify we received any container data at all
|
||||||
$pullRequestId = $parts[1];
|
// If containers collection is completely empty, Sentinel might have failed
|
||||||
|
if ($this->containers->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$applicationPreview = $this->previews->where('application_id', $applicationId)
|
// Collect IDs of previews that need to be marked as exited
|
||||||
->where('pull_request_id', $pullRequestId)
|
$previewIdsToUpdate = collect();
|
||||||
->first();
|
foreach ($notFoundApplicationPreviewsIds as $previewKey) {
|
||||||
|
// Parse the previewKey format "application_id:pull_request_id"
|
||||||
|
$parts = explode(':', $previewKey);
|
||||||
|
if (count($parts) !== 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if ($applicationPreview) {
|
$applicationId = $parts[0];
|
||||||
// Don't mark as exited if already exited
|
$pullRequestId = $parts[1];
|
||||||
if (str($applicationPreview->status)->startsWith('exited')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only protection: Verify we received any container data at all
|
$applicationPreview = $this->previews->where('application_id', $applicationId)
|
||||||
// If containers collection is completely empty, Sentinel might have failed
|
->where('pull_request_id', $pullRequestId)
|
||||||
if ($this->containers->isEmpty()) {
|
->first();
|
||||||
|
|
||||||
return;
|
if ($applicationPreview && ! str($applicationPreview->status)->startsWith('exited')) {
|
||||||
}
|
$previewIdsToUpdate->push($applicationPreview->id);
|
||||||
if ($applicationPreview->status !== 'exited') {
|
}
|
||||||
$applicationPreview->status = 'exited';
|
}
|
||||||
$applicationPreview->save();
|
|
||||||
}
|
// Batch update all collected preview IDs
|
||||||
}
|
if ($previewIdsToUpdate->isNotEmpty()) {
|
||||||
});
|
ApplicationPreview::whereIn('id', $previewIdsToUpdate)->update(['status' => 'exited']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -478,8 +472,8 @@ private function updateProxyStatus()
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
// Connect proxy to networks asynchronously to avoid blocking the status update
|
||||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
ConnectProxyToNetworksJob::dispatch($this->server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -554,27 +548,19 @@ private function updateNotFoundServiceStatus()
|
||||||
{
|
{
|
||||||
$notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds);
|
$notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds);
|
||||||
$notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds);
|
$notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds);
|
||||||
|
|
||||||
|
// Batch update service applications
|
||||||
if ($notFoundServiceApplicationIds->isNotEmpty()) {
|
if ($notFoundServiceApplicationIds->isNotEmpty()) {
|
||||||
$notFoundServiceApplicationIds->each(function ($serviceApplicationId) {
|
ServiceApplication::whereIn('id', $notFoundServiceApplicationIds)
|
||||||
$application = ServiceApplication::find($serviceApplicationId);
|
->where('status', '!=', 'exited')
|
||||||
if ($application) {
|
->update(['status' => 'exited']);
|
||||||
if ($application->status !== 'exited') {
|
|
||||||
$application->status = 'exited';
|
|
||||||
$application->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch update service databases
|
||||||
if ($notFoundServiceDatabaseIds->isNotEmpty()) {
|
if ($notFoundServiceDatabaseIds->isNotEmpty()) {
|
||||||
$notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) {
|
ServiceDatabase::whereIn('id', $notFoundServiceDatabaseIds)
|
||||||
$database = ServiceDatabase::find($serviceDatabaseId);
|
->where('status', '!=', 'exited')
|
||||||
if ($database) {
|
->update(['status' => 'exited']);
|
||||||
if ($database->status !== 'exited') {
|
|
||||||
$database->status = 'exited';
|
|
||||||
$database->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,7 @@ public function handle()
|
||||||
} else {
|
} else {
|
||||||
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
|
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
|
||||||
$this->server->save();
|
$this->server->save();
|
||||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
ConnectProxyToNetworksJob::dispatchSync($this->server);
|
||||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Livewire\Server;
|
namespace App\Livewire\Server;
|
||||||
|
|
||||||
|
use App\Jobs\ConnectProxyToNetworksJob;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\StandaloneDocker;
|
use App\Models\StandaloneDocker;
|
||||||
use App\Models\SwarmDocker;
|
use App\Models\SwarmDocker;
|
||||||
|
|
@ -29,8 +30,7 @@ public function mount(string $server_uuid)
|
||||||
|
|
||||||
private function createNetworkAndAttachToProxy()
|
private function createNetworkAndAttachToProxy()
|
||||||
{
|
{
|
||||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
ConnectProxyToNetworksJob::dispatchSync($this->server);
|
||||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function add($name)
|
public function add($name)
|
||||||
|
|
|
||||||
|
|
@ -831,34 +831,67 @@ public function hasDefinedResources()
|
||||||
|
|
||||||
public function databases()
|
public function databases()
|
||||||
{
|
{
|
||||||
return $this->destinations()->map(function ($standaloneDocker) {
|
// Get destination IDs for this server in two efficient queries
|
||||||
$postgresqls = data_get($standaloneDocker, 'postgresqls', collect([]));
|
$standaloneDockerIds = StandaloneDocker::where('server_id', $this->id)->pluck('id');
|
||||||
$redis = data_get($standaloneDocker, 'redis', collect([]));
|
$swarmDockerIds = SwarmDocker::where('server_id', $this->id)->pluck('id');
|
||||||
$mongodbs = data_get($standaloneDocker, 'mongodbs', collect([]));
|
|
||||||
$mysqls = data_get($standaloneDocker, 'mysqls', collect([]));
|
|
||||||
$mariadbs = data_get($standaloneDocker, 'mariadbs', collect([]));
|
|
||||||
$keydbs = data_get($standaloneDocker, 'keydbs', collect([]));
|
|
||||||
$dragonflies = data_get($standaloneDocker, 'dragonflies', collect([]));
|
|
||||||
$clickhouses = data_get($standaloneDocker, 'clickhouses', collect([]));
|
|
||||||
|
|
||||||
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
|
$destinationCondition = function ($query) use ($standaloneDockerIds, $swarmDockerIds) {
|
||||||
})->flatten()->filter(function ($item) {
|
$query->where(function ($q) use ($standaloneDockerIds) {
|
||||||
return data_get($item, 'name') !== 'coolify-db';
|
$q->where('destination_type', StandaloneDocker::class)
|
||||||
});
|
->whereIn('destination_id', $standaloneDockerIds);
|
||||||
|
})->orWhere(function ($q) use ($swarmDockerIds) {
|
||||||
|
$q->where('destination_type', SwarmDocker::class)
|
||||||
|
->whereIn('destination_id', $swarmDockerIds);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Query each database type with the destination condition
|
||||||
|
$postgresqls = StandalonePostgresql::where($destinationCondition)->get();
|
||||||
|
$redis = StandaloneRedis::where($destinationCondition)->get();
|
||||||
|
$mongodbs = StandaloneMongodb::where($destinationCondition)->get();
|
||||||
|
$mysqls = StandaloneMysql::where($destinationCondition)->get();
|
||||||
|
$mariadbs = StandaloneMariadb::where($destinationCondition)->get();
|
||||||
|
$keydbs = StandaloneKeydb::where($destinationCondition)->get();
|
||||||
|
$dragonflies = StandaloneDragonfly::where($destinationCondition)->get();
|
||||||
|
$clickhouses = StandaloneClickhouse::where($destinationCondition)->get();
|
||||||
|
|
||||||
|
return $postgresqls
|
||||||
|
->concat($redis)
|
||||||
|
->concat($mongodbs)
|
||||||
|
->concat($mysqls)
|
||||||
|
->concat($mariadbs)
|
||||||
|
->concat($keydbs)
|
||||||
|
->concat($dragonflies)
|
||||||
|
->concat($clickhouses)
|
||||||
|
->filter(fn ($item) => data_get($item, 'name') !== 'coolify-db');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applications()
|
public function applications()
|
||||||
{
|
{
|
||||||
$applications = $this->destinations()->map(function ($standaloneDocker) {
|
// Get destination IDs for this server in two efficient queries
|
||||||
return $standaloneDocker->applications;
|
$standaloneDockerIds = StandaloneDocker::where('server_id', $this->id)->pluck('id');
|
||||||
})->flatten();
|
$swarmDockerIds = SwarmDocker::where('server_id', $this->id)->pluck('id');
|
||||||
$additionalApplicationIds = DB::table('additional_destinations')->where('server_id', $this->id)->get('application_id');
|
|
||||||
$additionalApplicationIds = collect($additionalApplicationIds)->map(function ($item) {
|
// Query all applications in a single query using polymorphic conditions
|
||||||
return $item->application_id;
|
$applications = Application::where(function ($query) use ($standaloneDockerIds, $swarmDockerIds) {
|
||||||
});
|
$query->where(function ($q) use ($standaloneDockerIds) {
|
||||||
Application::whereIn('id', $additionalApplicationIds)->get()->each(function ($application) use ($applications) {
|
$q->where('destination_type', StandaloneDocker::class)
|
||||||
$applications->push($application);
|
->whereIn('destination_id', $standaloneDockerIds);
|
||||||
});
|
})->orWhere(function ($q) use ($swarmDockerIds) {
|
||||||
|
$q->where('destination_type', SwarmDocker::class)
|
||||||
|
->whereIn('destination_id', $swarmDockerIds);
|
||||||
|
});
|
||||||
|
})->get();
|
||||||
|
|
||||||
|
// Get additional server applications
|
||||||
|
$additionalApplicationIds = DB::table('additional_destinations')
|
||||||
|
->where('server_id', $this->id)
|
||||||
|
->pluck('application_id');
|
||||||
|
|
||||||
|
if ($additionalApplicationIds->isNotEmpty()) {
|
||||||
|
$additionalApps = Application::whereIn('id', $additionalApplicationIds)->get();
|
||||||
|
$applications = $applications->concat($additionalApps);
|
||||||
|
}
|
||||||
|
|
||||||
return $applications;
|
return $applications;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Jobs\ConnectProxyToNetworksJob;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
|
|
||||||
class StandaloneDocker extends BaseModel
|
class StandaloneDocker extends BaseModel
|
||||||
|
|
@ -18,8 +19,7 @@ protected static function boot()
|
||||||
instant_remote_process([
|
instant_remote_process([
|
||||||
"docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null",
|
"docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null",
|
||||||
], $server, false);
|
], $server, false);
|
||||||
$connectProxyToDockerNetworks = connectProxyToNetworks($server);
|
ConnectProxyToNetworksJob::dispatchSync($server);
|
||||||
instant_remote_process($connectProxyToDockerNetworks, $server, false);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4272,7 +4272,7 @@
|
||||||
"umami": {
|
"umami": {
|
||||||
"documentation": "https://umami.is?utm_source=coolify.io",
|
"documentation": "https://umami.is?utm_source=coolify.io",
|
||||||
"slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.",
|
"slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.",
|
||||||
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6cG9zdGdyZXNxbC1sYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
|
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9VTUFNSV8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgLSBEQVRBQkFTRV9UWVBFPXBvc3RncmVzCiAgICAgIC0gQVBQX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9VTUFNSQogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAvYXBpL2hlYXJ0YmVhdCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
|
||||||
"tags": [
|
"tags": [
|
||||||
"analytics",
|
"analytics",
|
||||||
"insights",
|
"insights",
|
||||||
|
|
|
||||||
|
|
@ -4272,7 +4272,7 @@
|
||||||
"umami": {
|
"umami": {
|
||||||
"documentation": "https://umami.is?utm_source=coolify.io",
|
"documentation": "https://umami.is?utm_source=coolify.io",
|
||||||
"slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.",
|
"slogan": "Umami is web analytics platform which provides insights into visitor behavior without compromising user privacy.",
|
||||||
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6cG9zdGdyZXNxbC1sYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
|
"compose": "c2VydmljZXM6CiAgdW1hbWk6CiAgICBpbWFnZTogJ2doY3IuaW8vdW1hbWktc29mdHdhcmUvdW1hbWk6My4wLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVU1BTUlfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gREFUQUJBU0VfVFlQRT1wb3N0Z3JlcwogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfVU1BTUkKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9oZWFydGJlYXQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdW1hbWl9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
|
||||||
"tags": [
|
"tags": [
|
||||||
"analytics",
|
"analytics",
|
||||||
"insights",
|
"insights",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue