Compare commits

..

25 Commits

Author SHA1 Message Date
Andras Bacsai
0131f5e341 Merge pull request #1807 from coollabsio/next
v4.0.0-beta.234
2024-03-04 13:40:42 +01:00
Andras Bacsai
b5ab9a8da6 Add custom docker run options for application 2024-03-04 13:39:34 +01:00
Andras Bacsai
57fa2709da Add font preloading and DNS prefetching 2024-03-04 13:34:20 +01:00
Andras Bacsai
96c6a198d7 Fix base64 encoding for TOTP_VAULT_KEY 2024-03-04 12:50:56 +01:00
Andras Bacsai
d106d4bd4e Refactor generateEnvValue function to use base64 encoding for certain cases 2024-03-04 12:46:37 +01:00
Andras Bacsai
53cd3091f7 Add Directus service fields to extraFields method 2024-03-04 12:46:33 +01:00
Andras Bacsai
f1e7b870aa Add TOTP_VAULT_KEY environment variable to Plausible service 2024-03-04 12:30:32 +01:00
Andras Bacsai
99fe076b5a Add scheduled task for database cleanup if not in cloud environment 2024-03-04 12:17:33 +01:00
Andras Bacsai
65fcaa17d9 Update exception in PreventRequestsDuringMaintenance middleware and version numbers 2024-03-04 11:41:02 +01:00
Andras Bacsai
89c6563e00 Merge pull request #1806 from coollabsio/next
v4.0.0-beta.233
2024-03-04 11:18:56 +01:00
Andras Bacsai
76b0bef32e Update slogan and logo for changedetection service 2024-03-04 11:17:05 +01:00
Andras Bacsai
c20aa0b256 Refactor method names to use camel case 2024-03-04 11:01:14 +01:00
Andras Bacsai
b4908cfcb4 Merge pull request #1804 from RayBB/change-detection
add changedetection.io template
2024-03-04 10:47:54 +01:00
Andras Bacsai
4fb5b04d27 Update proxy configuration layout 2024-03-04 10:46:53 +01:00
Andras Bacsai
8385bbb0a0 feat: gzip enabled & stipprefix setting
refactor: code
2024-03-04 10:46:13 +01:00
Andras Bacsai
cee6b54033 Add proxy start functionality when selecting a proxy type 2024-03-04 10:42:54 +01:00
Andras Bacsai
0dd591a5ff fix: raw compose make dirs
fix: raw compose add coolify labels
2024-03-04 10:13:40 +01:00
Andras Bacsai
62278126e4 fixes 2024-03-04 09:12:23 +01:00
Andras Bacsai
0aa85a3701 fix: service status updated 2024-03-04 08:57:18 +01:00
Andras Bacsai
0e1ba64836 fix: sentry error 2024-03-04 08:51:24 +01:00
Andras Bacsai
f7e1ce8656 fix: env value generation 2024-03-04 08:49:53 +01:00
RayBB
5030c14dc2 add changedetection.io template 2024-03-03 23:42:33 +01:00
Andras Bacsai
1333cd1d84 Merge pull request #1799 from coollabsio/next
v4.0.0-beta.232
2024-03-02 16:04:06 +01:00
Andras Bacsai
112c259d27 Refactor destinations method in Server model 2024-03-02 15:58:02 +01:00
Andras Bacsai
130d1e1756 Update DockerCleanupJob and version numbers 2024-03-02 15:18:49 +01:00
47 changed files with 453 additions and 82 deletions

View File

@@ -15,6 +15,9 @@ class StartProxy
{
try {
$proxyType = $server->proxyType();
if ($proxyType === 'NONE') {
return 'OK';
}
$commands = collect([]);
$proxy_path = get_proxy_path();
$configuration = CheckConfiguration::run($server);

View File

@@ -47,6 +47,10 @@ class Kernel extends ConsoleKernel
$this->check_resources($schedule);
$this->pull_helper_image($schedule);
$this->check_scheduled_tasks($schedule);
if (!isCloud()) {
$schedule->command('cleanup:database --yes')->daily();
}
}
}
private function pull_helper_image($schedule)
@@ -68,35 +72,35 @@ class Kernel extends ConsoleKernel
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
}
foreach ($containerServers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
// $schedule
// ->call(function () use ($server) {
// $randomSeconds = rand(1, 40);
// $job = new ContainerStatusJob($server);
// $job->delay($randomSeconds);
// ray('dispatching container status job in ' . $randomSeconds . ' seconds');
// dispatch($job);
// })->name('container-status-' . $server->id)->everyMinute()->onOneServer();
// $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
$schedule
->call(function () use ($server) {
$randomSeconds = rand(1, 40);
$job = new ContainerStatusJob($server);
$job->delay($randomSeconds);
ray('dispatching container status job in ' . $randomSeconds . ' seconds');
dispatch($job);
})->name('container-status-' . $server->id)->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
// $schedule
// ->call(function () use ($server) {
// $randomSeconds = rand(1, 40);
// $job = new CheckLogDrainContainerJob($server);
// $job->delay($randomSeconds);
// dispatch($job);
// })->name('log-drain-container-check-' . $server->id)->everyMinute()->onOneServer();
// $schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
$schedule
->call(function () use ($server) {
$randomSeconds = rand(1, 40);
$job = new CheckLogDrainContainerJob($server);
$job->delay($randomSeconds);
dispatch($job);
})->name('log-drain-container-check-' . $server->id)->everyMinute()->onOneServer();
}
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
// $schedule
// ->call(function () use ($server) {
// $randomSeconds = rand(1, 40);
// $job = new ServerStatusJob($server);
// $job->delay($randomSeconds);
// dispatch($job);
// })->name('server-status-job-' . $server->id)->everyMinute()->onOneServer();
// $schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
$schedule
->call(function () use ($server) {
$randomSeconds = rand(1, 40);
$job = new ServerStatusJob($server);
$job->delay($randomSeconds);
dispatch($job);
})->name('server-status-job-' . $server->id)->everyMinute()->onOneServer();
}
}
private function instance_auto_update($schedule)

View File

@@ -77,6 +77,9 @@ class Handler extends ExceptionHandler
);
}
);
if (str($e->getMessage())->contains('No space left on device')) {
return;
}
ray('reporting to sentry');
Integration::captureUnhandledException($e);
});

View File

@@ -13,5 +13,6 @@ class PreventRequestsDuringMaintenance extends Middleware
*/
protected $except = [
'webhooks/*',
'/api/health'
];
}

View File

@@ -374,6 +374,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->cleanup_git();
$this->application->loadComposeFile(isInit: false);
if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->application->parseRawCompose();
$yaml = $composeFile = $this->application->docker_compose_raw;
} else {
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id);
@@ -413,16 +414,33 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]);
}
$this->write_deployment_configurations();
// Start compose file
if ($this->docker_compose_custom_start_command) {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), "hidden" => true],
);
if ($this->application->settings->is_raw_compose_deployment_enabled) {
if ($this->docker_compose_custom_start_command) {
$this->execute_remote_command(
["cd {$this->basedir} && {$this->docker_compose_custom_start_command}", "hidden" => true],
);
} else {
$server_workdir = $this->application->workdir();
ray("SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d");
$this->execute_remote_command(
["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d", "hidden" => true],
);
}
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true],
);
if ($this->docker_compose_custom_start_command) {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true],
);
}
}
$this->application_deployment_queue->addLogEntry("New container started.");
}
private function deploy_dockerfile_buildpack()

View File

@@ -51,6 +51,9 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
{
$this->backup = $backup;
$this->team = Team::find($backup->team_id);
if (is_null($this->team)) {
return;
}
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->service->server;
@@ -316,7 +319,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
private function backup_standalone_mongodb(string $databaseWithCollections): void
{
try {
$url = $this->database->getDbUrl(useInternal: true);
$url = $this->database->get_db_url(useInternal: true);
if ($databaseWithCollections === 'all') {
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";

View File

@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Actions\Server\CleanupDocker;
use App\Models\Server;
use App\Notifications\Server\DockerCleanup;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -47,8 +48,9 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
CleanupDocker::run($this->server);
$usageAfter = $this->server->getDiskUsage();
if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
$this->server->team?->notify(new DockerCleanup($this->server, 'Saved ' . ($this->usageBefore - $usageAfter) . '% disk space.'));
// ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
// send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
Log::info('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
} else {
Log::info('DockerCleanupJob failed to save disk space on ' . $this->server->name);

View File

@@ -9,6 +9,8 @@ class Advanced extends Component
{
public Application $application;
public bool $is_force_https_enabled;
public bool $is_gzip_enabled;
public bool $is_stripprefix_enabled;
protected $rules = [
'application.settings.is_git_submodules_enabled' => 'boolean|required',
'application.settings.is_git_lfs_enabled' => 'boolean|required',
@@ -19,13 +21,17 @@ class Advanced extends Component
'application.settings.is_gpu_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_consistent_container_name_enabled' => 'boolean|required',
'application.settings.is_gzip_enabled' => 'boolean|required',
'application.settings.is_stripprefix_enabled' => 'boolean|required',
'application.settings.gpu_driver' => 'string|required',
'application.settings.gpu_count' => 'string|required',
'application.settings.gpu_device_ids' => 'string|required',
'application.settings.gpu_options' => 'string|required',
];
public function mount() {
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
$this->is_force_https_enabled = $this->application->isForceHttpsEnabled();
$this->is_gzip_enabled = $this->application->isGzipEnabled();
$this->is_stripprefix_enabled = $this->application->isStripprefixEnabled();
}
public function instantSave()
{
@@ -40,6 +46,14 @@ class Advanced extends Component
$this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
$this->dispatch('resetDefaultLabels', false);
}
if ($this->application->settings->is_gzip_enabled !== $this->is_gzip_enabled) {
$this->application->settings->is_gzip_enabled = $this->is_gzip_enabled;
$this->dispatch('resetDefaultLabels', false);
}
if ($this->application->settings->is_stripprefix_enabled !== $this->is_stripprefix_enabled) {
$this->application->settings->is_stripprefix_enabled = $this->is_stripprefix_enabled;
$this->dispatch('resetDefaultLabels', false);
}
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');
}

View File

@@ -263,7 +263,11 @@ class General extends Component
}
if ($this->application->build_pack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->parsedServices = $this->application->parseCompose();
if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->application->parseRawCompose();
} else {
$this->parsedServices = $this->application->parseCompose();
}
}
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();

View File

@@ -46,9 +46,9 @@ class General extends Component
public function mount()
{
$this->db_url = $this->database->getDbUrl(true);
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl();
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced() {
@@ -93,7 +93,7 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl();
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);

View File

@@ -44,9 +44,9 @@ class General extends Component
public function mount()
{
$this->db_url = $this->database->getDbUrl(true);
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl();
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced()
@@ -95,7 +95,7 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl();
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);

View File

@@ -46,9 +46,9 @@ class General extends Component
public function mount()
{
$this->db_url = $this->database->getDbUrl(true);
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl();
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced()
@@ -94,7 +94,7 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl();
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);

View File

@@ -53,9 +53,9 @@ class General extends Component
];
public function mount()
{
$this->db_url = $this->database->getDbUrl(true);
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl();
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced() {
@@ -87,7 +87,7 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl();
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);

View File

@@ -39,9 +39,9 @@ class General extends Component
];
public function mount()
{
$this->db_url = $this->database->getDbUrl(true);
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->getDbUrl();
$this->db_url_public = $this->database->get_db_url();
}
}
public function instantSaveAdvanced() {
@@ -86,7 +86,7 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getDbUrl();
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);

View File

@@ -18,6 +18,7 @@ class Configuration extends Component
$userId = auth()->user()->id;
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
"check_status"
];
}
public function render()

View File

@@ -6,7 +6,6 @@ use App\Actions\Shared\PullImage;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Events\ServiceStatusChanged;
use App\Jobs\ContainerStatusJob;
use App\Models\Service;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
@@ -27,6 +26,10 @@ class Navbar extends Component
{
$this->dispatch('refresh')->self();
}
public function check_status() {
$this->dispatch('check_status');
$this->dispatch('success', 'Service status updated.');
}
public function render()
{
return view('livewire.project.service.navbar');

View File

@@ -18,6 +18,7 @@ class ServiceApplicationView extends Component
'application.required_fqdn' => 'required|boolean',
'application.is_log_drain_enabled' => 'nullable|boolean',
'application.is_gzip_enabled' => 'nullable|boolean',
'application.is_stripprefix_enabled' => 'nullable|boolean',
];
public function render()
{

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Server;
use App\Actions\Proxy\CheckConfiguration;
use App\Actions\Proxy\SaveConfiguration;
use App\Actions\Proxy\StartProxy;
use App\Models\Server;
use Livewire\Component;
use Illuminate\Support\Str;
@@ -26,7 +27,7 @@ class Proxy extends Component
public function proxyStatusUpdated()
{
$this->server->refresh();
$this->dispatch('refresh')->self();
}
public function change_proxy()
@@ -41,6 +42,9 @@ class Proxy extends Component
$this->server->proxy->set('type', $proxy_type);
$this->server->save();
$this->selectedProxy = $this->server->proxy->type;
if ($this->selectedProxy !== 'NONE') {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated');
}

View File

@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
class Application extends BaseModel
@@ -79,6 +80,18 @@ class Application extends BaseModel
}
return false;
}
public function isForceHttpsEnabled()
{
return data_get($this, 'settings.is_force_https_enabled', false);
}
public function isStripprefixEnabled()
{
return data_get($this, 'settings.is_stripprefix_enabled', true);
}
public function isGzipEnabled()
{
return data_get($this, 'settings.is_gzip_enabled', true);
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
@@ -476,6 +489,10 @@ class Application extends BaseModel
}
return false;
}
public function workdir()
{
return application_configuration_dir() . "/{$this->uuid}";
}
public function isLogDrainEnabled()
{
return data_get($this, 'settings.is_log_drain_enabled', false);
@@ -710,6 +727,64 @@ class Application extends BaseModel
];
}
}
function parseRawCompose()
{
try {
$yaml = Yaml::parse($this->docker_compose_raw);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
$services = data_get($yaml, 'services');
$commands = collect([]);
$services = collect($services)->map(function ($service) use ($commands) {
$serviceVolumes = collect(data_get($service, 'volumes', []));
if ($serviceVolumes->count() > 0) {
foreach ($serviceVolumes as $volume) {
$workdir = $this->workdir();
$type = null;
$source = null;
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
$type = Str::of('bind');
}
} else if (is_array($volume)) {
$type = data_get_str($volume, 'type');
$source = data_get_str($volume, 'source');
}
if ($type->value() === 'bind') {
if ($source->value() === "/var/run/docker.sock") {
continue;
}
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
continue;
}
if ($source->startsWith('.')) {
$source = $source->after('.');
$source = $workdir . $source;
}
$commands->push("mkdir -p $source > /dev/null 2>&1 || true");
}
}
}
$labels = collect(data_get($service, 'labels', []));
if (!$labels->contains('coolify.managed')) {
$labels->push('coolify.managed=true');
}
if (!$labels->contains('coolify.applicationId')) {
$labels->push('coolify.applicationId=' . $this->id);
}
if (!$labels->contains('coolify.type')) {
$labels->push('coolify.type=application');
}
data_set($service, 'labels', $labels->toArray());
return $service;
});
data_set($yaml, 'services', $services->toArray());
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
instant_remote_process($commands, $this->destination->server, false);
}
function parseCompose(int $pull_request_id = 0)
{
if ($this->docker_compose_raw) {

View File

@@ -3,7 +3,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;
class LocalFileVolume extends BaseModel
{

View File

@@ -365,8 +365,9 @@ class Server extends BaseModel
{
$standalone_docker = $this->hasMany(StandaloneDocker::class)->get();
$swarm_docker = $this->hasMany(SwarmDocker::class)->get();
$asd = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get();
return $standalone_docker->concat($swarm_docker)->concat($asd);
// $additional_dockers = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get();
// return $standalone_docker->concat($swarm_docker)->concat($additional_dockers);
return $standalone_docker->concat($swarm_docker);
}
public function standaloneDockers()

View File

@@ -102,6 +102,32 @@ class Service extends BaseModel
foreach ($applications as $application) {
$image = str($application->image)->before(':')->value();
switch ($image) {
case str($image)?->contains('directus'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
if ($admin_email) {
$data = $data->merge([
'Admin Email' => [
'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Directus', $data);
break;
case str($image)?->contains('kong'):
$data = collect([]);
$dashboard_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();

View File

@@ -23,6 +23,10 @@ class ServiceApplication extends BaseModel
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function isStripprefixEnabled()
{
return data_get($this, 'is_stripprefix_enabled', true);
}
public function isGzipEnabled()
{
return data_get($this, 'is_gzip_enabled', true);

View File

@@ -21,9 +21,13 @@ class ServiceDatabase extends BaseModel
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function isStripprefixEnabled()
{
return data_get($this, 'is_stripprefix_enabled', true);
}
public function isGzipEnabled()
{
return true;
return data_get($this, 'is_gzip_enabled', true);
}
public function type()
{

View File

@@ -126,7 +126,7 @@ class StandaloneMariadb extends BaseModel
);
}
public function getDbUrl(bool $useInternal = false): string
public function get_db_url(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";

View File

@@ -142,7 +142,7 @@ class StandaloneMongodb extends BaseModel
{
return 'standalone-mongodb';
}
public function getDbUrl(bool $useInternal = false)
public function get_db_url(bool $useInternal = false)
{
if ($this->is_public && !$useInternal) {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";

View File

@@ -127,7 +127,7 @@ class StandaloneMysql extends BaseModel
);
}
public function getDbUrl(bool $useInternal = false): string
public function get_db_url(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";

View File

@@ -126,7 +126,7 @@ class StandalonePostgresql extends BaseModel
{
return 'standalone-postgresql';
}
public function getDbUrl(bool $useInternal = false): string
public function get_db_url(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";

View File

@@ -122,7 +122,7 @@ class StandaloneRedis extends BaseModel
{
return 'standalone-redis';
}
public function getDbUrl(bool $useInternal = false): string
public function get_db_url(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
class DockerCleanup extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server, public string $message)
{
}
public function via(object $notifiable): array
{
$channels = [];
// $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
// if ($isEmailEnabled) {
// $channels[] = EmailChannel::class;
// }
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
return $channels;
}
// public function toMail(): MailMessage
// {
// $mail = new MailMessage();
// $mail->subject("Coolify: Server ({$this->server->name}) high disk usage detected!");
// $mail->view('emails.high-disk-usage', [
// 'name' => $this->server->name,
// 'disk_usage' => $this->disk_usage,
// 'threshold' => $this->cleanup_after_percentage,
// ]);
// return $mail;
// }
public function toDiscord(): string
{
$message = "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}"
];
}
}

View File

@@ -215,7 +215,7 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource,
}
return $payload;
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?string $service_name = null)
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
@@ -281,8 +281,10 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port");
}
if ($path !== '/') {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares = collect(["{$https_label}-stripprefix"]);
if ($is_stripprefix_enabled) {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares = collect(["{$https_label}-stripprefix"]);
}
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
@@ -334,8 +336,10 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("traefik.http.routers.{$http_label}.service={$http_label}");
}
if ($path !== '/') {
$labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares = collect(["{$http_label}-stripprefix"]);
if ($is_stripprefix_enabled) {
$labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares = collect(["{$http_label}-stripprefix"]);
}
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
@@ -392,7 +396,14 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$domains = Str::of(data_get($application, 'fqdn'))->explode(',');
}
// Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($appUuid, $domains, $application->settings->is_force_https_enabled, $onlyPort));
$labels = $labels->merge(fqdnLabelsForTraefik(
uuid: $appUuid,
domains: $domains,
onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled()
));
}
return $labels->all();
}

View File

@@ -1047,7 +1047,15 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($resource->uuid, $fqdns, true, serviceLabels: $serviceLabels, is_gzip_enabled: $savedService->isGzipEnabled(), service_name: $serviceName));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
uuid: $resource->uuid,
domains: $fqdns,
is_force_https_enabled: true,
serviceLabels: $serviceLabels,
is_gzip_enabled: $savedService->isGzipEnabled(),
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
service_name: $serviceName
));
}
}
if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
@@ -1249,7 +1257,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// Collect/create/update networks
if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) {
ray($networkDetails);
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName;
});
@@ -1405,7 +1412,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
]);
}
} else {
$generatedValue = generateEnvValue($command, $service);
$generatedValue = generateEnvValue($command);
if (!$foundEnv) {
EnvironmentVariable::create([
'key' => $key,
@@ -1581,7 +1588,7 @@ function parseEnvVariable(Str|string $value)
'port' => $port,
];
}
function generateEnvValue(string $command, Service $service)
function generateEnvValue(string $command, ?Service $service = null)
{
switch ($command) {
case 'PASSWORD':
@@ -1590,6 +1597,7 @@ function generateEnvValue(string $command, Service $service)
case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false);
break;
// This is not base64, it's just a random string
case 'BASE64_64':
$generatedValue = Str::random(64);
break;
@@ -1597,8 +1605,20 @@ function generateEnvValue(string $command, Service $service)
$generatedValue = Str::random(128);
break;
case 'BASE64':
case 'BASE64_32':
$generatedValue = Str::random(32);
break;
// This is base64,
case 'REALBASE64_64':
$generatedValue = base64_encode(Str::random(64));
break;
case 'REALBASE64_128':
$generatedValue = base64_encode(Str::random(128));
break;
case 'REALBASE64':
case 'REALBASE64_32':
$generatedValue = base64_encode(Str::random(32));
break;
case 'USER':
$generatedValue = Str::random(16);
break;

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.231',
'release' => '4.0.0-beta.234',
// 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.231';
return '4.0.0-beta.234';

View File

@@ -0,0 +1,44 @@
<?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('application_settings', function (Blueprint $table) {
$table->boolean('is_gzip_enabled')->default(true);
$table->boolean('is_stripprefix_enabled')->default(true);
});
Schema::table('service_applications', function (Blueprint $table) {
$table->boolean('is_stripprefix_enabled')->default(true);
});
Schema::table('service_databases', function (Blueprint $table) {
$table->boolean('is_gzip_enabled')->default(true);
$table->boolean('is_stripprefix_enabled')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_gzip_enabled');
$table->dropColumn('is_stripprefix_enabled');
});
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('is_stripprefix_enabled');
});
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('is_gzip_enabled');
$table->dropColumn('is_stripprefix_enabled');
});
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -1,8 +1,10 @@
<div class="group">
@if (data_get($application, 'fqdn') ||
@if (
(data_get($application, 'fqdn') ||
collect(json_decode($this->application->docker_compose_domains))->count() > 0 ||
data_get($application, 'previews', collect([]))->count() > 0 ||
data_get($application, 'ports_mappings_array'))
data_get($application, 'ports_mappings_array')) &&
data_get($application, 'settings.is_raw_compose_deployment_enabled') !== true)
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
<x-chevron-down />
</label>

View File

@@ -5,6 +5,9 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://api.fonts.coollabs.io" crossorigin>
<link rel="dns-prefetch" href="https://api.fonts.coollabs.io" />
<link rel="preload" href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" as="style" />
<link rel="preload" href="https://cdn.fonts.coollabs.io/inter/normal/400.woff2" as="style" />
<link href="https://api.fonts.coollabs.io/css2?family=Inter&display=swap" rel="stylesheet">
<meta name="robots" content="noindex">
<title>Coolify</title>

View File

@@ -20,6 +20,11 @@
helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold text-warning'>You will lose the rolling update feature!</span>"
instantSave id="application.settings.is_consistent_container_name_enabled"
label="Consistent Container Names" />
<x-forms.checkbox label="Enable gzip compression"
helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this."
instantSave id="is_gzip_enabled" />
<x-forms.checkbox helper="Strip Prefix is used to remove prefixes from paths. Like /api/ to /api."
instantSave id="is_stripprefix_enabled" label="Strip Prefixes" />
<h3>Logs</h3>
@if (!$application->settings->is_raw_compose_deployment_enabled)
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."

View File

@@ -45,9 +45,11 @@
</div>
@endif
@if ($application->build_pack === 'dockercompose')
<div class="w-96">
<x-forms.checkbox instantSave id="application.settings.is_raw_compose_deployment_enabled"
label="Raw Compose Deployment"
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a href='https://coolify.io/docs/docker/compose#raw-docker-compose-deployment'>documentation.</a>" />
</div>
@if (count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled)
@foreach (data_get($parsedServices, 'services') as $serviceName => $service)
@if (!isDatabaseImage(data_get($service, 'image')))
@@ -207,7 +209,11 @@
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="application.custom_docker_run_options" label="Custom Docker Options" />
@endif
@else
<x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='text-white underline' href='https://coolify.io/docs/custom-docker-options'>docs.</a>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="application.custom_docker_run_options" label="Custom Docker Options" />
@endif
@if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>

View File

@@ -34,7 +34,9 @@
<h3 class="pt-2">Advanced</h3>
<div class="w-96">
<x-forms.checkbox instantSave id="application.is_gzip_enabled" label="Enable gzip compression"
helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this." />
helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this." />
<x-forms.checkbox instantSave id="application.is_stripprefix_enabled" label="Strip Prefixes"
helper="Strip Prefix is used to remove prefixes from paths. Like /api/ to /api." />
<x-forms.checkbox instantSave label="Exclude from service status"
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
id="application.exclude_from_status"></x-forms.checkbox>

View File

@@ -1,7 +1,7 @@
<div>
<div x-init="$wire.getLogs" id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }">
<div class="flex items-center gap-2">
@if ($resource->type() === 'application')
@if ($resource?->type() === 'application')
<h3>{{ $container }}</h3>
@else
<h3>{{ str($container)->beforeLast('-')->headline() }}</h3>

View File

@@ -25,7 +25,7 @@
</div>
<div wire:loading.remove wire:target="loadProxyConfiguration">
@if ($proxy_settings)
<div class="flex flex-col gap-2 pt-2">
<div class="flex flex-col gap-2 pt-4">
<x-forms.textarea label="Configuration file: traefik.conf" name="proxy_settings"
wire:model="proxy_settings" rows="30" />
<x-forms.button wire:click.prevent="reset_proxy_configuration">

View File

@@ -0,0 +1,32 @@
# documentation: https://github.com/dgtlmoon/changedetection.io/
# slogan: Website change detection monitor and notifications.
# tags: web, alert, monitor
# logo: svgs/changedetection.png
services:
changedetection:
image: ghcr.io/dgtlmoon/changedetection.io
volumes:
- changedetection-data:/datastore
environment:
- SERVICE_FQDN_CHANGEDETECTION
- PUID=1000
- PGID=1000
- BASE_URL=$SERVICE_FQDN_CHANGEDETECTION
- PLAYWRIGHT_DRIVER_URL=ws://playwright-chrome:3000/?stealth=1&--disable-web-security=true
# Hides the `Referer` header so that monitored websites can't see the changedetection.io hostname.
- HIDE_REFERER=true
depends_on:
playwright-chrome:
condition: service_started
playwright-chrome:
image: dgtlmoon/sockpuppetbrowser:latest
# cap_add:
# - SYS_ADMIN
# SYS_ADMIN might be too much, but it can be needed on your platform https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-on-gitlabci
restart: unless-stopped
environment:
- SCREEN_WIDTH=1920
- SCREEN_HEIGHT=1024
- SCREEN_DEPTH=16
- MAX_CONCURRENT_CHROME_PROCESSES=10

View File

@@ -12,6 +12,7 @@ services:
- DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@plausible_db/plausible
- BASE_URL=$SERVICE_FQDN_PLAUSIBLE
- SECRET_KEY_BASE=$SERVICE_BASE64_64_PLAUSIBLE
- TOTP_VAULT_KEY=$SERVICE_REALBASE64_TOTP
depends_on:
- plausible_db
- plausible_events_db

View File

@@ -51,6 +51,18 @@
"logo": "svgs\/unknown.svg",
"minversion": "0.0.0"
},
"changedetection": {
"documentation": "https:\/\/github.com\/dgtlmoon\/changedetection.io\/",
"slogan": "Website change detection monitor and notifications.",
"compose": "c2VydmljZXM6CiAgY2hhbmdlZGV0ZWN0aW9uOgogICAgaW1hZ2U6IGdoY3IuaW8vZGd0bG1vb24vY2hhbmdlZGV0ZWN0aW9uLmlvCiAgICB2b2x1bWVzOgogICAgICAtICdjaGFuZ2VkZXRlY3Rpb24tZGF0YTovZGF0YXN0b3JlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NIQU5HRURFVEVDVElPTgogICAgICAtIFBVSUQ9MTAwMAogICAgICAtIFBHSUQ9MTAwMAogICAgICAtIEJBU0VfVVJMPSRTRVJWSUNFX0ZRRE5fQ0hBTkdFREVURUNUSU9OCiAgICAgIC0gJ1BMQVlXUklHSFRfRFJJVkVSX1VSTD13czovL3BsYXl3cmlnaHQtY2hyb21lOjMwMDAvP3N0ZWFsdGg9MSYtLWRpc2FibGUtd2ViLXNlY3VyaXR5PXRydWUnCiAgICAgIC0gSElERV9SRUZFUkVSPXRydWUKICAgIGRlcGVuZHNfb246CiAgICAgIHBsYXl3cmlnaHQtY2hyb21lOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9zdGFydGVkCiAgcGxheXdyaWdodC1jaHJvbWU6CiAgICBpbWFnZTogJ2RndGxtb29uL3NvY2twdXBwZXRicm93c2VyOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTQ1JFRU5fV0lEVEg9MTkyMAogICAgICAtIFNDUkVFTl9IRUlHSFQ9MTAyNAogICAgICAtIFNDUkVFTl9ERVBUSD0xNgogICAgICAtIE1BWF9DT05DVVJSRU5UX0NIUk9NRV9QUk9DRVNTRVM9MTAK",
"tags": [
"web",
"alert",
"monitor"
],
"logo": "svgs\/changedetection.png",
"minversion": "0.0.0"
},
"code-server": {
"documentation": "https:\/\/coder.com\/docs\/code-server\/latest",
"slogan": "Code-Server is a web-based code editor that enables remote coding and collaboration from any device, anywhere.",

View File

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