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.
This commit is contained in:
parent
02ab4b39ef
commit
dcd325dc44
41 changed files with 1705 additions and 108 deletions
25
README.md
25
README.md
|
|
@ -29,6 +29,31 @@ ## Installation
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Please refer to the [docs](https://coolify.io/docs/installation) for more information about the installation.
|
> 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
|
## Support
|
||||||
|
|
||||||
Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
|
Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
|
||||||
|
|
|
||||||
44
app/Http/Controllers/V5/HomeController.php
Normal file
44
app/Http/Controllers/V5/HomeController.php
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\V5;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Flux\FluxHealth;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class HomeController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, FluxHealth $fluxHealth): Response
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $request->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,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
use App\Http\Middleware\TrimStrings;
|
use App\Http\Middleware\TrimStrings;
|
||||||
use App\Http\Middleware\TrustHosts;
|
use App\Http\Middleware\TrustHosts;
|
||||||
use App\Http\Middleware\TrustProxies;
|
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\ValidateSignature;
|
||||||
use App\Http\Middleware\VerifyCsrfToken;
|
use App\Http\Middleware\VerifyCsrfToken;
|
||||||
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
|
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' => [
|
'api' => [
|
||||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||||
ThrottleRequests::class.':api',
|
ThrottleRequests::class.':api',
|
||||||
|
|
|
||||||
54
app/Http/Middleware/V5/EnsureCurrentTeam.php
Normal file
54
app/Http/Middleware/V5/EnsureCurrentTeam.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware\V5;
|
||||||
|
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class EnsureCurrentTeam
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
/** @var User|null $user */
|
||||||
|
$user = $request->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Http/Middleware/V5/HandleInertiaRequests.php
Normal file
25
app/Http/Middleware/V5/HandleInertiaRequests.php
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware\V5;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Middleware;
|
||||||
|
|
||||||
|
class HandleInertiaRequests extends Middleware
|
||||||
|
{
|
||||||
|
protected $rootView = 'v5.app';
|
||||||
|
|
||||||
|
public function share(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
...parent::share($request),
|
||||||
|
'auth' => [
|
||||||
|
'user' => $request->user() ? [
|
||||||
|
'id' => $request->user()->id,
|
||||||
|
'name' => $request->user()->name,
|
||||||
|
'email' => $request->user()->email,
|
||||||
|
] : null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Models/V5/Project.php
Normal file
29
app/Models/V5/Project.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\V5;
|
||||||
|
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Project extends V5Model
|
||||||
|
{
|
||||||
|
protected $table = 'v5_projects';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'team_id',
|
||||||
|
'created_by_user_id',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function team(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Team::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function creator(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'created_by_user_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
10
app/Models/V5/V5Model.php
Normal file
10
app/Models/V5/V5Model.php
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\V5;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
abstract class V5Model extends Model
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,11 @@ public function boot(): void
|
||||||
Route::prefix('webhooks')
|
Route::prefix('webhooks')
|
||||||
->group(base_path('routes/webhooks.php'));
|
->group(base_path('routes/webhooks.php'));
|
||||||
|
|
||||||
|
Route::middleware('v5.web')
|
||||||
|
->prefix('v5')
|
||||||
|
->as('v5.')
|
||||||
|
->group(base_path('routes/v5.php'));
|
||||||
|
|
||||||
Route::middleware('web')
|
Route::middleware('web')
|
||||||
->group(base_path('routes/web.php'));
|
->group(base_path('routes/web.php'));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
66
app/Services/Flux/FluxHealth.php
Normal file
66
app/Services/Flux/FluxHealth.php
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Flux;
|
||||||
|
|
||||||
|
class FluxHealth
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array{available: bool, label: string, message: string, socket: string|null}
|
||||||
|
*/
|
||||||
|
public function check(): array
|
||||||
|
{
|
||||||
|
$socketPath = config('flux.unix_socket_path');
|
||||||
|
|
||||||
|
if (! is_string($socketPath) || $socketPath === '') {
|
||||||
|
return $this->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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"danharrin/livewire-rate-limiting": "^2.1.0",
|
"danharrin/livewire-rate-limiting": "^2.1.0",
|
||||||
"doctrine/dbal": "^4.4.1",
|
"doctrine/dbal": "^4.4.1",
|
||||||
"guzzlehttp/guzzle": "^7.10.0",
|
"guzzlehttp/guzzle": "^7.10.0",
|
||||||
|
"inertiajs/inertia-laravel": "^3.1",
|
||||||
"laravel/fortify": "^1.34.0",
|
"laravel/fortify": "^1.34.0",
|
||||||
"laravel/framework": "^12.49.0",
|
"laravel/framework": "^12.49.0",
|
||||||
"laravel/horizon": "^5.43.0",
|
"laravel/horizon": "^5.43.0",
|
||||||
|
|
|
||||||
111
composer.lock
generated
111
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "64b77285a7140ce68e83db2659e9a21d",
|
"content-hash": "d7e1f3eef5ae01d358f9482007814245",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
|
|
@ -1643,6 +1643,79 @@
|
||||||
],
|
],
|
||||||
"time": "2025-08-22T14:27:06+00:00"
|
"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",
|
"name": "jean85/pretty-package-versions",
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
|
|
@ -8973,16 +9046,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-foundation",
|
"name": "symfony/http-foundation",
|
||||||
"version": "v7.4.8",
|
"version": "v7.4.13",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/http-foundation.git",
|
"url": "https://github.com/symfony/http-foundation.git",
|
||||||
"reference": "9381209597ec66c25be154cbf2289076e64d1eab"
|
"reference": "bc354f47c62301e990b7874fa662326368508e2c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab",
|
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c",
|
||||||
"reference": "9381209597ec66c25be154cbf2289076e64d1eab",
|
"reference": "bc354f47c62301e990b7874fa662326368508e2c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -9031,7 +9104,7 @@
|
||||||
"description": "Defines an object-oriented layer for the HTTP specification",
|
"description": "Defines an object-oriented layer for the HTTP specification",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/http-foundation/tree/v7.4.8"
|
"source": "https://github.com/symfony/http-foundation/tree/v7.4.13"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -9051,7 +9124,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-03-24T13:12:05+00:00"
|
"time": "2026-05-24T11:20:33+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-kernel",
|
"name": "symfony/http-kernel",
|
||||||
|
|
@ -10008,16 +10081,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php83",
|
"name": "symfony/polyfill-php83",
|
||||||
"version": "v1.37.0",
|
"version": "v1.38.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php83.git",
|
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||||
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149"
|
"reference": "8339098cae28673c15cce00d80734af0453054e2"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149",
|
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/8339098cae28673c15cce00d80734af0453054e2",
|
||||||
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149",
|
"reference": "8339098cae28673c15cce00d80734af0453054e2",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -10064,7 +10137,7 @@
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0"
|
"source": "https://github.com/symfony/polyfill-php83/tree/v1.38.1"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -10084,7 +10157,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-04-10T17:25:58+00:00"
|
"time": "2026-05-26T12:51:13+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php84",
|
"name": "symfony/polyfill-php84",
|
||||||
|
|
@ -10650,16 +10723,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/routing",
|
"name": "symfony/routing",
|
||||||
"version": "v7.4.12",
|
"version": "v7.4.13",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/routing.git",
|
"url": "https://github.com/symfony/routing.git",
|
||||||
"reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204"
|
"reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/routing/zipball/3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204",
|
"url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d",
|
||||||
"reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204",
|
"reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -10711,7 +10784,7 @@
|
||||||
"url"
|
"url"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/routing/tree/v7.4.12"
|
"source": "https://github.com/symfony/routing/tree/v7.4.13"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -10731,7 +10804,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-05-20T07:20:23+00:00"
|
"time": "2026-05-24T11:20:33+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/serializer",
|
"name": "symfony/serializer",
|
||||||
|
|
|
||||||
6
config/flux.php
Normal file
6
config/flux.php
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'unix_socket_path' => env('COOLIFY_FLUX_UNIX_SOCKET_PATH', '/run/coolify/flux.sock'),
|
||||||
|
'health_timeout_seconds' => (float) env('COOLIFY_FLUX_HEALTH_TIMEOUT_SECONDS', 1.0),
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('v5_projects', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1321,6 +1321,16 @@ CREATE TABLE IF NOT EXISTS "users" (
|
||||||
"email_change_code_expires_at" TEXT
|
"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" (
|
CREATE TABLE IF NOT EXISTS "webhook_notification_settings" (
|
||||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
"team_id" INTEGER 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 (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 (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 (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);
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ services:
|
||||||
- GROUP_ID=${GROUPID:-1000}
|
- GROUP_ID=${GROUPID:-1000}
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-8000}:8080"
|
- "${APP_PORT:-8000}:8080"
|
||||||
|
- "${FORWARD_FLUX_PORT:-6443}:6443"
|
||||||
environment:
|
environment:
|
||||||
AUTORUN_ENABLED: false
|
AUTORUN_ENABLED: false
|
||||||
|
COOLIFY_CONTAINER_ROLE: "${COOLIFY_CONTAINER_ROLE:-all}"
|
||||||
PUSHER_HOST: "${PUSHER_HOST}"
|
PUSHER_HOST: "${PUSHER_HOST}"
|
||||||
PUSHER_PORT: "${PUSHER_PORT}"
|
PUSHER_PORT: "${PUSHER_PORT}"
|
||||||
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
|
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ services:
|
||||||
- /data/coolify/databases:/var/www/html/storage/app/databases
|
- /data/coolify/databases:/var/www/html/storage/app/databases
|
||||||
- /data/coolify/services:/var/www/html/storage/app/services
|
- /data/coolify/services:/var/www/html/storage/app/services
|
||||||
- /data/coolify/backups:/var/www/html/storage/app/backups
|
- /data/coolify/backups:/var/www/html/storage/app/backups
|
||||||
|
- /data/coolify/flux:/var/www/html/storage/app/flux
|
||||||
environment:
|
environment:
|
||||||
- APP_ENV=${APP_ENV:-production}
|
- APP_ENV=${APP_ENV:-production}
|
||||||
- PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT:-256M}
|
- 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_START_SERVERS=${PHP_FPM_PM_START_SERVERS:-1}
|
||||||
- PHP_FPM_PM_MIN_SPARE_SERVERS=${PHP_FPM_PM_MIN_SPARE_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}
|
- PHP_FPM_PM_MAX_SPARE_SERVERS=${PHP_FPM_PM_MAX_SPARE_SERVERS:-10}
|
||||||
|
- COOLIFY_CONTAINER_ROLE=${COOLIFY_CONTAINER_ROLE:-all}
|
||||||
env_file:
|
env_file:
|
||||||
- /data/coolify/source/.env
|
- /data/coolify/source/.env
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-8000}:8080"
|
- "${APP_PORT:-8000}:8080"
|
||||||
|
- "${COOLIFY_FLUX_PORT:-6443}:6443"
|
||||||
expose:
|
expose:
|
||||||
- "${APP_PORT:-8000}"
|
- "${APP_PORT:-8000}"
|
||||||
|
- "${COOLIFY_FLUX_PORT:-6443}"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: curl --fail http://127.0.0.1:8080/api/health || exit 1
|
test: curl --fail http://127.0.0.1:8080/api/health || exit 1
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine
|
||||||
ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z
|
ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z
|
||||||
# https://github.com/cloudflare/cloudflared/releases
|
# https://github.com/cloudflare/cloudflared/releases
|
||||||
ARG CLOUDFLARED_VERSION=2025.7.0
|
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/
|
# 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=
|
# 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
|
ARG POSTGRES_VERSION=18
|
||||||
|
|
@ -24,8 +26,10 @@ FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION}
|
||||||
ARG USER_ID
|
ARG USER_ID
|
||||||
ARG GROUP_ID
|
ARG GROUP_ID
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
ARG TARGETARCH
|
||||||
ARG POSTGRES_VERSION
|
ARG POSTGRES_VERSION
|
||||||
ARG CLOUDFLARED_VERSION
|
ARG CLOUDFLARED_VERSION
|
||||||
|
ARG COOLIFY_FLUX_VERSION
|
||||||
ARG NGINX_VERSION
|
ARG NGINX_VERSION
|
||||||
|
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
|
@ -58,6 +62,7 @@ RUN apk upgrade --no-cache && \
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
postgresql${POSTGRES_VERSION}-client \
|
postgresql${POSTGRES_VERSION}-client \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
|
openssl \
|
||||||
git \
|
git \
|
||||||
git-lfs \
|
git-lfs \
|
||||||
jq \
|
jq \
|
||||||
|
|
@ -81,6 +86,28 @@ RUN mkdir -p /usr/local/bin && \
|
||||||
fi && \
|
fi && \
|
||||||
chmod +x /usr/local/bin/cloudflared
|
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
|
# Configure PHP
|
||||||
COPY docker/development/etc/php/conf.d/zzz-custom-php.ini /usr/local/etc/php/conf.d/zzz-custom-php.ini
|
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
|
ENV PHP_OPCACHE_ENABLE=0
|
||||||
|
|
|
||||||
49
docker/development/etc/s6-overlay/s6-rc.d/flux/run
Executable file
49
docker/development/etc/s6-overlay/s6-rc.d/flux/run
Executable file
|
|
@ -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
|
||||||
1
docker/development/etc/s6-overlay/s6-rc.d/flux/type
Normal file
1
docker/development/etc/s6-overlay/s6-rc.d/flux/type
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
longrun
|
||||||
|
|
@ -2,6 +2,20 @@
|
||||||
|
|
||||||
cd /var/www/html
|
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
|
if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then
|
||||||
echo " INFO Horizon is disabled, sleeping."
|
echo " INFO Horizon is disabled, sleeping."
|
||||||
exec sleep infinity
|
exec sleep infinity
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,20 @@
|
||||||
|
|
||||||
cd /var/www/html
|
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
|
if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then
|
||||||
echo " INFO Nightwatch is enabled, starting..."
|
echo " INFO Nightwatch is enabled, starting..."
|
||||||
exec php artisan nightwatch:agent
|
exec php artisan nightwatch:agent
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,20 @@
|
||||||
|
|
||||||
cd /var/www/html
|
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
|
if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then
|
||||||
echo " INFO Scheduler is disabled, sleeping."
|
echo " INFO Scheduler is disabled, sleeping."
|
||||||
exec sleep infinity
|
exec sleep infinity
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine
|
||||||
ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z
|
ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z
|
||||||
# https://github.com/cloudflare/cloudflared/releases
|
# https://github.com/cloudflare/cloudflared/releases
|
||||||
ARG CLOUDFLARED_VERSION=2025.7.0
|
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/
|
# 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=
|
# 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
|
ARG POSTGRES_VERSION=18
|
||||||
|
|
@ -73,8 +75,10 @@ FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION}
|
||||||
ARG USER_ID
|
ARG USER_ID
|
||||||
ARG GROUP_ID
|
ARG GROUP_ID
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
ARG TARGETARCH
|
||||||
ARG POSTGRES_VERSION
|
ARG POSTGRES_VERSION
|
||||||
ARG CLOUDFLARED_VERSION
|
ARG CLOUDFLARED_VERSION
|
||||||
|
ARG COOLIFY_FLUX_VERSION
|
||||||
ARG NGINX_VERSION
|
ARG NGINX_VERSION
|
||||||
ARG CI=true
|
ARG CI=true
|
||||||
|
|
||||||
|
|
@ -108,6 +112,7 @@ RUN --mount=type=cache,target=/var/cache/apk \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
postgresql${POSTGRES_VERSION}-client \
|
postgresql${POSTGRES_VERSION}-client \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
|
openssl \
|
||||||
git \
|
git \
|
||||||
git-lfs \
|
git-lfs \
|
||||||
jq \
|
jq \
|
||||||
|
|
@ -128,6 +133,28 @@ RUN mkdir -p /usr/local/bin && \
|
||||||
fi && \
|
fi && \
|
||||||
chmod +x /usr/local/bin/cloudflared
|
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
|
# Configure PHP
|
||||||
COPY docker/production/etc/php/conf.d/zzz-custom-php.ini /usr/local/etc/php/conf.d/zzz-custom-php.ini
|
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
|
ENV PHP_OPCACHE_ENABLE=1
|
||||||
|
|
|
||||||
49
docker/production/etc/s6-overlay/s6-rc.d/flux/run
Executable file
49
docker/production/etc/s6-overlay/s6-rc.d/flux/run
Executable file
|
|
@ -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
|
||||||
1
docker/production/etc/s6-overlay/s6-rc.d/flux/type
Normal file
1
docker/production/etc/s6-overlay/s6-rc.d/flux/type
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
longrun
|
||||||
|
|
@ -2,6 +2,20 @@
|
||||||
|
|
||||||
cd /var/www/html
|
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
|
if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then
|
||||||
echo " INFO Horizon is disabled, sleeping."
|
echo " INFO Horizon is disabled, sleeping."
|
||||||
exec sleep infinity
|
exec sleep infinity
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,20 @@
|
||||||
|
|
||||||
cd /var/www/html
|
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
|
if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then
|
||||||
echo " INFO Nightwatch is enabled, starting..."
|
echo " INFO Nightwatch is enabled, starting..."
|
||||||
exec php artisan nightwatch:agent
|
exec php artisan nightwatch:agent
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,20 @@
|
||||||
|
|
||||||
cd /var/www/html
|
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
|
if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then
|
||||||
echo " INFO Scheduler is disabled, sleeping."
|
echo " INFO Scheduler is disabled, sleeping."
|
||||||
exec sleep infinity
|
exec sleep infinity
|
||||||
|
|
|
||||||
793
package-lock.json
generated
793
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "4.1.18",
|
"@tailwindcss/postcss": "4.1.18",
|
||||||
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
"laravel-vite-plugin": "2.0.1",
|
"laravel-vite-plugin": "2.0.1",
|
||||||
"postcss": "8.5.15",
|
"postcss": "8.5.15",
|
||||||
"tailwind-scrollbar": "4.0.2",
|
"tailwind-scrollbar": "4.0.2",
|
||||||
|
|
@ -15,10 +16,14 @@
|
||||||
"vite": "7.3.2"
|
"vite": "7.3.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@inertiajs/react": "^3.3.0",
|
||||||
|
"@inertiajs/vite": "^3.3.0",
|
||||||
"@tailwindcss/forms": "0.5.10",
|
"@tailwindcss/forms": "0.5.10",
|
||||||
"@tailwindcss/typography": "0.5.16",
|
"@tailwindcss/typography": "0.5.16",
|
||||||
"@xterm/addon-fit": "0.10.0",
|
"@xterm/addon-fit": "0.10.0",
|
||||||
"@xterm/xterm": "5.5.0",
|
"@xterm/xterm": "5.5.0",
|
||||||
"playwright": "^1.58.2"
|
"playwright": "^1.58.2",
|
||||||
|
"react": "^19.2.7",
|
||||||
|
"react-dom": "^19.2.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
resources/css/v5/app.css
Normal file
11
resources/css/v5/app.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
59
resources/js/v5/Pages/Home.jsx
Normal file
59
resources/js/v5/Pages/Home.jsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Head } from '@inertiajs/react';
|
||||||
|
|
||||||
|
export default function Home({ status, currentTeam, teams, flux }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head title="V5" />
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<p>{status}</p>
|
||||||
|
|
||||||
|
<h1>Coolify v5</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This page is served from Laravel through Inertia and React, with routes,
|
||||||
|
assets, and future v5 tables isolated from the current v4 Livewire app.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section aria-labelledby="flux-status-heading">
|
||||||
|
<h2 id="flux-status-heading">Flux status</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{flux.label}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>{flux.message}</p>
|
||||||
|
|
||||||
|
{flux.socket ? <p>Socket: {flux.socket}</p> : null}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<h2>Current team</h2>
|
||||||
|
|
||||||
|
{currentTeam ? (
|
||||||
|
<dl>
|
||||||
|
<dt>Name</dt>
|
||||||
|
<dd>{currentTeam.name}</dd>
|
||||||
|
|
||||||
|
<dt>Description</dt>
|
||||||
|
<dd>{currentTeam.description || 'No description'}</dd>
|
||||||
|
|
||||||
|
<dt>Your role</dt>
|
||||||
|
<dd>{currentTeam.role}</dd>
|
||||||
|
</dl>
|
||||||
|
) : (
|
||||||
|
<p>No team selected.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h2>Your teams</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{teams.map((team) => (
|
||||||
|
<li key={team.id}>
|
||||||
|
{team.name} ({team.role})
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
resources/js/v5/app.jsx
Normal file
11
resources/js/v5/app.jsx
Normal file
|
|
@ -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,
|
||||||
|
});
|
||||||
18
resources/views/v5/app.blade.php
Normal file
18
resources/views/v5/app.blade.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<title inertia>{{ config('app.name') }} v5</title>
|
||||||
|
@viteReactRefresh
|
||||||
|
@vite('resources/js/v5/app.jsx')
|
||||||
|
<x-inertia::head />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<x-inertia::app id="v5-app" />
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
8
routes/v5.php
Normal file
8
routes/v5.php
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\V5\HomeController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::middleware('v5.authenticated')->group(function () {
|
||||||
|
Route::get('/', HomeController::class)->name('home');
|
||||||
|
});
|
||||||
214
tests/Feature/V5/HomeTest.php
Normal file
214
tests/Feature/V5/HomeTest.php
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Kernel;
|
||||||
|
use App\Http\Middleware\CheckForcePasswordReset;
|
||||||
|
use App\Http\Middleware\DecideWhatToDoWithUser;
|
||||||
|
use App\Http\Middleware\V5\EnsureCurrentTeam;
|
||||||
|
use App\Http\Middleware\V5\HandleInertiaRequests;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Flux\FluxHealth;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Mockery\MockInterface;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Config::set('app.maintenance.store', 'array');
|
||||||
|
Config::set('cache.default', 'array');
|
||||||
|
|
||||||
|
Schema::dropIfExists('v5_projects');
|
||||||
|
Schema::dropIfExists('team_user');
|
||||||
|
Schema::dropIfExists('teams');
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('registers the v5 home route', function () {
|
||||||
|
expect(Route::has('v5.home'))->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']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { defineConfig, loadEnv } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import laravel from "laravel-vite-plugin";
|
import laravel from "laravel-vite-plugin";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import inertia from "@inertiajs/vite";
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd(), '')
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
|
|
@ -32,9 +34,15 @@ export default defineConfig(({ mode }) => {
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
laravel({
|
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,
|
refresh: true,
|
||||||
}),
|
}),
|
||||||
|
inertia({ ssr: false }),
|
||||||
|
react(),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue