diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 315f256c0..c69d411dd 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -125,6 +125,9 @@ class Init extends Command // Cleanup any failed deployments try { + if (isCloud()) { + return; + } $queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get(); foreach ($queued_inprogress_deployments as $deployment) { ray($deployment->id, $deployment->status); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index ccf1bba3d..aa68c20ad 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -133,6 +133,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id); + ray('New container name: ', $this->container_name); + savePrivateKeyToFs($this->server); $this->saved_outputs = collect(); @@ -711,9 +713,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->write_deployment_configurations(); $this->server = $this->original_server; } - if (count($this->application->ports_mappings_array) > 0) { + if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled) { $this->application_deployment_queue->addLogEntry("----------------------------------------"); - $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported."); + if (count($this->application->ports_mappings_array) > 0) { + $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported."); + } + if ((bool) $this->application->settings->is_consistent_container_name_enabled) { + $this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported."); + } $this->stop_running_container(force: true); $this->start_by_compose_file(); } else { @@ -1199,13 +1206,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // ]; // } - $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; - - data_forget($docker_compose, 'services.' . $this->container_name); - - $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); - if (count($custom_compose) > 0) { - $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); + if ((bool)$this->application->settings->is_consistent_container_name_enabled) { + $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); + if (count($custom_compose) > 0) { + $docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose); + } + } else { + $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; + data_forget($docker_compose, 'services.' . $this->container_name); + $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); + if (count($custom_compose) > 0) { + $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); + } } $this->docker_compose = Yaml::dump($docker_compose, 10); @@ -1490,6 +1502,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], ); }); + if ($this->application->settings->is_consistent_container_name_enabled) { + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], + ); + } } else { $this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container."); $this->application_deployment_queue->update([ diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index a2b7afa6a..08b4f9523 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -8,20 +8,25 @@ use Livewire\Component; class Advanced extends Component { public Application $application; + public bool $is_force_https_enabled; protected $rules = [ 'application.settings.is_git_submodules_enabled' => 'boolean|required', 'application.settings.is_git_lfs_enabled' => 'boolean|required', 'application.settings.is_preview_deployments_enabled' => 'boolean|required', 'application.settings.is_auto_deploy_enabled' => 'boolean|required', - 'application.settings.is_force_https_enabled' => 'boolean|required', + 'is_force_https_enabled' => 'boolean|required', 'application.settings.is_log_drain_enabled' => 'boolean|required', '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.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; + } public function instantSave() { if ($this->application->isLogDrainEnabled()) { @@ -31,7 +36,8 @@ class Advanced extends Component return; } } - if ($this->application->settings->is_force_https_enabled) { + if ($this->application->settings->is_force_https_enabled !== $this->is_force_https_enabled) { + $this->application->settings->is_force_https_enabled = $this->is_force_https_enabled; $this->dispatch('resetDefaultLabels', false); } $this->application->settings->save(); diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index c11dbfe4b..732be62d0 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -126,7 +126,6 @@ class General extends Component $this->application->save(); } $this->initialDockerComposeLocation = $this->application->docker_compose_location; - $this->checkLabelUpdates(); } public function instantSave() { @@ -164,6 +163,7 @@ class General extends Component } return $domain; } + public function updatedApplicationBuildPack() { if ($this->application->build_pack !== 'nixpacks') { @@ -184,15 +184,6 @@ class General extends Component $this->submit(); $this->dispatch('build_pack_updated'); } - public function checkLabelUpdates() - { - if (md5($this->application->custom_labels) !== md5(implode("|", generateLabelsApplication($this->application)))) { - $this->labelsChanged = true; - } else { - $this->labelsChanged = false; - } - } - public function getWildcardDomain() { $server = data_get($this->application, 'destination.server'); @@ -212,6 +203,13 @@ class General extends Component public function updatedApplicationFqdn() { + $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); + $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + return str($domain)->trim()->lower(); + }); + $this->application->fqdn = $this->application->fqdn->unique()->implode(','); + $this->application->save(); $this->resetDefaultLabels(false); // $this->dispatch('success', 'Labels reset to default!'); } @@ -238,22 +236,17 @@ class General extends Component ]); } if (data_get($this->application, 'fqdn')) { - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $domains = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { - return str($domain)->trim()->lower(); - }); - $domains = $domains->unique(); + $domains = str($this->application->fqdn)->trim()->explode(','); if ($this->application->additional_servers->count() === 0) { foreach ($domains as $domain) { if (!validate_dns_entry($domain, $this->application->destination->server)) { - $showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.","Make sure you have added the DNS records correctly.

Check this documentation for further help."); + $showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.

Check this documentation for further help."); } } } check_fqdn_usage($this->application); $this->application->fqdn = $domains->implode(','); } - if (data_get($this->application, 'custom_docker_run_options')) { $this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim(); } @@ -279,7 +272,6 @@ class General extends Component } catch (\Throwable $e) { return handleError($e, $this); } finally { - $this->checkLabelUpdates(); $this->isConfigurationChanged = $this->application->isConfigurationChanged(); } } diff --git a/app/Models/Application.php b/app/Models/Application.php index b37476565..df1fb8038 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -470,7 +470,7 @@ class Application extends BaseModel { return data_get($this, 'settings.is_log_drain_enabled', false); } - public function isConfigurationChanged($save = false) + public function isConfigurationChanged(bool $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->dockerfile . $this->dockerfile_location . $this->custom_labels; if ($this->pull_request_id === 0 || $this->pull_request_id === null) { diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 5308b3fa8..f01eb972f 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -123,10 +123,14 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data function generateApplicationContainerName(Application $application, $pull_request_id = 0) { + $consistent_container_name = $application->settings->is_consistent_container_name_enabled; $now = now()->format('Hisu'); if ($pull_request_id !== 0 && $pull_request_id !== null) { return $application->uuid . '-pr-' . $pull_request_id; } else { + if ($consistent_container_name) { + return $application->uuid; + } return $application->uuid . '-' . $now; } } @@ -209,15 +213,48 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource, } return $payload; } -function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null) +function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null) { $labels = collect([]); $labels->push('traefik.enable=true'); $labels->push("traefik.http.middlewares.gzip.compress=true"); $labels->push("traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"); + + $basic_auth = false; + $basic_auth_middleware = null; + $redirect = false; + $redirect_middleware = null; + if ($serviceLabels) { + $basic_auth = $serviceLabels->contains(function ($value) { + return str_contains($value, 'basicauth'); + }); + if ($basic_auth) { + $basic_auth_middleware = $serviceLabels + ->map(function ($item) { + if (preg_match('/traefik\.http\.middlewares\.(.*?)\.basicauth\.users/', $item, $matches)) { + return $matches[1]; + } + }) + ->filter() + ->first(); + } + $redirect = $serviceLabels->contains(function ($value) { + return str_contains($value, 'redirectregex'); + }); + if ($redirect) { + $redirect_middleware = $serviceLabels + ->map(function ($item) { + if (preg_match('/traefik\.http\.middlewares\.(.*?)\.redirectregex\.regex/', $item, $matches)) { + return $matches[1]; + } + }) + ->filter() + ->first(); + } + } foreach ($domains as $loop => $domain) { try { - $uuid = new Cuid2(7); + // $uuid = new Cuid2(7); $url = Url::fromString($domain); $host = $url->getHost(); $path = $url->getPath(); @@ -239,11 +276,24 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ } if ($path !== '/') { $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); - $labels->push("traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix,gzip"); + $middlewares = "gzip,{$https_label}-stripprefix"; + if ($basic_auth && $basic_auth_middleware) { + $middlewares = $middlewares . ',' . $basic_auth_middleware; + } + if ($redirect && $redirect_middleware) { + $middlewares = $middlewares . ',' . $redirect_middleware; + } + $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); } else { - $labels->push("traefik.http.routers.{$https_label}.middlewares=gzip"); + $middlewares = "gzip"; + if ($basic_auth && $basic_auth_middleware) { + $middlewares = $middlewares . ',' . $basic_auth_middleware; + } + if ($redirect && $redirect_middleware) { + $middlewares = $middlewares . ',' . $redirect_middleware; + } + $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); } - $labels->push("traefik.http.routers.{$https_label}.tls=true"); $labels->push("traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt"); @@ -267,16 +317,29 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ } if ($path !== '/') { $labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}"); - $labels->push("traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix,gzip"); + $middlewares = "gzip,{$http_label}-stripprefix"; + if ($basic_auth && $basic_auth_middleware) { + $middlewares = $middlewares . ',' . $basic_auth_middleware; + } + if ($redirect && $redirect_middleware) { + $middlewares = $middlewares . ',' . $redirect_middleware; + } + $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); } else { - $labels->push("traefik.http.routers.{$http_label}.middlewares=gzip"); + $middlewares = "gzip"; + if ($basic_auth && $basic_auth_middleware) { + $middlewares = $middlewares . ',' . $basic_auth_middleware; + } + if ($redirect && $redirect_middleware) { + $middlewares = $middlewares . ',' . $redirect_middleware; + } + $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); } } } catch (\Throwable $e) { continue; } } - return $labels->sort(); } function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 9ab26e59a..bc33388eb 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1039,7 +1039,7 @@ 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->merge(fqdnLabelsForTraefik($resource->uuid, $fqdns, true, serviceLabels: $serviceLabels)); } } if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) { @@ -1480,7 +1480,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $preview_fqdn; }); } - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns)); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns,serviceLabels: $serviceLabels)); } } } diff --git a/config/sentry.php b/config/sentry.php index 071a24929..a67eea44e 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.216', + 'release' => '4.0.0-beta.217', // 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 e1bf71945..d13c0ce6f 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ boolean('is_consistent_container_name_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('is_consistent_container_name_enabled'); + }); + } +}; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b4b81530c..407be245b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,7 +1,7 @@ version: '3.8' services: coolify: - image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:latest}" + image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-latest}" volumes: - type: bind source: /data/coolify/source/.env diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php index 49196b208..53d984698 100644 --- a/resources/views/livewire/project/application/advanced.blade.php +++ b/resources/views/livewire/project/application/advanced.blade.php @@ -15,7 +15,11 @@ @endif + instantSave id="is_force_https_enabled" label="Force Https" /> +

Logs

@if (!$application->settings->is_raw_compose_deployment_enabled) @if ($isConfigurationChanged && !is_null($application->config_hash)) -
Configuration not applied to the running application. You need to - redeploy.
+
+ + + +
@endif
General configuration for your application.
diff --git a/templates/compose/metabase.yaml b/templates/compose/metabase.yaml new file mode 100644 index 000000000..b038269e5 --- /dev/null +++ b/templates/compose/metabase.yaml @@ -0,0 +1,35 @@ +# documentation: https://www.metabase.com/docs/latest/installation-and-operation/running-metabase-on-docker +# slogan: Fast analytics with the friendly UX and integrated tooling to let your company explore data on their own. +# tags: analytics,bi,business,intelligence + +services: + metabase: + image: metabase/metabase:latest + volumes: + - /dev/urandom:/dev/random:ro + environment: + - SERVICE_FQDN_METABASE + - MB_DB_TYPE=postgres + - MB_DB_HOST=postgresql + - MB_DB_PORT=5432 + - MB_DB_DBNAME=${POSTGRESQL_DATABASE:-metabase} + - MB_DB_USER=$SERVICE_USER_POSTGRESQL + - MB_DB_PASS=$SERVICE_PASSWORD_POSTGRESQL + healthcheck: + test: curl --fail -I http://localhost:3000/api/health || exit 1 + interval: 5s + timeout: 20s + retries: 10 + postgresql: + image: postgres:16-alpine + volumes: + - metabase-postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRESQL} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL} + - POSTGRES_DB=${POSTGRESQL_DATABASE:-metabase} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/service-templates.json b/templates/service-templates.json index 8d13fe743..3aae101b5 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -329,6 +329,17 @@ "meilisearch" ] }, + "metabase": { + "documentation": "https:\/\/www.metabase.com\/docs\/latest\/installation-and-operation\/running-metabase-on-docker", + "slogan": "Fast analytics with the friendly UX and integrated tooling to let your company explore data on their own.", + "compose": "c2VydmljZXM6CiAgbWV0YWJhc2U6CiAgICBpbWFnZTogJ21ldGFiYXNlL21ldGFiYXNlOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJy9kZXYvdXJhbmRvbTovZGV2L3JhbmRvbTpybycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NRVRBQkFTRQogICAgICAtIE1CX0RCX1RZUEU9cG9zdGdyZXMKICAgICAgLSBNQl9EQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBNQl9EQl9QT1JUPTU0MzIKICAgICAgLSAnTUJfREJfREJOQU1FPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotbWV0YWJhc2V9JwogICAgICAtIE1CX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMCiAgICAgIC0gTUJfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ2N1cmwgLS1mYWlsIC1JIGh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC9hcGkvaGVhbHRoIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21ldGFiYXNlLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LW1ldGFiYXNlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "analytics", + "bi", + "business", + "intelligence" + ] + }, "metube": { "documentation": "https:\/\/github.com\/alexta69\/metube", "slogan": "A web GUI for youtube-dl with playlist support. It enables you to effortlessly download videos from YouTube and dozens of other sites.", diff --git a/versions.json b/versions.json index 880ee2308..cfe32cd2c 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "version": "3.12.36" }, "v4": { - "version": "4.0.0-beta.216" + "version": "4.0.0-beta.217" } } }