Compare commits

...

29 Commits

Author SHA1 Message Date
Andras Bacsai
c8b974820b Merge pull request #1282 from coollabsio/next
fix: volume names
2023-10-03 13:26:02 +02:00
Andras Bacsai
527373e297 fix: volume names 2023-10-03 13:25:41 +02:00
Andras Bacsai
a84be8dc33 Merge pull request #1281 from coollabsio/next
v4.0.0-beta.61
2023-10-03 13:14:39 +02:00
Andras Bacsai
bd856f7f67 fix: volume names in services 2023-10-03 13:14:11 +02:00
Andras Bacsai
8ff216e5fb revert 2023-10-03 12:20:09 +02:00
Andras Bacsai
5255311a2e hmm 2023-10-03 12:14:58 +02:00
Andras Bacsai
774a245e84 Merge pull request #1280 from coollabsio/next
v4.0.0-beta.60
2023-10-03 12:12:17 +02:00
Andras Bacsai
194675c838 update commands 2023-10-03 12:08:57 +02:00
Andras Bacsai
75862ca8de rename resource delete 2023-10-03 11:59:30 +02:00
Andras Bacsai
5580a4e704 feat: delete resource command 2023-10-03 11:56:56 +02:00
Andras Bacsai
51e601a303 fix: only use _ in volume names for services 2023-10-03 11:22:35 +02:00
Andras Bacsai
734e9fd68d more explanation 2023-10-03 11:01:01 +02:00
Andras Bacsai
09fc950ae8 fix: add _data to vite ignore 2023-10-03 11:00:52 +02:00
Andras Bacsai
cf6caa279d fix: ui 2023-10-03 09:02:36 +02:00
Andras Bacsai
68c976ab70 fix: show all storages in one place for services 2023-10-03 08:48:07 +02:00
Andras Bacsai
1560ab2a50 fix: UI 2023-10-03 08:22:03 +02:00
Andras Bacsai
e3a6458506 version++ 2023-10-03 08:16:19 +02:00
Andras Bacsai
1768b9374f fix: move /data to ./_data in dev 2023-10-03 08:14:49 +02:00
Andras Bacsai
51d0a30a6c Merge pull request #1279 from coollabsio/next
v4.0.0-beta.59
2023-10-02 18:03:07 +02:00
Andras Bacsai
9701c65297 fix: predefined content for files 2023-10-02 18:02:32 +02:00
Andras Bacsai
58e3bb2571 version++ 2023-10-02 18:01:24 +02:00
Andras Bacsai
31cbd1602d Merge pull request #1278 from coollabsio/next
v4.0.0-beta.58
2023-10-02 17:21:07 +02:00
Andras Bacsai
dd5723d596 service: uptime kume hc updated 2023-10-02 17:17:49 +02:00
Andras Bacsai
620f26a6f1 fix: add destination to new services 2023-10-02 17:12:50 +02:00
Andras Bacsai
540717e809 feat: attach Coolify defined networks to services 2023-10-02 16:57:55 +02:00
Andras Bacsai
d446cd4103 feat: reset root password 2023-10-02 16:38:05 +02:00
Andras Bacsai
7d1a76570c wip 2023-10-02 15:51:06 +02:00
Andras Bacsai
e18766ec21 fix: if waitlist is disabled, redirect to register 2023-10-02 15:09:57 +02:00
Andras Bacsai
3d0354cf7e add contribution guide 2023-10-02 14:58:29 +02:00
30 changed files with 395 additions and 73 deletions

28
CONTRIBUTION.md Normal file
View File

@@ -0,0 +1,28 @@
# Contributing
> "First, thanks for considering to contribute to my project.
It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
You can ask for guidance anytime on our
[Discord server](https://coollabs.io/discord) in the `#contribution` channel.
## 1) Setup your development environment
- You need to have Docker Engine (or equivalent) [installed](https://docs.docker.com/engine/install/) on your system.
- For better DX, install [Spin](https://serversideup.net/open-source/spin/).
## 2) Set your environment variables
- Copy [.env.development.example](./.env.development.example) to .env.
- If necessary, set `USERID` & `GROUPID` accordingly (read in .env file).
## 3) Start & setup Coolify
- Run `spin up` - You can notice that errors will be thrown. Don't worry.
- Run `./scripts/run setup:dev` - This will generate a secret key for you, delete any existing database layouts, migrate database to the new layout, and seed your database.
## 4) Start development
You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user.

View File

@@ -4,12 +4,14 @@ namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service; use App\Models\Service;
use Symfony\Component\Yaml\Yaml;
class StartService class StartService
{ {
use AsAction; use AsAction;
public function handle(Service $service) public function handle(Service $service)
{ {
$network = $service->destination->network;
$service->saveComposeConfigs(); $service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir(); $commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'"; $commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'";
@@ -21,6 +23,11 @@ class StartService
$commands[] = "echo '####### Starting containers.'"; $commands[] = "echo '####### Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate"; $commands[] = "docker compose up -d --remove-orphans --force-recreate";
$commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true"; $commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true";
$compose = data_get($service,'docker_compose',[]);
$serviceNames = data_get(Yaml::parse($compose),'services',[]);
foreach($serviceNames as $serviceName => $serviceConfig){
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} 2>/dev/null || true";
}
$activity = remote_process($commands, $service->server); $activity = remote_process($commands, $service->server);
return $activity; return $activity;
} }

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Console\Commands;
use App\Models\Application;
use App\Models\Service;
use App\Models\StandalonePostgresql;
use Illuminate\Console\Command;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\select;
class ResourcesDelete extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'resources:delete';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete a resource from the database';
/**
* Execute the console command.
*/
public function handle()
{
$resource = select(
'What resource do you want to delete?',
['Application', 'Database', 'Service'],
);
if ($resource === 'Application') {
$this->deleteApplication();
} elseif ($resource === 'Database') {
$this->deleteDatabase();
} elseif ($resource === 'Service') {
$this->deleteService();
}
}
private function deleteApplication()
{
$applications = Application::all();
if ($applications->count() === 0) {
$this->error('There are no applications to delete.');
return;
}
$application = select(
'What application do you want to delete?',
$applications->pluck('name')->toArray(),
);
$application = $applications->where('name', $application)->first();
$confirmed = confirm("Are you sure you want to delete {$application->name}?");
if (!$confirmed) {
return;
}
$application->delete();
}
private function deleteDatabase()
{
$databases = StandalonePostgresql::all();
if ($databases->count() === 0) {
$this->error('There are no databases to delete.');
return;
}
$database = select(
'What database do you want to delete?',
$databases->pluck('name')->toArray(),
);
$database = $databases->where('name', $database)->first();
$confirmed = confirm("Are you sure you want to delete {$database->name}?");
if (!$confirmed) {
return;
}
$database->delete();
}
private function deleteService()
{
$services = Service::all();
if ($services->count() === 0) {
$this->error('There are no services to delete.');
return;
}
$service = select(
'What service do you want to delete?',
$services->pluck('name')->toArray(),
);
$service = $services->where('name', $service)->first();
$confirmed = confirm("Are you sure you want to delete {$service->name}?");
if (!$confirmed) {
return;
}
$service->delete();
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use function Laravel\Prompts\password;
class UsersResetRoot extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'users:reset-root';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reset Root Password';
/**
* Execute the console command.
*/
public function handle()
{
//
$this->info('You are about to reset the root password.');
$password = password('Give me a new password for root user: ');
$passwordAgain = password('Again');
if ($password != $passwordAgain) {
$this->error('Passwords do not match.');
return;
}
$this->info('Updating root password...');
try {
User::find(0)->update(['password' => Hash::make($password)]);
$this->info('Root password updated successfully.');
} catch (\Exception $e) {
$this->error('Failed to update root password.');
return;
}
}
}

View File

@@ -6,7 +6,7 @@ use App\Models\EnvironmentVariable;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use Illuminate\Support\Facades\Cache; use App\Models\StandaloneDocker;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class ProjectController extends Controller class ProjectController extends Controller
@@ -75,11 +75,14 @@ class ProjectController extends Controller
$oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/'); $oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/');
} }
if ($oneClickService) { if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();
$service = Service::create([ $service = Service::create([
'name' => "$oneClickServiceName-" . Str::random(10), 'name' => "$oneClickServiceName-" . Str::random(10),
'docker_compose_raw' => base64_decode($oneClickService), 'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id, 'environment_id' => $environment->id,
'server_id' => (int) $server_id, 'server_id' => (int) $server_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]); ]);
$service->name = "$oneClickServiceName-" . $service->uuid; $service->name = "$oneClickServiceName-" . $service->uuid;
$service->save(); $service->save();

View File

@@ -32,8 +32,6 @@ class DockerCompose extends Component
- type: volume - type: volume
source: mydata source: mydata
target: /data target: /data
volume:
nocopy: true
- type: bind - type: bind
source: ./var/lib/ghost/data source: ./var/lib/ghost/data
target: /data target: /data

View File

@@ -33,9 +33,6 @@ class Show extends Component
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first(); $this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
$this->serviceDatabase->getFilesFromServer(); $this->serviceDatabase->getFilesFromServer();
} }
if (is_null($service)) {
throw new \Exception("Service not found.");
}
} catch(\Throwable $e) { } catch(\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -7,6 +7,7 @@ use Livewire\Component;
class All extends Component class All extends Component
{ {
public bool $isHeaderVisible = true;
public $resource; public $resource;
protected $listeners = ['refreshStorages', 'submit']; protected $listeners = ['refreshStorages', 'submit'];

View File

@@ -11,6 +11,7 @@ class Show extends Component
public LocalPersistentVolume $storage; public LocalPersistentVolume $storage;
public bool $isReadOnly = false; public bool $isReadOnly = false;
public ?string $modalId = null; public ?string $modalId = null;
public bool $isFirst = true;
protected $rules = [ protected $rules = [
'storage.name' => 'required|string', 'storage.name' => 'required|string',

View File

@@ -23,6 +23,9 @@ class Index extends Component
} }
public function mount() public function mount()
{ {
if (config('coolify.waitlist') == false) {
return redirect()->route('register');
}
$this->waitingInLine = Waitlist::whereVerified(true)->count(); $this->waitingInLine = Waitlist::whereVerified(true)->count();
$this->users = User::count(); $this->users = User::count();
if (isDev()) { if (isDev()) {

View File

@@ -18,10 +18,16 @@ class Application extends BaseModel
]); ]);
}); });
static::deleting(function ($application) { static::deleting(function ($application) {
// Stop Container
instant_remote_process(
["docker rm -f {$application->uuid}"],
$application->destination->server,
false
);
$application->settings()->delete(); $application->settings()->delete();
$storages = $application->persistentStorages()->get(); $storages = $application->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server); instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server, false);
} }
$application->persistentStorages()->delete(); $application->persistentStorages()->delete();
$application->environment_variables()->delete(); $application->environment_variables()->delete();
@@ -233,7 +239,7 @@ class Application extends BaseModel
} }
public function isHealthcheckDisabled(): bool public function isHealthcheckDisabled(): bool
{ {
if (data_get($this, 'dockerfile') || data_get($this, 'build_pack') === 'dockerfile' || data_get($this,'health_check_enabled') === false) { if (data_get($this, 'dockerfile') || data_get($this, 'build_pack') === 'dockerfile' || data_get($this, 'health_check_enabled') === false) {
ray('dockerfile'); ray('dockerfile');
return true; return true;
} }

View File

@@ -8,7 +8,6 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Url\Url;
class Service extends BaseModel class Service extends BaseModel
{ {
@@ -20,6 +19,7 @@ class Service extends BaseModel
static::deleted(function ($service) { static::deleted(function ($service) {
$storagesToDelete = collect([]); $storagesToDelete = collect([]);
foreach ($service->applications()->get() as $application) { foreach ($service->applications()->get() as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server, false);
$storages = $application->persistentStorages()->get(); $storages = $application->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
$storagesToDelete->push($storage); $storagesToDelete->push($storage);
@@ -27,6 +27,7 @@ class Service extends BaseModel
$application->persistentStorages()->delete(); $application->persistentStorages()->delete();
} }
foreach ($service->databases()->get() as $database) { foreach ($service->databases()->get() as $database) {
instant_remote_process(["docker rm -f {$database->name}-{$service->uuid}"], $service->server, false);
$storages = $database->persistentStorages()->get(); $storages = $database->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
$storagesToDelete->push($storage); $storagesToDelete->push($storage);
@@ -63,6 +64,10 @@ class Service extends BaseModel
{ {
return $this->hasMany(ServiceDatabase::class); return $this->hasMany(ServiceDatabase::class);
} }
public function destination()
{
return $this->morphTo();
}
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);
@@ -124,9 +129,16 @@ class Service extends BaseModel
$topLevelNetworks = collect(data_get($yaml, 'networks', [])); $topLevelNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8'; $dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services'); $services = data_get($yaml, 'services');
$definedNetwork = $this->uuid;
$generatedServiceFQDNS = collect([]); $generatedServiceFQDNS = collect([]);
if (is_null($this->destination)) {
$destination = $this->server->destinations()->first();
if ($destination) {
$this->destination()->associate($destination);
$this->save();
}
}
$definedNetwork = collect([$this->uuid]);
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS) { $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS) {
$serviceVolumes = collect(data_get($service, 'volumes', [])); $serviceVolumes = collect(data_get($service, 'volumes', []));
@@ -237,13 +249,19 @@ class Service extends BaseModel
return $value == $definedNetwork; return $value == $definedNetwork;
}); });
if (!$definedNetworkExists) { if (!$definedNetworkExists) {
$topLevelNetworks->put($definedNetwork, [ foreach ($definedNetwork as $network) {
'name' => $definedNetwork, $topLevelNetworks->put($network, [
'external' => true 'name' => $network,
]); 'external' => true
]);
}
} }
$networks = $serviceNetworks->toArray(); $networks = $serviceNetworks->toArray();
$networks = array_merge($networks, [$definedNetwork]); foreach ($definedNetwork as $key => $network) {
$networks = array_merge($networks, [
$network
]);
}
data_set($service, 'networks', $networks); data_set($service, 'networks', $networks);
// Collect/create/update volumes // Collect/create/update volumes
@@ -270,7 +288,10 @@ class Service extends BaseModel
$isDirectory = (bool) data_get($volume, 'isDirectory', false); $isDirectory = (bool) data_get($volume, 'isDirectory', false);
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first(); $foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
if ($foundConfig) { if ($foundConfig) {
$content = data_get($foundConfig, 'content'); $contentNotNull = data_get($foundConfig, 'content');
if ($contentNotNull) {
$content = $contentNotNull;
}
$isDirectory = (bool) data_get($foundConfig, 'is_directory'); $isDirectory = (bool) data_get($foundConfig, 'is_directory');
} }
} }
@@ -297,21 +318,19 @@ class Service extends BaseModel
] ]
); );
} else if ($type->value() === 'volume') { } else if ($type->value() === 'volume') {
$slug = Str::slug($source, '-'); $slugWithoutUuid = Str::slug($source, '-');
if ($isNew) { $name = "{$savedService->service->uuid}_{$slugWithoutUuid}";
$name = "{$savedService->service->uuid}-{$slug}";
} else {
$name = "{$savedService->service->uuid}_{$slug}";
}
if (is_string($volume)) { if (is_string($volume)) {
$source = Str::of($volume)->before(':'); $source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':'); $target = Str::of($volume)->after(':')->beforeLast(':');
$source = $name; $source = $name;
$volume = "$source:$target"; $volume = "$source:$target";
} else if(is_array($volume)) { } else if (is_array($volume)) {
data_set($volume, 'source', $name); data_set($volume, 'source', $name);
} }
$topLevelVolumes->put($name, null); $topLevelVolumes->put($name, [
'name' => $name,
]);
LocalPersistentVolume::updateOrCreate( LocalPersistentVolume::updateOrCreate(
[ [
'mount_path' => $target, 'mount_path' => $target,
@@ -326,7 +345,7 @@ class Service extends BaseModel
] ]
); );
} }
$savedService->getFilesFromServer(); $savedService->getFilesFromServer(isInit: true);
return $volume; return $volume;
}); });
data_set($service, 'volumes', $serviceVolumes->toArray()); data_set($service, 'volumes', $serviceVolumes->toArray());

View File

@@ -36,8 +36,8 @@ class ServiceApplication extends BaseModel
); );
} }
public function getFilesFromServer() public function getFilesFromServer(bool $isInit = false)
{ {
getFilesystemVolumesFromServer($this); getFilesystemVolumesFromServer($this, $isInit);
} }
} }

View File

@@ -26,8 +26,8 @@ class ServiceDatabase extends BaseModel
{ {
return $this->morphMany(LocalFileVolume::class, 'resource'); return $this->morphMany(LocalFileVolume::class, 'resource');
} }
public function getFilesFromServer() public function getFilesFromServer(bool $isInit = false)
{ {
getFilesystemVolumesFromServer($this); getFilesystemVolumesFromServer($this, $isInit);
} }
} }

View File

@@ -29,9 +29,20 @@ class StandalonePostgresql extends BaseModel
]); ]);
}); });
static::deleted(function ($database) { static::deleted(function ($database) {
// Stop Container
instant_remote_process(
["docker rm -f {$database->uuid}"],
$database->destination->server,
false
);
// Stop TCP Proxy
if ($database->is_public) {
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server, false);
}
$database->scheduledBackups()->delete(); $database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
// Remove Volume
instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false); instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false);
}); });
} }

View File

@@ -64,7 +64,7 @@ function serviceStatus(Service $service)
} }
return 'exited'; return 'exited';
} }
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneService) function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneService, bool $isInit = false)
{ {
// TODO: make this async // TODO: make this async
try { try {
@@ -87,6 +87,10 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS
} }
$isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server); $isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server); $isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server);
if ($isFile === 'NOK' &&!$fileVolume->is_directory && $isInit) {
$fileVolume->saveStorageOnServer($oneService);
continue;
}
if ($isFile == 'OK' && !$fileVolume->is_directory) { if ($isFile == 'OK' && !$fileVolume->is_directory) {
$filesystemContent = instant_remote_process(["cat $fileLocation"], $server); $filesystemContent = instant_remote_process(["cat $fileLocation"], $server);
if (base64_encode($filesystemContent) != base64_encode($content)) { if (base64_encode($filesystemContent) != base64_encode($content)) {

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.57', 'release' => '4.0.0-beta.62',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.57'; return '4.0.0-beta.62';

View File

@@ -0,0 +1,28 @@
<?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::table('services', function (Blueprint $table) {
$table->nullableMorphs('destination');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('services', function (Blueprint $table) {
$table->dropMorphs('destination');
});
}
};

View File

@@ -34,14 +34,14 @@ services:
POSTGRES_DB: "${DB_DATABASE:-coolify}" POSTGRES_DB: "${DB_DATABASE:-coolify}"
POSTGRES_HOST_AUTH_METHOD: "trust" POSTGRES_HOST_AUTH_METHOD: "trust"
volumes: volumes:
- /data/coolify/_volumes/database/:/var/lib/postgresql/data - ./_data/coolify/_volumes/database/:/var/lib/postgresql/data
redis: redis:
ports: ports:
- "${FORWARD_REDIS_PORT:-6379}:6379" - "${FORWARD_REDIS_PORT:-6379}:6379"
env_file: env_file:
- .env - .env
volumes: volumes:
- /data/coolify/_volumes/redis/:/data - ./_data/coolify/_volumes/redis/:/data
vite: vite:
image: node:19 image: node:19
working_dir: /var/www/html working_dir: /var/www/html
@@ -56,7 +56,7 @@ services:
volumes: volumes:
- /:/host - /:/host
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- /data/coolify/:/data/coolify - ./_data/coolify/:/data/coolify
mailpit: mailpit:
image: "axllent/mailpit:latest" image: "axllent/mailpit:latest"
container_name: coolify-mail container_name: coolify-mail
@@ -76,6 +76,6 @@ services:
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}" MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}" MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
volumes: volumes:
- /data/coolify/_volumes/minio/:/data - ./_data/coolify/_volumes/minio/:/data
networks: networks:
- coolify - coolify

View File

@@ -1,13 +1,17 @@
<dialog id="composeModal" class="modal" x-data="{ raw: true }"> <dialog id="composeModal" class="modal" x-data="{ raw: true }">
<form method="dialog" class="flex flex-col gap-2 rounded max-w-7xl modal-box" wire:submit.prevent='submit'> <form method="dialog" class="flex flex-col gap-2 rounded max-w-7xl modal-box" wire:submit.prevent='submit'>
<div class="flex items-end gap-2">
<h1>Docker Compose</h1> <h1>Docker Compose</h1>
<div x-cloak x-show="raw"> <div x-cloak x-show="raw">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Check Deployable Compose</x-forms.button> <x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable Compose</x-forms.button>
</div> </div>
<div x-cloak x-show="raw === false"> <div x-cloak x-show="raw === false">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source <x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source
Compose</x-forms.button> Compose</x-forms.button>
</div> </div>
</div>
<div>Volume names are updated upon save. The service UUID will be added as a prefix to all volumes, to prevent name collision. <br>To see the actual volume names, check the Deployable Compose file, or go to Storage menu.</div>
<div x-cloak x-show="raw"> <div x-cloak x-show="raw">
<x-forms.textarea rows="20" id="raw"> <x-forms.textarea rows="20" id="raw">
</x-forms.textarea> </x-forms.textarea>

View File

@@ -7,6 +7,8 @@
<a :class="activeTab === 'service-stack' && 'text-white'" <a :class="activeTab === 'service-stack' && 'text-white'"
@click.prevent="activeTab = 'service-stack'; window.location.hash = 'service-stack'" @click.prevent="activeTab = 'service-stack'; window.location.hash = 'service-stack'"
href="#">Service Stack</a> href="#">Service Stack</a>
<a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages</a>
<a :class="activeTab === 'environment-variables' && 'text-white'" <a :class="activeTab === 'environment-variables' && 'text-white'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'" @click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment href="#">Environment
@@ -34,7 +36,7 @@
</div> </div>
</form> </form>
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3"> <div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3">
@foreach ($service->applications as $application) @foreach ($applications as $application)
<div @class([ <div @class([
'border-l border-dashed border-red-500' => Str::of( 'border-l border-dashed border-red-500' => Str::of(
$application->status)->contains(['exited']), $application->status)->contains(['exited']),
@@ -98,6 +100,46 @@
@endforeach @endforeach
</div> </div>
</div>
<div x-cloak x-show="activeTab === 'storages'">
@foreach ($applications as $application)
@if ($loop->first)
<livewire:project.shared.storages.all :resource="$application" />
@else
<livewire:project.shared.storages.all :resource="$application" :isHeaderVisible="false" />
@endif
@if ($application->fileStorages()->get()->count() > 0)
<h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4">
@foreach ($application->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage"
wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
@endforeach
@foreach ($databases as $database)
@if ($loop->first)
<h3 class="pt-4">{{ Str::headline($database->name) }}</h3>
@if ($applications->count() > 0)
<livewire:project.shared.storages.all :resource="$database" :isHeaderVisible="false" />
@else
<livewire:project.shared.storages.all :resource="$database" />
@endif
@if ($database->fileStorages()->get()->count() > 0)
<h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4">
@foreach ($database->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage"
wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
@else
<livewire:project.shared.storages.all :resource="$database" :isHeaderVisible="false" />
@endif
@endforeach
</div> </div>
<div x-cloak x-show="activeTab === 'environment-variables'"> <div x-cloak x-show="activeTab === 'environment-variables'">
<div x-cloak x-show="activeTab === 'environment-variables'"> <div x-cloak x-show="activeTab === 'environment-variables'">

View File

@@ -26,7 +26,7 @@
<div x-cloak x-show="activeTab === 'storages'"> <div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$serviceApplication" /> <livewire:project.shared.storages.all :resource="$serviceApplication" />
@if ($serviceApplication->fileStorages()->get()->count() > 0) @if ($serviceApplication->fileStorages()->get()->count() > 0)
<h3 class="py-4">Mounted Files (binds)</h3> <h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
@foreach ($serviceApplication->fileStorages()->get()->sort() as $fileStorage) @foreach ($serviceApplication->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" /> <livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@@ -42,7 +42,7 @@
<div x-cloak x-show="activeTab === 'storages'"> <div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$serviceDatabase" /> <livewire:project.shared.storages.all :resource="$serviceDatabase" />
@if ($serviceDatabase->fileStorages()->get()->count() > 0) @if ($serviceDatabase->fileStorages()->get()->count() > 0)
<h3 class="py-4">Mounted Files (binds)</h3> <h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
@foreach ($serviceDatabase->fileStorages()->get()->sort() as $fileStorage) @foreach ($serviceDatabase->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" /> <livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />

View File

@@ -1,28 +1,33 @@
<div> <div>
<div> @if ($isHeaderVisible)
<div class="flex items-center gap-2"> <div>
<h2>Storages</h2> <div class="flex items-center gap-2">
@if ($resource->type() !== 'service') <h2>Storages</h2>
<x-helper @if ($resource->type() !== 'service')
helper="For Preview Deployments, storage has a <span class='text-helper'>-pr-#PRNumber</span> in their <x-helper
helper="For Preview Deployments, storage has a <span class='text-helper'>-pr-#PRNumber</span> in their
volume volume
name, example: <span class='text-helper'>-pr-1</span>" /> name, example: <span class='text-helper'>-pr-1</span>" />
<x-forms.button class="btn" onclick="newStorage.showModal()">+ Add</x-forms.button> <x-forms.button class="btn" onclick="newStorage.showModal()">+ Add</x-forms.button>
<livewire:project.shared.storages.add :uuid="$resource->uuid" /> <livewire:project.shared.storages.add :uuid="$resource->uuid" />
@endif
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
@if ($resource->type() === 'service')
<span class="text-warning">Please modify storage layout in your <a class="underline"
href="{{ Str::of(url()->current())->beforeLast('/') }}">Docker Compose</a> file.</span>
<h2 class="pt-4">{{ Str::headline($resource->name) }} </h2>
@endif @endif
</div> </div>
<div>Persistent storage to preserve data between deployments.</div> @endif
</div> <div class="flex flex-col gap-4">
<div class="flex flex-col gap-2 py-4"> @foreach ($resource->persistentStorages as $storage)
@forelse ($resource->persistentStorages as $storage)
@if ($resource->type() === 'service') @if ($resource->type() === 'service')
<livewire:project.shared.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage" <livewire:project.shared.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage" :isFirst="$loop->first"
isReadOnly='true' /> isReadOnly='true' />
@else @else
<livewire:project.shared.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage" /> <livewire:project.shared.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage" />
@endif @endif
@empty @endforeach
<div class="text-neutral-500">No volume storages found.</div>
@endforelse
</div> </div>
</div> </div>

View File

@@ -6,19 +6,28 @@
reversible. <br>Please think again.</p> reversible. <br>Please think again.</p>
</x-slot:modalBody> </x-slot:modalBody>
</x-modal> </x-modal>
@once ($isReadOnly)
<span class="text-warning">Please modify storage layout in your <a <form wire:submit.prevent='submit' class="flex flex-col gap-2 xl:items-end xl:flex-row">
class="underline" href="{{ Str::of(url()->current())->beforeLast('/') }}#compose">Docker Compose</a> file.</span>
@endonce
<form wire:submit.prevent='submit' class="flex flex-col gap-2 pt-4 xl:items-end xl:flex-row">
@if ($isReadOnly) @if ($isReadOnly)
<x-forms.input id="storage.name" label="Volume Name" required readonly /> @if ($isFirst)
<x-forms.input id="storage.host_path" label="Source Path" readonly /> <x-forms.input id="storage.name" label="Volume Name" required readonly />
<x-forms.input id="storage.mount_path" label="Destination Path" required readonly /> <x-forms.input id="storage.host_path" label="Source Path (on host)" readonly />
<x-forms.input id="storage.mount_path" label="Destination Path (in container)" required readonly />
@else
<x-forms.input id="storage.name" required readonly />
<x-forms.input id="storage.host_path" readonly />
<x-forms.input id="storage.mount_path" required readonly />
@endif
@else @else
<x-forms.input id="storage.name" label="Name" required /> @if ($isFirst)
<x-forms.input id="storage.host_path" label="Source Path" /> <x-forms.input id="storage.name" label="Volume Name" required />
<x-forms.input id="storage.mount_path" label="Destination Path" required /> <x-forms.input id="storage.host_path" label="Source Path (on host)" />
<x-forms.input id="storage.mount_path" label="Destination Path (in container)" required />
@else
<x-forms.input id="storage.name" required />
<x-forms.input id="storage.host_path" />
<x-forms.input id="storage.mount_path" required />
@endif
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.button type="submit"> <x-forms.button type="submit">
Update Update

View File

@@ -17,17 +17,16 @@
@forelse ($projects as $project) @forelse ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group" x-data <div class="gap-2 border border-transparent cursor-pointer box group" x-data
x-on:click="goto('{{ $project->uuid }}')"> x-on:click="goto('{{ $project->uuid }}')">
<div class="flex flex-col mx-6"> <div class="flex flex-col flex-1 mx-6">
<a class=" group-hover:text-white hover:no-underline" <a class=" group-hover:text-white hover:no-underline"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">{{ $project->name }}</a> href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">{{ $project->name }}</a>
<div class="text-xs group-hover:text-white hover:no-underline" <div class="text-xs group-hover:text-white hover:no-underline"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
{{ $project->description }}</div> {{ $project->description }}</div>
</div> </div>
<div class="flex-1"></div> <a class="mx-4 rounded group-hover:text-white"
<a class="mx-4 rounded hover:text-white"
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path <path

View File

@@ -20,6 +20,11 @@ function help {
compgen -A function | cat -n compgen -A function | cat -n
} }
function setup:dev {
docker exec coolify bash -c "composer install"
docker exec coolify bash -c "php artisan key:generate"
docker exec coolify bash -c "php artisan migrate:fresh --seed"
}
function sync:v3 { function sync:v3 {
if [ -z "$1" ]; then if [ -z "$1" ]; then
echo -e "Please provide a version.\n\nExample: run sync:v3 3.12.32" echo -e "Please provide a version.\n\nExample: run sync:v3 3.12.32"

View File

@@ -7,7 +7,7 @@
"uptime-kuma": { "uptime-kuma": {
"documentation": "https://github.com/louislam/uptime-kuma", "documentation": "https://github.com/louislam/uptime-kuma",
"slogan": "A free and fancy self-hosted monitoring tool.", "slogan": "A free and fancy self-hosted monitoring tool.",
"compose": "c2VydmljZXM6CiAgdXB0aW1lLWt1bWE6CiAgICBpbWFnZTogbG91aXNsYW0vdXB0aW1lLWt1bWE6MQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROCiAgICB2b2x1bWVzOgogICAgICAtIHVwdGltZS1rdW1hOi9hcHAvZGF0YQ==" "compose": "c2VydmljZXM6CiAgdXB0aW1lLWt1bWE6CiAgICBpbWFnZTogbG91aXNsYW0vdXB0aW1lLWt1bWE6MQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROCiAgICB2b2x1bWVzOgogICAgICAtIHVwdGltZS1rdW1hOi9hcHAvZGF0YQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6IFsiQ01ELVNIRUxMIiwgImV4dHJhL2hlYWx0aGNoZWNrIl0KICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQ=="
}, },
"appsmith": { "appsmith": {
"documentation": "https://docs.appsmith.com/", "documentation": "https://docs.appsmith.com/",

View File

@@ -4,7 +4,7 @@
"version": "3.12.36" "version": "3.12.36"
}, },
"v4": { "v4": {
"version": "4.0.0-beta.57" "version": "4.0.0-beta.62"
} }
} }
} }

View File

@@ -4,6 +4,9 @@ import vue from "@vitejs/plugin-vue";
export default defineConfig({ export default defineConfig({
server: { server: {
watch: {
ignored: ['**/_data/**'],
},
host: "0.0.0.0", host: "0.0.0.0",
hmr: process.env.GITPOD_WORKSPACE_URL hmr: process.env.GITPOD_WORKSPACE_URL
? { ? {