Compare commits

...

44 Commits

Author SHA1 Message Date
Andras Bacsai
b9427d2ec1 Merge pull request #1398 from coollabsio/next
v4.0.0-beta.112
2023-11-07 15:01:56 +01:00
Andras Bacsai
332a0b9e04 Remove ANSI colors from console output. 2023-11-07 14:40:58 +01:00
Andras Bacsai
18e98aaf52 Add S3 storage to Livewire components and fix
backup job network issue
2023-11-07 14:09:24 +01:00
Andras Bacsai
a7f9fad627 Add support for Dockerfile target build 2023-11-07 13:49:15 +01:00
Andras Bacsai
b01f6ac414 Fix docker network connection in StartService.php 2023-11-07 13:29:05 +01:00
Andras Bacsai
e1bc2cc406 Fix docker network connection issue in
StartService.php
2023-11-07 13:28:48 +01:00
Andras Bacsai
74830b12f3 Fix Docker network creation command in
StartService.php
2023-11-07 13:28:10 +01:00
Andras Bacsai
56a977c676 update n8n 2023-11-07 12:50:18 +01:00
Andras Bacsai
a0bb5733e6 lol n8n with umami db name 2023-11-07 12:30:37 +01:00
Andras Bacsai
516e10ddf2 feat: service database backups 2023-11-07 12:11:47 +01:00
Andras Bacsai
2976c72e09 fix: ui 2023-11-07 10:18:28 +01:00
Andras Bacsai
7377e9e415 fix: dockercleanupjob should be released back 2023-11-07 09:51:48 +01:00
Andras Bacsai
d77c55148b fix: github source view 2023-11-07 09:47:25 +01:00
Andras Bacsai
ad7aa2eed6 fix: github source view 2023-11-07 09:44:47 +01:00
Andras Bacsai
5cec50efbe update install script 2023-11-06 21:14:32 +01:00
Andras Bacsai
ef8686d4da Merge pull request #1383 from krsilas/fix/check-docker-installation
Check if docker installation was successful
2023-11-06 21:13:29 +01:00
Andras Bacsai
581cc73cd4 Merge pull request #1396 from coollabsio/next
v4.0.0-beta.111
2023-11-06 21:08:16 +01:00
Andras Bacsai
358fbf6b3d cleanup not forced 2023-11-06 21:08:02 +01:00
Andras Bacsai
ca0535c285 update cleanup 2023-11-06 20:58:03 +01:00
Andras Bacsai
9007a645a6 fix: build_image not found 2023-11-06 20:53:51 +01:00
Andras Bacsai
68b1b9774d Merge pull request #1385 from theh2so4/next
[+] Templates: NextCloud and Gitea
2023-11-06 19:13:51 +01:00
Andras Bacsai
b9b4c23d5b update init 2023-11-06 18:15:23 +01:00
Andras Bacsai
149fee2452 fix: deletions 2023-11-06 18:04:18 +01:00
Andras Bacsai
87af9e46a6 fix:ui 2023-11-06 17:27:22 +01:00
Andras Bacsai
d6f87d3fb6 fix: ui for labels 2023-11-06 17:25:54 +01:00
Andras Bacsai
493af61233 fix 2023-11-06 15:51:27 +01:00
Andras Bacsai
ab03908f1d updates 2023-11-06 15:48:15 +01:00
Andras Bacsai
9ef7cf3c12 update service templates 2023-11-06 15:43:56 +01:00
Andras Bacsai
eab7fd44d4 fix: service dockercompose predefined networks
version++
fix: modal of changing service stack
fix: appwrite template
2023-11-06 15:22:11 +01:00
Andras Bacsai
0d1d25a945 Merge pull request #1393 from coollabsio/next
v4.0.0-beta.110
2023-11-06 14:13:38 +01:00
Andras Bacsai
534372c29c fix: env variables
fix: revert custom network for a bit
2023-11-06 14:12:22 +01:00
Andras Bacsai
1ccb239797 version++ 2023-11-06 13:54:00 +01:00
Andras Bacsai
66287b43d0 fix: container logs are now followable in full-screen and sorted by timestamp 2023-11-06 13:53:05 +01:00
Andras Bacsai
143e4e0d23 lol 2023-11-06 13:30:37 +01:00
Andras Bacsai
73f3a09157 oops 2023-11-06 13:29:44 +01:00
TheH2SO4
4031e477ee [+] Template: Gitea (PostgreSQL) 2023-11-03 13:55:14 +01:00
TheH2SO4
0c1991d1de [+] Template: Gitea MariaDB + (fix) 2023-11-03 13:40:36 +01:00
TheH2SO4
05b697b18c [+] Template: MySQL + (Fix) 2023-11-03 13:40:07 +01:00
TheH2SO4
061aeba605 [+] Template: Gitea (MariaDB) 2023-11-03 13:38:50 +01:00
TheH2SO4
f446e784cc [+] Template: Gitea (MySQL) 2023-11-03 12:21:15 +01:00
TheH2SO4
72fe24d98e [+] Template: Gitea 2023-11-03 12:04:01 +01:00
TheH2SO4
126b2dc65b [+] Template: NextCloud
🆕 **New Template**:

-> ℹ️ **NextCloud**: NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management.
2023-11-03 08:34:24 +01:00
Silas Krause
8ae18f49dc Add missing fi 2023-11-01 22:13:25 +01:00
Silas Krause
4feb99cbe0 Check if docker installation was successful 2023-11-01 21:52:08 +01:00
59 changed files with 997 additions and 477 deletions

View File

@@ -16,17 +16,17 @@ class StartService
$commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo '####### Creating Docker network.'";
$commands[] = "docker network create --attachable {$service->uuid} >/dev/null 2>/dev/null || true";
$commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null || true";
$commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo '####### Pulling images.'";
$commands[] = "docker compose pull";
$commands[] = "echo '####### Starting containers.'";
$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 || 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";
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
}
$activity = remote_process($commands, $service->server);
return $activity;

View File

@@ -6,6 +6,9 @@ use App\Enums\ApplicationDeploymentStatus;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
@@ -15,15 +18,18 @@ use Illuminate\Support\Facades\Storage;
class Init extends Command
{
protected $signature = 'app:init';
protected $signature = 'app:init {--cleanup}';
protected $description = 'Cleanup instance related stuffs';
public function handle()
{
ray()->clearAll();
$cleanup = $this->option('cleanup');
if ($cleanup) {
$this->cleanup_stucked_resources();
$this->cleanup_ssh();
}
$this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_resources();
// $this->cleanup_ssh();
}
private function cleanup_ssh()
@@ -38,7 +44,7 @@ class Init extends Command
Storage::delete($file);
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
echo "Error in cleaning ssh: {$e->getMessage()}\n";
}
}
private function cleanup_in_progress_application_deployments()
@@ -61,77 +67,129 @@ class Init extends Command
try {
$applications = Application::all();
foreach ($applications as $application) {
if (!$application->environment) {
if (!data_get($application, 'environment')) {
ray('Application without environment', $application->name);
$application->delete();
}
if (!data_get($application, 'destination.server')) {
ray('Application without server', $application->name);
$application->delete();
}
if (!$application->destination()) {
ray('Application without destination', $application->name);
$application->delete();
}
}
} catch (\Throwable $e) {
echo "Error in application: {$e->getMessage()}\n";
}
try {
$postgresqls = StandalonePostgresql::all();
foreach ($postgresqls as $postgresql) {
if (!$postgresql->environment) {
if (!data_get($postgresql, 'environment')) {
ray('Postgresql without environment', $postgresql->name);
$postgresql->delete();
}
if (!data_get($postgresql, 'destination.server')) {
ray('Postgresql without server', $postgresql->name);
$postgresql->delete();
}
if (!$postgresql->destination()) {
ray('Postgresql without destination', $postgresql->name);
$postgresql->delete();
}
}
} catch (\Throwable $e) {
echo "Error in postgresql: {$e->getMessage()}\n";
}
try {
$redis = StandaloneRedis::all();
foreach ($redis as $redis) {
if (!$redis->environment) {
if (!data_get($redis, 'environment')) {
ray('Redis without environment', $redis->name);
$redis->delete();
}
if (!data_get($redis, 'destination.server')) {
ray('Redis without server', $redis->name);
$redis->delete();
}
if (!$redis->destination()) {
ray('Redis without destination', $redis->name);
$redis->delete();
}
}
} catch (\Throwable $e) {
echo "Error in redis: {$e->getMessage()}\n";
}
try {
$mongodbs = StandaloneMongodb::all();
foreach ($mongodbs as $mongodb) {
if (!$mongodb->environment) {
if (!data_get($mongodb, 'environment')) {
ray('Mongodb without environment', $mongodb->name);
$mongodb->delete();
}
if (!data_get($mongodb, 'destination.server')) {
ray('Mongodb without server', $mongodb->name);
$mongodb->delete();
}
if (!$mongodb->destination()) {
ray('Mongodb without destination', $mongodb->name);
$mongodb->delete();
}
}
} catch (\Throwable $e) {
echo "Error in mongodb: {$e->getMessage()}\n";
}
try {
$mysqls = StandaloneMysql::all();
foreach ($mysqls as $mysql) {
if (!$mysql->environment) {
if (!data_get($mysql, 'environment')) {
ray('Mysql without environment', $mysql->name);
$mysql->delete();
}
if (!data_get($mysql, 'destination.server')) {
ray('Mysql without server', $mysql->name);
$mysql->delete();
}
if (!$mysql->destination()) {
ray('Mysql without destination', $mysql->name);
$mysql->delete();
}
}
$mariadbs = StandaloneMysql::all();
} catch (\Throwable $e) {
echo "Error in mysql: {$e->getMessage()}\n";
}
try {
$mariadbs = StandaloneMariadb::all();
foreach ($mariadbs as $mariadb) {
if (!$mariadb->environment) {
if (!data_get($mariadb, 'environment')) {
ray('Mariadb without environment', $mariadb->name);
$mariadb->delete();
}
if (!data_get($mariadb, 'destination.server')) {
ray('Mariadb without server', $mariadb->name);
$mariadb->delete();
}
if (!$mariadb->destination()) {
ray('Mariadb without destination', $mariadb->name);
$mariadb->delete();
}
}
} catch (\Throwable $e) {
echo "Error in mariadb: {$e->getMessage()}\n";
}
try {
$services = Service::all();
foreach ($services as $service) {
if (!$service->environment) {
if (!data_get($service, 'environment')) {
ray('Service without environment', $service->name);
$service->delete();
}
if (!$service->server) {
if (!data_get($service, 'server')) {
ray('Service without server', $service->name);
$service->delete();
}
@@ -141,7 +199,29 @@ class Init extends Command
}
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
echo "Error in service: {$e->getMessage()}\n";
}
try {
$serviceApplications = ServiceApplication::all();
foreach ($serviceApplications as $service) {
if (!data_get($service, 'service')) {
ray('ServiceApplication without service', $service->name);
$service->delete();
}
}
} catch (\Throwable $e) {
echo "Error in serviceApplications: {$e->getMessage()}\n";
}
try {
$serviceDatabases = ServiceDatabase::all();
foreach ($serviceDatabases as $service) {
if (!data_get($service, 'service')) {
ray('ServiceDatabase without service', $service->name);
$service->delete();
}
}
} catch (\Throwable $e) {
echo "Error in ServiceDatabases: {$e->getMessage()}\n";
}
}
}

View File

@@ -81,7 +81,9 @@ class ProjectController extends Controller
$oneClickService = data_get($services, "$oneClickServiceName.compose");
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
if ($oneClickDotEnvs) {
$oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/');
$oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
return !empty($value);
});
}
if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();

View File

@@ -55,6 +55,7 @@ class General extends Component
'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable',
'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -77,6 +78,7 @@ class General extends Component
'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location',
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
];
public function mount()

View File

@@ -104,6 +104,7 @@ class CloneProject extends Component
$uuid = (string)new Cuid2(7);
$newDatabase = $database->replicate()->fill([
'uuid' => $uuid,
'status' => 'exited',
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
]);
@@ -111,15 +112,15 @@ class CloneProject extends Component
$environmentVaribles = $database->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$payload = [];
if ($database->type() === 'standalone-postgres') {
if ($database->type() === 'standalone-postgresql') {
$payload['standalone_postgresql_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_redis') {
} else if ($database->type() === 'standalone-redis') {
$payload['standalone_redis_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_mongodb') {
} else if ($database->type() === 'standalone-mongodb') {
$payload['standalone_mongodb_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_mysql') {
} else if ($database->type() === 'standalone-mysql') {
$payload['standalone_mysql_id'] = $newDatabase->id;
}else if ($database->type() === 'standalone_mariadb') {
} else if ($database->type() === 'standalone-mariadb') {
$payload['standalone_mariadb_id'] = $newDatabase->id;
}
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
@@ -134,6 +135,16 @@ class CloneProject extends Component
'destination_id' => $this->selectedServer,
]);
$newService->save();
foreach ($newService->applications() as $application) {
$application->update([
'status' => 'exited',
]);
}
foreach ($newService->databases() as $database) {
$database->update([
'status' => 'exited',
]);
}
$newService->parse();
}
return redirect()->route('project.resources', [

View File

@@ -3,6 +3,7 @@
namespace App\Http\Livewire\Project\Database;
use Livewire\Component;
use Spatie\Url\Url;
class BackupEdit extends Component
{
@@ -43,14 +44,23 @@ class BackupEdit extends Component
{
// TODO: Delete backup from server and add a confirmation modal
$this->backup->delete();
redirect()->route('project.database.backups.all', $this->parameters);
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$previousUrl = url()->previous();
$url = Url::fromString($previousUrl);
$url = $url->withoutQueryParameter('selectedBackupId');
$url = $url->withFragment('backups');
$url = $url->getPath() . "#{$url->getFragment()}";
return redirect()->to($url);
} else {
redirect()->route('project.database.backups.all', $this->parameters);
}
}
public function instantSave()
{
try {
$this->custom_validate();
$this->backup->save();
$this->backup->refresh();
$this->emit('success', 'Backup updated successfully');

View File

@@ -19,7 +19,11 @@ class BackupExecutions extends Component
$this->emit('error', 'Backup execution not found.');
return;
}
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
} else {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
}
$execution->delete();
$this->emit('success', 'Backup deleted successfully.');
$this->emit('refreshBackupExecutions');
@@ -33,7 +37,11 @@ class BackupExecutions extends Component
return;
}
$filename = data_get($execution, 'filename');
$server = $execution->scheduledDatabaseBackup->database->destination->server;
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$server = $execution->scheduledDatabaseBackup->database->service->destination->server;
} else {
$server = $execution->scheduledDatabaseBackup->database->destination->server;
}
$privateKeyLocation = savePrivateKeyToFs($server);
$disk = Storage::build([
'driver' => 'sftp',

View File

@@ -22,7 +22,8 @@ class CreateScheduledBackup extends Component
'frequency' => 'Backup Frequency',
'save_s3' => 'Save to S3',
];
public function mount() {
public function mount()
{
if ($this->s3s->count() > 0) {
$this->s3_storage_id = $this->s3s->first()->id;
}
@@ -50,11 +51,16 @@ class CreateScheduledBackup extends Component
$payload['databases_to_backup'] = $this->database->postgres_db;
} else if ($this->database->type() === 'standalone-mysql') {
$payload['databases_to_backup'] = $this->database->mysql_database;
}else if ($this->database->type() === 'standalone-mariadb') {
} else if ($this->database->type() === 'standalone-mariadb') {
$payload['databases_to_backup'] = $this->database->mariadb_database;
}
ScheduledDatabaseBackup::create($payload);
$this->emit('refreshScheduledBackups');
$databaseBackup = ScheduledDatabaseBackup::create($payload);
if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$this->emit('refreshScheduledBackups', $databaseBackup->id);
} else {
$this->emit('refreshScheduledBackups');
}
} catch (\Throwable $e) {
handleError($e, $this);
} finally {

View File

@@ -8,13 +8,33 @@ class ScheduledBackups extends Component
{
public $database;
public $parameters;
public $type;
public $selectedBackup;
public $selectedBackupId;
public $s3s;
protected $listeners = ['refreshScheduledBackups'];
protected $queryString = ['selectedBackupId'];
public function mount(): void
{
if ($this->selectedBackupId) {
$this->setSelectedBackup($this->selectedBackupId);
}
$this->parameters = get_route_parameters();
if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$this->type = 'service-database';
} else {
$this->type = 'database';
}
$this->s3s = currentTeam()->s3s;
}
public function setSelectedBackup($backupId) {
$this->selectedBackupId = $backupId;
$this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId);
if (is_null($this->selectedBackup)) {
$this->selectedBackupId = null;
}
}
public function delete($scheduled_backup_id): void
{
$this->database->scheduledBackups->find($scheduled_backup_id)->delete();
@@ -22,9 +42,11 @@ class ScheduledBackups extends Component
$this->refreshScheduledBackups();
}
public function refreshScheduledBackups(): void
public function refreshScheduledBackups(?int $id = null): void
{
ray('refreshScheduledBackups');
$this->database->refresh();
if ($id) {
$this->setSelectedBackup($id);
}
}
}

View File

@@ -27,11 +27,15 @@ class Navbar extends Component
$activity = StartService::run($this->service);
$this->emit('newMonitorActivity', $activity->id);
}
public function stop()
public function stop(bool $forceCleanup = false)
{
StopService::run($this->service);
$this->service->refresh();
$this->emit('success', 'Service stopped successfully.');
if ($forceCleanup) {
$this->emit('success', 'Force cleanup service successfully.');
} else {
$this->emit('success', 'Service stopped successfully.');
}
$this->emit('checkStatus');
}
}

View File

@@ -16,6 +16,8 @@ class Show extends Component
public array $parameters;
public array $query;
public Collection $services;
public $s3s;
protected $listeners = ['generateDockerCompose'];
public function mount()
@@ -33,6 +35,7 @@ class Show extends Component
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
$this->serviceDatabase->getFilesFromServer();
}
$this->s3s = currentTeam()->s3s;
} catch(\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -17,7 +17,7 @@ class GetLogs extends Component
public int $numberOfLines = 100;
public function doSomethingWithThisChunkOfOutput($output)
{
$this->outputs .= $output;
$this->outputs .= removeAnsiColors($output);
}
public function instantSave()
{
@@ -33,11 +33,16 @@ class GetLogs extends Component
if ($refresh) {
$this->outputs = '';
}
$command = Process::run($sshCommand);
$output = $command->output();
$error = $command->errorOutput();
$this->doSomethingWithThisChunkOfOutput($output);
$this->doSomethingWithThisChunkOfOutput($error);
Process::run($sshCommand, function (string $type, string $output) {
$this->doSomethingWithThisChunkOfOutput($output);
});
if ($this->showTimeStamps) {
$this->outputs = str($this->outputs)->split('/\n/')->sort(function ($a, $b) {
$a = explode(' ', $a);
$b = explode(' ', $b);
return $a[0] <=> $b[0];
})->join("\n");
}
}
}
public function render()

View File

@@ -14,7 +14,7 @@ class Delete extends Component
{
try {
$this->authorize('delete', $this->server);
if (!$this->server->isEmpty()) {
if ($this->server->hasDefinedResources()) {
$this->emit('error', 'Server has defined resources. Please delete them first.');
return;
}

View File

@@ -69,7 +69,6 @@ class Backup extends Component
]);
$this->database->refresh();
$this->backup->refresh();
ray($this->backup);
$this->s3s = S3Storage::whereTeamId(0)->get();
}

View File

@@ -3,17 +3,18 @@
namespace App\Http\Livewire\Source\Github;
use App\Models\GithubApp;
use App\Models\InstanceSettings;
use Livewire\Component;
class Change extends Component
{
public string $webhook_endpoint;
public string|null $ipv4;
public string|null $ipv6;
public string|null $fqdn;
public ?string $ipv4;
public ?string $ipv6;
public ?string $fqdn;
public bool|null $default_permissions = true;
public bool|null $preview_deployment_permissions = true;
public ?bool $default_permissions = true;
public ?bool $preview_deployment_permissions = true;
public $parameters;
public GithubApp $github_app;
@@ -28,29 +29,68 @@ class Change extends Component
'github_app.custom_user' => 'required|string',
'github_app.custom_port' => 'required|int',
'github_app.app_id' => 'required|int',
'github_app.installation_id' => 'nullable',
'github_app.client_id' => 'nullable',
'github_app.client_secret' => 'nullable',
'github_app.webhook_secret' => 'nullable',
'github_app.installation_id' => 'required|int',
'github_app.client_id' => 'required|string',
'github_app.client_secret' => 'required|string',
'github_app.webhook_secret' => 'required|string',
'github_app.is_system_wide' => 'required|bool',
];
public function mount()
{
$github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::where('uuid', $github_app_uuid)->first();
if (!$this->github_app) {
return redirect()->route('source.all');
}
$settings = InstanceSettings::get();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->name = str($this->github_app->name)->kebab();
$this->fqdn = $settings->fqdn;
if ($settings->public_ipv4) {
$this->ipv4 = 'http://' . $settings->public_ipv4 . ':' . config('app.port');
}
if ($settings->public_ipv6) {
$this->ipv6 = 'http://' . $settings->public_ipv6 . ':' . config('app.port');
}
if ($this->github_app->installation_id && session('from')) {
$source_id = data_get(session('from'), 'source_id');
if (!$source_id || $this->github_app->id !== $source_id) {
session()->forget('from');
} else {
$parameters = data_get(session('from'), 'parameters');
$back = data_get(session('from'), 'back');
$environment_name = data_get($parameters, 'environment_name');
$project_uuid = data_get($parameters, 'project_uuid');
$type = data_get($parameters, 'type');
$destination = data_get($parameters, 'destination');
session()->forget('from');
return redirect()->route($back, [
'environment_name' => $environment_name,
'project_uuid' => $project_uuid,
'type' => $type,
'destination' => $destination,
]);
}
}
$this->parameters = get_route_parameters();
if (isCloud() && !isDev()) {
$this->webhook_endpoint = config('app.url');
} else {
$this->webhook_endpoint = $this->ipv4;
$this->is_system_wide = $this->github_app->is_system_wide;
}
$this->parameters = get_route_parameters();
}
public function submit()
{
try {
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->validate();
$this->github_app->save();
$this->emit('success', 'Github App updated successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -58,6 +98,7 @@ class Change extends Component
public function instantSave()
{
$this->submit();
}
public function delete()

View File

@@ -69,6 +69,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile';
private ?string $addHosts = null;
private ?string $buildTarget = null;
private $log_model;
private Collection $saved_outputs;
@@ -178,6 +179,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return "--add-host $name:$ip";
})->implode(' ');
if ($this->application->dockerfile_target_build) {
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
}
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
@@ -293,7 +298,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
}
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
}
private function just_restart()
{
@@ -922,7 +926,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->application->settings->is_static) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
]);
$dockerfile = base64_encode("FROM {$this->application->static_image}
@@ -960,7 +964,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
);
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
}
}

View File

@@ -7,6 +7,7 @@ use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -32,7 +33,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
public ?Team $team = null;
public Server $server;
public ScheduledDatabaseBackup $backup;
public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database;
public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database;
public ?string $container_name = null;
public ?ScheduledDatabaseBackupExecution $backup_log = null;
@@ -48,9 +49,15 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
{
$this->backup = $backup;
$this->team = Team::find($backup->team_id);
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->s3 = $this->backup->s3;
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->service->server;
$this->s3 = $this->backup->s3;
} else {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->s3 = $this->backup->s3;
}
}
public function middleware(): array
@@ -73,14 +80,38 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$this->database->delete();
return;
}
$status = Str::of(data_get($this->database, 'status'));
if (!$status->startsWith('running') && $this->database->id !== 0) {
ray('database not running');
return;
}
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$databaseType = $this->database->databaseType();
$serviceUuid = $this->database->service->uuid;
if ($databaseType === 'standalone-postgresql') {
$this->container_name = "postgresql-$serviceUuid";
$commands[] = "docker exec $this->container_name env | grep POSTGRES_";
$envs = instant_remote_process($commands, $this->server);
$databasesToBackup = Str::of($envs)->after('POSTGRES_DB=')->before("\n")->value();
$this->database->postgres_user = Str::of($envs)->after('POSTGRES_USER=')->before("\n")->value();
} else if ($databaseType === 'standalone-mysql') {
$this->container_name = "mysql-$serviceUuid";
$commands[] = "docker exec $this->container_name env | grep MYSQL_";
$envs = instant_remote_process($commands, $this->server);
$databasesToBackup = Str::of($envs)->after('MYSQL_DATABASE=')->before("\n")->value();
$this->database->mysql_root_password = Str::of($envs)->after('MYSQL_ROOT_PASSWORD=')->before("\n")->value();
} else if ($databaseType === 'standalone-mariadb') {
$this->container_name = "mariadb-$serviceUuid";
$commands[] = "docker exec $this->container_name env | grep MARIADB_";
$envs = instant_remote_process($commands, $this->server);
$databasesToBackup = Str::of($envs)->after('MARIADB_DATABASE=')->before("\n")->value();
$this->database->mysql_root_password = Str::of($envs)->after('MARIADB_ROOT_PASSWORD=')->before("\n")->value();
}
} else {
$this->container_name = $this->database->uuid;
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
}
if (is_null($databasesToBackup)) {
if ($databaseType === 'standalone-postgresql') {
@@ -116,7 +147,6 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
return;
}
}
$this->container_name = $this->database->uuid;
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
if ($this->database->name === 'coolify-db') {
@@ -314,7 +344,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
if ($this->backup->number_of_backups_locally === 0) {
$deletable = $this->backup->executions()->where('status', 'success');
} else {
$deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally);
$deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally - 1);
}
foreach ($deletable->get() as $execution) {
delete_backup_locally($execution->filename, $this->server);
@@ -334,8 +364,12 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$bucket = $this->s3->bucket;
$endpoint = $this->s3->endpoint;
$this->s3->testConnection();
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1";
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$network = $this->database->service->destination->network;
} else {
$network = $this->database->destination->network;
}
$commands[] = "docker run --pull=always -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server);

View File

@@ -2,7 +2,6 @@
namespace App\Jobs;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -22,7 +21,7 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): string

View File

@@ -34,8 +34,11 @@ class Application extends BaseModel
static::deleting(function ($application) {
$application->settings()->delete();
$storages = $application->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server, false);
$server = data_get($application, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$application->persistentStorages()->delete();
$application->environment_variables()->delete();

View File

@@ -109,11 +109,12 @@ class Server extends BaseModel
return $this->proxy->modelScope();
}
public function isEmpty()
public function hasDefinedResources()
{
$applications = $this->applications()->count() === 0;
$databases = $this->databases()->count() === 0;
if ($applications && $databases) {
$services = $this->services()->count() === 0;
if ($applications || $databases || $services) {
return true;
}
return false;

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\Yaml\Yaml;
use Illuminate\Support\Str;
@@ -23,21 +22,21 @@ class Service extends BaseModel
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
$application->persistentStorages()->delete();
}
foreach ($service->databases()->get() as $database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
}
$database->persistentStorages()->delete();
}
$service->environment_variables()->delete();
$service->applications()->delete();
$service->databases()->delete();
if ($storagesToDelete->count() > 0) {
$storagesToDelete->each(function ($storage) use ($service) {
instant_remote_process(["docker volume rm -f $storage->name"], $service->server, false);
$server = data_get($service, 'server');
if ($server && $storagesToDelete->count() > 0) {
$storagesToDelete->each(function ($storage) use ($server) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
});
}
});
@@ -118,7 +117,7 @@ class Service extends BaseModel
public function parse(bool $isNew = false): Collection
{
ray()->clearAll();
// ray()->clearAll();
if ($this->docker_compose_raw) {
try {
$yaml = Yaml::parse($this->docker_compose_raw);
@@ -257,13 +256,25 @@ class Service extends BaseModel
]);
}
}
$networks = $serviceNetworks->toArray();
foreach ($definedNetwork as $key => $network) {
$networks = array_merge($networks, [
$network => null
]);
$networks = collect();
foreach ($serviceNetworks as $key =>$serviceNetwork) {
if (gettype($serviceNetwork) === 'string') {
// networks:
// - appwrite
$networks->put($serviceNetwork, null);
} else if (gettype($serviceNetwork) === 'array') {
// networks:
// default:
// ipv4_address: 192.168.203.254
// $networks->put($serviceNetwork, null);
ray($key);
$networks->put($key,$serviceNetwork);
}
}
data_set($service, 'networks', $networks);
foreach ($definedNetwork as $key => $network) {
$networks->put($network, null);
}
data_set($service, 'networks', $networks->toArray());
// Collect/create/update volumes
if ($serviceVolumes->count() > 0) {

View File

@@ -11,6 +11,13 @@ class ServiceApplication extends BaseModel
use HasFactory;
protected $guarded = [];
protected static function booted()
{
static::deleting(function ($service) {
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
}
public function type()
{
return 'service';

View File

@@ -9,10 +9,25 @@ class ServiceDatabase extends BaseModel
use HasFactory;
protected $guarded = [];
protected static function booted()
{
static::deleting(function ($service) {
$service->persistentStorages()->delete();
$service->fileStorages()->delete();
});
}
public function type()
{
return 'service';
}
public function databaseType()
{
$image = str($this->image)->before(':');
if ($image->value() === 'postgres') {
$image = 'postgresql';
}
return "standalone-$image";
}
public function service()
{
return $this->belongsTo(Service::class);
@@ -29,4 +44,8 @@ class ServiceDatabase extends BaseModel
{
getFilesystemVolumesFromServer($this, $isInit);
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@@ -30,8 +30,11 @@ class StandaloneMariadb extends BaseModel
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();

View File

@@ -33,8 +33,11 @@ class StandaloneMongodb extends BaseModel
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();

View File

@@ -30,8 +30,11 @@ class StandaloneMysql extends BaseModel
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();

View File

@@ -30,8 +30,11 @@ class StandalonePostgresql extends BaseModel
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();

View File

@@ -26,8 +26,11 @@ class StandaloneRedis extends BaseModel
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();

View File

@@ -6,6 +6,7 @@ use App\Models\Server;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection
{
@@ -141,6 +142,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = collect([]);
$labels->push('traefik.enable=true');
foreach ($domains as $loop => $domain) {
$uuid = new Cuid2(7);
$url = Url::fromString($domain);
$host = $url->getHost();
$path = $url->getPath();

View File

@@ -501,3 +501,6 @@ function generateDeployWebhook($resource) {
$url = $api . $endpoint . "?uuid=$uuid&force=false";
return $url;
}
function removeAnsiColors($text) {
return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text);
}

View File

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

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.109';
return '4.0.0-beta.112';

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('applications', function (Blueprint $table) {
$table->string('dockerfile_target_build')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('dockerfile_target_build');
});
}
};

View File

@@ -60,6 +60,14 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- /data/coolify/:/data/coolify
# - coolify-data-dev:/data/coolify
remote-host:
<<: *testing-host-base
container_name: coolify-remote-host
volumes:
- /:/host
- /var/run/docker.sock:/var/run/docker.sock
- /data/coolify/:/data/coolify
# - coolify-data-dev:/data/coolify
mailpit:
image: "axllent/mailpit:latest"
container_name: coolify-mail

View File

@@ -38,13 +38,13 @@
</button>
@endif
@if (serviceStatus($service) === 'exited')
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<button wire:click='stop(true)' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path fill="red" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" />
<path fill="red"
d="M17.003 20a4.895 4.895 0 0 0-2.404-4.173L22 3l-1.73-1l-7.577 13.126a5.699 5.699 0 0 0-5.243 1.503C3.706 20.24 3.996 28.682 4.01 29.04a1 1 0 0 0 1 .96h14.991a1 1 0 0 0 .6-1.8c-3.54-2.656-3.598-8.146-3.598-8.2Zm-5.073-3.003A3.11 3.11 0 0 1 15.004 20c0 .038.002.208.017.469l-5.9-2.624a3.8 3.8 0 0 1 2.809-.848ZM15.45 28A5.2 5.2 0 0 1 14 25h-2a6.5 6.5 0 0 0 .968 3h-2.223A16.617 16.617 0 0 1 10 24H8a17.342 17.342 0 0 0 .665 4H6c.031-1.836.29-5.892 1.803-8.553l7.533 3.35A13.025 13.025 0 0 0 17.596 28Z" />
</svg>
Force cleanup containers
Force Cleanup Containers
</button>
<button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">

View File

@@ -50,15 +50,16 @@
<h3>Build</h3>
@if ($application->could_set_build_commands())
@if ($application->build_pack === 'nixpacks')
<div>Nixpacks will detect your package manager/configurations: <a class="underline" href="https://nixpacks.com/docs/providers">Nixpacks documentation</a></div>
<div>Nixpacks will detect your package manager/configurations: <a class="underline"
href="https://nixpacks.com/docs/providers">Nixpacks documentation</a></div>
<div class="text-warning">You probably do not need to modify the commands below.</div>
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml" id="application.install_command"
label="Install Command" />
<x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml" id="application.build_command"
label="Build Command" />
<x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml" id="application.start_command"
label="Start Command" />
<x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml"
id="application.install_command" label="Install Command" />
<x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml"
id="application.build_command" label="Build Command" />
<x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml"
id="application.start_command" label="Start Command" />
</div>
@endif
@endif
@@ -71,6 +72,7 @@
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
label="Dockerfile Location"
helper="It is calculated together with the Base Directory: {{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}" />
<x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target" helper="Useful if you have multi-staged dockerfile." />
@endif
@if ($application->could_set_build_commands())
@if ($application->settings->is_static)
@@ -103,11 +105,7 @@
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
</div>
@if ($labelsChanged)
<x-forms.textarea label="Custom Labels" rows="15" id="customLabels"></x-forms.textarea>
@else
<x-forms.textarea label="Coolify Generated Labels" rows="15" id="customLabels"></x-forms.textarea>
@endif
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea>
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>
</div>
<h3>Advanced</h3>

View File

@@ -1,12 +1,36 @@
<div class="flex flex-wrap gap-2">
@forelse($database->scheduledBackups as $backup)
<a class="flex flex-col box"
href="{{ route('project.database.backups.executions', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
<div>Frequency: {{ $backup->frequency }}</div>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
<div>Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}</div>
</a>
@empty
<div>No scheduled backups configured.</div>
@endforelse
<div>
<div class="flex flex-wrap gap-2">
@forelse($database->scheduledBackups as $backup)
@if ($type === 'database')
<a class="flex flex-col box"
href="{{ route('project.database.backups.executions', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
<div>Frequency: {{ $backup->frequency }}</div>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
<div>Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}</div>
</a>
@else
<div @class([
'border-coollabs' =>
data_get($backup, 'id') === data_get($selectedBackup, 'id'),
'flex flex-col box border-l-2 border-transparent',
])
wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')">
<div>Frequency: {{ $backup->frequency }}</div>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
<div>Number of backups to keep (locally): {{ $backup->number_of_backups_locally }}</div>
</div>
@endif
@empty
<div>No scheduled backups configured.</div>
@endforelse
</div>
@if ($type === 'service-database' && $selectedBackup)
<div class="pt-10">
<livewire:project.database.backup-edit key="{{ $selectedBackup->id }}" :backup="$selectedBackup" :s3s="$s3s"
:status="data_get($database, 'status')" />
<h3 class="py-4">Executions</h3>
<livewire:project.database.backup-executions key="{{ $selectedBackup->id }}" :backup="$selectedBackup"
:executions="$selectedBackup->executions" />
</div>
@endif
</div>

View File

@@ -1,6 +1,6 @@
<div x-data="{ raw: true, activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" x-init="$wire.checkStatus" wire:poll.10000ms="checkStatus">
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<livewire:project.service.compose-modal key={{ now() }} :raw="$service->docker_compose_raw" :actual="$service->docker_compose" />
<livewire:project.service.compose-modal :raw="$service->docker_compose_raw" :actual="$service->docker_compose" />
<div class="flex h-full pt-6">
<div class="flex flex-col items-start gap-4 min-w-fit">
<a target="_blank" href="{{ $service->documentation() }}">Documentation <x-external-link /></a>

View File

@@ -7,10 +7,14 @@
<button><- Back</button>
</a>
<a :class="activeTab === 'general' && 'text-white'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
@click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''"
href="#">General</a>
<a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'; if(window.location.search) window.location.search = ''"
href="#">Storages
</a>
<a :class="activeTab === 'backups' && 'text-white'"
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#">Backups</a>
@if (data_get($parameters, 'service_name'))
<a class="{{ request()->routeIs('project.service.logs') ? 'text-white' : '' }}"
href="{{ route('project.service.logs', $parameters) }}">
@@ -43,8 +47,15 @@
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="text-warning">Please modify storage layout in your Docker Compose file.</span>
<livewire:project.service.storage wire:key="application-{{ $serviceDatabase->id }}"
:resource="$serviceDatabase" />
<livewire:project.service.storage wire:key="application-{{ $serviceDatabase->id }}" :resource="$serviceDatabase" />
</div>
<div x-cloak x-show="activeTab === 'backups'">
<div class="flex gap-2 ">
<h2 class="pb-4">Scheduled Backups</h2>
<x-forms.button onclick="createScheduledBackup.showModal()">+ Add</x-forms.button>
</div>
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" :s3s="$s3s" />
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
</div>
@endisset
</div>

View File

@@ -13,17 +13,28 @@
<x-forms.input label="Only Show Number of Lines" placeholder="1000" required id="numberOfLines"></x-forms.input>
<x-forms.button type="submit">Refresh</x-forms.button>
</form>
<div x-data="{ fullscreen: false }" :class="fullscreen ? 'fullscreen' : 'container w-full pt-4 mx-auto'">
<div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'container w-full pt-4 mx-auto'">
<div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white scrollbar border-coolgray-300"
:class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'">
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4"
x-on:click="fullscreen = !fullscreen"><svg class="icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg
class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
</svg></button>
<button title="Go Top" x-show="fullscreen" class="fixed top-4 right-28" x-on:click="goTop"> <svg
class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M12 5v14m4-10l-4-4M8 9l4-4" />
</svg></button>
<button title="Follow Logs" x-show="fullscreen" :class="alwaysScroll ? 'text-warning' : ''"
class="fixed top-4 right-16" x-on:click="toggleScroll"><svg class="icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
</svg></button>
<button title="Fullscreen" x-show="!fullscreen" class="absolute top-2 right-8"
x-on:click="fullscreen = !fullscreen"><svg class="fixed icon" viewBox="0 0 24 24"
x-on:click="makeFullscreen"><svg class="fixed icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path
@@ -32,7 +43,40 @@
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
</g>
</svg></button>
<pre class="font-mono whitespace-pre-wrap">{{ $outputs }}</pre>
<pre id="logs" class="font-mono whitespace-pre-wrap">{{ $outputs }}</pre>
</div>
</div>
<script>
function makeFullscreen() {
this.fullscreen = !this.fullscreen;
if (this.fullscreen === false) {
this.alwaysScroll = false;
clearInterval(this.intervalId);
}
}
function toggleScroll() {
this.alwaysScroll = !this.alwaysScroll;
if (this.alwaysScroll) {
this.intervalId = setInterval(() => {
const screen = document.getElementById('screen');
const logs = document.getElementById('logs');
if (screen.scrollTop !== logs.scrollHeight) {
screen.scrollTop = logs.scrollHeight;
}
}, 100);
} else {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
function goTop() {
this.alwaysScroll = false;
clearInterval(this.intervalId);
const screen = document.getElementById('screen');
screen.scrollTop = 0;
}
</script>
</div>

View File

@@ -11,8 +11,12 @@
<div class="pb-4">This will remove this server from Coolify. Beware! There is no coming
back!
</div>
<x-forms.button isError isModal modalId="deleteServer">
Delete
</x-forms.button>
@if ($server->hasDefinedResources())
<div class="text-warning">Please delete all resources before deleting this server.</div>
@else
<x-forms.button isError isModal modalId="deleteServer">
Delete
</x-forms.button>
@endif
@endif
</div>

View File

@@ -62,7 +62,6 @@
helper="Disk cleanup job will be executed if disk usage is more than this number." />
@endif
</form>
<livewire:server.delete :server="$server" />
<script>
Livewire.on('installDocker', () => {
installDocker.showModal();

View File

@@ -11,4 +11,5 @@
</x-modal>
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.form :server="$server" />
<livewire:server.delete :server="$server" />
</div>

View File

@@ -4,32 +4,26 @@
<p>This source will be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<form wire:submit.prevent='submit' x-data>
<div class="flex items-center gap-2">
<h1>GitHub App</h1>
<div class="flex gap-2">
@if ($github_app->app_id)
<x-forms.button type="submit">Save</x-forms.button>
@if (data_get($github_app, 'app_id'))
<form wire:submit.prevent='submit'>
<div class="flex items-center gap-2">
<h1>GitHub App</h1>
<div class="flex gap-2">
@if (data_get($github_app, 'installation_id'))
<x-forms.button type="submit">Save</x-forms.button>
<a href="{{ get_installation_path($github_app) }}">
<x-forms.button>
Update Repositories
<x-external-link />
</x-forms.button>
</a>
@endif
@else
<x-forms.button disabled type="submit">Save</x-forms.button>
@endif
<x-forms.button isError isModal modalId="deleteSource">
Delete
</x-forms.button>
<x-forms.button isError isModal modalId="deleteSource">
Delete
</x-forms.button>
</div>
</div>
</div>
<div class="subtitle">Your Private GitHub App for private repositories.</div>
@if (data_get($github_app, 'app_id'))
<div class="subtitle">Your Private GitHub App for private repositories.</div>
@if (!data_get($github_app, 'installation_id'))
<div class="mb-10 rounded alert alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
@@ -39,7 +33,7 @@
</svg>
<span>You must complete this step before you can use this source!</span>
</div>
<a class="justify-center box" href="{{ get_installation_path($github_app) }}">
<a class="items-center justify-center box" href="{{ get_installation_path($github_app) }}">
Install Repositories on GitHub
</a>
@else
@@ -47,7 +41,7 @@
<div class="w-48">
<x-forms.checkbox label="System Wide?"
helper="If checked, this GitHub App will be available for everyone in this Coolify instance."
instantSave id="is_system_wide" />
instantSave id="github_app.is_system_wide" />
</div>
@endif
<div class="flex gap-2">
@@ -78,110 +72,118 @@
<x-forms.input id="github_app.webhook_secret" label="Webhook Secret" type="password" />
</div>
@endif
@else
<div class="mb-10 rounded alert alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>You must complete this step before you can use this source!</span>
</form>
@else
<div class="flex items-center gap-2 pb-4">
<h1>GitHub App</h1>
<div class="flex gap-2">
<x-forms.button isError isModal modalId="deleteSource">
Delete
</x-forms.button>
</div>
<form class="flex gap-4">
<h2>Register a GitHub App</h2>
<div class="pt-1 pb-2 ">You need to register a GitHub App before using this source.</div>
<div class="pt-2 pb-10">
@if (!isCloud() || isDev())
<div class="flex items-end gap-2">
<x-forms.select wire:model='webhook_endpoint' label="Webhook Endpoint"
helper="All Git webhooks will be sent to this endpoint. <br><br>If you would like to use domain instead of IP address, set your Coolify instance's FQDN in the Settings menu.">
@if ($ipv4)
<option value="{{ $ipv4 }}">Use {{ $ipv4 }}</option>
@endif
@if ($ipv6)
<option value="{{ $ipv6 }}">Use {{ $ipv6 }}</option>
@endif
@if ($fqdn)
<option value="{{ $fqdn }}">Use {{ $fqdn }}</option>
@endif
@if (config('app.url'))
<option value="{{ config('app.url') }}">Use {{ config('app.url') }}</option>
@endif
</x-forms.select>
<x-forms.button
x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}')">
Register
</x-forms.button>
</div>
@else
</div>
<div class="mb-10 rounded alert alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>You must complete this step before you can use this source!</span>
</div>
<div class="flex flex-col">
<h2 >Register a GitHub App</h2>
<div >You need to register a GitHub App before using this source.</div>
<div class="py-10">
@if (!isCloud() || isDev())
<div class="flex items-end gap-2">
<x-forms.select wire:model='webhook_endpoint' label="Webhook Endpoint"
helper="All Git webhooks will be sent to this endpoint. <br><br>If you would like to use domain instead of IP address, set your Coolify instance's FQDN in the Settings menu.">
@if ($ipv4)
<option value="{{ $ipv4 }}">Use {{ $ipv4 }}</option>
@endif
@if ($ipv6)
<option value="{{ $ipv6 }}">Use {{ $ipv6 }}</option>
@endif
@if ($fqdn)
<option value="{{ $fqdn }}">Use {{ $fqdn }}</option>
@endif
@if (config('app.url'))
<option value="{{ config('app.url') }}">Use {{ config('app.url') }}</option>
@endif
</x-forms.select>
<x-forms.button
x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}')">
Register Now
Register
</x-forms.button>
@endif
<div class="flex flex-col gap-2 pt-4">
<x-forms.checkbox disabled instantSave id="default_permissions" label="Default Permissions"
helper="Contents: read<br>Metadata: read<br>Email: read" />
<x-forms.checkbox instantSave id="preview_deployment_permissions"
label="Preview Deployments Permission"
helper="Necessary for updating pull requests with useful comments (deployment status, links, etc.)<br><br>Pull Request: read & write" />
</div>
@else
<x-forms.button
x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}')">
Register Now
</x-forms.button>
@endif
<div class="flex flex-col gap-2 pt-4">
<x-forms.checkbox disabled instantSave id="default_permissions" label="Default Permissions"
helper="Contents: read<br>Metadata: read<br>Email: read" />
<x-forms.checkbox instantSave id="preview_deployment_permissions"
label="Preview Deployments Permission"
helper="Necessary for updating pull requests with useful comments (deployment status, links, etc.)<br><br>Pull Request: read & write" />
</div>
</form>
<script>
function createGithubApp(webhook_endpoint, preview_deployment_permissions) {
const {
organization,
uuid,
html_url
} = @json($github_app);
let baseUrl = webhook_endpoint;
const name = @js($name);
const isDev = @js(config('app.env')) ===
'local';
const devWebhook = @js(config('coolify.dev_webhook'));
if (isDev && devWebhook) {
baseUrl = devWebhook;
}
const webhookBaseUrl = `${baseUrl}/webhooks`;
const path = organization ? `organizations/${organization}/settings/apps/new` : 'settings/apps/new';
const default_permissions = {
contents: 'read',
metadata: 'read',
emails: 'read'
};
if (preview_deployment_permissions) {
default_permissions.pull_requests = 'write';
}
const data = {
name,
url: baseUrl,
hook_attributes: {
url: `${webhookBaseUrl}/source/github/events`,
active: true,
},
redirect_url: `${webhookBaseUrl}/source/github/redirect`,
callback_urls: [`${baseUrl}/login/github/app`],
public: false,
request_oauth_on_install: false,
setup_url: `${webhookBaseUrl}/source/github/install?source=${uuid}`,
setup_on_update: true,
default_permissions,
default_events: ['pull_request', 'push']
};
const form = document.createElement('form');
form.setAttribute('method', 'post');
form.setAttribute('action', `${html_url}/${path}?state=${uuid}`);
const input = document.createElement('input');
input.setAttribute('id', 'manifest');
input.setAttribute('name', 'manifest');
input.setAttribute('type', 'hidden');
input.setAttribute('value', JSON.stringify(data));
form.appendChild(input);
document.getElementsByTagName('body')[0].appendChild(form);
form.submit();
</div>
</div>
<script>
function createGithubApp(webhook_endpoint, preview_deployment_permissions) {
const {
organization,
uuid,
html_url
} = @json($github_app);
let baseUrl = webhook_endpoint;
const name = @js($name);
const isDev = @js(config('app.env')) ===
'local';
const devWebhook = @js(config('coolify.dev_webhook'));
if (isDev && devWebhook) {
baseUrl = devWebhook;
}
</script>
@endif
</form>
const webhookBaseUrl = `${baseUrl}/webhooks`;
const path = organization ? `organizations/${organization}/settings/apps/new` : 'settings/apps/new';
const default_permissions = {
contents: 'read',
metadata: 'read',
emails: 'read'
};
if (preview_deployment_permissions) {
default_permissions.pull_requests = 'write';
}
const data = {
name,
url: baseUrl,
hook_attributes: {
url: `${webhookBaseUrl}/source/github/events`,
active: true,
},
redirect_url: `${webhookBaseUrl}/source/github/redirect`,
callback_urls: [`${baseUrl}/login/github/app`],
public: false,
request_oauth_on_install: false,
setup_url: `${webhookBaseUrl}/source/github/install?source=${uuid}`,
setup_on_update: true,
default_permissions,
default_events: ['pull_request', 'push']
};
const form = document.createElement('form');
form.setAttribute('method', 'post');
form.setAttribute('action', `${html_url}/${path}?state=${uuid}`);
const input = document.createElement('input');
input.setAttribute('id', 'manifest');
input.setAttribute('name', 'manifest');
input.setAttribute('type', 'hidden');
input.setAttribute('value', JSON.stringify(data));
form.appendChild(input);
document.getElementsByTagName('body')[0].appendChild(form);
form.submit();
}
</script>
@endif
</div>

View File

@@ -10,7 +10,7 @@
<div class="text-xs truncate subtitle lg:text-sm">{{ $project->name }}</div>
<div class="grid gap-2 lg:grid-cols-2">
@forelse ($project->environments as $environment)
<a class="items-center justify-center box description" href="{{ route('project.resources', [$project->uuid, $environment->name]) }}">
<a class="items-center justify-center font-bold box" href="{{ route('project.resources', [$project->uuid, $environment->name]) }}">
{{ $environment->name }}
</a>
@empty

View File

@@ -20,11 +20,10 @@ use App\Http\Livewire\Server\PrivateKey\Show as PrivateKeyShow;
use App\Http\Livewire\Server\Proxy\Show as ProxyShow;
use App\Http\Livewire\Server\Proxy\Logs as ProxyLogs;
use App\Http\Livewire\Server\Show;
use App\Http\Livewire\Source\Github\Change as GitHubChange;
use App\Http\Livewire\Subscription\Show as SubscriptionShow;
use App\Http\Livewire\Waitlist\Index as WaitlistIndex;
use App\Models\GithubApp;
use App\Models\GitlabApp;
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Server;
use App\Models\StandaloneDocker;
@@ -178,49 +177,7 @@ Route::middleware(['auth'])->group(function () {
'sources' => $sources,
]);
})->name('source.all');
Route::get('/source/github/{github_app_uuid}', function (Request $request) {
$github_app = GithubApp::where('uuid', request()->github_app_uuid)->first();
if (!$github_app) {
abort(404);
}
$github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$settings = InstanceSettings::get();
$name = Str::of(Str::kebab($github_app->name));
if ($settings->public_ipv4) {
$ipv4 = 'http://' . $settings->public_ipv4 . ':' . config('app.port');
}
if ($settings->public_ipv6) {
$ipv6 = 'http://' . $settings->public_ipv6 . ':' . config('app.port');
}
if ($github_app->installation_id && session('from')) {
$source_id = data_get(session('from'), 'source_id');
if (!$source_id || $github_app->id !== $source_id) {
session()->forget('from');
} else {
$parameters = data_get(session('from'), 'parameters');
$back = data_get(session('from'), 'back');
$environment_name = data_get($parameters, 'environment_name');
$project_uuid = data_get($parameters, 'project_uuid');
$type = data_get($parameters, 'type');
$destination = data_get($parameters, 'destination');
session()->forget('from');
return redirect()->route($back, [
'environment_name' => $environment_name,
'project_uuid' => $project_uuid,
'type' => $type,
'destination' => $destination,
]);
}
}
return view('source.github.show', [
'github_app' => $github_app,
'name' => $name,
'ipv4' => $ipv4 ?? null,
'ipv6' => $ipv6 ?? null,
'fqdn' => $settings->fqdn,
]);
})->name('source.github.show');
Route::get('/source/github/{github_app_uuid}', GitHubChange::class)->name('source.github.show');
Route::get('/source/gitlab/{gitlab_app_uuid}', function (Request $request) {
$gitlab_app = GitlabApp::where('uuid', request()->gitlab_app_uuid)->first();
return view('source.gitlab.show', [

View File

@@ -284,7 +284,7 @@ Route::post('/payments/stripe/events', function () {
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (!$subscription) {
Sleep::for(5);
Sleep::for(5)->seconds();
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
}
$planId = data_get($data, 'lines.data.0.plan.id');

View File

@@ -5,7 +5,7 @@
## Always run "php artisan app:sync-to-bunny-cdn --env=secrets" or "scripts/run sync-bunny" if you update this file.
###########
VERSION="1.0.2"
VERSION="1.0.3"
DOCKER_VERSION="24.0"
CDN="https://cdn.coollabs.io/coolify"
@@ -46,7 +46,14 @@ apt install -y curl wget git jq jc >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo "Docker is not installed. Installing Docker..."
curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh
echo "Docker installed successfully"
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Docker installation failed."
echo "Maybe your OS is not supported."
echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
fi
echo -e "-------------"
echo -e "Check Docker Configuration..."

View File

@@ -3,6 +3,8 @@ _APP_LOCALE=en
_APP_OPTIONS_ABUSE=enabled
_APP_OPTIONS_FORCE_HTTPS=disabled
_APP_OPENSSL_KEY_V1=
_APP_DOMAIN=
_APP_DOMAIN_TARGET=
_APP_DOMAIN_FUNCTIONS=
_APP_CONSOLE_WHITELIST_ROOT=enabled
_APP_CONSOLE_WHITELIST_EMAILS=
@@ -18,19 +20,19 @@ _APP_USAGE_AGGREGATION_INTERVAL=30
_APP_USAGE_TIMESERIES_INTERVAL=30
_APP_USAGE_DATABASE_INTERVAL=900
_APP_WORKER_PER_CORE=6
_APP_REDIS_HOST=redis
_APP_REDIS_HOST=appwrite-redis
_APP_REDIS_PORT=6379
_APP_REDIS_USER=
_APP_REDIS_PASS=
_APP_DB_HOST=mariadb
_APP_DB_HOST=appwrite-mariadb
_APP_DB_PORT=3306
_APP_DB_SCHEMA=appwrite
_APP_DB_USER=$SERVICE_USER_MYSQL
_APP_DB_PASS=$SERVICE_PASSWORD_MYSQL
_APP_DB_ROOT_PASS=$SERVICE_PASSWORD_ROOTMYSQL
_APP_INFLUXDB_HOST=influxdb
_APP_INFLUXDB_HOST=appwrite-influxdb
_APP_INFLUXDB_PORT=8086
_APP_STATSD_HOST=telegraf
_APP_STATSD_HOST=appwrite-telegraf
_APP_STATSD_PORT=8125
_APP_SMTP_HOST=
_APP_SMTP_PORT=
@@ -42,7 +44,7 @@ _APP_SMS_FROM=
_APP_STORAGE_LIMIT=30000000
_APP_STORAGE_PREVIEW_LIMIT=20000000
_APP_STORAGE_ANTIVIRUS=disabled
_APP_STORAGE_ANTIVIRUS_HOST=clamav
_APP_STORAGE_ANTIVIRUS_HOST=appwrite-clamav
_APP_STORAGE_ANTIVIRUS_PORT=3310
_APP_STORAGE_DEVICE=local
_APP_STORAGE_S3_ACCESS_KEY=
@@ -72,11 +74,10 @@ _APP_FUNCTIONS_CONTAINERS=10
_APP_FUNCTIONS_CPUS=0
_APP_FUNCTIONS_MEMORY=0
_APP_FUNCTIONS_MEMORY_SWAP=0
_APP_FUNCTIONS_RUNTIMES=node-16.0,php-8.0,python-3.9,ruby-3.0
_APP_EXECUTOR_SECRET=your-secret-key
_APP_FUNCTIONS_RUNTIMES=node-20.0,php-8.2,python-3.11,ruby-3.2
_APP_EXECUTOR_SECRET=$SERVICE_PASSWORD_64_APPWRITE
_APP_EXECUTOR_HOST=http://appwrite-executor/v1
_APP_EXECUTOR_RUNTIME_NETWORK=appwrite_runtimes
_APP_FUNCTIONS_ENVS=node-16.0,php-7.4,python-3.9,ruby-3.0
_APP_FUNCTIONS_INACTIVE_THRESHOLD=60
DOCKERHUB_PULL_USERNAME=
DOCKERHUB_PULL_PASSWORD=

View File

@@ -17,19 +17,6 @@ services:
image: appwrite/appwrite:1.4
container_name: appwrite
<<: *x-logging
labels:
- traefik.constraint-label-stack=appwrite
- traefik.docker.network=appwrite
- traefik.http.services.appwrite_api.loadbalancer.server.port=80
#http
- traefik.http.routers.appwrite_api_http.entrypoints=web
- traefik.http.routers.appwrite_api_http.rule=PathPrefix(`/`)
- traefik.http.routers.appwrite_api_http.service=appwrite_api
# https
- traefik.http.routers.appwrite_api_https.entrypoints=websecure
- traefik.http.routers.appwrite_api_https.rule=PathPrefix(`/`)
- traefik.http.routers.appwrite_api_https.service=appwrite_api
- traefik.http.routers.appwrite_api_https.tls=true
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-cache:/storage/cache:rw
@@ -37,10 +24,10 @@ services:
- appwrite-certificates:/storage/certificates:rw
- appwrite-functions:/storage/functions:rw
depends_on:
- mariadb
- redis
# - clamav
- influxdb
- appwrite-mariadb
- appwrite-redis
# - appwrite-clamav
- appwrite-influxdb
environment:
- SERVICE_FQDN_APPWRITE=/
- _APP_ENV
@@ -56,9 +43,9 @@ services:
- _APP_OPTIONS_ABUSE
- _APP_OPTIONS_FORCE_HTTPS
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_DOMAIN_FUNCTIONS
- _APP_DOMAIN=$SERVICE_FQDN_APPWRITE
- _APP_DOMAIN_TARGET=$SERVICE_FQDN_APPWRITE
- _APP_DOMAIN_FUNCTIONS=$SERVICE_FQDN_APPWRITE
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -137,29 +124,12 @@ services:
- _APP_ASSISTANT_OPENAI_API_KEY
appwrite-realtime:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: realtime
container_name: appwrite-realtime
<<: *x-logging
labels:
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=appwrite"
- "traefik.http.services.appwrite_realtime.loadbalancer.server.port=80"
#ws
- traefik.http.routers.appwrite_realtime_ws.entrypoints=web
- traefik.http.routers.appwrite_realtime_ws.rule=PathPrefix(`/v1/realtime`)
- traefik.http.routers.appwrite_realtime_ws.service=appwrite_realtime
# wss
- traefik.http.routers.appwrite_realtime_wss.entrypoints=websecure
- traefik.http.routers.appwrite_realtime_wss.rule=PathPrefix(`/v1/realtime`)
- traefik.http.routers.appwrite_realtime_wss.service=appwrite_realtime
- traefik.http.routers.appwrite_realtime_wss.tls=true
- traefik.http.routers.appwrite_realtime_wss.tls.certresolver=dns
networks:
- appwrite
depends_on:
- mariadb
- redis
- appwrite-mariadb
- appwrite-redis
environment:
- SERVICE_FQDN_APPWRITE=/v1/realtime
- _APP_ENV
@@ -180,16 +150,13 @@ services:
- _APP_LOGGING_CONFIG
appwrite-worker-audits:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: worker-audits
<<: *x-logging
container_name: appwrite-worker-audits
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
- appwrite-redis
- appwrite-mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -207,16 +174,13 @@ services:
- _APP_LOGGING_CONFIG
appwrite-worker-webhooks:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: worker-webhooks
<<: *x-logging
container_name: appwrite-worker-webhooks
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
- appwrite-redis
- appwrite-mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -230,16 +194,13 @@ services:
- _APP_LOGGING_CONFIG
appwrite-worker-deletes:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: worker-deletes
<<: *x-logging
container_name: appwrite-worker-deletes
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
- appwrite-redis
- appwrite-mariadb
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-cache:/storage/cache:rw
@@ -286,16 +247,13 @@ services:
- _APP_EXECUTOR_HOST
appwrite-worker-databases:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: worker-databases
<<: *x-logging
container_name: appwrite-worker-databases
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
- appwrite-redis
- appwrite-mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -313,16 +271,13 @@ services:
- _APP_LOGGING_CONFIG
appwrite-worker-builds:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: worker-builds
<<: *x-logging
container_name: appwrite-worker-builds
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
- appwrite-redis
- appwrite-mariadb
volumes:
- appwrite-functions:/storage/functions:rw
- appwrite-builds:/storage/builds:rw
@@ -375,16 +330,13 @@ services:
- _APP_STORAGE_WASABI_BUCKET
appwrite-worker-certificates:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: worker-certificates
<<: *x-logging
container_name: appwrite-worker-certificates
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
- appwrite-redis
- appwrite-mariadb
volumes:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
@@ -409,16 +361,13 @@ services:
- _APP_LOGGING_CONFIG
appwrite-worker-functions:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: worker-functions
<<: *x-logging
container_name: appwrite-worker-functions
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
- appwrite-redis
- appwrite-mariadb
- openruntimes-executor
environment:
- _APP_ENV
@@ -446,15 +395,12 @@ services:
- _APP_LOGGING_PROVIDER
appwrite-worker-mails:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: worker-mails
<<: *x-logging
container_name: appwrite-worker-mails
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- appwrite-redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -474,15 +420,12 @@ services:
- _APP_LOGGING_CONFIG
appwrite-worker-messaging:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: worker-messaging
<<: *x-logging
container_name: appwrite-worker-messaging
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- appwrite-redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -496,15 +439,12 @@ services:
- _APP_LOGGING_CONFIG
appwrite-worker-migrations:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: worker-migrations
<<: *x-logging
container_name: appwrite-worker-migrations
restart: unless-stopped
networks:
- appwrite
depends_on:
- mariadb
- appwrite-mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -527,15 +467,12 @@ services:
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
appwrite-maintenance:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: maintenance
<<: *x-logging
container_name: appwrite-maintenance
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- appwrite-redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -561,16 +498,13 @@ services:
- _APP_MAINTENANCE_RETENTION_SCHEDULES
appwrite-usage:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: usage
container_name: appwrite-usage
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
depends_on:
- influxdb
- mariadb
- appwrite-influxdb
- appwrite-mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -592,16 +526,13 @@ services:
- _APP_LOGGING_CONFIG
appwrite-schedule:
image: appwrite/appwrite:1.4.3
image: appwrite/appwrite:1.4
entrypoint: schedule
container_name: appwrite-schedule
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
depends_on:
- mariadb
- redis
- appwrite-mariadb
- appwrite-redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -616,15 +547,13 @@ services:
- _APP_DB_USER
- _APP_DB_PASS
appwrite-assistant:
image: appwrite/assistant:0.2.1
container_name: appwrite-assistant
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
environment:
- _APP_ASSISTANT_OPENAI_API_KEY
# appwrite-assistant:
# image: appwrite/assistant:0.2.1
# container_name: appwrite-assistant
# <<: *x-logging
#
# environment:
# - _APP_ASSISTANT_OPENAI_API_KEY
openruntimes-executor:
container_name: openruntimes-executor
@@ -632,9 +561,6 @@ services:
<<: *x-logging
stop_signal: SIGINT
image: openruntimes/executor:0.4.1
networks:
- appwrite
- runtimes
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- appwrite-builds:/storage/builds:rw
@@ -675,13 +601,10 @@ services:
- OPR_EXECUTOR_STORAGE_WASABI_REGION=$_APP_STORAGE_WASABI_REGION
- OPR_EXECUTOR_STORAGE_WASABI_BUCKET=$_APP_STORAGE_WASABI_BUCKET
mariadb:
appwrite-mariadb:
image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p
container_name: appwrite-mariadb
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
volumes:
- appwrite-mariadb:/var/lib/mysql:rw
environment:
@@ -691,59 +614,40 @@ services:
- MYSQL_PASSWORD=${_APP_DB_PASS}
command: 'mysqld --innodb-flush-method=fsync'
redis:
appwrite-redis:
image: redis:7.0.4-alpine
container_name: appwrite-redis
<<: *x-logging
restart: unless-stopped
command: >
redis-server
--maxmemory 512mb
--maxmemory-policy allkeys-lru
--maxmemory-samples 5
networks:
- appwrite
volumes:
- appwrite-redis:/data:rw
# clamav:
# appwrite-clamav:
# image: appwrite/clamav:1.2.0
# container_name: appwrite-clamav
# restart: unless-stopped
# networks:
# - appwrite
#
# volumes:
# - appwrite-uploads:/storage/uploads
influxdb:
appwrite-influxdb:
image: appwrite/influxdb:1.5.0
container_name: appwrite-influxdb
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
volumes:
- appwrite-influxdb:/var/lib/influxdb:rw
telegraf:
appwrite-telegraf:
image: appwrite/telegraf:1.4.0
container_name: appwrite-telegraf
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
environment:
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
networks:
gateway:
name: gateway
appwrite:
name: appwrite
runtimes:
name: runtimes
volumes:
appwrite-mariadb:
appwrite-redis:

View File

@@ -0,0 +1,45 @@
# documentation: https://docs.gitea.com
# slogan: Gitea (with MariaDB) is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.
# tags: version control, collaboration, code, hosting, lightweight, mariadb
services:
gitea:
image: gitea/gitea:latest
environment:
- SERVICE_FQDN_GITEA_3000
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=mysql
- GITEA__database__HOST=mariadb
- GITEA__database__NAME=${MYSQL_DATABASE-gitea}
- GITEA__database__USER=$SERVICE_USER_MYSQL
- GITEA__database__PASSWD=$SERVICE_PASSWORD_MYSQL
volumes:
- gitea-data:/var/lib/gitea
- gitea-timezone:/etc/timezone:ro
- gitea-localtime:/etc/localtime:ro
ports:
- 22222:22
depends_on:
mariadb:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000"]
interval: 2s
timeout: 10s
retries: 15
mariadb:
image: mariadb:11
volumes:
- gitea-mariadb-data:/var/lib/mysql
environment:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 20s
retries: 10

View File

@@ -0,0 +1,45 @@
# documentation: https://docs.gitea.com
# slogan: Gitea (with MySQL) is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.
# tags: version control, collaboration, code, hosting, lightweight, mysql
services:
gitea:
image: gitea/gitea:latest
environment:
- SERVICE_FQDN_GITEA_3000
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=mysql
- GITEA__database__HOST=mysql
- GITEA__database__NAME=${MYSQL_DATABASE-gitea}
- GITEA__database__USER=$SERVICE_USER_MYSQL
- GITEA__database__PASSWD=$SERVICE_PASSWORD_MYSQL
volumes:
- gitea-data:/var/lib/gitea
- gitea-timezone:/etc/timezone:ro
- gitea-localtime:/etc/localtime:ro
ports:
- 22222:22
depends_on:
mysql:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000"]
interval: 2s
timeout: 10s
retries: 15
mysql:
image: mysql:8.0
volumes:
- gitea-mysql-data:/var/lib/mysql
environment:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 20s
retries: 10

View File

@@ -0,0 +1,44 @@
# documentation: https://docs.gitea.com
# slogan: Gitea (with PostgreSQL)vis a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.
# tags: version control, collaboration, code, hosting, lightweight, postgresql
services:
gitea:
image: gitea/gitea:latest
environment:
- SERVICE_FQDN_GITEA_3000
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=postgresql
- GITEA__database__NAME=${POSTGRESQL_DATABASE-gitea}
- GITEA__database__USER=$SERVICE_USER_POSTGRESQL
- GITEA__database__PASSWD=$SERVICE_PASSWORD_POSTGRESQL
volumes:
- gitea-data:/var/lib/gitea
- gitea-timezone:/etc/timezone:ro
- gitea-localtime:/etc/localtime:ro
ports:
- 22222:22
depends_on:
postgresql:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000"]
interval: 2s
timeout: 10s
retries: 15
postgresql:
image: postgres:15-alpine
volumes:
- gitea-postgresql-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${SERVICE_USER_POSTGRESQL}
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
- POSTGRES_DB=${POSTGRESQL_DATABASE}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 20s
retries: 10

View File

@@ -0,0 +1,22 @@
# documentation: https://docs.gitea.com
# slogan: Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.
# tags: version control, collaboration, code, hosting, lightweight
services:
gitea:
image: gitea/gitea:latest
environment:
- SERVICE_FQDN_GITEA_3000
- USER_UID=1000
- USER_GID=1000
ports:
- 22222:22
volumes:
- gitea-data:/var/lib/gitea
- gitea-timezone:/etc/timezone:ro
- gitea-localtime:/etc/localtime:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000"]
interval: 2s
timeout: 10s
retries: 15

View File

@@ -8,11 +8,12 @@ services:
environment:
- SERVICE_FQDN_N8N
- N8N_EDITOR_BASE_URL=${SERVICE_FQDN_N8N}
- N8N_HOST=${SERVICE_FQDN_N8N}
- WEBHOOK_URL=${SERVICE_FQDN_N8N}
- N8N_HOST=${SERVICE_URL_N8N}
- GENERIC_TIMEZONE="Europe/Berlin"
- TZ="Europe/Berlin"
- DB_TYPE=postgresdb
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-umami}
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n}
- DB_POSTGRESDB_HOST=postgresql
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_USER=$SERVICE_USER_POSTGRES
@@ -29,7 +30,7 @@ services:
environment:
- POSTGRES_USER=$SERVICE_USER_POSTGRES
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
- POSTGRES_DB=${POSTGRES_DB:-umami}
- POSTGRES_DB=${POSTGRES_DB:-n8n}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s

View File

@@ -8,7 +8,8 @@ services:
environment:
- SERVICE_FQDN_N8N
- N8N_EDITOR_BASE_URL=${SERVICE_FQDN_N8N}
- N8N_HOST=${SERVICE_FQDN_N8N}
- WEBHOOK_URL=${SERVICE_FQDN_N8N}
- N8N_HOST=${SERVICE_URL_N8N}
- GENERIC_TIMEZONE="Europe/Berlin"
- TZ="Europe/Berlin"
volumes:

View File

@@ -0,0 +1,20 @@
# documentation: https://docs.nextcloud.com
# slogan: NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management.
# tags: cloud, collaboration, communication, filestorage, data
services:
nextcloud:
image: lscr.io/linuxserver/nextcloud:latest
environment:
- SERVICE_FQDN_NEXTCLOUD
- PUID=1000
- PGID=1000
- TZ=Europe/Madrid
volumes:
- nextcloud-config:/config
- nextcloud-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80"]
interval: 2s
timeout: 10s
retries: 15

File diff suppressed because one or more lines are too long

View File

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