From 0706e5b502b853a142710d55ecc2b03858bf605e Mon Sep 17 00:00:00 2001
From: Alberto Rizzi
Date: Wed, 7 Jan 2026 16:33:52 +0100
Subject: [PATCH 001/174] feat(ui): improve global search with UUID and PR
support
- Add UUID to search terms for applications, services, and databases
- Add PR search terms for applications with preview enabled
- Include service component names (applications and databases) in search
- Add eager loading for previews and databases relationships
---
app/Livewire/GlobalSearch.php | 38 ++++++++++++++++++++++++-----------
1 file changed, 26 insertions(+), 12 deletions(-)
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
index e4108668b..ed9115093 100644
--- a/app/Livewire/GlobalSearch.php
+++ b/app/Livewire/GlobalSearch.php
@@ -257,7 +257,7 @@ class GlobalSearch extends Component
// Get all applications
$applications = Application::ownedByCurrentTeam()
- ->with(['environment.project'])
+ ->with(['environment.project', 'previews:id,application_id,pull_request_id'])
->get()
->map(function ($app) {
// Collect all FQDNs from the application
@@ -286,6 +286,16 @@ class GlobalSearch extends Component
$fqdnsString = $fqdns->implode(' ');
+ // Add PR search terms if preview is enabled
+ $prSearchTerms = '';
+ if ($app->preview_enabled ?? false) {
+ $prIds = collect($app->previews ?? [])
+ ->pluck('pull_request_id')
+ ->map(fn ($id) => "pr-{$id} pr{$id} {$id}")
+ ->implode(' ');
+ $prSearchTerms = $prIds;
+ }
+
return [
'id' => $app->id,
'name' => $app->name,
@@ -296,13 +306,13 @@ class GlobalSearch extends Component
'project' => $app->environment->project->name ?? null,
'environment' => $app->environment->name ?? null,
'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
- 'search_text' => strtolower($app->name.' '.$app->description.' '.$fqdnsString.' application applications app apps'),
+ 'search_text' => strtolower($app->name.' '.$app->description.' '.$fqdnsString.' '.$app->uuid.' '.$prSearchTerms.' application applications app apps'),
];
});
// Get all services
$services = Service::ownedByCurrentTeam()
- ->with(['environment.project', 'applications'])
+ ->with(['environment.project', 'applications', 'databases'])
->get()
->map(function ($service) {
// Collect all FQDNs from service applications
@@ -315,6 +325,10 @@ class GlobalSearch extends Component
}
$fqdnsString = $fqdns->implode(' ');
+ // Collect service component names for container search
+ $serviceAppNames = collect($service->applications ?? [])->pluck('name')->implode(' ');
+ $serviceDbNames = collect($service->databases ?? [])->pluck('name')->implode(' ');
+
return [
'id' => $service->id,
'name' => $service->name,
@@ -325,7 +339,7 @@ class GlobalSearch extends Component
'project' => $service->environment->project->name ?? null,
'environment' => $service->environment->name ?? null,
'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
- 'search_text' => strtolower($service->name.' '.$service->description.' '.$fqdnsString.' service services'),
+ 'search_text' => strtolower($service->name.' '.$service->description.' '.$fqdnsString.' '.$service->uuid.' '.$serviceAppNames.' '.$serviceDbNames.' service services'),
];
});
@@ -348,7 +362,7 @@ class GlobalSearch extends Component
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
- 'search_text' => strtolower($db->name.' postgresql '.$db->description.' database databases db'),
+ 'search_text' => strtolower($db->name.' '.$db->uuid.' postgresql '.$db->description.' database databases db'),
];
})
);
@@ -369,7 +383,7 @@ class GlobalSearch extends Component
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
- 'search_text' => strtolower($db->name.' mysql '.$db->description.' database databases db'),
+ 'search_text' => strtolower($db->name.' '.$db->uuid.' mysql '.$db->description.' database databases db'),
];
})
);
@@ -390,7 +404,7 @@ class GlobalSearch extends Component
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
- 'search_text' => strtolower($db->name.' mariadb '.$db->description.' database databases db'),
+ 'search_text' => strtolower($db->name.' '.$db->uuid.' mariadb '.$db->description.' database databases db'),
];
})
);
@@ -411,7 +425,7 @@ class GlobalSearch extends Component
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
- 'search_text' => strtolower($db->name.' mongodb '.$db->description.' database databases db'),
+ 'search_text' => strtolower($db->name.' '.$db->uuid.' mongodb '.$db->description.' database databases db'),
];
})
);
@@ -432,7 +446,7 @@ class GlobalSearch extends Component
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
- 'search_text' => strtolower($db->name.' redis '.$db->description.' database databases db'),
+ 'search_text' => strtolower($db->name.' '.$db->uuid.' redis '.$db->description.' database databases db'),
];
})
);
@@ -453,7 +467,7 @@ class GlobalSearch extends Component
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
- 'search_text' => strtolower($db->name.' keydb '.$db->description.' database databases db'),
+ 'search_text' => strtolower($db->name.' '.$db->uuid.' keydb '.$db->description.' database databases db'),
];
})
);
@@ -474,7 +488,7 @@ class GlobalSearch extends Component
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
- 'search_text' => strtolower($db->name.' dragonfly '.$db->description.' database databases db'),
+ 'search_text' => strtolower($db->name.' '.$db->uuid.' dragonfly '.$db->description.' database databases db'),
];
})
);
@@ -495,7 +509,7 @@ class GlobalSearch extends Component
'link' => $db->link(),
'project' => $db->environment->project->name ?? null,
'environment' => $db->environment->name ?? null,
- 'search_text' => strtolower($db->name.' clickhouse '.$db->description.' database databases db'),
+ 'search_text' => strtolower($db->name.' '.$db->uuid.' clickhouse '.$db->description.' database databases db'),
];
})
);
From dcd325dc44f20806d983c92d4fa60fb28ad961d7 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 15 Jun 2026 23:09:49 +0200
Subject: [PATCH 002/174] feat(v5): add Inertia app shell with Flux health
Adds v5 routing, middleware, home page rendering, team context sharing,
project model/table support, and Flux health reporting. Installs Flux in
container builds with role-aware s6 services and documents runtime roles.
---
README.md | 25 +
app/Http/Controllers/V5/HomeController.php | 44 +
app/Http/Kernel.php | 18 +
app/Http/Middleware/V5/EnsureCurrentTeam.php | 54 ++
.../Middleware/V5/HandleInertiaRequests.php | 25 +
app/Models/V5/Project.php | 29 +
app/Models/V5/V5Model.php | 10 +
app/Providers/RouteServiceProvider.php | 5 +
app/Services/Flux/FluxHealth.php | 66 ++
composer.json | 1 +
composer.lock | 111 ++-
config/flux.php | 6 +
..._06_04_050157_create_v5_projects_table.php | 33 +
database/schema/testing-schema.sql | 11 +
docker-compose.dev.yml | 2 +
docker-compose.prod.yml | 4 +
docker/development/Dockerfile | 27 +
.../s6-rc.d/flux/dependencies.d/init-setup | 0
.../etc/s6-overlay/s6-rc.d/flux/run | 49 ++
.../etc/s6-overlay/s6-rc.d/flux/type | 1 +
.../etc/s6-overlay/s6-rc.d/horizon/run | 14 +
.../s6-overlay/s6-rc.d/nightwatch-agent/run | 14 +
.../s6-overlay/s6-rc.d/scheduler-worker/run | 14 +
.../s6-overlay/s6-rc.d/user/contents.d/flux | 0
docker/production/Dockerfile | 27 +
.../s6-rc.d/flux/dependencies.d/init-script | 0
.../etc/s6-overlay/s6-rc.d/flux/run | 49 ++
.../etc/s6-overlay/s6-rc.d/flux/type | 1 +
.../etc/s6-overlay/s6-rc.d/horizon/run | 14 +
.../s6-overlay/s6-rc.d/nightwatch-agent/run | 14 +
.../s6-overlay/s6-rc.d/scheduler-worker/run | 14 +
.../s6-overlay/s6-rc.d/user/contents.d/flux | 0
package-lock.json | 793 ++++++++++++++++--
package.json | 7 +-
resources/css/v5/app.css | 11 +
resources/js/v5/Pages/Home.jsx | 59 ++
resources/js/v5/app.jsx | 11 +
resources/views/v5/app.blade.php | 18 +
routes/v5.php | 8 +
tests/Feature/V5/HomeTest.php | 214 +++++
vite.config.js | 10 +-
41 files changed, 1705 insertions(+), 108 deletions(-)
create mode 100644 app/Http/Controllers/V5/HomeController.php
create mode 100644 app/Http/Middleware/V5/EnsureCurrentTeam.php
create mode 100644 app/Http/Middleware/V5/HandleInertiaRequests.php
create mode 100644 app/Models/V5/Project.php
create mode 100644 app/Models/V5/V5Model.php
create mode 100644 app/Services/Flux/FluxHealth.php
create mode 100644 config/flux.php
create mode 100644 database/migrations/2026_06_04_050157_create_v5_projects_table.php
create mode 100644 docker/development/etc/s6-overlay/s6-rc.d/flux/dependencies.d/init-setup
create mode 100755 docker/development/etc/s6-overlay/s6-rc.d/flux/run
create mode 100644 docker/development/etc/s6-overlay/s6-rc.d/flux/type
create mode 100644 docker/development/etc/s6-overlay/s6-rc.d/user/contents.d/flux
create mode 100644 docker/production/etc/s6-overlay/s6-rc.d/flux/dependencies.d/init-script
create mode 100755 docker/production/etc/s6-overlay/s6-rc.d/flux/run
create mode 100644 docker/production/etc/s6-overlay/s6-rc.d/flux/type
create mode 100644 docker/production/etc/s6-overlay/s6-rc.d/user/contents.d/flux
create mode 100644 resources/css/v5/app.css
create mode 100644 resources/js/v5/Pages/Home.jsx
create mode 100644 resources/js/v5/app.jsx
create mode 100644 resources/views/v5/app.blade.php
create mode 100644 routes/v5.php
create mode 100644 tests/Feature/V5/HomeTest.php
diff --git a/README.md b/README.md
index b387d87e8..f4e289bcd 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,31 @@ You can find the installation script source [here](./scripts/install.sh).
> [!NOTE]
> Please refer to the [docs](https://coolify.io/docs/installation) for more information about the installation.
+
+## Container roles and Flux
+
+The Coolify image can run different process roles with `COOLIFY_CONTAINER_ROLE`:
+
+- `all` (default): self-hosted mode; runs the web process, worker services, and Flux when configured.
+- `web`: web/API process only; s6 worker services and Flux sleep.
+- `worker`: Horizon, Laravel scheduler worker, and optional Nightwatch agent.
+- `flux`: Flux only; used by Cloud/HA deployments that scale coold connection routers separately.
+
+Flux is installed from the coold nightly release into `/usr/local/bin/flux`. Containers running the `all` or `flux` role expose Flux on port `6443` and use these runtime variables:
+
+```env
+COOLIFY_FLUX_GRPC_BIND=0.0.0.0:6443
+COOLIFY_FLUX_UNIX_SOCKET_PATH=/run/coolify/flux.sock
+COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH=/var/www/html/storage/app/flux/jwt.priv
+COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH=/var/www/html/storage/app/flux/jwt.pub
+COOLIFY_FLUX_ALLOW_PUBLIC_BIND=1
+COOLIFY_FLUX_ID=flux-eu-1
+COOLIFY_FLUX_PUBLIC_URL=grpcs://flux-eu-1.example.com:6443
+COOLIFY_FLUX_INTERNAL_URL=http://coolify-flux-eu-1.internal:6443
+```
+
+If `COOLIFY_FLUX_ENABLED=false` is set, the Flux s6 service sleeps even for `all` and `flux` roles. The Flux s6 service creates a persistent JWT keypair on first boot when the key files are missing. The current Coolify image only installs the nightly Flux artifact during the Docker build; the s6 service verifies the binary at runtime and sleeps with a clear error if the published artifact is not compatible with the Alpine base image.
+
## Support
Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
diff --git a/app/Http/Controllers/V5/HomeController.php b/app/Http/Controllers/V5/HomeController.php
new file mode 100644
index 000000000..b8f9fa6db
--- /dev/null
+++ b/app/Http/Controllers/V5/HomeController.php
@@ -0,0 +1,44 @@
+user();
+ $currentTeam = $request->attributes->get('v5.currentTeam');
+
+ return Inertia::render('Home', [
+ 'status' => 'v5-ready',
+ 'flux' => $fluxHealth->check(),
+ 'currentTeam' => $currentTeam instanceof Team ? [
+ 'id' => $currentTeam->id,
+ 'name' => $currentTeam->name,
+ 'description' => $currentTeam->description,
+ 'role' => $currentTeam->pivot?->role ?? $user->roleInTeam($currentTeam->id),
+ 'personal' => $currentTeam->personal_team,
+ ] : null,
+ 'teams' => $user->teams()
+ ->select('teams.id', 'teams.name', 'teams.description', 'teams.personal_team')
+ ->orderBy('teams.name')
+ ->get()
+ ->map(fn (Team $team) => [
+ 'id' => $team->id,
+ 'name' => $team->name,
+ 'description' => $team->description,
+ 'role' => $team->pivot->role,
+ 'personal' => $team->personal_team,
+ ]),
+ ]);
+ }
+}
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index 02a49aaa8..dfd66284a 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -18,6 +18,8 @@ use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustHosts;
use App\Http\Middleware\TrustProxies;
+use App\Http\Middleware\V5\EnsureCurrentTeam as V5EnsureCurrentTeam;
+use App\Http\Middleware\V5\HandleInertiaRequests as V5HandleInertiaRequests;
use App\Http\Middleware\ValidateSignature;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
@@ -76,6 +78,22 @@ class Kernel extends HttpKernel
],
+ 'v5.web' => [
+ EncryptCookies::class,
+ AddQueuedCookiesToResponse::class,
+ StartSession::class,
+ ShareErrorsFromSession::class,
+ VerifyCsrfToken::class,
+ SubstituteBindings::class,
+ V5HandleInertiaRequests::class,
+ ],
+
+ 'v5.authenticated' => [
+ 'auth',
+ 'verified',
+ V5EnsureCurrentTeam::class,
+ ],
+
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
ThrottleRequests::class.':api',
diff --git a/app/Http/Middleware/V5/EnsureCurrentTeam.php b/app/Http/Middleware/V5/EnsureCurrentTeam.php
new file mode 100644
index 000000000..f8aa78f92
--- /dev/null
+++ b/app/Http/Middleware/V5/EnsureCurrentTeam.php
@@ -0,0 +1,54 @@
+user();
+
+ if (! $user) {
+ return $next($request);
+ }
+
+ $currentTeam = $this->resolveCurrentTeam($user);
+
+ if (! $currentTeam) {
+ abort(403, 'No team available for this user.');
+ }
+
+ session(['currentTeam' => $currentTeam]);
+ $request->attributes->set('v5.currentTeam', $currentTeam);
+
+ return $next($request);
+ }
+
+ private function resolveCurrentTeam(User $user): ?Team
+ {
+ $sessionTeamId = data_get(session('currentTeam'), 'id');
+
+ if ($sessionTeamId) {
+ $sessionTeam = $user->teams()
+ ->select('teams.id', 'teams.name', 'teams.description', 'teams.personal_team')
+ ->whereKey($sessionTeamId)
+ ->first();
+
+ if ($sessionTeam) {
+ return $sessionTeam;
+ }
+ }
+
+ return $user->teams()
+ ->select('teams.id', 'teams.name', 'teams.description', 'teams.personal_team')
+ ->orderBy('teams.id')
+ ->first();
+ }
+}
diff --git a/app/Http/Middleware/V5/HandleInertiaRequests.php b/app/Http/Middleware/V5/HandleInertiaRequests.php
new file mode 100644
index 000000000..2772c9c2e
--- /dev/null
+++ b/app/Http/Middleware/V5/HandleInertiaRequests.php
@@ -0,0 +1,25 @@
+ [
+ 'user' => $request->user() ? [
+ 'id' => $request->user()->id,
+ 'name' => $request->user()->name,
+ 'email' => $request->user()->email,
+ ] : null,
+ ],
+ ];
+ }
+}
diff --git a/app/Models/V5/Project.php b/app/Models/V5/Project.php
new file mode 100644
index 000000000..a04a381f9
--- /dev/null
+++ b/app/Models/V5/Project.php
@@ -0,0 +1,29 @@
+belongsTo(Team::class);
+ }
+
+ public function creator(): BelongsTo
+ {
+ return $this->belongsTo(User::class, 'created_by_user_id');
+ }
+}
diff --git a/app/Models/V5/V5Model.php b/app/Models/V5/V5Model.php
new file mode 100644
index 000000000..9907e827d
--- /dev/null
+++ b/app/Models/V5/V5Model.php
@@ -0,0 +1,10 @@
+group(base_path('routes/webhooks.php'));
+ Route::middleware('v5.web')
+ ->prefix('v5')
+ ->as('v5.')
+ ->group(base_path('routes/v5.php'));
+
Route::middleware('web')
->group(base_path('routes/web.php'));
});
diff --git a/app/Services/Flux/FluxHealth.php b/app/Services/Flux/FluxHealth.php
new file mode 100644
index 000000000..85d49d948
--- /dev/null
+++ b/app/Services/Flux/FluxHealth.php
@@ -0,0 +1,66 @@
+unavailable(null, 'Flux socket is not configured.');
+ }
+
+ if (! file_exists($socketPath)) {
+ return $this->unavailable($socketPath, 'Flux socket was not found.');
+ }
+
+ $timeout = (float) config('flux.health_timeout_seconds', 1.0);
+ $stream = @stream_socket_client("unix://{$socketPath}", $errorCode, $errorMessage, $timeout);
+
+ if ($stream === false) {
+ return $this->unavailable($socketPath, $errorMessage ?: "Could not connect to Flux socket ({$errorCode}).");
+ }
+
+ stream_set_timeout($stream, (int) ceil($timeout));
+
+ fwrite($stream, "GET /v1/health HTTP/1.1\r\nHost: flux\r\nAccept: application/json\r\nConnection: close\r\n\r\n");
+ $response = stream_get_contents($stream) ?: '';
+ fclose($stream);
+
+ if (! str_starts_with($response, 'HTTP/1.1 200') && ! str_starts_with($response, 'HTTP/1.0 200')) {
+ return $this->unavailable($socketPath, 'Flux health endpoint did not return HTTP 200.');
+ }
+
+ $body = str_contains($response, "\r\n\r\n") ? substr($response, strpos($response, "\r\n\r\n") + 4) : '';
+ $payload = json_decode($body, true);
+
+ if (! is_array($payload) || ($payload['ok'] ?? false) !== true) {
+ return $this->unavailable($socketPath, 'Flux health endpoint returned an invalid response.');
+ }
+
+ return [
+ 'available' => true,
+ 'label' => 'Running',
+ 'message' => 'Flux is running.',
+ 'socket' => $socketPath,
+ ];
+ }
+
+ /**
+ * @return array{available: false, label: string, message: string, socket: string|null}
+ */
+ private function unavailable(?string $socketPath, string $message): array
+ {
+ return [
+ 'available' => false,
+ 'label' => 'Unavailable',
+ 'message' => $message,
+ 'socket' => $socketPath,
+ ];
+ }
+}
diff --git a/composer.json b/composer.json
index 9415aa624..1d94dca00 100644
--- a/composer.json
+++ b/composer.json
@@ -15,6 +15,7 @@
"danharrin/livewire-rate-limiting": "^2.1.0",
"doctrine/dbal": "^4.4.1",
"guzzlehttp/guzzle": "^7.10.0",
+ "inertiajs/inertia-laravel": "^3.1",
"laravel/fortify": "^1.34.0",
"laravel/framework": "^12.49.0",
"laravel/horizon": "^5.43.0",
diff --git a/composer.lock b/composer.lock
index 7d958a9cc..ded6d4b6c 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "64b77285a7140ce68e83db2659e9a21d",
+ "content-hash": "d7e1f3eef5ae01d358f9482007814245",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -1643,6 +1643,79 @@
],
"time": "2025-08-22T14:27:06+00:00"
},
+ {
+ "name": "inertiajs/inertia-laravel",
+ "version": "v3.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/inertiajs/inertia-laravel.git",
+ "reference": "f588ce4a5beb25d166f4e372bac0c7f46a4f52ac"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/f588ce4a5beb25d166f4e372bac0c7f46a4f52ac",
+ "reference": "f588ce4a5beb25d166f4e372bac0c7f46a4f52ac",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "laravel/framework": "^11.0|^12.0|^13.0",
+ "php": "^8.2.0",
+ "symfony/console": "^7.0|^8.0"
+ },
+ "conflict": {
+ "laravel/boost": "<2.2.0"
+ },
+ "require-dev": {
+ "guzzlehttp/guzzle": "^7.2",
+ "larastan/larastan": "^3.0",
+ "laravel/pint": "^1.16",
+ "mockery/mockery": "^1.3.3",
+ "orchestra/testbench": "^9.2|^10.0|^11.0",
+ "phpunit/phpunit": "^11.5|^12.0",
+ "roave/security-advisories": "dev-master"
+ },
+ "suggest": {
+ "ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command."
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Inertia\\ServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "files": [
+ "./helpers.php"
+ ],
+ "psr-4": {
+ "Inertia\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jonathan Reinink",
+ "email": "jonathan@reinink.ca",
+ "homepage": "https://reinink.ca"
+ }
+ ],
+ "description": "The Laravel adapter for Inertia.js.",
+ "keywords": [
+ "inertia",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/inertiajs/inertia-laravel/issues",
+ "source": "https://github.com/inertiajs/inertia-laravel/tree/v3.1.0"
+ },
+ "time": "2026-04-30T15:30:29+00:00"
+ },
{
"name": "jean85/pretty-package-versions",
"version": "2.1.1",
@@ -8973,16 +9046,16 @@
},
{
"name": "symfony/http-foundation",
- "version": "v7.4.8",
+ "version": "v7.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "9381209597ec66c25be154cbf2289076e64d1eab"
+ "reference": "bc354f47c62301e990b7874fa662326368508e2c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab",
- "reference": "9381209597ec66c25be154cbf2289076e64d1eab",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c",
+ "reference": "bc354f47c62301e990b7874fa662326368508e2c",
"shasum": ""
},
"require": {
@@ -9031,7 +9104,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v7.4.8"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.4.13"
},
"funding": [
{
@@ -9051,7 +9124,7 @@
"type": "tidelift"
}
],
- "time": "2026-03-24T13:12:05+00:00"
+ "time": "2026-05-24T11:20:33+00:00"
},
{
"name": "symfony/http-kernel",
@@ -10008,16 +10081,16 @@
},
{
"name": "symfony/polyfill-php83",
- "version": "v1.37.0",
+ "version": "v1.38.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
- "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149"
+ "reference": "8339098cae28673c15cce00d80734af0453054e2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149",
- "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/8339098cae28673c15cce00d80734af0453054e2",
+ "reference": "8339098cae28673c15cce00d80734af0453054e2",
"shasum": ""
},
"require": {
@@ -10064,7 +10137,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0"
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.1"
},
"funding": [
{
@@ -10084,7 +10157,7 @@
"type": "tidelift"
}
],
- "time": "2026-04-10T17:25:58+00:00"
+ "time": "2026-05-26T12:51:13+00:00"
},
{
"name": "symfony/polyfill-php84",
@@ -10650,16 +10723,16 @@
},
{
"name": "symfony/routing",
- "version": "v7.4.12",
+ "version": "v7.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204"
+ "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204",
- "reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d",
+ "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d",
"shasum": ""
},
"require": {
@@ -10711,7 +10784,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v7.4.12"
+ "source": "https://github.com/symfony/routing/tree/v7.4.13"
},
"funding": [
{
@@ -10731,7 +10804,7 @@
"type": "tidelift"
}
],
- "time": "2026-05-20T07:20:23+00:00"
+ "time": "2026-05-24T11:20:33+00:00"
},
{
"name": "symfony/serializer",
diff --git a/config/flux.php b/config/flux.php
new file mode 100644
index 000000000..e82914de0
--- /dev/null
+++ b/config/flux.php
@@ -0,0 +1,6 @@
+ env('COOLIFY_FLUX_UNIX_SOCKET_PATH', '/run/coolify/flux.sock'),
+ 'health_timeout_seconds' => (float) env('COOLIFY_FLUX_HEALTH_TIMEOUT_SECONDS', 1.0),
+];
diff --git a/database/migrations/2026_06_04_050157_create_v5_projects_table.php b/database/migrations/2026_06_04_050157_create_v5_projects_table.php
new file mode 100644
index 000000000..8148c6cce
--- /dev/null
+++ b/database/migrations/2026_06_04_050157_create_v5_projects_table.php
@@ -0,0 +1,33 @@
+id();
+ $table->foreignId('team_id')->constrained('teams')->cascadeOnDelete();
+ $table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete();
+ $table->string('name');
+ $table->text('description')->nullable();
+ $table->timestamps();
+
+ $table->index(['team_id', 'name']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('v5_projects');
+ }
+};
diff --git a/database/schema/testing-schema.sql b/database/schema/testing-schema.sql
index edbc35db4..7555922ce 100644
--- a/database/schema/testing-schema.sql
+++ b/database/schema/testing-schema.sql
@@ -1321,6 +1321,16 @@ CREATE TABLE IF NOT EXISTS "users" (
"email_change_code_expires_at" TEXT
);
+CREATE TABLE IF NOT EXISTS "v5_projects" (
+ "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ "team_id" INTEGER NOT NULL,
+ "created_by_user_id" INTEGER NOT NULL,
+ "name" TEXT NOT NULL,
+ "description" TEXT,
+ "created_at" TEXT,
+ "updated_at" TEXT
+);
+
CREATE TABLE IF NOT EXISTS "webhook_notification_settings" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"team_id" INTEGER NOT NULL,
@@ -1752,3 +1762,4 @@ INSERT INTO "migrations" ("id", "migration", "batch") VALUES (311, '2025_12_10_1
INSERT INTO "migrations" ("id", "migration", "batch") VALUES (312, '2025_12_15_143052_trim_s3_storage_credentials', 312);
INSERT INTO "migrations" ("id", "migration", "batch") VALUES (313, '2025_12_17_000001_add_is_wire_navigate_enabled_to_instance_settings_table', 313);
INSERT INTO "migrations" ("id", "migration", "batch") VALUES (314, '2025_12_17_000002_add_restart_tracking_to_standalone_databases', 314);
+INSERT INTO "migrations" ("id", "migration", "batch") VALUES (315, '2026_06_04_050157_create_v5_projects_table', 315);
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 9c93678af..bad72a22d 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -10,8 +10,10 @@ services:
- GROUP_ID=${GROUPID:-1000}
ports:
- "${APP_PORT:-8000}:8080"
+ - "${FORWARD_FLUX_PORT:-6443}:6443"
environment:
AUTORUN_ENABLED: false
+ COOLIFY_CONTAINER_ROLE: "${COOLIFY_CONTAINER_ROLE:-all}"
PUSHER_HOST: "${PUSHER_HOST}"
PUSHER_PORT: "${PUSHER_PORT}"
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index 8907a30b9..fd4aabeef 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -11,6 +11,7 @@ services:
- /data/coolify/databases:/var/www/html/storage/app/databases
- /data/coolify/services:/var/www/html/storage/app/services
- /data/coolify/backups:/var/www/html/storage/app/backups
+ - /data/coolify/flux:/var/www/html/storage/app/flux
environment:
- APP_ENV=${APP_ENV:-production}
- PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT:-256M}
@@ -18,12 +19,15 @@ services:
- PHP_FPM_PM_START_SERVERS=${PHP_FPM_PM_START_SERVERS:-1}
- PHP_FPM_PM_MIN_SPARE_SERVERS=${PHP_FPM_PM_MIN_SPARE_SERVERS:-1}
- PHP_FPM_PM_MAX_SPARE_SERVERS=${PHP_FPM_PM_MAX_SPARE_SERVERS:-10}
+ - COOLIFY_CONTAINER_ROLE=${COOLIFY_CONTAINER_ROLE:-all}
env_file:
- /data/coolify/source/.env
ports:
- "${APP_PORT:-8000}:8080"
+ - "${COOLIFY_FLUX_PORT:-6443}:6443"
expose:
- "${APP_PORT:-8000}"
+ - "${COOLIFY_FLUX_PORT:-6443}"
healthcheck:
test: curl --fail http://127.0.0.1:8080/api/health || exit 1
interval: 5s
diff --git a/docker/development/Dockerfile b/docker/development/Dockerfile
index 8fc46e32d..08450e817 100644
--- a/docker/development/Dockerfile
+++ b/docker/development/Dockerfile
@@ -5,6 +5,8 @@ ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine
ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2025.7.0
+# https://github.com/coollabsio/coold/releases/tag/nightly
+ARG COOLIFY_FLUX_VERSION=nightly
# https://www.postgresql.org/support/versioning/
# Note: We are using version 18 of the postgres client (while still using postgres 15 for the postgres server) as version 15 has been removed from Alpine 3.23+ https://pkgs.alpinelinux.org/packages?name=postgresql*-client&branch=v3.23&repo=&arch=x86_64&origin=&flagged=&maintainer=
ARG POSTGRES_VERSION=18
@@ -24,8 +26,10 @@ FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION}
ARG USER_ID
ARG GROUP_ID
ARG TARGETPLATFORM
+ARG TARGETARCH
ARG POSTGRES_VERSION
ARG CLOUDFLARED_VERSION
+ARG COOLIFY_FLUX_VERSION
ARG NGINX_VERSION
WORKDIR /var/www/html
@@ -58,6 +62,7 @@ RUN apk upgrade --no-cache && \
RUN apk add --no-cache \
postgresql${POSTGRES_VERSION}-client \
openssh-client \
+ openssl \
git \
git-lfs \
jq \
@@ -81,6 +86,28 @@ RUN mkdir -p /usr/local/bin && \
fi && \
chmod +x /usr/local/bin/cloudflared
+# Install Flux from coold nightly release based on architecture
+RUN set -eux; \
+ mkdir -p /usr/local/bin /run/coolify /etc/coolify; \
+ chown -R www-data:www-data /run/coolify /etc/coolify; \
+ case "${TARGETARCH:-}" in \
+ amd64|arm64) FLUX_ARCH="${TARGETARCH}" ;; \
+ "") \
+ case "$(uname -m)" in \
+ x86_64) FLUX_ARCH="amd64" ;; \
+ aarch64) FLUX_ARCH="arm64" ;; \
+ *) echo "unsupported Flux arch: $(uname -m)" >&2; exit 1 ;; \
+ esac ;; \
+ *) echo "unsupported Flux TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
+ esac; \
+ curl -fsSL --retry 3 --max-time 120 \
+ -o /tmp/flux.tar.gz \
+ "https://github.com/coollabsio/coold/releases/download/${COOLIFY_FLUX_VERSION}/flux-linux-musl-${FLUX_ARCH}.tar.gz"; \
+ tar -xzf /tmp/flux.tar.gz -C /tmp; \
+ test -f /tmp/flux; \
+ install -m 0755 /tmp/flux /usr/local/bin/flux; \
+ rm -f /tmp/flux /tmp/flux.tar.gz
+
# Configure PHP
COPY docker/development/etc/php/conf.d/zzz-custom-php.ini /usr/local/etc/php/conf.d/zzz-custom-php.ini
ENV PHP_OPCACHE_ENABLE=0
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/flux/dependencies.d/init-setup b/docker/development/etc/s6-overlay/s6-rc.d/flux/dependencies.d/init-setup
new file mode 100644
index 000000000..e69de29bb
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/flux/run b/docker/development/etc/s6-overlay/s6-rc.d/flux/run
new file mode 100755
index 000000000..95f648f8e
--- /dev/null
+++ b/docker/development/etc/s6-overlay/s6-rc.d/flux/run
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+cd /var/www/html
+
+role="${COOLIFY_CONTAINER_ROLE:-}"
+if [ -z "$role" ]; then
+ role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
+fi
+role="${role:-all}"
+
+case "$role" in
+ all|flux) ;;
+ *)
+ echo " INFO Flux is disabled for role '$role', sleeping."
+ exec sleep infinity
+ ;;
+esac
+
+if grep -qE '^COOLIFY_FLUX_ENABLED=false' .env 2>/dev/null || [ "${COOLIFY_FLUX_ENABLED:-}" = "false" ]; then
+ echo " INFO Flux is disabled, sleeping."
+ exec sleep infinity
+fi
+
+export COOLIFY_FLUX_GRPC_BIND="${COOLIFY_FLUX_GRPC_BIND:-0.0.0.0:6443}"
+export COOLIFY_FLUX_UNIX_SOCKET_PATH="${COOLIFY_FLUX_UNIX_SOCKET_PATH:-/run/coolify/flux.sock}"
+export COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH="${COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH:-/var/www/html/storage/app/flux/jwt.priv}"
+export COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH="${COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH:-/var/www/html/storage/app/flux/jwt.pub}"
+export COOLIFY_FLUX_ALLOW_PUBLIC_BIND="${COOLIFY_FLUX_ALLOW_PUBLIC_BIND:-1}"
+
+if [ ! -r "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH" ]; then
+ echo " INFO Flux JWT public key not found at $COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH, generating keypair..."
+ mkdir -p "$(dirname "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH")" "$(dirname "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH")"
+ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH.tmp"
+ chmod 0600 "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH.tmp"
+ mv "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH.tmp" "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH"
+ openssl pkey -in "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH" -pubout -out "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH.tmp"
+ chmod 0644 "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH.tmp"
+ mv "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH.tmp" "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH"
+fi
+
+if ! /usr/local/bin/flux --version >/dev/null 2>&1; then
+ echo " ERROR Flux binary cannot run in this container. Check that the installed coold nightly Flux artifact is compatible with Alpine."
+ exec sleep infinity
+fi
+
+mkdir -p "$(dirname "$COOLIFY_FLUX_UNIX_SOCKET_PATH")"
+
+echo " INFO Flux is enabled for role '$role', starting..."
+exec /usr/local/bin/flux
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/flux/type b/docker/development/etc/s6-overlay/s6-rc.d/flux/type
new file mode 100644
index 000000000..1780f9f44
--- /dev/null
+++ b/docker/development/etc/s6-overlay/s6-rc.d/flux/type
@@ -0,0 +1 @@
+longrun
\ No newline at end of file
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run
index dbc472d06..9a8d96d2a 100644
--- a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run
+++ b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run
@@ -2,6 +2,20 @@
cd /var/www/html
+role="${COOLIFY_CONTAINER_ROLE:-}"
+if [ -z "$role" ]; then
+ role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
+fi
+role="${role:-all}"
+
+case "$role" in
+ all|worker) ;;
+ *)
+ echo " INFO Horizon is disabled for role '$role', sleeping."
+ exec sleep infinity
+ ;;
+esac
+
if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then
echo " INFO Horizon is disabled, sleeping."
exec sleep infinity
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
index ee46dba7e..7242b3f79 100644
--- a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
+++ b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
@@ -2,6 +2,20 @@
cd /var/www/html
+role="${COOLIFY_CONTAINER_ROLE:-}"
+if [ -z "$role" ]; then
+ role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
+fi
+role="${role:-all}"
+
+case "$role" in
+ all|worker) ;;
+ *)
+ echo " INFO Nightwatch is disabled for role '$role', sleeping."
+ exec sleep infinity
+ ;;
+esac
+
if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then
echo " INFO Nightwatch is enabled, starting..."
exec php artisan nightwatch:agent
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run
index bfa44c7e3..0dbc2e716 100644
--- a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run
+++ b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run
@@ -2,6 +2,20 @@
cd /var/www/html
+role="${COOLIFY_CONTAINER_ROLE:-}"
+if [ -z "$role" ]; then
+ role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
+fi
+role="${role:-all}"
+
+case "$role" in
+ all|worker) ;;
+ *)
+ echo " INFO Scheduler worker is disabled for role '$role', sleeping."
+ exec sleep infinity
+ ;;
+esac
+
if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then
echo " INFO Scheduler is disabled, sleeping."
exec sleep infinity
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/user/contents.d/flux b/docker/development/etc/s6-overlay/s6-rc.d/user/contents.d/flux
new file mode 100644
index 000000000..e69de29bb
diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile
index 0f849785e..b6dd5fac0 100644
--- a/docker/production/Dockerfile
+++ b/docker/production/Dockerfile
@@ -5,6 +5,8 @@ ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine
ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2025.7.0
+# https://github.com/coollabsio/coold/releases/tag/nightly
+ARG COOLIFY_FLUX_VERSION=nightly
# https://www.postgresql.org/support/versioning/
# Note: We are using version 18 of the postgres client (while still using postgres 15 for the postgres server) as version 15 has been removed from Alpine 3.23+ https://pkgs.alpinelinux.org/packages?name=postgresql*-client&branch=v3.23&repo=&arch=x86_64&origin=&flagged=&maintainer=
ARG POSTGRES_VERSION=18
@@ -73,8 +75,10 @@ FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION}
ARG USER_ID
ARG GROUP_ID
ARG TARGETPLATFORM
+ARG TARGETARCH
ARG POSTGRES_VERSION
ARG CLOUDFLARED_VERSION
+ARG COOLIFY_FLUX_VERSION
ARG NGINX_VERSION
ARG CI=true
@@ -108,6 +112,7 @@ RUN --mount=type=cache,target=/var/cache/apk \
apk add --no-cache \
postgresql${POSTGRES_VERSION}-client \
openssh-client \
+ openssl \
git \
git-lfs \
jq \
@@ -128,6 +133,28 @@ RUN mkdir -p /usr/local/bin && \
fi && \
chmod +x /usr/local/bin/cloudflared
+# Install Flux from coold nightly release based on architecture
+RUN set -eux; \
+ mkdir -p /usr/local/bin /run/coolify /etc/coolify; \
+ chown -R www-data:www-data /run/coolify /etc/coolify; \
+ case "${TARGETARCH:-}" in \
+ amd64|arm64) FLUX_ARCH="${TARGETARCH}" ;; \
+ "") \
+ case "$(uname -m)" in \
+ x86_64) FLUX_ARCH="amd64" ;; \
+ aarch64) FLUX_ARCH="arm64" ;; \
+ *) echo "unsupported Flux arch: $(uname -m)" >&2; exit 1 ;; \
+ esac ;; \
+ *) echo "unsupported Flux TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
+ esac; \
+ curl -fsSL --retry 3 --max-time 120 \
+ -o /tmp/flux.tar.gz \
+ "https://github.com/coollabsio/coold/releases/download/${COOLIFY_FLUX_VERSION}/flux-linux-musl-${FLUX_ARCH}.tar.gz"; \
+ tar -xzf /tmp/flux.tar.gz -C /tmp; \
+ test -f /tmp/flux; \
+ install -m 0755 /tmp/flux /usr/local/bin/flux; \
+ rm -f /tmp/flux /tmp/flux.tar.gz
+
# Configure PHP
COPY docker/production/etc/php/conf.d/zzz-custom-php.ini /usr/local/etc/php/conf.d/zzz-custom-php.ini
ENV PHP_OPCACHE_ENABLE=1
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/flux/dependencies.d/init-script b/docker/production/etc/s6-overlay/s6-rc.d/flux/dependencies.d/init-script
new file mode 100644
index 000000000..e69de29bb
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/flux/run b/docker/production/etc/s6-overlay/s6-rc.d/flux/run
new file mode 100755
index 000000000..95f648f8e
--- /dev/null
+++ b/docker/production/etc/s6-overlay/s6-rc.d/flux/run
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+cd /var/www/html
+
+role="${COOLIFY_CONTAINER_ROLE:-}"
+if [ -z "$role" ]; then
+ role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
+fi
+role="${role:-all}"
+
+case "$role" in
+ all|flux) ;;
+ *)
+ echo " INFO Flux is disabled for role '$role', sleeping."
+ exec sleep infinity
+ ;;
+esac
+
+if grep -qE '^COOLIFY_FLUX_ENABLED=false' .env 2>/dev/null || [ "${COOLIFY_FLUX_ENABLED:-}" = "false" ]; then
+ echo " INFO Flux is disabled, sleeping."
+ exec sleep infinity
+fi
+
+export COOLIFY_FLUX_GRPC_BIND="${COOLIFY_FLUX_GRPC_BIND:-0.0.0.0:6443}"
+export COOLIFY_FLUX_UNIX_SOCKET_PATH="${COOLIFY_FLUX_UNIX_SOCKET_PATH:-/run/coolify/flux.sock}"
+export COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH="${COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH:-/var/www/html/storage/app/flux/jwt.priv}"
+export COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH="${COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH:-/var/www/html/storage/app/flux/jwt.pub}"
+export COOLIFY_FLUX_ALLOW_PUBLIC_BIND="${COOLIFY_FLUX_ALLOW_PUBLIC_BIND:-1}"
+
+if [ ! -r "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH" ]; then
+ echo " INFO Flux JWT public key not found at $COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH, generating keypair..."
+ mkdir -p "$(dirname "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH")" "$(dirname "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH")"
+ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH.tmp"
+ chmod 0600 "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH.tmp"
+ mv "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH.tmp" "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH"
+ openssl pkey -in "$COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH" -pubout -out "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH.tmp"
+ chmod 0644 "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH.tmp"
+ mv "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH.tmp" "$COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH"
+fi
+
+if ! /usr/local/bin/flux --version >/dev/null 2>&1; then
+ echo " ERROR Flux binary cannot run in this container. Check that the installed coold nightly Flux artifact is compatible with Alpine."
+ exec sleep infinity
+fi
+
+mkdir -p "$(dirname "$COOLIFY_FLUX_UNIX_SOCKET_PATH")"
+
+echo " INFO Flux is enabled for role '$role', starting..."
+exec /usr/local/bin/flux
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/flux/type b/docker/production/etc/s6-overlay/s6-rc.d/flux/type
new file mode 100644
index 000000000..1780f9f44
--- /dev/null
+++ b/docker/production/etc/s6-overlay/s6-rc.d/flux/type
@@ -0,0 +1 @@
+longrun
\ No newline at end of file
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run
index dbc472d06..9a8d96d2a 100644
--- a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run
+++ b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run
@@ -2,6 +2,20 @@
cd /var/www/html
+role="${COOLIFY_CONTAINER_ROLE:-}"
+if [ -z "$role" ]; then
+ role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
+fi
+role="${role:-all}"
+
+case "$role" in
+ all|worker) ;;
+ *)
+ echo " INFO Horizon is disabled for role '$role', sleeping."
+ exec sleep infinity
+ ;;
+esac
+
if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then
echo " INFO Horizon is disabled, sleeping."
exec sleep infinity
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
index ee46dba7e..7242b3f79 100644
--- a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
+++ b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
@@ -2,6 +2,20 @@
cd /var/www/html
+role="${COOLIFY_CONTAINER_ROLE:-}"
+if [ -z "$role" ]; then
+ role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
+fi
+role="${role:-all}"
+
+case "$role" in
+ all|worker) ;;
+ *)
+ echo " INFO Nightwatch is disabled for role '$role', sleeping."
+ exec sleep infinity
+ ;;
+esac
+
if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then
echo " INFO Nightwatch is enabled, starting..."
exec php artisan nightwatch:agent
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run
index bfa44c7e3..0dbc2e716 100644
--- a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run
+++ b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run
@@ -2,6 +2,20 @@
cd /var/www/html
+role="${COOLIFY_CONTAINER_ROLE:-}"
+if [ -z "$role" ]; then
+ role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
+fi
+role="${role:-all}"
+
+case "$role" in
+ all|worker) ;;
+ *)
+ echo " INFO Scheduler worker is disabled for role '$role', sleeping."
+ exec sleep infinity
+ ;;
+esac
+
if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then
echo " INFO Scheduler is disabled, sleeping."
exec sleep infinity
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/user/contents.d/flux b/docker/production/etc/s6-overlay/s6-rc.d/user/contents.d/flux
new file mode 100644
index 000000000..e69de29bb
diff --git a/package-lock.json b/package-lock.json
index 9d495c412..288474a88 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,14 +6,19 @@
"": {
"name": "coolify",
"dependencies": {
+ "@inertiajs/react": "^3.3.0",
+ "@inertiajs/vite": "^3.3.0",
"@tailwindcss/forms": "0.5.10",
"@tailwindcss/typography": "0.5.16",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0",
- "playwright": "^1.58.2"
+ "playwright": "^1.58.2",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7"
},
"devDependencies": {
"@tailwindcss/postcss": "4.1.18",
+ "@vitejs/plugin-react": "^5.2.0",
"laravel-vite-plugin": "2.0.1",
"postcss": "8.5.15",
"tailwind-scrollbar": "4.0.2",
@@ -34,6 +39,289 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
@@ -41,7 +329,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -58,7 +345,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -75,7 +361,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -92,7 +377,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -109,7 +393,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -126,7 +409,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -143,7 +425,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -160,7 +441,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -177,7 +457,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -194,7 +473,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -211,7 +489,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -228,7 +505,6 @@
"cpu": [
"loong64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -245,7 +521,6 @@
"cpu": [
"mips64el"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -262,7 +537,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -279,7 +553,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -296,7 +569,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -313,7 +585,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -330,7 +601,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -347,7 +617,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -364,7 +633,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -381,7 +649,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -398,7 +665,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -415,7 +681,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -432,7 +697,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -449,7 +713,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -466,7 +729,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -476,6 +738,53 @@
"node": ">=18"
}
},
+ "node_modules/@inertiajs/core": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-3.3.0.tgz",
+ "integrity": "sha512-6tEe5vtjwXJj68h0nkz84WkeZ8XA0phJ8ep1l/V+im62KTzlZTw9sxlmf8uNKu4seleXVw+idw48svxUCWla/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "es-toolkit": "^1.33.0",
+ "laravel-precognition": "^2.0.0"
+ },
+ "peerDependencies": {
+ "axios": "^1.15.2"
+ },
+ "peerDependenciesMeta": {
+ "axios": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inertiajs/react": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@inertiajs/react/-/react-3.3.0.tgz",
+ "integrity": "sha512-8Es6i1FOyP5sHWjLlE3yFi+dOuJp3IBkl1ccoxb101Jr4/vTcNsdXvpl+U8IZmvnmyDS4dikeAa29p7xFUwgyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@inertiajs/core": "3.3.0",
+ "es-toolkit": "^1.33.0",
+ "laravel-precognition": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
+ "node_modules/@inertiajs/vite": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@inertiajs/vite/-/vite-3.3.0.tgz",
+ "integrity": "sha512-UWm+I5OpxGnm1LBjgaPOI6/M8gurloxXRnzeNMd2UC6f6ObQo6RZvyLMMWInwS0fBief7lfkpj5sPZy+680CRA==",
+ "license": "MIT",
+ "dependencies": {
+ "@inertiajs/core": "3.3.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "peerDependencies": {
+ "vite": "^7.0.0 || ^8.0.0"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -502,7 +811,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -512,20 +820,25 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
@@ -533,7 +846,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -547,7 +859,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -561,7 +872,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -575,7 +885,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -589,7 +898,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -603,7 +911,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -617,7 +924,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -631,7 +937,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -645,7 +950,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -659,7 +963,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -673,7 +976,6 @@
"cpu": [
"loong64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -687,7 +989,6 @@
"cpu": [
"loong64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -701,7 +1002,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -715,7 +1015,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -729,7 +1028,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -743,7 +1041,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -757,7 +1054,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -771,7 +1067,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -785,7 +1080,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -799,7 +1093,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -813,7 +1106,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -827,7 +1119,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -841,7 +1132,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -855,7 +1145,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -869,7 +1158,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1234,11 +1522,55 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/prismjs": {
@@ -1248,6 +1580,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.29.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-rc.3",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
"node_modules/@xterm/addon-fit": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
@@ -1261,7 +1614,77 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.33",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz",
+ "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001793",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/clsx": {
"version": "2.1.1",
@@ -1273,6 +1696,13 @@
"node": ">=6"
}
},
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -1285,16 +1715,41 @@
"node": ">=4"
}
},
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.366",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.366.tgz",
+ "integrity": "sha512-OlRuhb688YTCzzU3gXPLn6nGyd+F+53INE1qaKKlu6kETErE8FYsyDh0XqXEU+uBRn0MpCzz2vfNwORhkap8qg==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/enhanced-resolve": {
"version": "5.19.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
@@ -1309,11 +1764,20 @@
"node": ">=10.13.0"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.47.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz",
+ "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/esbuild": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -1351,11 +1815,20 @@
"@esbuild/win32-x64": "0.27.2"
}
},
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -1373,7 +1846,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -1384,6 +1856,16 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1395,12 +1877,62 @@
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/laravel-precognition": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/laravel-precognition/-/laravel-precognition-2.0.0.tgz",
+ "integrity": "sha512-dmA4HGc9m+TsVNsJs9/XQBI8u6j7coilN+qKkBuhuXQzH3HypwS/c5dFQ4UqUGjBbcxIM7zdk91kM/SRZwIvWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-toolkit": "^1.32.0"
+ },
+ "peerDependencies": {
+ "axios": "^1.4.0"
+ },
+ "peerDependenciesMeta": {
+ "axios": {
+ "optional": true
+ }
+ }
+ },
"node_modules/laravel-vite-plugin": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz",
@@ -1425,7 +1957,7 @@
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
- "dev": true,
+ "devOptional": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
@@ -1458,7 +1990,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1479,7 +2010,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1500,7 +2030,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1521,7 +2050,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1542,7 +2070,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1563,7 +2090,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1584,7 +2110,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1605,7 +2130,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1626,7 +2150,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1647,7 +2170,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1668,7 +2190,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1700,6 +2221,16 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
},
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1719,11 +2250,17 @@
"mini-svg-data-uri": "cli.js"
}
},
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -1738,19 +2275,28 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/node-releases": {
+ "version": "2.0.47",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
+ "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -1806,7 +2352,6 @@
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -1859,21 +2404,42 @@
}
},
"node_modules/react": {
- "version": "19.2.4",
- "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
- "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
- "dev": true,
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/react-dom": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/rollup": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -1914,11 +2480,26 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -1944,7 +2525,8 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -1964,7 +2546,6 @@
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -1977,6 +2558,37 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -1987,8 +2599,8 @@
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
- "dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -2081,6 +2693,13 @@
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
}
}
}
diff --git a/package.json b/package.json
index c3fb1bc5f..43c4c738a 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "4.1.18",
+ "@vitejs/plugin-react": "^5.2.0",
"laravel-vite-plugin": "2.0.1",
"postcss": "8.5.15",
"tailwind-scrollbar": "4.0.2",
@@ -15,10 +16,14 @@
"vite": "7.3.2"
},
"dependencies": {
+ "@inertiajs/react": "^3.3.0",
+ "@inertiajs/vite": "^3.3.0",
"@tailwindcss/forms": "0.5.10",
"@tailwindcss/typography": "0.5.16",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0",
- "playwright": "^1.58.2"
+ "playwright": "^1.58.2",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7"
}
}
diff --git a/resources/css/v5/app.css b/resources/css/v5/app.css
new file mode 100644
index 000000000..a363687ee
--- /dev/null
+++ b/resources/css/v5/app.css
@@ -0,0 +1,11 @@
+@font-face {
+ font-display: swap;
+ font-family: 'Geist Sans';
+ font-style: normal;
+ font-weight: 100 900;
+ src: url('../../fonts/geist-sans-variable.woff2') format('woff2');
+}
+
+body {
+ font-family: 'Geist Sans', sans-serif;
+}
diff --git a/resources/js/v5/Pages/Home.jsx b/resources/js/v5/Pages/Home.jsx
new file mode 100644
index 000000000..0fa9a231b
--- /dev/null
+++ b/resources/js/v5/Pages/Home.jsx
@@ -0,0 +1,59 @@
+import { Head } from '@inertiajs/react';
+
+export default function Home({ status, currentTeam, teams, flux }) {
+ return (
+ <>
+
+
+
+ {status}
+
+ Coolify v5
+
+
+ This page is served from Laravel through Inertia and React, with routes,
+ assets, and future v5 tables isolated from the current v4 Livewire app.
+
+
+
+ Flux status
+
+
+ {flux.label}
+
+
+ {flux.message}
+
+ {flux.socket ? Socket: {flux.socket}
: null}
+
+
+ Current team
+
+ {currentTeam ? (
+
+ - Name
+ - {currentTeam.name}
+
+ - Description
+ - {currentTeam.description || 'No description'}
+
+ - Your role
+ - {currentTeam.role}
+
+ ) : (
+ No team selected.
+ )}
+
+ Your teams
+
+
+ {teams.map((team) => (
+ -
+ {team.name} ({team.role})
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/resources/js/v5/app.jsx b/resources/js/v5/app.jsx
new file mode 100644
index 000000000..fa391dab8
--- /dev/null
+++ b/resources/js/v5/app.jsx
@@ -0,0 +1,11 @@
+import { createInertiaApp } from '@inertiajs/react';
+import '../../css/v5/app.css';
+
+createInertiaApp({
+ id: 'v5-app',
+ pages: {
+ path: './Pages',
+ extension: '.jsx',
+ },
+ strictMode: true,
+});
diff --git a/resources/views/v5/app.blade.php b/resources/views/v5/app.blade.php
new file mode 100644
index 000000000..7f1a5fd70
--- /dev/null
+++ b/resources/views/v5/app.blade.php
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ {{ config('app.name') }} v5
+ @viteReactRefresh
+ @vite('resources/js/v5/app.jsx')
+
+
+
+
+
+
+
+
diff --git a/routes/v5.php b/routes/v5.php
new file mode 100644
index 000000000..16525bf73
--- /dev/null
+++ b/routes/v5.php
@@ -0,0 +1,8 @@
+group(function () {
+ Route::get('/', HomeController::class)->name('home');
+});
diff --git a/tests/Feature/V5/HomeTest.php b/tests/Feature/V5/HomeTest.php
new file mode 100644
index 000000000..c37b47496
--- /dev/null
+++ b/tests/Feature/V5/HomeTest.php
@@ -0,0 +1,214 @@
+toBeTrue();
+});
+
+it('uses separated v5 middleware groups', function () {
+ $kernel = app(Kernel::class);
+ $reflection = new ReflectionClass($kernel);
+ $property = $reflection->getProperty('middlewareGroups');
+ $property->setAccessible(true);
+ $groups = $property->getValue($kernel);
+
+ expect($groups)->toHaveKey('v5.web')
+ ->and($groups)->toHaveKey('v5.authenticated')
+ ->and($groups['v5.web'])->toContain(HandleInertiaRequests::class)
+ ->and($groups['v5.web'])->not->toContain(CheckForcePasswordReset::class)
+ ->and($groups['v5.web'])->not->toContain(DecideWhatToDoWithUser::class)
+ ->and($groups['v5.authenticated'])->toContain('auth')
+ ->and($groups['v5.authenticated'])->toContain('verified')
+ ->and($groups['v5.authenticated'])->toContain(EnsureCurrentTeam::class);
+});
+
+it('creates v5 project tables in the shared database', function () {
+ createSharedUserAndTeamTables();
+
+ $migration = include database_path('migrations/2026_06_04_050157_create_v5_projects_table.php');
+ $migration->up();
+
+ expect(Schema::hasTable('v5_projects'))->toBeTrue()
+ ->and(Schema::hasColumns('v5_projects', [
+ 'id',
+ 'team_id',
+ 'created_by_user_id',
+ 'name',
+ 'description',
+ 'created_at',
+ 'updated_at',
+ ]))->toBeTrue();
+});
+
+it('includes v5 project tables in the dev testing schema', function () {
+ $schema = file_get_contents(database_path('schema/testing-schema.sql'));
+
+ expect($schema)->toContain('CREATE TABLE IF NOT EXISTS "v5_projects"')
+ ->and($schema)->toContain('"team_id" INTEGER NOT NULL')
+ ->and($schema)->toContain('"created_by_user_id" INTEGER NOT NULL')
+ ->and($schema)->toContain('2026_06_04_050157_create_v5_projects_table');
+});
+
+it('redirects guests to the shared login', function () {
+ $this->get('/v5')
+ ->assertRedirect('/login');
+});
+
+it('serves the v5 inertia shell', function () {
+ $this->withoutVite();
+ fakeFluxHealth();
+ createSharedUserAndTeamTables();
+
+ $user = User::withoutEvents(fn () => User::query()->create([
+ 'name' => 'Ada Lovelace',
+ 'email' => 'ada@example.com',
+ 'email_verified_at' => now(),
+ 'password' => 'password',
+ ]));
+ $team = Team::withoutEvents(fn () => Team::query()->create([
+ 'name' => 'V5 Shared Team',
+ 'description' => 'Shared team details',
+ 'personal_team' => true,
+ 'show_boarding' => false,
+ ]));
+ $user->teams()->attach($team, ['role' => 'owner']);
+
+ $this
+ ->actingAs($user)
+ ->withSession(['currentTeam' => $team])
+ ->get('/v5')
+ ->assertSuccessful()
+ ->assertSee('v5-app', false)
+ ->assertSee('Home', false)
+ ->assertSee('v5-ready', false)
+ ->assertSee('Running')
+ ->assertSee('Flux is running.')
+ ->assertSee('V5 Shared Team')
+ ->assertSee('Shared team details')
+ ->assertSee('owner')
+ ->assertSee($user->email);
+});
+
+it('selects a shared team when the session has no current team', function () {
+ $this->withoutVite();
+ fakeFluxHealth();
+ createSharedUserAndTeamTables();
+
+ $user = User::withoutEvents(fn () => User::query()->create([
+ 'name' => 'Grace Hopper',
+ 'email' => 'grace@example.com',
+ 'email_verified_at' => now(),
+ 'password' => 'password',
+ ]));
+ $team = Team::withoutEvents(fn () => Team::query()->create([
+ 'name' => 'Auto Selected Team',
+ 'description' => null,
+ 'personal_team' => false,
+ 'show_boarding' => false,
+ ]));
+ $user->teams()->attach($team, ['role' => 'admin']);
+
+ $this
+ ->actingAs($user)
+ ->get('/v5')
+ ->assertSuccessful()
+ ->assertSessionHas('currentTeam')
+ ->assertSee('Auto Selected Team')
+ ->assertSee('admin');
+});
+
+it('shows when flux is unavailable', function () {
+ $this->withoutVite();
+ fakeFluxHealth(false, 'Flux socket was not found.');
+ createSharedUserAndTeamTables();
+
+ $user = User::withoutEvents(fn () => User::query()->create([
+ 'name' => 'Katherine Johnson',
+ 'email' => 'katherine@example.com',
+ 'email_verified_at' => now(),
+ 'password' => 'password',
+ ]));
+ $team = Team::withoutEvents(fn () => Team::query()->create([
+ 'name' => 'Flux Test Team',
+ 'description' => null,
+ 'personal_team' => false,
+ 'show_boarding' => false,
+ ]));
+ $user->teams()->attach($team, ['role' => 'owner']);
+
+ $this
+ ->actingAs($user)
+ ->withSession(['currentTeam' => $team])
+ ->get('/v5')
+ ->assertSuccessful()
+ ->assertSee('Unavailable')
+ ->assertSee('Flux socket was not found.');
+});
+
+function fakeFluxHealth(bool $available = true, string $message = 'Flux is running.'): void
+{
+ app()->instance(FluxHealth::class, Mockery::mock(FluxHealth::class, function (MockInterface $mock) use ($available, $message) {
+ $mock->shouldReceive('check')
+ ->once()
+ ->andReturn([
+ 'available' => $available,
+ 'label' => $available ? 'Running' : 'Unavailable',
+ 'message' => $message,
+ 'socket' => '/run/coolify/flux.sock',
+ ]);
+ }));
+}
+
+function createSharedUserAndTeamTables(): void
+{
+ Schema::create('users', function ($table) {
+ $table->id();
+ $table->string('name')->default('Anonymous');
+ $table->string('email');
+ $table->timestamp('email_verified_at')->nullable();
+ $table->string('password')->nullable();
+ $table->rememberToken();
+ $table->timestamps();
+ });
+
+ Schema::create('teams', function ($table) {
+ $table->id();
+ $table->string('name');
+ $table->string('description')->nullable();
+ $table->boolean('personal_team')->default(false);
+ $table->boolean('show_boarding')->default(false);
+ $table->timestamps();
+ });
+
+ Schema::create('team_user', function ($table) {
+ $table->id();
+ $table->foreignId('team_id');
+ $table->foreignId('user_id');
+ $table->string('role')->default('member');
+ $table->timestamps();
+
+ $table->unique(['team_id', 'user_id']);
+ });
+}
diff --git a/vite.config.js b/vite.config.js
index 36069fb28..3c36862f8 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,5 +1,7 @@
import { defineConfig, loadEnv } from "vite";
import laravel from "laravel-vite-plugin";
+import react from "@vitejs/plugin-react";
+import inertia from "@inertiajs/vite";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
@@ -32,9 +34,15 @@ export default defineConfig(({ mode }) => {
},
plugins: [
laravel({
- input: ["resources/css/app.css", "resources/js/app.js"],
+ input: [
+ "resources/css/app.css",
+ "resources/js/app.js",
+ "resources/js/v5/app.jsx",
+ ],
refresh: true,
}),
+ inertia({ ssr: false }),
+ react(),
],
}
});
From 762daf83a1af9766608cd9a43078c104659b4795 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 15 Jun 2026 23:36:47 +0200
Subject: [PATCH 003/174] feat(container): support comma-separated roles
Allow s6 services to start for specific roles like horizon,
scheduler, nightwatch, and flux while keeping all as the default.
---
.env.development.example | 1 +
README.md | 9 +++-
.../etc/s6-overlay/s6-rc.d/flux/run | 18 +++----
.../etc/s6-overlay/s6-rc.d/horizon/run | 18 +++----
.../s6-overlay/s6-rc.d/nightwatch-agent/run | 18 +++----
.../s6-overlay/s6-rc.d/scheduler-worker/run | 18 +++----
.../etc/s6-overlay/scripts/container-role | 26 +++++++++
.../etc/s6-overlay/s6-rc.d/flux/run | 18 +++----
.../etc/s6-overlay/s6-rc.d/horizon/run | 18 +++----
.../s6-overlay/s6-rc.d/nightwatch-agent/run | 18 +++----
.../s6-overlay/s6-rc.d/scheduler-worker/run | 18 +++----
.../etc/s6-overlay/scripts/container-role | 26 +++++++++
package-lock.json | 12 +----
tests/Feature/ContainerRoleScriptTest.php | 53 +++++++++++++++++++
14 files changed, 163 insertions(+), 108 deletions(-)
create mode 100755 docker/development/etc/s6-overlay/scripts/container-role
create mode 100755 docker/production/etc/s6-overlay/scripts/container-role
create mode 100644 tests/Feature/ContainerRoleScriptTest.php
diff --git a/.env.development.example b/.env.development.example
index d02b8ba59..887f62974 100644
--- a/.env.development.example
+++ b/.env.development.example
@@ -7,6 +7,7 @@ APP_URL=http://localhost
APP_PORT=8000
APP_DEBUG=true
SSH_MUX_ENABLED=true
+COOLIFY_CONTAINER_ROLE=all
# PostgreSQL Database Configuration
DB_DATABASE=coolify
diff --git a/README.md b/README.md
index f4e289bcd..378e75ac7 100644
--- a/README.md
+++ b/README.md
@@ -32,13 +32,18 @@ You can find the installation script source [here](./scripts/install.sh).
## Container roles and Flux
-The Coolify image can run different process roles with `COOLIFY_CONTAINER_ROLE`:
+The Coolify image can run different process roles with `COOLIFY_CONTAINER_ROLE`. The value can be a single role or a comma-separated list. If `all` is present anywhere in the list, every role starts.
- `all` (default): self-hosted mode; runs the web process, worker services, and Flux when configured.
-- `web`: web/API process only; s6 worker services and Flux sleep.
+- `web`: web/API process only; s6 worker services and Flux sleep unless they are also listed.
- `worker`: Horizon, Laravel scheduler worker, and optional Nightwatch agent.
+- `horizon`: Horizon only.
+- `scheduler` or `scheduler-worker`: Laravel scheduler worker only.
+- `nightwatch` or `nightwatch-agent`: optional Nightwatch agent only.
- `flux`: Flux only; used by Cloud/HA deployments that scale coold connection routers separately.
+For example, `COOLIFY_CONTAINER_ROLE=web,flux` starts the web container plus Flux, while `COOLIFY_CONTAINER_ROLE=web,all` still starts all services.
+
Flux is installed from the coold nightly release into `/usr/local/bin/flux`. Containers running the `all` or `flux` role expose Flux on port `6443` and use these runtime variables:
```env
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/flux/run b/docker/development/etc/s6-overlay/s6-rc.d/flux/run
index 95f648f8e..d99bdaac4 100755
--- a/docker/development/etc/s6-overlay/s6-rc.d/flux/run
+++ b/docker/development/etc/s6-overlay/s6-rc.d/flux/run
@@ -2,19 +2,13 @@
cd /var/www/html
-role="${COOLIFY_CONTAINER_ROLE:-}"
-if [ -z "$role" ]; then
- role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
-fi
-role="${role:-all}"
+. /etc/s6-overlay/scripts/container-role
+role="$(coolify_container_role_value)"
-case "$role" in
- all|flux) ;;
- *)
- echo " INFO Flux is disabled for role '$role', sleeping."
- exec sleep infinity
- ;;
-esac
+if ! coolify_container_has_role flux; then
+ echo " INFO Flux is disabled for role '$role', sleeping."
+ exec sleep infinity
+fi
if grep -qE '^COOLIFY_FLUX_ENABLED=false' .env 2>/dev/null || [ "${COOLIFY_FLUX_ENABLED:-}" = "false" ]; then
echo " INFO Flux is disabled, sleeping."
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run
index 9a8d96d2a..584bd0d9d 100644
--- a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run
+++ b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run
@@ -2,19 +2,13 @@
cd /var/www/html
-role="${COOLIFY_CONTAINER_ROLE:-}"
-if [ -z "$role" ]; then
- role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
-fi
-role="${role:-all}"
+. /etc/s6-overlay/scripts/container-role
+role="$(coolify_container_role_value)"
-case "$role" in
- all|worker) ;;
- *)
- echo " INFO Horizon is disabled for role '$role', sleeping."
- exec sleep infinity
- ;;
-esac
+if ! coolify_container_has_role worker && ! coolify_container_has_role horizon; then
+ echo " INFO Horizon is disabled for role '$role', sleeping."
+ exec sleep infinity
+fi
if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then
echo " INFO Horizon is disabled, sleeping."
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
index 7242b3f79..1f8b37079 100644
--- a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
+++ b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
@@ -2,19 +2,13 @@
cd /var/www/html
-role="${COOLIFY_CONTAINER_ROLE:-}"
-if [ -z "$role" ]; then
- role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
-fi
-role="${role:-all}"
+. /etc/s6-overlay/scripts/container-role
+role="$(coolify_container_role_value)"
-case "$role" in
- all|worker) ;;
- *)
- echo " INFO Nightwatch is disabled for role '$role', sleeping."
- exec sleep infinity
- ;;
-esac
+if ! coolify_container_has_role worker && ! coolify_container_has_role nightwatch && ! coolify_container_has_role nightwatch-agent; then
+ echo " INFO Nightwatch is disabled for role '$role', sleeping."
+ exec sleep infinity
+fi
if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then
echo " INFO Nightwatch is enabled, starting..."
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run
index 0dbc2e716..2dcd493a1 100644
--- a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run
+++ b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run
@@ -2,19 +2,13 @@
cd /var/www/html
-role="${COOLIFY_CONTAINER_ROLE:-}"
-if [ -z "$role" ]; then
- role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
-fi
-role="${role:-all}"
+. /etc/s6-overlay/scripts/container-role
+role="$(coolify_container_role_value)"
-case "$role" in
- all|worker) ;;
- *)
- echo " INFO Scheduler worker is disabled for role '$role', sleeping."
- exec sleep infinity
- ;;
-esac
+if ! coolify_container_has_role worker && ! coolify_container_has_role scheduler && ! coolify_container_has_role scheduler-worker; then
+ echo " INFO Scheduler worker is disabled for role '$role', sleeping."
+ exec sleep infinity
+fi
if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then
echo " INFO Scheduler is disabled, sleeping."
diff --git a/docker/development/etc/s6-overlay/scripts/container-role b/docker/development/etc/s6-overlay/scripts/container-role
new file mode 100755
index 000000000..c7ee7dd3c
--- /dev/null
+++ b/docker/development/etc/s6-overlay/scripts/container-role
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+coolify_container_role_value() {
+ role="${COOLIFY_CONTAINER_ROLE:-}"
+
+ if [ -z "$role" ]; then
+ role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
+ fi
+
+ printf '%s\n' "${role:-all}"
+}
+
+coolify_container_has_role() {
+ wanted_role="$1"
+ roles="$(coolify_container_role_value | tr '[:upper:]' '[:lower:]' | tr ',' ' ')"
+
+ for role in $roles; do
+ case "$role" in
+ all|"$wanted_role")
+ return 0
+ ;;
+ esac
+ done
+
+ return 1
+}
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/flux/run b/docker/production/etc/s6-overlay/s6-rc.d/flux/run
index 95f648f8e..d99bdaac4 100755
--- a/docker/production/etc/s6-overlay/s6-rc.d/flux/run
+++ b/docker/production/etc/s6-overlay/s6-rc.d/flux/run
@@ -2,19 +2,13 @@
cd /var/www/html
-role="${COOLIFY_CONTAINER_ROLE:-}"
-if [ -z "$role" ]; then
- role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
-fi
-role="${role:-all}"
+. /etc/s6-overlay/scripts/container-role
+role="$(coolify_container_role_value)"
-case "$role" in
- all|flux) ;;
- *)
- echo " INFO Flux is disabled for role '$role', sleeping."
- exec sleep infinity
- ;;
-esac
+if ! coolify_container_has_role flux; then
+ echo " INFO Flux is disabled for role '$role', sleeping."
+ exec sleep infinity
+fi
if grep -qE '^COOLIFY_FLUX_ENABLED=false' .env 2>/dev/null || [ "${COOLIFY_FLUX_ENABLED:-}" = "false" ]; then
echo " INFO Flux is disabled, sleeping."
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run
index 9a8d96d2a..584bd0d9d 100644
--- a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run
+++ b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run
@@ -2,19 +2,13 @@
cd /var/www/html
-role="${COOLIFY_CONTAINER_ROLE:-}"
-if [ -z "$role" ]; then
- role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
-fi
-role="${role:-all}"
+. /etc/s6-overlay/scripts/container-role
+role="$(coolify_container_role_value)"
-case "$role" in
- all|worker) ;;
- *)
- echo " INFO Horizon is disabled for role '$role', sleeping."
- exec sleep infinity
- ;;
-esac
+if ! coolify_container_has_role worker && ! coolify_container_has_role horizon; then
+ echo " INFO Horizon is disabled for role '$role', sleeping."
+ exec sleep infinity
+fi
if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then
echo " INFO Horizon is disabled, sleeping."
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
index 7242b3f79..1f8b37079 100644
--- a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
+++ b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
@@ -2,19 +2,13 @@
cd /var/www/html
-role="${COOLIFY_CONTAINER_ROLE:-}"
-if [ -z "$role" ]; then
- role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
-fi
-role="${role:-all}"
+. /etc/s6-overlay/scripts/container-role
+role="$(coolify_container_role_value)"
-case "$role" in
- all|worker) ;;
- *)
- echo " INFO Nightwatch is disabled for role '$role', sleeping."
- exec sleep infinity
- ;;
-esac
+if ! coolify_container_has_role worker && ! coolify_container_has_role nightwatch && ! coolify_container_has_role nightwatch-agent; then
+ echo " INFO Nightwatch is disabled for role '$role', sleeping."
+ exec sleep infinity
+fi
if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then
echo " INFO Nightwatch is enabled, starting..."
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run
index 0dbc2e716..2dcd493a1 100644
--- a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run
+++ b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run
@@ -2,19 +2,13 @@
cd /var/www/html
-role="${COOLIFY_CONTAINER_ROLE:-}"
-if [ -z "$role" ]; then
- role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
-fi
-role="${role:-all}"
+. /etc/s6-overlay/scripts/container-role
+role="$(coolify_container_role_value)"
-case "$role" in
- all|worker) ;;
- *)
- echo " INFO Scheduler worker is disabled for role '$role', sleeping."
- exec sleep infinity
- ;;
-esac
+if ! coolify_container_has_role worker && ! coolify_container_has_role scheduler && ! coolify_container_has_role scheduler-worker; then
+ echo " INFO Scheduler worker is disabled for role '$role', sleeping."
+ exec sleep infinity
+fi
if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then
echo " INFO Scheduler is disabled, sleeping."
diff --git a/docker/production/etc/s6-overlay/scripts/container-role b/docker/production/etc/s6-overlay/scripts/container-role
new file mode 100755
index 000000000..c7ee7dd3c
--- /dev/null
+++ b/docker/production/etc/s6-overlay/scripts/container-role
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+coolify_container_role_value() {
+ role="${COOLIFY_CONTAINER_ROLE:-}"
+
+ if [ -z "$role" ]; then
+ role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")"
+ fi
+
+ printf '%s\n' "${role:-all}"
+}
+
+coolify_container_has_role() {
+ wanted_role="$1"
+ roles="$(coolify_container_role_value | tr '[:upper:]' '[:lower:]' | tr ',' ' ')"
+
+ for role in $roles; do
+ case "$role" in
+ all|"$wanted_role")
+ return 0
+ ;;
+ esac
+ done
+
+ return 1
+}
diff --git a/package-lock.json b/package-lock.json
index 288474a88..04a133240 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -70,7 +70,6 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -1614,8 +1613,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.33",
@@ -1650,7 +1648,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -2296,7 +2293,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -2408,7 +2404,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2418,7 +2413,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -2525,8 +2519,7 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -2600,7 +2593,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
diff --git a/tests/Feature/ContainerRoleScriptTest.php b/tests/Feature/ContainerRoleScriptTest.php
new file mode 100644
index 000000000..5d55a3877
--- /dev/null
+++ b/tests/Feature/ContainerRoleScriptTest.php
@@ -0,0 +1,53 @@
+successful())->toBeTrue();
+})->with([
+ ['worker,flux', 'worker'],
+ ['worker,flux', 'flux'],
+ ['web, all', 'flux'],
+ ['flux,all,worker', 'worker'],
+ ['horizon,scheduler,nightwatch,flux', 'horizon'],
+ ['horizon,scheduler,nightwatch,flux', 'scheduler'],
+ ['horizon,scheduler,nightwatch,flux', 'nightwatch'],
+]);
+
+it('rejects services missing from the comma separated container roles', function () {
+ $result = runContainerRoleHelper('web,flux', 'worker');
+
+ expect($result->failed())->toBeTrue();
+});
+
+it('falls back to the container role from the local env file', function () {
+ $directory = sys_get_temp_dir().'/coolify-container-role-'.bin2hex(random_bytes(4));
+ mkdir($directory);
+ file_put_contents($directory.'/.env', 'COOLIFY_CONTAINER_ROLE=worker,flux'.PHP_EOL);
+
+ $result = runContainerRoleHelper('', 'flux', $directory);
+
+ expect($result->successful())->toBeTrue();
+});
+
+it('keeps production and development role helpers in sync', function () {
+ expect(file_get_contents(base_path('docker/production/etc/s6-overlay/scripts/container-role')))
+ ->toBe(file_get_contents(base_path('docker/development/etc/s6-overlay/scripts/container-role')));
+});
+
+function runContainerRoleHelper(string $roles, string $serviceRole, ?string $workingDirectory = null): ProcessResult
+{
+ $script = base_path('docker/development/etc/s6-overlay/scripts/container-role');
+ $command = sprintf(
+ '. %s && coolify_container_has_role %s',
+ escapeshellarg($script),
+ escapeshellarg($serviceRole),
+ );
+
+ return Process::path($workingDirectory ?? base_path())
+ ->env(['COOLIFY_CONTAINER_ROLE' => $roles])
+ ->run($command);
+}
From ed89d83d6838485ca87aab88d35b48125223c8cd Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 16 Jun 2026 10:57:43 +0200
Subject: [PATCH 004/174] chore(scripts): remove conductor setup script
---
scripts/conductor-setup.sh | 97 --------------------------------------
1 file changed, 97 deletions(-)
delete mode 100755 scripts/conductor-setup.sh
diff --git a/scripts/conductor-setup.sh b/scripts/conductor-setup.sh
deleted file mode 100755
index a88b457fb..000000000
--- a/scripts/conductor-setup.sh
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/bin/bash
-set -e
-
-# Validate CONDUCTOR_ROOT_PATH is set and valid before any operations
-if [ -z "$CONDUCTOR_ROOT_PATH" ]; then
- echo "ERROR: CONDUCTOR_ROOT_PATH environment variable is not set"
- echo "This script must be run by Conductor with CONDUCTOR_ROOT_PATH set to the main repository path"
- exit 1
-fi
-
-if [ ! -d "$CONDUCTOR_ROOT_PATH" ]; then
- echo "ERROR: CONDUCTOR_ROOT_PATH ($CONDUCTOR_ROOT_PATH) is not a valid directory"
- exit 1
-fi
-
-# Copy .env file
-cp "$CONDUCTOR_ROOT_PATH/.env" .env
-
-# Setup shared dependencies via symlinks to main repo
-echo "Setting up shared node_modules and vendor directories..."
-
-# Ensure main repo has the directories
-mkdir -p "$CONDUCTOR_ROOT_PATH/node_modules"
-mkdir -p "$CONDUCTOR_ROOT_PATH/vendor"
-
-# Get current worktree path
-WORKTREE_PATH=$(pwd)
-
-# Safety check 1: ensure WORKTREE_PATH is valid
-if [ -z "$WORKTREE_PATH" ]; then
- echo "ERROR: WORKTREE_PATH is empty"
- exit 1
-fi
-
-# Safety check 2: CRITICAL FIRST - blacklist system directories
-# This check runs BEFORE the positive check to prevent dangerous operations
-# even if someone misconfigures CONDUCTOR_ROOT_PATH
-case "$WORKTREE_PATH" in
- /|/bin|/sbin|/usr|/usr/*|/etc|/etc/*|/var|/var/*|/System|/System/*|/Library|/Library/*|/Applications|/Applications/*|"$HOME")
- echo "ERROR: WORKTREE_PATH ($WORKTREE_PATH) is in a dangerous system location"
- exit 1
- ;;
-esac
-
-# Safety check 3: positive check - verify we're under CONDUCTOR_ROOT_PATH
-case "$WORKTREE_PATH" in
- "$CONDUCTOR_ROOT_PATH"|"$CONDUCTOR_ROOT_PATH"/.conductor/*)
- # Valid: either main repo or under .conductor/
- ;;
- *)
- echo "ERROR: WORKTREE_PATH ($WORKTREE_PATH) is not under CONDUCTOR_ROOT_PATH ($CONDUCTOR_ROOT_PATH)"
- exit 1
- ;;
-esac
-
-# Safety check 4: verify we're in a git repository
-if [ ! -f ".git" ] && [ ! -d ".git" ]; then
- echo "ERROR: Not in a git repository"
- exit 1
-fi
-
-# Remove existing directories/symlinks if they exist
-# For symlinks: use 'rm' without -r to remove the symlink itself (not following it)
-# For directories: use 'rm -rf' to remove the directory and contents
-if [ -L "node_modules" ]; then
- # It's a symlink - remove it without following (no -r flag)
- rm "$WORKTREE_PATH/node_modules"
-elif [ -e "node_modules" ]; then
- # It's a regular directory or file - safe to use -rf
- rm -rf "$WORKTREE_PATH/node_modules"
-fi
-
-if [ -L "vendor" ]; then
- # It's a symlink - remove it without following (no -r flag)
- rm "$WORKTREE_PATH/vendor"
-elif [ -e "vendor" ]; then
- # It's a regular directory or file - safe to use -rf
- rm -rf "$WORKTREE_PATH/vendor"
-fi
-
-# Calculate relative path from worktree to main repo
-# Use bash-native approach: try realpath first (GNU coreutils), fallback to perl
-if command -v realpath &> /dev/null && realpath --relative-to / / &> /dev/null 2>&1; then
- # GNU coreutils realpath with --relative-to support
- RELATIVE_PATH=$(realpath --relative-to="$WORKTREE_PATH" "$CONDUCTOR_ROOT_PATH")
-else
- # Fallback: use perl which is standard on macOS and most Unix systems
- RELATIVE_PATH=$(perl -e 'use File::Spec; print File::Spec->abs2rel($ARGV[0], $ARGV[1])' "$CONDUCTOR_ROOT_PATH" "$WORKTREE_PATH")
-fi
-
-# Create symlinks to main repo's node_modules and vendor
-ln -sf "$RELATIVE_PATH/node_modules" node_modules
-ln -sf "$RELATIVE_PATH/vendor" vendor
-
-echo "✓ Shared dependencies linked successfully"
-echo " node_modules -> $RELATIVE_PATH/node_modules"
-echo " vendor -> $RELATIVE_PATH/vendor"
\ No newline at end of file
From 1b1b0726a21038140ef33a41e985033fbfc4aaaf Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 16 Jun 2026 11:39:36 +0200
Subject: [PATCH 005/174] feat(dev): add coold VM dev workflow
Add scripts and Lima config for managing local coold endpoint VMs,
mint Flux dev host JWTs, expose coold host details on the V5 home
page, and document the local development workflow.
---
.gitignore | 3 +
README.md | 19 +
app/Console/Commands/FluxDev.php | 75 +++
app/Http/Controllers/V5/HomeController.php | 26 +
config/coold.php | 11 +
config/flux.php | 2 +
dev/lima/coold.yaml | 125 ++++
docker-compose-maxio.dev.yml | 4 +-
docker-compose.dev.yml | 8 +-
resources/js/v5/Pages/Home.jsx | 20 +-
scripts/coold-vm.sh | 591 +++++++++++++++++
scripts/dev.sh | 680 ++++++++++++++++++++
tests/Feature/DevEnvironmentExampleTest.php | 31 +
tests/Feature/FluxDevCommandTest.php | 65 ++
tests/Feature/V5/HomeTest.php | 6 +
15 files changed, 1661 insertions(+), 5 deletions(-)
create mode 100644 app/Console/Commands/FluxDev.php
create mode 100644 config/coold.php
create mode 100644 dev/lima/coold.yaml
create mode 100755 scripts/coold-vm.sh
create mode 100755 scripts/dev.sh
create mode 100644 tests/Feature/DevEnvironmentExampleTest.php
create mode 100644 tests/Feature/FluxDevCommandTest.php
diff --git a/.gitignore b/.gitignore
index 403028761..b09e074f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,6 @@ CHANGELOG.md
/.workspaces
tests/Browser/Screenshots
tests/v4/Browser/Screenshots
+
+# Local generated Lima configs
+.dev/lima/*.generated.yaml
diff --git a/README.md b/README.md
index 378e75ac7..46204ea5e 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,25 @@ You can find the installation script source [here](./scripts/install.sh).
> Please refer to the [docs](https://coolify.io/docs/installation) for more information about the installation.
+## Local dev with coold VM
+
+For v5 development that needs a packaged `coold` endpoint VM with Corrosion, use:
+
+```bash
+scripts/dev.sh up
+```
+
+This starts two Lima endpoint VMs by default, configures a WireGuard mesh between them, starts the Docker stack in the background, mints dev host JWTs with `php artisan flux:dev --caps=coold,builder`, installs them into the VMs, starts each VM `coold` agent service with builder capacity, and follows Coolify + VM agent logs by default. Flux runs with the Coolify container, not inside the VM. Set `COOLIFY_COOLD_VM_COUNT=1` for a single endpoint, `COOLIFY_COOLD_VM_ENABLED=false` to skip the VMs, `COOLIFY_COOLD_VM_BUILDER_CAPACITY=0` to disable builder capability, or `COOLIFY_DEV_FOLLOW_LOGS=false` to leave `up` detached. Use `scripts/dev.sh down` to stop the stack and VM agent; set `COOLIFY_COOLD_VM_STOP_ON_DOWN=true` if you also want the VM stopped. The dev entrypoints are intentionally kept to `scripts/dev.sh` for the full stack and `scripts/coold-vm.sh` for VM-only operations.
+
+Useful Corrosion checks:
+
+```bash
+scripts/dev.sh corrosion check
+scripts/dev.sh corrosion containers
+scripts/dev.sh corrosion config
+scripts/dev.sh corrosion sql 'select count(*) from service_endpoints;'
+```
+
## Container roles and Flux
The Coolify image can run different process roles with `COOLIFY_CONTAINER_ROLE`. The value can be a single role or a comma-separated list. If `all` is present anywhere in the list, every role starts.
diff --git a/app/Console/Commands/FluxDev.php b/app/Console/Commands/FluxDev.php
new file mode 100644
index 000000000..2c6cfc84e
--- /dev/null
+++ b/app/Console/Commands/FluxDev.php
@@ -0,0 +1,75 @@
+environment(['local', 'development', 'testing']) && ! $this->option('force')) {
+ $this->error('This command is intended for development only. Use --force to override.');
+
+ return self::FAILURE;
+ }
+
+ $privateKeyPath = config('flux.jwt_private_key_path');
+
+ if (! is_string($privateKeyPath) || $privateKeyPath === '' || ! File::isReadable($privateKeyPath)) {
+ $this->error("Flux JWT private key not found at {$privateKeyPath}.");
+
+ return self::FAILURE;
+ }
+
+ $hostId = (string) $this->argument('host_id');
+ $ttl = max(60, (int) $this->option('ttl'));
+ $now = time();
+ $caps = collect(explode(',', (string) $this->option('caps')))
+ ->map(fn (string $cap) => trim($cap))
+ ->filter()
+ ->unique()
+ ->values()
+ ->all();
+
+ if ($caps === []) {
+ $caps = ['coold'];
+ }
+
+ $token = JWT::encode([
+ 'sub' => $hostId,
+ 'aud' => 'coold',
+ 'caps' => $caps,
+ 'iat' => $now,
+ 'exp' => $now + $ttl,
+ ], File::get($privateKeyPath), 'ES256');
+
+ $output = $this->option('output');
+
+ if (is_string($output) && $output !== '') {
+ $outputPath = Str::startsWith($output, '/') ? $output : base_path($output);
+ File::ensureDirectoryExists(dirname($outputPath));
+ File::put($outputPath, $token.PHP_EOL);
+ chmod($outputPath, 0600);
+ $this->info("Host JWT written to {$outputPath}.");
+
+ return self::SUCCESS;
+ }
+
+ $this->line($token);
+
+ return self::SUCCESS;
+ }
+}
diff --git a/app/Http/Controllers/V5/HomeController.php b/app/Http/Controllers/V5/HomeController.php
index b8f9fa6db..116952153 100644
--- a/app/Http/Controllers/V5/HomeController.php
+++ b/app/Http/Controllers/V5/HomeController.php
@@ -21,6 +21,7 @@ class HomeController extends Controller
return Inertia::render('Home', [
'status' => 'v5-ready',
'flux' => $fluxHealth->check(),
+ 'cooldHosts' => $this->cooldHosts(),
'currentTeam' => $currentTeam instanceof Team ? [
'id' => $currentTeam->id,
'name' => $currentTeam->name,
@@ -41,4 +42,29 @@ class HomeController extends Controller
]),
]);
}
+
+ /**
+ * @return array, builderEnabled: bool, builderCapacity: int}>
+ */
+ private function cooldHosts(): array
+ {
+ $baseId = (string) config('coold.dev_host_id');
+ $count = max(0, (int) config('coold.dev_host_count'));
+ $builderCapacity = (int) config('coold.dev_builder_capacity');
+ $builderEnabled = $builderCapacity > 0;
+
+ if ($count === 0 || $baseId === '') {
+ return [];
+ }
+
+ return collect(range(1, $count))
+ ->map(fn (int $index) => [
+ 'id' => $index === 1 ? $baseId : (string) config("coold.dev_host_id_{$index}", "{$baseId}-{$index}"),
+ 'wireguardIp' => (string) config("coold.dev_wireguard_ip_{$index}") ?: null,
+ 'capabilities' => $builderEnabled ? ['coold', 'builder'] : ['coold'],
+ 'builderEnabled' => $builderEnabled,
+ 'builderCapacity' => $builderCapacity,
+ ])
+ ->all();
+ }
}
diff --git a/config/coold.php b/config/coold.php
new file mode 100644
index 000000000..20dac4445
--- /dev/null
+++ b/config/coold.php
@@ -0,0 +1,11 @@
+ (int) env('COOLIFY_COOLD_VM_COUNT', 2),
+ 'dev_host_id' => env('COOLIFY_COOLD_DEV_HOST_ID', 'coolify-coold-dev'),
+ 'dev_host_id_2' => env('COOLIFY_COOLD_LIMA_INSTANCE_2', env('COOLIFY_COOLD_DEV_HOST_ID', 'coolify-coold-dev').'-2'),
+ 'dev_wireguard_ip_1' => env('COOLIFY_COOLD_VM_WG_IP_1', '100.64.0.10'),
+ 'dev_wireguard_ip_2' => env('COOLIFY_COOLD_VM_WG_IP_2', '100.64.0.11'),
+ 'dev_builder_capacity' => (int) env('COOLIFY_COOLD_VM_BUILDER_CAPACITY', 2),
+ 'dev_builder_enabled' => (int) env('COOLIFY_COOLD_VM_BUILDER_CAPACITY', 2) > 0,
+];
diff --git a/config/flux.php b/config/flux.php
index e82914de0..dea770e47 100644
--- a/config/flux.php
+++ b/config/flux.php
@@ -2,5 +2,7 @@
return [
'unix_socket_path' => env('COOLIFY_FLUX_UNIX_SOCKET_PATH', '/run/coolify/flux.sock'),
+ 'jwt_private_key_path' => env('COOLIFY_FLUX_JWT_PRIVATE_KEY_PATH', storage_path('app/flux/jwt.priv')),
+ 'jwt_public_key_path' => env('COOLIFY_FLUX_JWT_PUBLIC_KEY_PATH', storage_path('app/flux/jwt.pub')),
'health_timeout_seconds' => (float) env('COOLIFY_FLUX_HEALTH_TIMEOUT_SECONDS', 1.0),
];
diff --git a/dev/lima/coold.yaml b/dev/lima/coold.yaml
new file mode 100644
index 000000000..d0354b215
--- /dev/null
+++ b/dev/lima/coold.yaml
@@ -0,0 +1,125 @@
+# Lima VM for testing Coolify v5 against a packaged coold endpoint.
+# Start with: scripts/coold-vm.sh up
+# Run endpoint: scripts/coold-vm.sh dev
+#
+# This file is copied into .dev/lima/coold.generated.yaml by the wrapper script,
+# with {{COOLIFY_REPO}}, {{COOLIFY_COOLD_VERSION}}, and {{COOLIFY_CORROSION_VERSION}} replaced before Lima sees it.
+
+vmType: "vz"
+arch: "default"
+cpus: 4
+memory: "8GiB"
+disk: "100GiB"
+containerd:
+ system: false
+ user: false
+
+images:
+ - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
+ arch: "x86_64"
+ - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
+ arch: "aarch64"
+
+mountType: "virtiofs"
+mounts:
+ - location: "{{COOLIFY_REPO}}"
+ mountPoint: "/workspace/coolify"
+ writable: true
+
+portForwards:
+ - guestPort: 5173
+ hostPort: 5173
+ hostIP: "127.0.0.1"
+ - guestPort: 3000
+ hostPort: 3000
+ hostIP: "127.0.0.1"
+provision:
+ - mode: system
+ script: |
+ #!/usr/bin/env bash
+ set -euxo pipefail
+ export DEBIAN_FRONTEND=noninteractive
+ echo "[coold-vm] Updating Ubuntu package index..."
+ apt-get update
+ echo "[coold-vm] Installing VM dependencies..."
+ apt-get install -y --no-install-recommends \
+ bash \
+ buildah \
+ ca-certificates \
+ clang \
+ curl \
+ git \
+ iproute2 \
+ iptables \
+ jq \
+ libssl-dev \
+ mold \
+ nftables \
+ openssl \
+ perl \
+ pkg-config \
+ podman \
+ protobuf-compiler \
+ sqlite3 \
+ sudo \
+ wireguard-tools
+ echo "[coold-vm] Enabling Podman socket..."
+ systemctl enable --now podman.socket
+ cat >/usr/local/bin/rtk <<'SH'
+ #!/usr/bin/env bash
+ exec "$@"
+ SH
+ chmod +x /usr/local/bin/rtk
+ mkdir -p /etc/coolify /var/lib/corrosion /run/corrosion /var/lib/coolify-dev /var/lib/coolify-builder/work
+ chmod 755 /etc/coolify
+ chmod 777 /var/lib/corrosion /run/corrosion
+
+ install_coolify_binary() {
+ name="$1"
+ version="{{COOLIFY_COOLD_VERSION}}"
+ arch_raw="$(uname -m)"
+ case "$arch_raw" in
+ x86_64) arch=amd64 ;;
+ aarch64) arch=arm64 ;;
+ *) echo "unsupported arch: $arch_raw" >&2; exit 1 ;;
+ esac
+
+ url="https://github.com/coollabsio/coold/releases/download/${version}/${name}-linux-${arch}.tar.gz"
+ echo "[coold-vm] Installing ${name} from ${url}..."
+ tmpdir="$(mktemp -d)"
+ trap 'rm -rf "$tmpdir"' RETURN
+ curl -fsSL --retry 3 --max-time 120 -o "$tmpdir/${name}.tar.gz" "$url"
+ tar -xzf "$tmpdir/${name}.tar.gz" -C "$tmpdir"
+ test -f "$tmpdir/$name"
+ install -m 0755 "$tmpdir/$name" "/usr/local/bin/${name}.tmp"
+ mv "/usr/local/bin/${name}.tmp" "/usr/local/bin/${name}"
+ echo "$version" > "/usr/local/bin/${name}.version"
+ }
+
+ install_corrosion_binary() {
+ version="{{COOLIFY_CORROSION_VERSION}}"
+ arch_raw="$(uname -m)"
+ case "$arch_raw" in
+ x86_64) arch=x86_64-unknown-linux-gnu ;;
+ aarch64) arch=aarch64-unknown-linux-gnu ;;
+ *) echo "unsupported arch: $arch_raw" >&2; exit 1 ;;
+ esac
+
+ url="https://github.com/superfly/corrosion/releases/download/${version}/corrosion-${arch}.tar.gz"
+ echo "[coold-vm] Installing corrosion from ${url}..."
+ tmpdir="$(mktemp -d)"
+ trap 'rm -rf "$tmpdir"' RETURN
+ curl -fsSL --retry 3 --max-time 180 -o "$tmpdir/corrosion.tar.gz" "$url"
+ tar -xzf "$tmpdir/corrosion.tar.gz" -C "$tmpdir"
+ test -f "$tmpdir/corrosion"
+ install -m 0755 "$tmpdir/corrosion" /usr/local/bin/corrosion.tmp
+ mv /usr/local/bin/corrosion.tmp /usr/local/bin/corrosion
+ echo "$version" > /usr/local/bin/corrosion.version
+ }
+
+ install_coolify_binary coold
+ install_coolify_binary builder
+ install_corrosion_binary
+ echo "[coold-vm] Endpoint binaries installed."
+
+ echo "[coold-vm] Provisioning complete."
diff --git a/docker-compose-maxio.dev.yml b/docker-compose-maxio.dev.yml
index bbb483d7a..164b1b338 100644
--- a/docker-compose-maxio.dev.yml
+++ b/docker-compose-maxio.dev.yml
@@ -12,8 +12,8 @@ services:
- "${APP_PORT:-8000}:8080"
environment:
AUTORUN_ENABLED: false
- PUSHER_HOST: "${PUSHER_HOST}"
- PUSHER_PORT: "${PUSHER_PORT}"
+ PUSHER_HOST: "${PUSHER_HOST:-}"
+ PUSHER_PORT: "${PUSHER_PORT:-}"
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
PUSHER_APP_ID: "${PUSHER_APP_ID:-coolify}"
PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index bad72a22d..ea095f17f 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -8,14 +8,18 @@ services:
args:
- USER_ID=${USERID:-1000}
- GROUP_ID=${GROUPID:-1000}
+ - COOLIFY_FLUX_VERSION=${COOLIFY_FLUX_VERSION:-nightly}
ports:
- "${APP_PORT:-8000}:8080"
- "${FORWARD_FLUX_PORT:-6443}:6443"
environment:
AUTORUN_ENABLED: false
COOLIFY_CONTAINER_ROLE: "${COOLIFY_CONTAINER_ROLE:-all}"
- PUSHER_HOST: "${PUSHER_HOST}"
- PUSHER_PORT: "${PUSHER_PORT}"
+ COOLIFY_COOLD_VERSION: "${COOLIFY_COOLD_VERSION:-nightly}"
+ COOLIFY_FLUX_VERSION: "${COOLIFY_FLUX_VERSION:-nightly}"
+ COOLIFY_CORROSION_VERSION: "${COOLIFY_CORROSION_VERSION:-v1.0.0}"
+ PUSHER_HOST: "${PUSHER_HOST:-}"
+ PUSHER_PORT: "${PUSHER_PORT:-}"
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
PUSHER_APP_ID: "${PUSHER_APP_ID:-coolify}"
PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
diff --git a/resources/js/v5/Pages/Home.jsx b/resources/js/v5/Pages/Home.jsx
index 0fa9a231b..4ed49d968 100644
--- a/resources/js/v5/Pages/Home.jsx
+++ b/resources/js/v5/Pages/Home.jsx
@@ -1,6 +1,6 @@
import { Head } from '@inertiajs/react';
-export default function Home({ status, currentTeam, teams, flux }) {
+export default function Home({ status, currentTeam, teams, flux, cooldHosts }) {
return (
<>
@@ -27,6 +27,24 @@ export default function Home({ status, currentTeam, teams, flux }) {
{flux.socket ? Socket: {flux.socket}
: null}
+
+ coold host
+
+
+ {cooldHosts.map((host) => (
+ -
+ {host.id}
+ {host.wireguardIp ? ` (${host.wireguardIp})` : ''}:
+ {' '}
+ {host.capabilities.join(', ')}; builder{' '}
+ {host.builderEnabled
+ ? `enabled, capacity ${host.builderCapacity}`
+ : 'disabled'}
+
+ ))}
+
+
+
Current team
{currentTeam ? (
diff --git a/scripts/coold-vm.sh b/scripts/coold-vm.sh
new file mode 100755
index 000000000..627717181
--- /dev/null
+++ b/scripts/coold-vm.sh
@@ -0,0 +1,591 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+read_coolify_env() {
+ key="$1"
+ default_value="$2"
+ current_value="${!key:-}"
+
+ if [ -n "$current_value" ]; then
+ printf '%s\n' "$current_value"
+ return
+ fi
+
+ if [ -f "$ROOT/.env" ]; then
+ env_value="$(grep -E "^${key}=" "$ROOT/.env" 2>/dev/null | tail -n1 | cut -d= -f2- | sed "s/^['\"]//; s/['\"]$//")"
+ if [ -n "$env_value" ]; then
+ printf '%s\n' "$env_value"
+ return
+ fi
+ fi
+
+ printf '%s\n' "$default_value"
+}
+
+INSTANCE="$(read_coolify_env COOLIFY_COOLD_LIMA_INSTANCE coold-dev)"
+VERSION="$(read_coolify_env COOLIFY_COOLD_VERSION nightly)"
+CORROSION_VERSION="$(read_coolify_env COOLIFY_CORROSION_VERSION v1.0.0)"
+FLUX_URL="$(read_coolify_env COOLIFY_COOLD_VM_FLUX_URL http://host.lima.internal:6443)"
+BUILDER_CAPACITY="$(read_coolify_env COOLIFY_COOLD_VM_BUILDER_CAPACITY 2)"
+WG_IP="$(read_coolify_env COOLIFY_COOLD_VM_WG_IP "")"
+WG_PEER_IP="$(read_coolify_env COOLIFY_COOLD_VM_WG_PEER_IP "")"
+WG_PEER_ENDPOINT="$(read_coolify_env COOLIFY_COOLD_VM_WG_PEER_ENDPOINT "")"
+WG_PEER_PUBLIC_KEY="$(read_coolify_env COOLIFY_COOLD_VM_WG_PEER_PUBLIC_KEY "")"
+CONTAINER_SUBNET="$(read_coolify_env COOLIFY_COOLD_VM_CONTAINER_SUBNET 10.210.0.0/24)"
+CONTAINER_GATEWAY="$(read_coolify_env COOLIFY_COOLD_VM_CONTAINER_GATEWAY 10.210.0.1)"
+BUILDER_ENABLED="true"
+if [ "$BUILDER_CAPACITY" = "0" ]; then
+ BUILDER_ENABLED="false"
+fi
+TEMPLATE="$ROOT/dev/lima/coold.yaml"
+GENERATED="$ROOT/.dev/lima/coold.generated.yaml"
+GUEST_COOLIFY_ROOT="/workspace/coolify"
+
+usage() {
+ cat <
+
+Commands:
+ up Create/start the Lima VM and install packaged coold endpoint binaries
+ dev Start packaged coold + Corrosion inside the VM
+ start-agent
+ Start production-like coold.service + corrosion.service inside the VM
+ stop-agent
+ Stop the VM coold.service + corrosion.service
+ logs-agent
+ Follow the VM coold.service + corrosion.service logs
+ install-host-jwt [token]
+ Install a Flux host JWT into the VM at /etc/coolify/host-jwt
+ shell Open a shell inside the VM
+ status Show Lima instance status
+ stop Stop the VM
+ delete Delete the VM and all VM-local runtime state
+
+Environment:
+ COOLIFY_COOLD_LIMA_INSTANCE Override Lima instance name (default: coold-dev)
+ COOLIFY_COOLD_VERSION coold release tag to install (default: nightly)
+ COOLIFY_CORROSION_VERSION corrosion release tag to install (default: v1.0.0)
+ COOLIFY_COOLD_VM_FLUX_URL Flux gRPC URL visible from the VM (default: http://host.lima.internal:6443)
+ COOLIFY_COOLD_VM_BUILDER_CAPACITY VM builder capacity to advertise (default: 2; set 0 to disable)
+ COOLIFY_COOLD_VM_WG_IP Optional WireGuard mgmt IP for this host
+ COOLIFY_COOLD_VM_CONTAINER_SUBNET Podman mesh subnet for this host
+ COOLIFY_COOLD_VM_CONTAINER_GATEWAY Podman mesh gateway for this host
+
+Guest mounts:
+ $ROOT -> $GUEST_COOLIFY_ROOT
+
+Installed from:
+ https://github.com/coollabsio/coold/releases/tag/$VERSION
+ https://github.com/superfly/corrosion/releases/tag/$CORROSION_VERSION
+USAGE
+}
+
+require_lima() {
+ command -v limactl >/dev/null 2>&1 || {
+ echo "limactl is required. Install Lima first: brew install lima" >&2
+ exit 1
+ }
+}
+
+instance_exists() {
+ limactl list 2>/dev/null | awk 'NR > 1 {print $1}' | grep -qx "$INSTANCE"
+}
+
+instance_running() {
+ limactl list 2>/dev/null | awk -v name="$INSTANCE" 'NR > 1 && $1 == name {print $2}' | grep -qx Running
+}
+
+lima_shell() {
+ (cd /tmp && limactl shell "$INSTANCE" -- "$@")
+}
+
+vm_primary_ip() {
+ lima_shell sh -lc "ip -4 route get 1.1.1.1 | awk '{print \$7; exit}'"
+}
+
+wireguard_public_key() {
+ lima_shell sudo sh -lc 'install -d -m 0700 /etc/wireguard; if [ ! -s /etc/wireguard/privatekey ]; then wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey; chmod 600 /etc/wireguard/privatekey; fi; cat /etc/wireguard/publickey'
+}
+
+setup_wireguard() {
+ local ip="${1:-$WG_IP}"
+ local peer_ip="${2:-$WG_PEER_IP}"
+ local peer_endpoint="${3:-$WG_PEER_ENDPOINT}"
+ local peer_public_key="${4:-$WG_PEER_PUBLIC_KEY}"
+ local listen_port="${5:-51820}"
+ local peer_port="${6:-51820}"
+ local peer_subnet="${7:-}"
+
+ if [ -z "$ip" ]; then
+ echo "ERROR: WireGuard IP is required." >&2
+ exit 1
+ fi
+
+ wireguard_public_key >/dev/null
+
+ if [ -n "$peer_ip" ] && [ -n "$peer_endpoint" ] && [ -n "$peer_public_key" ]; then
+ lima_shell sudo sh -lc "cat >/etc/wireguard/wg0.conf.tmp </dev/null 2>&1 || true; wg-quick up wg0"
+ else
+ lima_shell sudo sh -lc "cat >/etc/wireguard/wg0.conf.tmp </dev/null 2>&1 || true; wg-quick up wg0"
+ fi
+}
+
+install_host_jwt() {
+ token="${1:-}"
+
+ if [ -z "$token" ]; then
+ token="$(cat)"
+ fi
+
+ if [ -z "$token" ]; then
+ echo "ERROR: host JWT is empty." >&2
+ exit 1
+ fi
+
+ printf '%s\n' "$token" | lima_shell sudo sh -c 'install -d -m 0755 /etc/coolify && cat > /tmp/coolify-host-jwt && install -m 0600 /tmp/coolify-host-jwt /etc/coolify/host-jwt && rm -f /tmp/coolify-host-jwt'
+}
+
+
+stop_agent_processes() {
+ lima_shell sudo systemctl stop coold.service corrosion.service coold-dev-agent.service >/dev/null 2>&1 || true
+ lima_shell sudo pkill -x coold >/dev/null 2>&1 || true
+ lima_shell sudo pkill -x corrosion >/dev/null 2>&1 || true
+}
+
+ensure_podman_networks() {
+ local current_subnet
+ current_subnet="$(lima_shell sudo podman network inspect coolify-default-mesh --format '{{range .Subnets}}{{.Subnet}}{{end}}' 2>/dev/null || true)"
+
+ if [ -n "$current_subnet" ] && [ "$current_subnet" != "$CONTAINER_SUBNET" ]; then
+ lima_shell sudo podman network rm coolify-default-mesh >/dev/null
+ current_subnet=""
+ fi
+
+ if [ -z "$current_subnet" ]; then
+ lima_shell sudo podman network create --subnet "$CONTAINER_SUBNET" --gateway "$CONTAINER_GATEWAY" coolify-default-mesh >/dev/null
+ fi
+}
+
+write_runtime_config() {
+ local gossip_addr="127.0.0.1:8787"
+ local bootstrap=""
+
+ if [ -n "$WG_IP" ]; then
+ gossip_addr="$WG_IP:8787"
+ fi
+
+ if [ -n "$WG_PEER_IP" ]; then
+ bootstrap="\"$WG_PEER_IP:8787\""
+ fi
+
+ lima_shell sudo install -d -m 0755 /etc/corrosion/schemas /etc/coolify /run/coolify /var/lib/corrosion /var/run/corrosion /var/lib/coolify-dev /var/lib/coolify-builder/work
+
+ lima_shell sudo tee /etc/corrosion/schemas/coolify.sql >/dev/null <<'SQL'
+CREATE TABLE service_endpoints (
+ container_id TEXT NOT NULL DEFAULT '' PRIMARY KEY,
+ container_name TEXT NOT NULL DEFAULT '',
+ namespace TEXT NOT NULL DEFAULT '',
+ host_mgmt_ip TEXT NOT NULL DEFAULT '',
+ container_ip TEXT NOT NULL DEFAULT '',
+ state TEXT NOT NULL DEFAULT '',
+ health TEXT NOT NULL DEFAULT 'unknown',
+ updated_at INTEGER NOT NULL DEFAULT 0
+);
+SQL
+
+ lima_shell sudo tee /etc/corrosion/config.toml >/dev/null </dev/null || cat /usr/local/bin/corrosion.version)"
+echo "starting packaged coold endpoint with corrosion in local mode"
+
+cleanup() {
+ jobs -pr | xargs -r kill || true
+}
+trap cleanup EXIT INT TERM
+
+/usr/local/bin/corrosion agent --config /etc/corrosion/config.toml &
+
+COOLIFY_COOLD_HOST_MGMT_IP="${COOLIFY_COOLD_HOST_MGMT_IP:-127.0.0.1}" \
+COOLIFY_COOLD_PODMAN_SOCKET="${COOLIFY_COOLD_PODMAN_SOCKET:-/run/podman/podman.sock}" \
+COOLIFY_COOLD_CORROSION_URL="${COOLIFY_COOLD_CORROSION_URL:-http://127.0.0.1:8080}" \
+COOLIFY_COOLD_NAMESPACES="${COOLIFY_COOLD_NAMESPACES:-default:coolify-default-mesh:$CONTAINER_GATEWAY}" \
+COOLIFY_COOLD_DNS_ZONE="${COOLIFY_COOLD_DNS_ZONE:-coolify.internal}" \
+COOLIFY_COOLD_API_BIND="${COOLIFY_COOLD_API_BIND:-${WG_IP:-127.0.0.1}:8443}" \
+COOLIFY_COOLD_API_TOKEN_FILE="${COOLIFY_COOLD_API_TOKEN_FILE:-/etc/coolify/api-token}" \
+COOLIFY_COOLD_FLUX_URL="${COOLIFY_COOLD_FLUX_URL:-http://host.lima.internal:6443}" \
+COOLIFY_COOLD_HOST_JWT_PATH="${COOLIFY_COOLD_HOST_JWT_PATH:-/etc/coolify/host-jwt}" \
+COOLIFY_COOLD_BUILDER_ENABLED="${COOLIFY_COOLD_BUILDER_ENABLED:-true}" \
+COOLIFY_COOLD_BUILDER_CAPACITY="${COOLIFY_COOLD_BUILDER_CAPACITY:-2}" \
+COOLIFY_COOLD_BUILDER_BIN="${COOLIFY_COOLD_BUILDER_BIN:-/usr/local/bin/builder}" \
+COOLIFY_COOLD_BUILDER_WORK_DIR="${COOLIFY_COOLD_BUILDER_WORK_DIR:-/var/lib/coolify-builder/work}" \
+/usr/local/bin/coold &
+
+wait
+RUNNER
+}
+
+install_mesh_firewall() {
+ lima_shell sudo install -d -m 0755 /etc/coolify
+ lima_shell sudo touch /etc/coolify/allow.rules /etc/coolify/allow.nft
+
+ lima_shell sudo tee /etc/coolify/bridge-fw.nft >/dev/null </dev/null </dev/null || /usr/sbin/iptables -t nat -I POSTROUTING -s $CONTAINER_SUBNET -o wg0 -j RETURN"
+ExecStart=/bin/sh -c "/usr/sbin/iptables -D FORWARD -s $CONTAINER_SUBNET -j ACCEPT 2>/dev/null || true"
+ExecStart=/bin/sh -c "/usr/sbin/iptables -D FORWARD -d $CONTAINER_SUBNET -j ACCEPT 2>/dev/null || true"
+ExecStart=/bin/sh -c "/usr/sbin/iptables -N COOLIFY-ALLOW 2>/dev/null || true"
+ExecStart=/bin/sh -c "/usr/sbin/iptables -N COOLIFY-INTRA 2>/dev/null || true"
+ExecStart=/usr/sbin/iptables -F COOLIFY-ALLOW
+ExecStart=/usr/sbin/iptables -F COOLIFY-INTRA
+ExecStart=/usr/sbin/iptables -A COOLIFY-INTRA -j COOLIFY-ALLOW
+ExecStart=/usr/sbin/iptables -A COOLIFY-INTRA -j DROP
+ExecStart=/bin/sh -c "/usr/sbin/iptables -C FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || /usr/sbin/iptables -I FORWARD 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT"
+ExecStart=/bin/sh -c "/usr/sbin/iptables -C FORWARD -d $CONTAINER_SUBNET -j COOLIFY-INTRA 2>/dev/null || /usr/sbin/iptables -A FORWARD -d $CONTAINER_SUBNET -j COOLIFY-INTRA"
+ExecStart=/bin/sh -c "/usr/sbin/iptables -C FORWARD -s $CONTAINER_SUBNET -j COOLIFY-INTRA 2>/dev/null || /usr/sbin/iptables -A FORWARD -s $CONTAINER_SUBNET -j COOLIFY-INTRA"
+ExecStart=/bin/sh -c "nft delete table bridge coolify_bridge 2>/dev/null || true"
+ExecStart=/bin/sh -c "nft -f /etc/coolify/bridge-fw.nft"
+ExecStart=/bin/sh -c "[ -s /etc/coolify/allow.nft ] && nft -f /etc/coolify/allow.nft || true"
+
+[Install]
+WantedBy=multi-user.target
+UNIT
+
+ lima_shell sudo systemctl daemon-reload
+ lima_shell sudo systemctl enable --now coolify-mesh-fw.service
+ lima_shell sudo systemctl restart coolify-mesh-fw.service
+}
+
+start_agent() {
+ stop_agent_processes
+ write_runtime_config
+ ensure_podman_networks
+ install_mesh_firewall
+ lima_shell sudo sh -c 'if [ ! -s /etc/coolify/api-token ]; then openssl rand -hex 32 > /etc/coolify/api-token.tmp && chmod 600 /etc/coolify/api-token.tmp && mv /etc/coolify/api-token.tmp /etc/coolify/api-token; fi'
+
+ lima_shell sudo tee /etc/systemd/system/corrosion.service >/dev/null <<'UNIT'
+[Unit]
+Description=Corrosion local state store
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+ExecStart=/usr/local/bin/corrosion agent --config /etc/corrosion/config.toml
+Restart=on-failure
+RestartSec=2s
+
+[Install]
+WantedBy=multi-user.target
+UNIT
+
+ lima_shell sudo tee /etc/systemd/system/coold.service >/dev/null < "$GENERATED"
+}
+
+start_vm() {
+ generate_yaml
+
+ if instance_running; then
+ return
+ fi
+
+ if instance_exists; then
+ limactl start --tty=false "$INSTANCE"
+ else
+ limactl start --tty=false --name="$INSTANCE" "$GENERATED"
+ fi
+}
+
+latest_lima_message() {
+ log_file="$HOME/.lima/$INSTANCE/ha.stderr.log"
+
+ if [ ! -f "$log_file" ]; then
+ echo "creating Lima instance directory"
+ return 0
+ fi
+
+ grep -E '"msg":"(Starting VZ|Waiting for|The essential requirement|Executing /mnt/lima|SSH Local Port|Port is available|Attempting|Downloaded|Using the existing instance|The instance)' "$log_file" | tail -n 1 | sed -E 's/^.*"msg":"(.*)","time":.*$/\1/' | sed 's/\\"/"/g' || true
+}
+
+wait_for_lima_start() {
+ start_vm &
+ start_pid=$!
+ elapsed=0
+
+ while kill -0 "$start_pid" 2>/dev/null; do
+ if instance_exists && lima_shell true >/dev/null 2>&1; then
+ status="$(lima_shell cloud-init status 2>/dev/null || true)"
+ printf '==> [%3ss] Lima start: guest SSH ready, cloud-init %s
+' "$elapsed" "${status:-unknown}"
+ lima_shell sudo sh -c 'test -f /var/log/cloud-init-output.log && tail -n 12 /var/log/cloud-init-output.log || true' 2>/dev/null | awk '{ print "[guest] " $0; fflush(); }' || true
+
+ if ! printf '%s' "$status" | grep -q running; then
+ kill "$start_pid" >/dev/null 2>&1 || true
+ break
+ fi
+ else
+ message="$(latest_lima_message)"
+ printf '==> [%3ss] Lima start: %s
+' "$elapsed" "${message:-booting}"
+ fi
+
+ sleep 5
+ elapsed=$((elapsed + 5))
+ done
+
+ wait "$start_pid" 2>/dev/null || true
+}
+
+wait_for_guest_provisioning() {
+ echo "==> Waiting for guest SSH..."
+ until instance_exists && lima_shell true >/dev/null 2>&1; do
+ message="$(latest_lima_message)"
+ printf '==> Waiting for guest SSH: %s
+' "${message:-booting}"
+ sleep 5
+ done
+
+ status="$(lima_shell cloud-init status 2>/dev/null || true)"
+
+ if printf '%s' "$status" | grep -q running; then
+ echo "==> Guest SSH is ready; streaming cloud-init output until provisioning completes..."
+ (
+ lima_shell sudo sh -c 'touch /var/log/cloud-init-output.log; tail -n 40 -F /var/log/cloud-init-output.log' 2>/dev/null | awk '{ print "[guest] " $0; fflush(); }'
+ ) &
+ tail_pid=$!
+
+ while true; do
+ status="$(lima_shell cloud-init status 2>/dev/null || true)"
+ printf '==> Guest cloud-init: %s
+' "${status:-unknown}"
+
+ if ! printf '%s' "$status" | grep -q running; then
+ break
+ fi
+
+ sleep 5
+ done
+
+ kill "$tail_pid" >/dev/null 2>&1 || true
+ wait "$tail_pid" 2>/dev/null || true
+ else
+ printf '==> Guest cloud-init: %s
+' "${status:-unknown}"
+ fi
+
+ echo "==> Final guest provisioning status:"
+ lima_shell bash -lc 'cloud-init status 2>/dev/null || true; if command -v coold >/dev/null; then coold --version; else echo "coold not installed yet"; fi; if command -v corrosion >/dev/null; then echo "corrosion installed"; else echo "corrosion not installed yet"; fi; if command -v builder >/dev/null; then echo "builder installed"; else echo "builder not installed yet"; fi; true' | awk '{ print "[guest] " $0; fflush(); }' || true
+
+ if ! lima_shell bash -lc 'command -v coold >/dev/null && command -v corrosion >/dev/null && command -v builder >/dev/null' >/dev/null 2>&1; then
+ echo "ERROR: VM provisioning finished but coold, corrosion, or builder is missing." >&2
+ echo "Check with: scripts/coold-vm.sh shell" >&2
+ exit 1
+ fi
+}
+
+up_with_logs() {
+ echo "==> Coolify coold VM: $INSTANCE"
+ echo "==> coold package tag: $VERSION"
+ echo "==> corrosion package tag: $CORROSION_VERSION"
+ echo "==> Lima config: $GENERATED"
+
+ wait_for_lima_start
+ wait_for_guest_provisioning
+
+ echo "==> VM is ready. Run: scripts/coold-vm.sh dev"
+}
+
+cmd="${1:-}"
+case "$cmd" in
+ up)
+ require_lima
+ up_with_logs
+ ;;
+ dev)
+ require_lima
+ start_vm >/dev/null
+ run_foreground
+ ;;
+ start-agent)
+ require_lima
+ start_vm >/dev/null
+ start_agent
+ ;;
+ stop-agent)
+ require_lima
+ if instance_running; then
+ stop_agent_processes
+ fi
+ ;;
+ logs-agent)
+ require_lima
+ start_vm >/dev/null
+ exec bash -lc "cd /tmp && limactl shell '$INSTANCE' -- sudo journalctl -u coold.service -u corrosion.service -f -n 100"
+ ;;
+ install-host-jwt)
+ require_lima
+ start_vm >/dev/null
+ install_host_jwt "${2:-}"
+ ;;
+ wg-public-key)
+ require_lima
+ start_vm >/dev/null
+ wireguard_public_key
+ ;;
+ vm-ip)
+ require_lima
+ start_vm >/dev/null
+ vm_primary_ip
+ ;;
+ setup-wireguard)
+ require_lima
+ start_vm >/dev/null
+ setup_wireguard "${2:-}" "${3:-}" "${4:-}" "${5:-}" "${6:-}" "${7:-}" "${8:-}"
+ ;;
+ shell)
+ require_lima
+ start_vm >/dev/null
+ exec bash -lc "cd /tmp && limactl shell '$INSTANCE' -- env TERM=xterm-256color SYSTEMD_PAGER=cat SYSTEMD_LESS=FRXMK bash -l"
+ ;;
+ status)
+ require_lima
+ if instance_exists; then
+ exec limactl list "$INSTANCE"
+ fi
+
+ echo "No Lima instance named '$INSTANCE' exists yet."
+ echo "Run: scripts/coold-vm.sh up"
+ ;;
+ stop)
+ require_lima
+ exec limactl stop "$INSTANCE"
+ ;;
+ delete|destroy)
+ require_lima
+ exec limactl delete --force --tty=false "$INSTANCE"
+ ;;
+ -h|--help|help|"")
+ usage
+ ;;
+ *)
+ echo "unknown command: $cmd" >&2
+ usage >&2
+ exit 1
+ ;;
+esac
diff --git a/scripts/dev.sh b/scripts/dev.sh
new file mode 100755
index 000000000..f361a0fac
--- /dev/null
+++ b/scripts/dev.sh
@@ -0,0 +1,680 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT"
+
+read_coolify_env() {
+ key="$1"
+ default_value="$2"
+ current_value="${!key:-}"
+
+ if [ -n "$current_value" ]; then
+ printf '%s\n' "$current_value"
+ return
+ fi
+
+ if [ -f .env ]; then
+ env_value="$(grep -E "^${key}=" .env 2>/dev/null | tail -n1 | cut -d= -f2- | sed "s/^['\"]//; s/['\"]$//")"
+ if [ -n "$env_value" ]; then
+ printf '%s\n' "$env_value"
+ return
+ fi
+ fi
+
+ printf '%s\n' "$default_value"
+}
+
+coold_vm_count() {
+ local count
+ count="$(read_coolify_env COOLIFY_COOLD_VM_COUNT 2)"
+
+ if [ "$count" != "1" ] && [ "$count" != "2" ]; then
+ echo "ERROR: COOLIFY_COOLD_VM_COUNT supports 1 or 2 for now." >&2
+ exit 1
+ fi
+
+ printf '%s\n' "$count"
+}
+
+coold_vm_instance() {
+ local index="$1"
+ local base
+ base="$(read_coolify_env COOLIFY_COOLD_LIMA_INSTANCE coold-dev)"
+
+ if [ "$index" = "1" ]; then
+ printf '%s\n' "$base"
+ return
+ fi
+
+ read_coolify_env "COOLIFY_COOLD_LIMA_INSTANCE_${index}" "${base}-${index}"
+}
+
+coold_vm_wg_ip() {
+ local index="$1"
+ read_coolify_env "COOLIFY_COOLD_VM_WG_IP_${index}" "100.64.0.$((9 + index))"
+}
+
+coold_vm_wg_port() {
+ local index="$1"
+ read_coolify_env "COOLIFY_COOLD_VM_WG_PORT_${index}" "$((51820 + index))"
+}
+
+coold_vm_container_subnet() {
+ local index="$1"
+ read_coolify_env "COOLIFY_COOLD_VM_CONTAINER_SUBNET_${index}" "10.210.$((index - 1)).0/24"
+}
+
+coold_vm_container_gateway() {
+ local index="$1"
+ read_coolify_env "COOLIFY_COOLD_VM_CONTAINER_GATEWAY_${index}" "10.210.$((index - 1)).1"
+}
+
+coold_vm() {
+ local index="$1"
+ shift
+ COOLIFY_COOLD_LIMA_INSTANCE="$(coold_vm_instance "$index")" \
+ COOLIFY_COOLD_VM_WG_IP="$(coold_vm_wg_ip "$index")" \
+ COOLIFY_COOLD_VM_CONTAINER_SUBNET="$(coold_vm_container_subnet "$index")" \
+ COOLIFY_COOLD_VM_CONTAINER_GATEWAY="$(coold_vm_container_gateway "$index")" \
+ scripts/coold-vm.sh "$@"
+}
+
+mint_host_jwt_for_host() {
+ local host_id="$1"
+ local attempts=60
+ local output
+ local caps
+ local builder_capacity
+
+ builder_capacity="$(read_coolify_env COOLIFY_COOLD_VM_BUILDER_CAPACITY 2)"
+ caps="coold"
+ if [ "$builder_capacity" != "0" ]; then
+ caps="coold,builder"
+ fi
+
+ for attempt in $(seq 1 "$attempts"); do
+ if output="$(spin exec -T coolify php artisan flux:dev "$host_id" --caps="$caps" 2>&1)"; then
+ printf '%s\n' "$output" | tail -n 1
+ return 0
+ fi
+
+ echo "==> Waiting for Flux dev JWT key for ${host_id} (${attempt}/${attempts})..." >&2
+ printf '%s\n' "$output" | tail -n 3 | sed 's/^/[coolify] /' >&2
+ sleep 2
+ done
+
+ echo "ERROR: Could not mint Flux dev host JWT for ${host_id}." >&2
+ return 1
+}
+
+setup_wireguard_mesh() {
+ local count="$1"
+
+ if [ "$count" -lt 2 ]; then
+ return
+ fi
+
+ echo "==> Configuring WireGuard mesh for ${count} coold VMs..."
+
+ local pub1 pub2 ip1 ip2 port1 port2 subnet1 subnet2
+ pub1="$(coold_vm 1 wg-public-key)"
+ pub2="$(coold_vm 2 wg-public-key)"
+ ip1="$(coold_vm_wg_ip 1)"
+ ip2="$(coold_vm_wg_ip 2)"
+ port1="$(coold_vm_wg_port 1)"
+ port2="$(coold_vm_wg_port 2)"
+ subnet1="$(coold_vm_container_subnet 1)"
+ subnet2="$(coold_vm_container_subnet 2)"
+
+ coold_vm 1 setup-wireguard "$ip1" "$ip2" "host.lima.internal" "$pub2" "$port1" "$port2" "$subnet2"
+ coold_vm 2 setup-wireguard "$ip2" "$ip1" "host.lima.internal" "$pub1" "$port2" "$port1" "$subnet1"
+}
+
+follow_logs() {
+ local coold_vm_enabled="$1"
+ local count
+ local vm_logs_pids=""
+ count="$(coold_vm_count)"
+
+ echo "==> Following dev logs. Press Ctrl-C to stop the dev environment."
+
+ cleanup_logs() {
+ for pid in $vm_logs_pids; do
+ kill "$pid" >/dev/null 2>&1 || true
+ done
+ }
+
+ stop_from_signal() {
+ trap - INT TERM EXIT
+ cleanup_logs
+ echo
+ echo "==> Ctrl-C received; stopping dev environment..."
+ down
+ exit 130
+ }
+
+ trap cleanup_logs EXIT
+ trap stop_from_signal INT TERM
+
+ if [ "$coold_vm_enabled" != "false" ]; then
+ for index in $(seq 1 "$count"); do
+ instance="$(coold_vm_instance "$index")"
+ coold_vm "$index" logs-agent | sed "s/^/[${instance}] /" &
+ vm_logs_pids="$vm_logs_pids $!"
+ done
+ fi
+
+ spin logs -f
+}
+
+up() {
+ local coold_vm_enabled
+ local follow_dev_logs
+ local count
+ coold_vm_enabled="$(read_coolify_env COOLIFY_COOLD_VM_ENABLED true)"
+ follow_dev_logs="$(read_coolify_env COOLIFY_DEV_FOLLOW_LOGS true)"
+ count="$(coold_vm_count)"
+
+ if [ "$coold_vm_enabled" != "false" ]; then
+ echo "==> Starting ${count} Coolify coold VM(s) before Spin..."
+ for index in $(seq 1 "$count"); do
+ coold_vm "$index" up
+ done
+ setup_wireguard_mesh "$count"
+ else
+ echo "==> COOLIFY_COOLD_VM_ENABLED=false; skipping coold VM."
+ fi
+
+ echo "==> Starting Coolify Docker stack with Spin..."
+ spin up -d "$@"
+
+ if [ "$coold_vm_enabled" != "false" ]; then
+ for index in $(seq 1 "$count"); do
+ host_id="$(coold_vm_instance "$index")"
+ echo "==> Minting Flux dev host JWT for ${host_id}..."
+ host_jwt="$(mint_host_jwt_for_host "$host_id")"
+ echo "==> Installing host JWT into ${host_id}..."
+ coold_vm "$index" install-host-jwt "$host_jwt"
+ done
+
+ for index in $(seq 1 "$count"); do
+ echo "==> Starting coold VM agent service on $(coold_vm_instance "$index")..."
+ if [ "$count" -ge 2 ]; then
+ peer_index=1
+ if [ "$index" = "1" ]; then
+ peer_index=2
+ fi
+ COOLIFY_COOLD_VM_WG_PEER_IP="$(coold_vm_wg_ip "$peer_index")" coold_vm "$index" start-agent
+ else
+ coold_vm "$index" start-agent
+ fi
+ done
+ fi
+
+ if [ "$follow_dev_logs" = "false" ]; then
+ echo "==> Dev environment is ready. Use 'spin logs -f' or 'scripts/coold-vm.sh logs-agent' to follow logs."
+ return
+ fi
+
+ follow_logs "$coold_vm_enabled"
+}
+
+down() {
+ local coold_vm_enabled
+ local stop_coold_vm
+ coold_vm_enabled="$(read_coolify_env COOLIFY_COOLD_VM_ENABLED true)"
+ stop_coold_vm="$(read_coolify_env COOLIFY_COOLD_VM_STOP_ON_DOWN false)"
+
+ if [ "$coold_vm_enabled" != "false" ]; then
+ for index in $(seq 1 "$(coold_vm_count)"); do
+ echo "==> Stopping coold VM agent service on $(coold_vm_instance "$index")..."
+ coold_vm "$index" stop-agent || true
+ done
+ fi
+
+ echo "==> Stopping Coolify Docker stack with Spin..."
+ spin down "$@"
+
+ if [ "$stop_coold_vm" = "true" ]; then
+ echo "==> Stopping Coolify coold VM..."
+ for index in $(seq 1 "$(coold_vm_count)"); do
+ coold_vm "$index" stop
+ done
+ fi
+}
+
+corrosion_for_each_vm() {
+ local label="$1"
+ shift
+ local count
+ local script
+ count="$(coold_vm_count)"
+ script="$(cat)"
+
+ for index in $(seq 1 "$count"); do
+ instance="$(coold_vm_instance "$index")"
+ echo "--- ${instance}: ${label} ---"
+ printf '%s\n' "$script" | COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell "$@"
+ done
+}
+
+corrosion_check() {
+ local count
+ count="$(coold_vm_count)"
+
+ for index in $(seq 1 "$count"); do
+ instance="$(coold_vm_instance "$index")"
+ peer_index=1
+ if [ "$index" = "1" ]; then
+ peer_index=2
+ fi
+ peer_ip="$(coold_vm_wg_ip "$peer_index")"
+
+ echo "--- ${instance}: check ---"
+ COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell </dev/null 2>&1 && echo active || echo unavailable
+printf 'peer ping (${peer_ip}): '
+ping -c 1 -W 2 ${peer_ip} >/dev/null 2>&1 && echo ok || echo failed
+printf 'gossip: '
+awk '/^\[gossip\]/{section=1; next} /^\[/{section=0} section && /^addr =|^bootstrap =/ {printf "%s ", \$0} END {print ""}' /etc/corrosion/config.toml
+printf 'registered containers: '
+sudo sqlite3 /var/lib/corrosion/corrosion.db 'select count(*) from service_endpoints;' 2>/dev/null || echo unavailable
+SH
+ done
+}
+
+corrosion_containers() {
+ corrosion_for_each_vm containers <<'SH'
+echo '[corrosion service_endpoints]'
+sudo sqlite3 -header -column /var/lib/corrosion/corrosion.db \
+ 'select container_id, container_name, namespace, host_mgmt_ip, container_ip, state, health, updated_at from service_endpoints order by updated_at desc;' 2>/dev/null \
+ || echo 'service_endpoints table unavailable'
+
+echo
+echo '[rootful podman]'
+sudo podman ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null \
+ || echo 'rootful podman unavailable'
+
+echo
+echo '[rootless podman]'
+podman ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null \
+ || echo 'rootless podman unavailable'
+SH
+}
+
+corrosion_config() {
+ corrosion_for_each_vm config <<'SH'
+sudo cat /etc/corrosion/config.toml
+SH
+}
+
+corrosion_logs() {
+ local index="${1:-1}"
+ coold_vm "$index" logs-agent
+}
+
+corrosion_sql() {
+ local query="$*"
+
+ if [ -z "$query" ]; then
+ echo "Usage: scripts/dev.sh corrosion sql " >&2
+ exit 1
+ fi
+
+ corrosion_for_each_vm sql <
+
+Commands:
+ check Show Corrosion service, WireGuard, gossip, and row-count state
+ containers List registered service_endpoints rows on each VM
+ config Print /etc/corrosion/config.toml from each VM
+ logs [n] Follow coold/corrosion logs for VM n (default: 1)
+ sql Run a read-only sqlite query against each Corrosion DB
+USAGE
+ ;;
+ *)
+ echo "unknown corrosion command: $command" >&2
+ echo "Run: scripts/dev.sh corrosion help" >&2
+ exit 1
+ ;;
+ esac
+}
+
+example_nginx_name() {
+ local index="$1"
+
+ if [ "$index" = "1" ]; then
+ printf '%s\n' coolify-example-nginx
+ return
+ fi
+
+ printf 'coolify-example-nginx-%s\n' "$index"
+}
+
+example_nginx_up() {
+ local count
+ count="$(coold_vm_count)"
+
+ for index in $(seq 1 "$count"); do
+ instance="$(coold_vm_instance "$index")"
+ name="$(example_nginx_name "$index")"
+ gateway="$(coold_vm_container_gateway "$index")"
+
+ echo "--- ${instance}: starting ${name} with coold DNS (${gateway}) ---"
+ COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell </dev/null 2>&1 || true
+sudo podman run -d \\
+ --name ${name} \\
+ --network coolify-default-mesh \\
+ --dns ${gateway} \\
+ --dns-search default.coolify.internal \\
+ docker.io/library/nginx:alpine
+SH
+ done
+}
+
+example_nginx_down() {
+ local count
+ count="$(coold_vm_count)"
+
+ for index in $(seq 1 "$count"); do
+ instance="$(coold_vm_instance "$index")"
+ name="$(example_nginx_name "$index")"
+
+ echo "--- ${instance}: removing ${name} ---"
+ COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell </dev/null 2>&1 || true
+SH
+ done
+}
+
+example_nginx_check_dns() {
+ COOLIFY_COOLD_LIMA_INSTANCE="$(coold_vm_instance 1)" scripts/coold-vm.sh shell <<'SH'
+set -e
+echo '--- resolv.conf ---'
+sudo podman exec coolify-example-nginx cat /etc/resolv.conf
+echo
+echo '--- coold DNS lookup through search domain ---'
+sudo podman exec coolify-example-nginx nslookup coolify-example-nginx-2
+echo
+echo '--- coold DNS lookup by full name ---'
+sudo podman exec coolify-example-nginx nslookup coolify-example-nginx-2.default.coolify.internal
+SH
+}
+
+example_nginx_help() {
+ cat <<'USAGE'
+Usage: scripts/dev.sh example-nginx
+
+Commands:
+ up Start one nginx container on each coold VM with coold DNS configured
+ down Remove the example nginx containers
+ check-dns Verify host 1 nginx can resolve host 2 nginx through coold DNS
+USAGE
+}
+
+example_nginx() {
+ local command="${1:-help}"
+ if [ $# -gt 0 ]; then
+ shift
+ fi
+
+ case "$command" in
+ up)
+ example_nginx_up
+ ;;
+ down)
+ example_nginx_down
+ ;;
+ check-dns)
+ example_nginx_check_dns
+ ;;
+ -h|--help|help)
+ example_nginx_help
+ ;;
+ *)
+ echo "unknown example-nginx command: $command" >&2
+ echo "Run: scripts/dev.sh example-nginx help" >&2
+ exit 1
+ ;;
+ esac
+}
+
+firewall_help() {
+ cat <<'USAGE'
+Usage: scripts/dev.sh firewall
+
+Commands:
+ allow [proto] [port] Allow traffic on every coold VM (proto/port optional)
+ revoke [id|src] [dst] [proto] [port]
+ Remove an allow rule from every coold VM
+ list List allow rules on every coold VM
+ reconcile Re-apply firewall snapshot on every coold VM
+
+Examples:
+ scripts/dev.sh firewall allow 10.210.0.2 10.210.1.2 tcp 80
+ scripts/dev.sh firewall revoke
+ scripts/dev.sh firewall revoke 10.210.0.2 10.210.1.2 tcp 80
+ scripts/dev.sh firewall revoke 3ba6e0c235a6
+ scripts/dev.sh firewall list
+USAGE
+}
+
+firewall_api_for_each_vm() {
+ local label="$1"
+ local method="$2"
+ local path="$3"
+ local body="${4:-}"
+ local count
+ count="$(coold_vm_count)"
+
+ for index in $(seq 1 "$count"); do
+ instance="$(coold_vm_instance "$index")"
+ api_ip="$(coold_vm_wg_ip "$index")"
+ echo "--- ${instance}: ${label} ---"
+ COOLIFY_COOLD_LIMA_INSTANCE="$instance" scripts/coold-vm.sh shell <&2
+ exit 1
+ fi
+
+ if [ -n "$port" ] && [ -z "$proto" ]; then
+ echo "ERROR: port requires proto (tcp or udp)." >&2
+ exit 1
+ fi
+
+ body="$(printf '{"namespace":"default","src":"%s","dst":"%s"' "$src" "$dst")"
+ if [ -n "$proto" ]; then
+ body="${body}$(printf ',"proto":"%s"' "$proto")"
+ fi
+ if [ -n "$port" ]; then
+ body="${body}$(printf ',"port":%s' "$port")"
+ fi
+ body="${body}}"
+
+ firewall_api_for_each_vm "allow ${src} -> ${dst}" POST /api/v1/firewall/allow "$body"
+}
+
+firewall_rule_id() {
+ local src="$1"
+ local dst="$2"
+ local proto="${3:-}"
+ local port="${4:-0}"
+
+ if [ -z "$proto" ]; then
+ port=0
+ fi
+
+ printf 'default|%s|%s|%s|%s' "$src" "$dst" "$proto" "$port" \
+ | shasum -a 256 \
+ | awk '{print substr($1, 1, 12)}'
+}
+
+firewall_revoke() {
+ local id_or_src="${1:-}"
+ local dst="${2:-}"
+ local proto="${3:-}"
+ local port="${4:-}"
+ local id
+
+ if [ -z "$id_or_src" ]; then
+ echo "Current firewall allow rule IDs:"
+ firewall_list
+ echo
+ echo "Revoke one with: scripts/dev.sh firewall revoke "
+ return
+ fi
+
+ if [ -z "$dst" ]; then
+ id="$id_or_src"
+ else
+ id="$(firewall_rule_id "$id_or_src" "$dst" "$proto" "$port")"
+ fi
+
+ firewall_api_for_each_vm "revoke ${id}" DELETE "/api/v1/firewall/allow/${id}"
+}
+
+firewall_list() {
+ firewall_api_for_each_vm list GET '/api/v1/firewall/allow?namespace=default'
+}
+
+firewall_reconcile() {
+ firewall_api_for_each_vm reconcile POST /api/v1/firewall/reconcile
+}
+
+firewall() {
+ local command="${1:-help}"
+ if [ $# -gt 0 ]; then
+ shift
+ fi
+
+ case "$command" in
+ allow)
+ firewall_allow "$@"
+ ;;
+ revoke|remove|delete|deny)
+ firewall_revoke "$@"
+ ;;
+ list)
+ firewall_list
+ ;;
+ reconcile)
+ firewall_reconcile
+ ;;
+ -h|--help|help)
+ firewall_help
+ ;;
+ *)
+ echo "unknown firewall command: $command" >&2
+ echo "Run: scripts/dev.sh firewall help" >&2
+ exit 1
+ ;;
+ esac
+}
+
+usage() {
+ cat <<'USAGE'
+Usage: scripts/dev.sh [spin args]
+
+Commands:
+ up Start the coold VM, Spin stack, and dev coold agent
+ down Stop the dev coold agent and Spin stack
+ shell [n] Open a shell inside coold VM n (default: 1)
+ list Show Lima instances
+ corrosion Inspect Corrosion state, config, logs, and registered containers
+ firewall Manage dev coold firewall allow rules
+ example-nginx Start/check example nginx containers with coold DNS
+USAGE
+}
+
+cmd="${1:-}"
+if [ $# -gt 0 ]; then
+ shift
+fi
+
+case "$cmd" in
+ up)
+ up "$@"
+ ;;
+ down)
+ down "$@"
+ ;;
+ shell)
+ coold_vm "${1:-1}" shell
+ ;;
+ list)
+ limactl list
+ ;;
+ corrosion)
+ corrosion "$@"
+ ;;
+ firewall)
+ firewall "$@"
+ ;;
+ example-nginx)
+ example_nginx "$@"
+ ;;
+ -h|--help|help|"")
+ usage
+ ;;
+ *)
+ echo "unknown command: $cmd" >&2
+ usage >&2
+ exit 1
+ ;;
+esac
diff --git a/tests/Feature/DevEnvironmentExampleTest.php b/tests/Feature/DevEnvironmentExampleTest.php
new file mode 100644
index 000000000..d5f2320cd
--- /dev/null
+++ b/tests/Feature/DevEnvironmentExampleTest.php
@@ -0,0 +1,31 @@
+not->toContain($variable.'=');
+})->with([
+ 'coold package version default' => 'COOLIFY_COOLD_VERSION',
+ 'flux package version default' => 'COOLIFY_FLUX_VERSION',
+ 'corrosion package version default' => 'COOLIFY_CORROSION_VERSION',
+ 'coold VM count default' => 'COOLIFY_COOLD_VM_COUNT',
+ 'coold VM flux URL default' => 'COOLIFY_COOLD_VM_FLUX_URL',
+ 'coold VM WireGuard IP 1 default' => 'COOLIFY_COOLD_VM_WG_IP_1',
+ 'coold VM WireGuard IP 2 default' => 'COOLIFY_COOLD_VM_WG_IP_2',
+ 'coold VM WireGuard port 1 default' => 'COOLIFY_COOLD_VM_WG_PORT_1',
+ 'coold VM WireGuard port 2 default' => 'COOLIFY_COOLD_VM_WG_PORT_2',
+ 'coold VM builder capacity default' => 'COOLIFY_COOLD_VM_BUILDER_CAPACITY',
+ 'coold VM enabled default' => 'COOLIFY_COOLD_VM_ENABLED',
+ 'coold VM stop on down default' => 'COOLIFY_COOLD_VM_STOP_ON_DOWN',
+ 'dev follow logs default' => 'COOLIFY_DEV_FOLLOW_LOGS',
+]);
+
+it('defaults coold dev VM settings in Laravel config', function () {
+ expect(config('coold.dev_host_count'))->toBe(2)
+ ->and(config('coold.dev_host_id'))->toBe('coolify-coold-dev')
+ ->and(config('coold.dev_host_id_2'))->toBe('coolify-coold-dev-2')
+ ->and(config('coold.dev_wireguard_ip_1'))->toBe('100.64.0.10')
+ ->and(config('coold.dev_wireguard_ip_2'))->toBe('100.64.0.11')
+ ->and(config('coold.dev_builder_capacity'))->toBe(2)
+ ->and(config('coold.dev_builder_enabled'))->toBeTrue();
+});
diff --git a/tests/Feature/FluxDevCommandTest.php b/tests/Feature/FluxDevCommandTest.php
new file mode 100644
index 000000000..6c9a3c810
--- /dev/null
+++ b/tests/Feature/FluxDevCommandTest.php
@@ -0,0 +1,65 @@
+ 'coold-dev',
+ '--caps' => 'coold,builder',
+ '--ttl' => '600',
+ ]);
+
+ expect($exitCode)->toBe(0);
+
+ $token = trim(Artisan::output());
+ $claims = JWT::decode($token, new Key(file_get_contents($publicKeyPath), 'ES256'));
+
+ expect($claims->sub)->toBe('coold-dev')
+ ->and($claims->aud)->toBe('coold')
+ ->and($claims->caps)->toBe(['coold', 'builder'])
+ ->and($claims->exp)->toBeGreaterThan(time());
+});
+
+it('writes the host jwt to an output path with owner-only permissions', function () {
+ [$privateKeyPath] = createFluxJwtKeypair();
+ $outputPath = storage_path('framework/testing/host-jwt');
+
+ Config::set('flux.jwt_private_key_path', $privateKeyPath);
+
+ $exitCode = Artisan::call('flux:dev', [
+ 'host_id' => 'coold-dev',
+ '--output' => $outputPath,
+ ]);
+
+ expect($exitCode)->toBe(0);
+
+ expect($outputPath)->toBeFile()
+ ->and(substr(sprintf('%o', fileperms($outputPath)), -4))->toBe('0600');
+});
+
+/**
+ * @return array{0: string, 1: string}
+ */
+function createFluxJwtKeypair(): array
+{
+ $directory = storage_path('framework/testing/flux-keys-'.bin2hex(random_bytes(4)));
+ mkdir($directory, 0777, true);
+
+ $privateKeyPath = $directory.'/jwt.priv';
+ $publicKeyPath = $directory.'/jwt.pub';
+
+ exec('openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out '.escapeshellarg($privateKeyPath), $output, $exitCode);
+ expect($exitCode)->toBe(0);
+
+ exec('openssl pkey -in '.escapeshellarg($privateKeyPath).' -pubout -out '.escapeshellarg($publicKeyPath), $output, $exitCode);
+ expect($exitCode)->toBe(0);
+
+ return [$privateKeyPath, $publicKeyPath];
+}
diff --git a/tests/Feature/V5/HomeTest.php b/tests/Feature/V5/HomeTest.php
index c37b47496..d4b4b96dc 100644
--- a/tests/Feature/V5/HomeTest.php
+++ b/tests/Feature/V5/HomeTest.php
@@ -105,6 +105,12 @@ it('serves the v5 inertia shell', function () {
->assertSee('v5-ready', false)
->assertSee('Running')
->assertSee('Flux is running.')
+ ->assertSee('coolify-coold-dev')
+ ->assertSee('coolify-coold-dev-2')
+ ->assertSee('100.64.0.10')
+ ->assertSee('100.64.0.11')
+ ->assertSee('builder')
+ ->assertSee('builderCapacity')
->assertSee('V5 Shared Team')
->assertSee('Shared team details')
->assertSee('owner')
From d92c6a8dabf85f25996f7ab0f0fd4257e23b3526 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 16 Jun 2026 12:33:26 +0200
Subject: [PATCH 006/174] feat(dev): bootstrap coold VMs via cooldctl
---
.dev/bin/cooldctl | Bin 0 -> 16026008 bytes
dev/coold-dev.md | 182 ++++++++++++++++++++++
dev/lima/coold.yaml | 74 +--------
scripts/coold-vm.sh | 15 +-
scripts/dev.sh | 367 ++++++++++++++++++++++++++++++++++++++------
5 files changed, 515 insertions(+), 123 deletions(-)
create mode 100755 .dev/bin/cooldctl
create mode 100644 dev/coold-dev.md
diff --git a/.dev/bin/cooldctl b/.dev/bin/cooldctl
new file mode 100755
index 0000000000000000000000000000000000000000..57a106667956481b345b857cbf21e4e50ce0ee1a
GIT binary patch
literal 16026008
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