mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-19 12:25:36 +00:00
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:
@@ -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).
|
||||
|
||||
@@ -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\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',
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\V5;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
abstract class V5Model extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -34,6 +34,11 @@ class RouteServiceProvider extends ServiceProvider
|
||||
Route::prefix('webhooks')
|
||||
->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'));
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
"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",
|
||||
|
||||
Generated
+92
-19
@@ -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",
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+49
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+49
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
longrun
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+706
-87
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
+9
-1
@@ -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(),
|
||||
],
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user