diff --git a/README.md b/README.md index 2abca8f58..000134c64 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,12 @@ Thank you so much! Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and [Appwrite](https://appwrite.io)! - + ## Github Sponsors ($15+) + + @@ -32,7 +34,6 @@ Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and - ## Organizations diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index e7cc049b1..2d2c10ccb 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -21,14 +21,14 @@ class DeleteService foreach ($storages as $storage) { $storagesToDelete->push($storage); } - $application->delete(); + $application->forceDelete(); } foreach ($service->databases()->get() as $database) { $storages = $database->persistentStorages()->get(); foreach ($storages as $storage) { $storagesToDelete->push($storage); } - $database->delete(); + $database->forceDelete(); } foreach ($storagesToDelete as $storage) { $commands[] = "docker volume rm -f $storage->name"; diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index b784c4e67..b0073e1f0 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -55,7 +55,8 @@ class Init extends Command } } } - private function restore_coolify_db_backup() { + private function restore_coolify_db_backup() + { try { $database = StandalonePostgresql::withTrashed()->find(0); if ($database && $database->trashed()) { @@ -73,9 +74,8 @@ class Init extends Command 'team_id' => 0, ]); } - } - } catch(\Throwable $e) { + } catch (\Throwable $e) { echo "Error in restoring coolify db backup: {$e->getMessage()}\n"; } } @@ -138,6 +138,89 @@ class Init extends Command } private function cleanup_stucked_resources() { + + try { + $applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($applications as $application) { + echo "Deleting stucked application: {$application->name}\n"; + $application->forceDelete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning stucked application: {$e->getMessage()}\n"; + } + try { + $postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($postgresqls as $postgresql) { + echo "Deleting stucked postgresql: {$postgresql->name}\n"; + $postgresql->forceDelete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning stucked postgresql: {$e->getMessage()}\n"; + } + try { + $redis = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($redis as $redis) { + echo "Deleting stucked redis: {$redis->name}\n"; + $redis->forceDelete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning stucked redis: {$e->getMessage()}\n"; + } + try { + $mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($mongodbs as $mongodb) { + echo "Deleting stucked mongodb: {$mongodb->name}\n"; + $mongodb->forceDelete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning stucked mongodb: {$e->getMessage()}\n"; + } + try { + $mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($mysqls as $mysql) { + echo "Deleting stucked mysql: {$mysql->name}\n"; + $mysql->forceDelete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning stucked mysql: {$e->getMessage()}\n"; + } + try { + $mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($mariadbs as $mariadb) { + echo "Deleting stucked mariadb: {$mariadb->name}\n"; + $mariadb->forceDelete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning stucked mariadb: {$e->getMessage()}\n"; + } + try { + $services = Service::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($services as $service) { + echo "Deleting stucked service: {$service->name}\n"; + $service->forceDelete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning stucked service: {$e->getMessage()}\n"; + } + try { + $serviceApps = ServiceApplication::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($serviceApps as $serviceApp) { + echo "Deleting stucked serviceapp: {$serviceApp->name}\n"; + $serviceApp->forceDelete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning stucked serviceapp: {$e->getMessage()}\n"; + } + try { + $serviceDbs = ServiceDatabase::withTrashed()->whereNotNull('deleted_at')->get(); + foreach ($serviceDbs as $serviceDb) { + echo "Deleting stucked serviceapp: {$serviceDb->name}\n"; + $serviceDb->forceDelete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning stucked serviceapp: {$e->getMessage()}\n"; + } + // Cleanup any resources that are not attached to any environment or destination or server try { $applications = Application::all(); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index cb0b11679..5ce4040e7 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -164,11 +164,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $ips = collect([]); if (count($allContainers) > 0) { $allContainers = $allContainers[0]; + $allContainers = collect($allContainers)->sort()->values(); foreach ($allContainers as $container) { $containerName = data_get($container, 'Name'); if ($containerName === 'coolify-proxy') { continue; } + if (preg_match('/-(\d{12})/',$containerName)) { + continue; + } $containerIp = data_get($container, 'IPv4Address'); if ($containerName && $containerIp) { $containerIp = str($containerIp)->before('/'); @@ -446,8 +450,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->generate_image_names(); $this->cleanup_git(); $this->application->loadComposeFile(isInit: false); - $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id); - $yaml = Yaml::dump($composeFile->toArray(), 10); + if ($this->application->settings->is_raw_compose_deployment_enabled) { + $yaml = $composeFile = $this->application->docker_compose_raw; + } else { + $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id); + $yaml = Yaml::dump($composeFile->toArray(), 10); + } $this->docker_compose_base64 = base64_encode($yaml); $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}{$this->docker_compose_location}"), "hidden" => true @@ -871,11 +879,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private function nixpacks_build_cmd() { $this->generate_env_variables(); - $cacheKey = $this->application->uuid; - if ($this->pull_request_id !== 0) { - $cacheKey = "{$this->application->uuid}-pr-{$this->pull_request_id}"; - } - $nixpacks_command = "nixpacks build --cache-key '{$cacheKey}' -o {$this->workdir} {$this->env_args} --no-error-without-start"; + // $cacheKey = $this->application->uuid; + // if ($this->pull_request_id !== 0) { + // $cacheKey = "{$this->application->uuid}-pr-{$this->pull_request_id}"; + // } + $nixpacks_command = "nixpacks build {$this->env_args} --no-error-without-start"; if ($this->application->build_command) { $nixpacks_command .= " --build-cmd \"{$this->application->build_command}\""; } @@ -885,7 +893,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->application->install_command) { $nixpacks_command .= " --install-cmd \"{$this->application->install_command}\""; } - $nixpacks_command .= " {$this->workdir}"; + $nixpacks_command .= " -o {$this->workdir} {$this->workdir}"; return $nixpacks_command; } @@ -1129,7 +1137,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // Add PORT if not exists, use the first port as default if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) { $environment_variables->push("PORT={$ports[0]}"); - } if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('SOURCE_COMMIT'))->isEmpty()) { + } + if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('SOURCE_COMMIT'))->isEmpty()) { if (!is_null($this->commit)) { $environment_variables->push("SOURCE_COMMIT={$this->commit}"); } @@ -1192,6 +1201,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->application->settings->is_static || $this->application->build_pack === 'static') { if ($this->application->static_image) { $this->pull_latest_image($this->application->static_image); + $this->execute_remote_command( + ["echo -n 'Continue with the building process.'"], + ); } if ($this->application->build_pack === 'static') { $dockerfile = base64_encode("FROM {$this->application->static_image} @@ -1218,9 +1230,23 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } }"); } else { - $this->execute_remote_command([ - 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 - ]); + if ($this->application->build_pack === 'nixpacks') { + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "cp {$this->workdir}/Dockerfile {$this->workdir}/.nixpacks/Dockerfile") + ], + ); + } + if ($this->force_rebuild) { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "docker build --no-cache $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 + ]); + } else { + $this->execute_remote_command([ + 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} WORKDIR /usr/share/nginx/html/ @@ -1263,9 +1289,23 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); executeInDocker($this->deployment_uuid, "docker build --pull $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 ]); } else { - $this->execute_remote_command([ - 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 - ]); + if ($this->application->build_pack === 'nixpacks') { + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "cp {$this->workdir}/Dockerfile {$this->workdir}/.nixpacks/Dockerfile") + ], + ); + } + + if ($this->force_rebuild) { + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "docker build --no-cache $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 + ]); + } else { + $this->execute_remote_command([ + 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 + ]); + } } } $this->execute_remote_command([ @@ -1291,6 +1331,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); }); } else { $this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container."); + $this->application_deployment_queue->update([ + 'status' => ApplicationDeploymentStatus::FAILED->value, + ]); $this->execute_remote_command( [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ); diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 39cb15b27..f54a46e09 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -66,6 +66,7 @@ class General extends Component 'application.settings.is_static' => 'boolean|required', 'application.docker_compose_custom_start_command' => 'nullable', 'application.docker_compose_custom_build_command' => 'nullable', + 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', ]; protected $validationAttributes = [ 'application.name' => 'name', @@ -98,6 +99,7 @@ class General extends Component 'application.settings.is_static' => 'Is static', 'application.docker_compose_custom_start_command' => 'Docker compose custom start command', 'application.docker_compose_custom_build_command' => 'Docker compose custom build command', + 'application.settings.is_raw_compose_deployment_enabled' => 'Is raw compose deployment enabled', ]; public function mount() { @@ -114,6 +116,11 @@ class General extends Component } $this->isConfigurationChanged = $this->application->isConfigurationChanged(); $this->customLabels = $this->application->parseContainerLabels(); + if (!$this->customLabels && $this->application->destination->server->proxyType() === 'TRAEFIK_V2') { + $this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n"); + $this->application->custom_labels = base64_encode($this->customLabels); + $this->application->save(); + } $this->initialDockerComposeLocation = $this->application->docker_compose_location; $this->checkLabelUpdates(); } @@ -199,7 +206,12 @@ class General extends Component public function submit($showToaster = true) { try { - ray($this->initialDockerComposeLocation, $this->application->docker_compose_location); + if (!$this->customLabels && $this->application->destination->server->proxyType() === 'TRAEFIK_V2') { + $this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n"); + $this->application->custom_labels = base64_encode($this->customLabels); + $this->application->save(); + } + if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) { $this->loadComposeFile(); } @@ -207,6 +219,7 @@ class General extends Component if ($this->ports_exposes !== $this->application->ports_exposes) { $this->resetDefaultLabels(false); } + if (data_get($this->application, 'build_pack') === 'dockerimage') { $this->validate([ 'application.docker_registry_image_name' => 'required', diff --git a/app/Models/Application.php b/app/Models/Application.php index fc1b7d500..995c28f7d 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -430,7 +430,7 @@ class Application extends BaseModel public function isConfigurationChanged($save = false) { $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->health_check_path . $this->health_check_port . $this->health_check_host . $this->health_check_method . $this->health_check_return_code . $this->health_check_scheme . $this->health_check_response_text . $this->health_check_interval . $this->health_check_timeout . $this->health_check_retries . $this->health_check_start_period . $this->health_check_enabled . $this->limits_memory . $this->limits_swap . $this->limits_swappiness . $this->limits_reservation . $this->limits_cpus . $this->limits_cpuset . $this->limits_cpu_shares . $this->dockerfile . $this->dockerfile_location . $this->custom_labels; - if ($this->pull_request_id === 0) { + if ($this->pull_request_id === 0 || $this->pull_request_id === null) { $newConfigHash .= json_encode($this->environment_variables->all()); } else { $newConfigHash .= json_encode($this->environment_variables_preview->all()); diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index cfd5c4ebc..d3e28d524 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -41,11 +41,11 @@ function queue_application_deployment(int $application_id, string $deployment_uu dispatch(new ApplicationDeploymentNewJob( deployment: $deployment, application: Application::find($application_id) - ))->onConnection('long-running')->onQueue('long-running'); + )); } else { dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $deployment->id, - ))->onConnection('long-running')->onQueue('long-running'); + )); } } @@ -57,11 +57,11 @@ function queue_next_deployment(Application $application, bool $isNew = false) dispatch(new ApplicationDeploymentNewJob( deployment: $next_found, application: $application - ))->onConnection('long-running')->onQueue('long-running'); + )); } else { dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $next_found->id, - ))->onConnection('long-running')->onQueue('long-running'); + )); } } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 2bb619aa8..08315b422 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1035,7 +1035,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } data_set($service, 'labels', $serviceLabels->toArray()); data_forget($service, 'is_database'); - data_set($service, 'restart', RESTART_MODE); + if (!data_get($service, 'restart')) { + data_set($service, 'restart', RESTART_MODE); + } data_set($service, 'container_name', $containerName); data_forget($service, 'volumes.*.content'); data_forget($service, 'volumes.*.isDirectory'); @@ -1399,6 +1401,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $key = $value; $defaultValue = null; } + $foundEnv = EnvironmentVariable::where([ + 'key' => $key, + 'application_id' => $resource->id, + 'is_preview' => false, + ])->first(); if ($foundEnv) { $defaultValue = data_get($foundEnv, 'value'); } @@ -1468,7 +1475,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } data_set($service, 'labels', $serviceLabels->toArray()); data_forget($service, 'is_database'); - data_set($service, 'restart', RESTART_MODE); + if (!data_get($service, 'restart')) { + data_set($service, 'restart', RESTART_MODE); + } data_set($service, 'container_name', $containerName); data_forget($service, 'volumes.*.content'); data_forget($service, 'volumes.*.isDirectory'); diff --git a/config/horizon.php b/config/horizon.php index 060e67bad..49df24b57 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -184,19 +184,8 @@ return [ 'connection' => 'redis', 'queue' => ['default'], 'balance' => 'auto', - 'autoScalingStrategy' => 'time', - 'maxProcesses' => 1, - 'maxTime' => 0, - 'maxJobs' => 0, - 'memory' => 128, - 'tries' => 1, - 'timeout' => 300, - 'nice' => 0, - ], - 'long-running' => [ - 'connection' => 'redis', - 'queue' => ['long-running'], - 'balance' => 'auto', + // 'autoScalingStrategy' => 'time', + // 'maxProcesses' => 1, 'maxTime' => 0, 'maxJobs' => 0, 'memory' => 128, @@ -209,27 +198,15 @@ return [ 'environments' => [ 'production' => [ 's6' => [ - 'autoScalingStrategy' => 'size', - 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 2), - 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), - 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), - ], - 'long-running' => [ 'autoScalingStrategy' => 'size', 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), ], - ], + ], 'local' => [ 's6' => [ - 'autoScalingStrategy' => 'size', - 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 2), - 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), - 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), - ], - 'long-running' => [ 'autoScalingStrategy' => 'size', 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), diff --git a/config/queue.php b/config/queue.php index fb846f999..29b79353a 100644 --- a/config/queue.php +++ b/config/queue.php @@ -33,14 +33,6 @@ return [ 'sync' => [ 'driver' => 'sync', ], - 'long-running' => [ - 'driver' => 'redis', - 'connection' => 'default', - 'queue' => 'long-running', - 'retry_after' => 3600, - 'block_for' => null, - 'after_commit' => true, - ], 'database' => [ 'driver' => 'database', 'table' => 'jobs', diff --git a/config/sentry.php b/config/sentry.php index 150fd9d8b..e243eb466 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -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.175', + 'release' => '4.0.0-beta.182', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 6b0870bf8..e853b7527 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ boolean('is_raw_compose_deployment_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('is_raw_compose_deployment_enabled'); + }); + } +}; diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index 850cad7a1..4cf996904 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -8,9 +8,9 @@ ARG DOCKER_COMPOSE_VERSION=2.21.0 # https://github.com/docker/buildx/releases ARG DOCKER_BUILDX_VERSION=0.11.2 # https://github.com/buildpacks/pack/releases -ARG PACK_VERSION=0.31.0 +ARG PACK_VERSION=0.32.1 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.18.0 +ARG NIXPACKS_VERSION=1.20.0 USER root WORKDIR /artifacts diff --git a/resources/views/components/collapsible.blade.php b/resources/views/components/collapsible.blade.php deleted file mode 100644 index f8d4d92fd..000000000 --- a/resources/views/components/collapsible.blade.php +++ /dev/null @@ -1,12 +0,0 @@ -@isset($title, $action) -