From 9b65b82ccba58af4691f58ee095298007854bf96 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen Date: Sat, 31 Jan 2026 01:56:17 +1100 Subject: [PATCH 001/158] feat(template): cloudflare-ddns --- public/svgs/cloudflare-ddns.svg | 8 ++++++++ templates/compose/cloudflare-ddns.yaml | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 public/svgs/cloudflare-ddns.svg create mode 100644 templates/compose/cloudflare-ddns.yaml diff --git a/public/svgs/cloudflare-ddns.svg b/public/svgs/cloudflare-ddns.svg new file mode 100644 index 000000000..efe800bcc --- /dev/null +++ b/public/svgs/cloudflare-ddns.svg @@ -0,0 +1,8 @@ + + + + + + DDNS + + diff --git a/templates/compose/cloudflare-ddns.yaml b/templates/compose/cloudflare-ddns.yaml new file mode 100644 index 000000000..874f2cffb --- /dev/null +++ b/templates/compose/cloudflare-ddns.yaml @@ -0,0 +1,20 @@ +# documentation: https://github.com/favonia/cloudflare-ddns +# slogan: A small, feature-rich, and robust Cloudflare DDNS updater. +# category: automation +# tags: cloud, ddns +# logo: svgs/cloudflare-ddns.svg + +services: + cloudflare-ddns: + image: favonia/cloudflare-ddns:1 + network_mode: host + restart: unless-stopped + user: "1000:1000" + read_only: true + cap_drop: [all] + security_opt: [no-new-privileges:true] + environment: + - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN} + - DOMAINS=${DOMAINS} + - PROXIED=false + - IP6_PROVIDER=none From 90449d2bb5e7b8370dadb0899f511bb9c5b6c2cc Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen <86177399+nktnet1@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:55:44 +1100 Subject: [PATCH 002/158] fix: remove restart: unless-stopped Update templates/compose/cloudflare-ddns.yaml Co-authored-by: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> --- templates/compose/cloudflare-ddns.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/compose/cloudflare-ddns.yaml b/templates/compose/cloudflare-ddns.yaml index 874f2cffb..b44828a70 100644 --- a/templates/compose/cloudflare-ddns.yaml +++ b/templates/compose/cloudflare-ddns.yaml @@ -8,7 +8,6 @@ services: cloudflare-ddns: image: favonia/cloudflare-ddns:1 network_mode: host - restart: unless-stopped user: "1000:1000" read_only: true cap_drop: [all] From 96b9cd3fa543c4e78554af27607ab231d7122e60 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen <86177399+nktnet1@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:56:09 +1100 Subject: [PATCH 003/158] fix: mark the API token env as required, and other env as configurable from the UI Update templates/compose/cloudflare-ddns.yaml Co-authored-by: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> --- templates/compose/cloudflare-ddns.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/compose/cloudflare-ddns.yaml b/templates/compose/cloudflare-ddns.yaml index b44828a70..92f857c41 100644 --- a/templates/compose/cloudflare-ddns.yaml +++ b/templates/compose/cloudflare-ddns.yaml @@ -13,7 +13,7 @@ services: cap_drop: [all] security_opt: [no-new-privileges:true] environment: - - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN} + - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:?} - DOMAINS=${DOMAINS} - - PROXIED=false - - IP6_PROVIDER=none + - PROXIED=${PROXIED:-false} + - IP6_PROVIDER=${IP6_PROVIDER:-none} From b65f6399df736777a48498aca17c43f9609a5a1f Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen <86177399+nktnet1@users.noreply.github.com> Date: Sun, 1 Feb 2026 16:52:14 +1100 Subject: [PATCH 004/158] fix: make domains env compulsory Update templates/compose/cloudflare-ddns.yaml --- templates/compose/cloudflare-ddns.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/cloudflare-ddns.yaml b/templates/compose/cloudflare-ddns.yaml index 92f857c41..4f29a98d4 100644 --- a/templates/compose/cloudflare-ddns.yaml +++ b/templates/compose/cloudflare-ddns.yaml @@ -14,6 +14,6 @@ services: security_opt: [no-new-privileges:true] environment: - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:?} - - DOMAINS=${DOMAINS} + - DOMAINS=${DOMAINS:?} - PROXIED=${PROXIED:-false} - IP6_PROVIDER=${IP6_PROVIDER:-none} From dcd976ae065e1a2a003f62a12b29e49e41ac157e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:30:58 +0100 Subject: [PATCH 005/158] fix(git): ensure ssh credentials are propagated to submodule operations - Add gitSshCommand parameter to setGitImportSettings and buildGitCheckoutCommand - Extract hardcoded ssh commands to variables for consistency - Apply custom ssh credentials to all git operations including submodule sync/update - Configure git url rewriting for github token-based authentication with submodules - Add comprehensive test suite for submodule credential propagation --- app/Models/Application.php | 41 +++++--- openapi.json | 27 +++++ openapi.yaml | 21 ++++ tests/Unit/GitSubmoduleCredentialTest.php | 122 ++++++++++++++++++++++ 4 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 tests/Unit/GitSubmoduleCredentialTest.php diff --git a/app/Models/Application.php b/app/Models/Application.php index 34ab4141e..e622c9d54 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1087,12 +1087,14 @@ public function dirOnServer() return application_configuration_dir()."/{$this->uuid}"; } - public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false, ?string $commit = null) + public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false, ?string $commit = null, ?string $gitSshCommand = null) { $baseDir = $this->generateBaseDir($deployment_uuid); $escapedBaseDir = escapeshellarg($baseDir); $isShallowCloneEnabled = $this->settings?->is_git_shallow_clone_enabled ?? false; + $sshCommand = $gitSshCommand ?? 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'; + // Use the explicitly passed commit (e.g. from rollback), falling back to the application's git_commit_sha. // Invalid refs will cause the git checkout/fetch command to fail on the remote server. $commitToUse = $commit ?? $this->git_commit_sha; @@ -1102,9 +1104,9 @@ public function setGitImportSettings(string $deployment_uuid, string $git_clone_ // If shallow clone is enabled and we need a specific commit, // we need to fetch that specific commit with depth=1 if ($isShallowCloneEnabled) { - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git fetch --depth=1 origin {$escapedCommit} && git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1"; + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$sshCommand}\" git fetch --depth=1 origin {$escapedCommit} && git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1"; } else { - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1"; + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$sshCommand}\" git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1"; } } if ($this->settings->is_git_submodules_enabled) { @@ -1115,10 +1117,10 @@ public function setGitImportSettings(string $deployment_uuid, string $git_clone_ } // Add shallow submodules flag if shallow clone is enabled $submoduleFlags = $isShallowCloneEnabled ? '--depth=1' : ''; - $git_clone_command = "{$git_clone_command} git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git submodule update --init --recursive {$submoduleFlags}; fi"; + $git_clone_command = "{$git_clone_command} git submodule sync && GIT_SSH_COMMAND=\"{$sshCommand}\" git submodule update --init --recursive {$submoduleFlags}; fi"; } if ($this->settings->is_git_lfs_enabled) { - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git lfs pull"; + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$sshCommand}\" git lfs pull"; } return $git_clone_command; @@ -1301,12 +1303,18 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } } else { $github_access_token = generateGithubInstallationToken($this->source); + // Configure git to rewrite URLs with the token so submodules on the same host authenticate correctly + $escapedTokenUrl = escapeshellarg("{$source_html_url_scheme}://x-access-token:{$github_access_token}@{$source_html_url_host}/"); + $escapedHostUrl = escapeshellarg("{$source_html_url_scheme}://{$source_html_url_host}/"); + $gitConfigCommand = "git config --global url.{$escapedTokenUrl}.insteadOf {$escapedHostUrl}"; if ($exec_in_docker) { + $commands->push(executeInDocker($deployment_uuid, $gitConfigCommand)); $repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git"; $escapedRepoUrl = escapeshellarg($repoUrl); $git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}"; $fullRepoUrl = $repoUrl; } else { + $commands->push($gitConfigCommand); $repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}"; $escapedRepoUrl = escapeshellarg($repoUrl); $git_clone_command = "{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}"; @@ -1348,11 +1356,12 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } $private_key = base64_encode($private_key); $escapedCustomRepository = escapeshellarg($customRepository); - $git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}"; + $deployKeySshCommand = "ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa"; + $git_clone_command_base = "GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}"; if ($only_checkout) { $git_clone_command = $git_clone_command_base; } else { - $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit); + $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit, gitSshCommand: $deployKeySshCommand); } if ($exec_in_docker) { $commands = collect([ @@ -1375,7 +1384,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $deployKeySshCommand); } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { @@ -1383,14 +1392,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $deployKeySshCommand); } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$deployKeySshCommand}\" ".$this->buildGitCheckoutCommand($commit, $deployKeySshCommand); } } @@ -1411,6 +1420,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req $escapedCustomRepository = escapeshellarg($customRepository); $git_clone_command = "{$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}"; $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true, commit: $commit); + $otherSshCommand = "ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa"; if ($pull_request_id !== 0) { if ($git_type === 'gitlab') { @@ -1420,7 +1430,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $otherSshCommand); } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { @@ -1428,14 +1438,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $otherSshCommand); } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" ".$this->buildGitCheckoutCommand($commit, $otherSshCommand); } } @@ -1684,13 +1694,14 @@ public function fqdns(): Attribute ); } - protected function buildGitCheckoutCommand($target): string + protected function buildGitCheckoutCommand($target, ?string $gitSshCommand = null): string { $escapedTarget = escapeshellarg($target); $command = "git checkout {$escapedTarget}"; if ($this->settings->is_git_submodules_enabled) { - $command .= ' && git submodule update --init --recursive'; + $sshCommand = $gitSshCommand ?? 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'; + $command .= " && GIT_SSH_COMMAND=\"{$sshCommand}\" git submodule update --init --recursive"; } return $command; diff --git a/openapi.json b/openapi.json index 69f5ef53d..849dee363 100644 --- a/openapi.json +++ b/openapi.json @@ -3339,6 +3339,15 @@ "schema": { "type": "string" } + }, + { + "name": "docker_cleanup", + "in": "query", + "description": "Perform docker cleanup (prune networks, volumes, etc.).", + "schema": { + "type": "boolean", + "default": true + } } ], "responses": { @@ -5864,6 +5873,15 @@ "schema": { "type": "string" } + }, + { + "name": "docker_cleanup", + "in": "query", + "description": "Perform docker cleanup (prune networks, volumes, etc.).", + "schema": { + "type": "boolean", + "default": true + } } ], "responses": { @@ -10561,6 +10579,15 @@ "schema": { "type": "string" } + }, + { + "name": "docker_cleanup", + "in": "query", + "description": "Perform docker cleanup (prune networks, volumes, etc.).", + "schema": { + "type": "boolean", + "default": true + } } ], "responses": { diff --git a/openapi.yaml b/openapi.yaml index fab3df54e..226295cdb 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2111,6 +2111,13 @@ paths: required: true schema: type: string + - + name: docker_cleanup + in: query + description: 'Perform docker cleanup (prune networks, volumes, etc.).' + schema: + type: boolean + default: true responses: '200': description: 'Stop application.' @@ -3806,6 +3813,13 @@ paths: required: true schema: type: string + - + name: docker_cleanup + in: query + description: 'Perform docker cleanup (prune networks, volumes, etc.).' + schema: + type: boolean + default: true responses: '200': description: 'Stop database.' @@ -6645,6 +6659,13 @@ paths: required: true schema: type: string + - + name: docker_cleanup + in: query + description: 'Perform docker cleanup (prune networks, volumes, etc.).' + schema: + type: boolean + default: true responses: '200': description: 'Stop service.' diff --git a/tests/Unit/GitSubmoduleCredentialTest.php b/tests/Unit/GitSubmoduleCredentialTest.php new file mode 100644 index 000000000..1adaf735f --- /dev/null +++ b/tests/Unit/GitSubmoduleCredentialTest.php @@ -0,0 +1,122 @@ +application = new Application; + $this->application->forceFill([ + 'uuid' => 'test-app-uuid', + 'git_commit_sha' => 'HEAD', + ]); + + $settings = new ApplicationSetting; + $settings->is_git_shallow_clone_enabled = false; + $settings->is_git_submodules_enabled = true; + $settings->is_git_lfs_enabled = false; + $this->application->setRelation('settings', $settings); + }); + + test('setGitImportSettings uses provided gitSshCommand for submodule update', function () { + $sshCommand = 'ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa'; + + $result = $this->application->setGitImportSettings( + deployment_uuid: 'test-uuid', + git_clone_command: 'git clone', + public: false, + gitSshCommand: $sshCommand + ); + + expect($result) + ->toContain('GIT_SSH_COMMAND="'.$sshCommand.'" git submodule update --init --recursive') + ->toContain('git submodule sync'); + }); + + test('setGitImportSettings uses default ssh command when no gitSshCommand provided', function () { + $result = $this->application->setGitImportSettings( + deployment_uuid: 'test-uuid', + git_clone_command: 'git clone', + public: false, + ); + + expect($result) + ->toContain('GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" git submodule update --init --recursive'); + }); + + test('setGitImportSettings uses provided gitSshCommand for fetch and checkout', function () { + $this->application->git_commit_sha = 'abc123def456'; + $sshCommand = 'ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa'; + + $result = $this->application->setGitImportSettings( + deployment_uuid: 'test-uuid', + git_clone_command: 'git clone', + public: false, + gitSshCommand: $sshCommand + ); + + expect($result) + ->toContain('GIT_SSH_COMMAND="'.$sshCommand.'" git -c advice.detachedHead=false checkout'); + }); + + test('setGitImportSettings uses provided gitSshCommand for shallow fetch', function () { + $this->application->git_commit_sha = 'abc123def456'; + $this->application->settings->is_git_shallow_clone_enabled = true; + $sshCommand = 'ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa'; + + $result = $this->application->setGitImportSettings( + deployment_uuid: 'test-uuid', + git_clone_command: 'git clone', + public: false, + gitSshCommand: $sshCommand + ); + + expect($result) + ->toContain('GIT_SSH_COMMAND="'.$sshCommand.'" git fetch --depth=1 origin'); + }); + + test('setGitImportSettings uses provided gitSshCommand for lfs pull', function () { + $this->application->settings->is_git_lfs_enabled = true; + $sshCommand = 'ssh -o ConnectTimeout=30 -p 22 -i /root/.ssh/id_rsa'; + + $result = $this->application->setGitImportSettings( + deployment_uuid: 'test-uuid', + git_clone_command: 'git clone', + public: false, + gitSshCommand: $sshCommand + ); + + expect($result) + ->toContain('GIT_SSH_COMMAND="'.$sshCommand.'" git lfs pull'); + }); + + test('buildGitCheckoutCommand includes GIT_SSH_COMMAND for submodule update when provided', function () { + $sshCommand = 'ssh -o ConnectTimeout=30 -p 22 -i /root/.ssh/id_rsa'; + + $method = new ReflectionMethod($this->application, 'buildGitCheckoutCommand'); + $result = $method->invoke($this->application, 'main', $sshCommand); + + expect($result) + ->toContain("git checkout 'main'") + ->toContain('GIT_SSH_COMMAND="'.$sshCommand.'" git submodule update --init --recursive'); + }); + + test('buildGitCheckoutCommand uses default ssh command for submodule update when none provided', function () { + $method = new ReflectionMethod($this->application, 'buildGitCheckoutCommand'); + $result = $method->invoke($this->application, 'main'); + + expect($result) + ->toContain('GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" git submodule update --init --recursive'); + }); + + test('buildGitCheckoutCommand omits submodule update when submodules disabled', function () { + $this->application->settings->is_git_submodules_enabled = false; + + $method = new ReflectionMethod($this->application, 'buildGitCheckoutCommand'); + $result = $method->invoke($this->application, 'main'); + + expect($result) + ->toContain("git checkout 'main'") + ->not->toContain('submodule'); + }); +}); From d4e4e446b02c2a0f09bc838e7869ea16138aeffc Mon Sep 17 00:00:00 2001 From: Mohmmad Qunibi Date: Wed, 15 Apr 2026 11:30:43 +0300 Subject: [PATCH 006/158] feat: add emqx service template --- public/svgs/emqx-enterprise.svg | 7 +++++++ templates/compose/emqx-enterprise.yaml | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 public/svgs/emqx-enterprise.svg create mode 100644 templates/compose/emqx-enterprise.yaml diff --git a/public/svgs/emqx-enterprise.svg b/public/svgs/emqx-enterprise.svg new file mode 100644 index 000000000..e67e1bffe --- /dev/null +++ b/public/svgs/emqx-enterprise.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/templates/compose/emqx-enterprise.yaml b/templates/compose/emqx-enterprise.yaml new file mode 100644 index 000000000..1d62ea6f9 --- /dev/null +++ b/templates/compose/emqx-enterprise.yaml @@ -0,0 +1,22 @@ +# documentation: https://www.emqx.io/docs/en/latest/deploy/install-docker.html +# slogan: Open-source MQTT broker for IoT, IIoT, and connected vehicles. +# category: iot +# tags: mqtt,broker,iot,messaging,emqx,iiot +# logo: svgs/emqx-enterprise.svg +# port: 18083 + +services: + emqx: + image: emqx/emqx-enterprise:6.2.0 + environment: + - SERVICE_URL_EMQX_18083 + - EMQX_DASHBOARD__DEFAULT_PASSWORD=${SERVICE_PASSWORD_EMQX} + volumes: + - emqx_data:/opt/emqx/data + - emqx_log:/opt/emqx/log + healthcheck: + test: ["CMD", "/opt/emqx/bin/emqx", "ctl", "status"] + interval: 10s + timeout: 30s + retries: 5 + start_period: 30s From be6acd6f24983d47d3da0d7e0f520a98320e2da7 Mon Sep 17 00:00:00 2001 From: Mohmmad Qunibi <82610649+MohmmadQunibi@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:11:03 +0300 Subject: [PATCH 007/158] Changed the category of emqx to Networking --- templates/compose/emqx-enterprise.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/emqx-enterprise.yaml b/templates/compose/emqx-enterprise.yaml index 1d62ea6f9..0f62c1983 100644 --- a/templates/compose/emqx-enterprise.yaml +++ b/templates/compose/emqx-enterprise.yaml @@ -1,6 +1,6 @@ # documentation: https://www.emqx.io/docs/en/latest/deploy/install-docker.html # slogan: Open-source MQTT broker for IoT, IIoT, and connected vehicles. -# category: iot +# category: Networking # tags: mqtt,broker,iot,messaging,emqx,iiot # logo: svgs/emqx-enterprise.svg # port: 18083 From 950c4e5936c9e5cd179bc913047940bf0825ceaa Mon Sep 17 00:00:00 2001 From: Mohmmad Qunibi <82610649+MohmmadQunibi@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:17:08 +0300 Subject: [PATCH 008/158] Update EMQX image to use 'latest' tag --- templates/compose/emqx-enterprise.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/emqx-enterprise.yaml b/templates/compose/emqx-enterprise.yaml index 0f62c1983..e68c894bf 100644 --- a/templates/compose/emqx-enterprise.yaml +++ b/templates/compose/emqx-enterprise.yaml @@ -7,7 +7,7 @@ services: emqx: - image: emqx/emqx-enterprise:6.2.0 + image: emqx/emqx-enterprise:latest environment: - SERVICE_URL_EMQX_18083 - EMQX_DASHBOARD__DEFAULT_PASSWORD=${SERVICE_PASSWORD_EMQX} From 60d8aba323bf3332f2e5772d321106e2e5fd617c Mon Sep 17 00:00:00 2001 From: Hendrik Kleinwaechter Date: Wed, 22 Apr 2026 21:18:18 +0200 Subject: [PATCH 009/158] feat: configurable stop grace period for applications Adds stop_grace_period to application settings (seconds, 1-3600, default 30). Used in place of the hardcoded docker stop -t 30 in the four places that stop application containers: rolling update shutdown, manual stop, stop on another server, and preview deployment stop. Non-positive values fall back to the default via ($val > 0) ? $val : default, with tests covering 0 and -10 so the cast does not blow up if a bad value ever lands in the db. Picks up Jack Coy's work from #7125 which went dormant. His commits are squashed here with credit below. Co-authored-by: Jack Coy --- app/Actions/Application/StopApplication.php | 5 +- .../Application/StopApplicationOneServer.php | 6 ++- app/Jobs/ApplicationDeploymentJob.php | 13 ++++-- app/Livewire/Project/Application/Advanced.php | 35 ++++++++++++++ app/Livewire/Project/Application/Previews.php | 5 +- app/Models/ApplicationSetting.php | 1 + bootstrap/helpers/constants.php | 1 + ...p_grace_period_to_application_settings.php | 31 +++++++++++++ .../project/application/advanced.blade.php | 16 ++++++- .../Unit/ApplicationSettingStaticCastTest.php | 46 +++++++++++++++++++ 10 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 database/migrations/2025_11_05_091558_add_stop_grace_period_to_application_settings.php diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index e86e30f04..2badcbb67 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -36,10 +36,13 @@ public function handle(Application $application, bool $previewDeployments = fals : getCurrentApplicationContainerStatus($server, $application->id, 0); $containersToStop = $containers->pluck('Names')->toArray(); + $timeout = ($application->settings->stop_grace_period > 0) + ? $application->settings->stop_grace_period + : DEFAULT_STOP_GRACE_PERIOD_SECONDS; foreach ($containersToStop as $containerName) { instant_remote_process(command: [ - "docker stop -t 30 $containerName", + "docker stop --time=$timeout $containerName", "docker rm -f $containerName", ], server: $server, throwError: false); } diff --git a/app/Actions/Application/StopApplicationOneServer.php b/app/Actions/Application/StopApplicationOneServer.php index bf9fdee72..cb51bc865 100644 --- a/app/Actions/Application/StopApplicationOneServer.php +++ b/app/Actions/Application/StopApplicationOneServer.php @@ -20,13 +20,17 @@ public function handle(Application $application, Server $server) } try { $containers = getCurrentApplicationContainerStatus($server, $application->id, 0); + $timeout = ($application->settings->stop_grace_period > 0) + ? $application->settings->stop_grace_period + : DEFAULT_STOP_GRACE_PERIOD_SECONDS; + if ($containers->count() > 0) { foreach ($containers as $container) { $containerName = data_get($container, 'Names'); if ($containerName) { instant_remote_process( [ - "docker stop -t 30 $containerName", + "docker stop --time=$timeout $containerName", "docker rm -f $containerName", ], $server diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 7e5025c8a..229f46cd8 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -3310,14 +3310,21 @@ private function build_image() private function graceful_shutdown_container(string $containerName, bool $skipRemove = false) { try { - $timeout = isDev() ? 1 : 30; + if (isDev()) { + $timeout = 1; + } else { + $timeout = ($this->application->settings->stop_grace_period > 0) + ? $this->application->settings->stop_grace_period + : DEFAULT_STOP_GRACE_PERIOD_SECONDS; + } + if ($skipRemove) { $this->execute_remote_command( - ["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true] + ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true] ); } else { $this->execute_remote_command( - ["docker stop -t $timeout $containerName", 'hidden' => true, 'ignore_errors' => true], + ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true], ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] ); } diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index cf7ef3e0b..862181ecf 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -61,6 +61,9 @@ class Advanced extends Component #[Validate(['string', 'nullable'])] public ?string $gpuOptions = null; + #[Validate(['string', 'nullable'])] + public ?string $stopGracePeriod = null; + #[Validate(['boolean'])] public bool $isBuildServerEnabled = false; @@ -145,6 +148,10 @@ public function syncData(bool $toModel = false) $this->injectBuildArgsToDockerfile = $this->application->settings->inject_build_args_to_dockerfile ?? true; $this->includeSourceCommitInBuild = $this->application->settings->include_source_commit_in_build ?? false; } + + // Load stop_grace_period separately since it has its own save handler + // Convert null to empty string to prevent dirty detection issues + $this->stopGracePeriod = $this->application->settings->stop_grace_period ?? ''; } private function resetDefaultLabels() @@ -252,6 +259,34 @@ public function saveCustomName() } } + public function saveStopGracePeriod() + { + try { + $this->authorize('update', $this->application); + + // Convert empty string to null, otherwise cast to integer + $value = ($this->stopGracePeriod === '' || $this->stopGracePeriod === null) + ? null + : (int) $this->stopGracePeriod; + + // Validate the integer value + if ($value !== null && ($value < 1 || $value > 3600)) { + $this->dispatch('error', 'Stop grace period must be between 1 and 3600 seconds.'); + + return; + } + + // Save to model + $this->application->settings->stop_grace_period = $value; + $this->application->settings->save(); + + // User feedback + $this->dispatch('success', 'Stop grace period updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function render() { return view('livewire.project.application.advanced'); diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index c887e9b83..9dd494f5c 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -338,10 +338,13 @@ public function addDockerImagePreview() private function stopContainers(array $containers, $server) { $containersToStop = collect($containers)->pluck('Names')->toArray(); + $timeout = ($this->application->settings->stop_grace_period > 0) + ? $this->application->settings->stop_grace_period + : DEFAULT_STOP_GRACE_PERIOD_SECONDS; foreach ($containersToStop as $containerName) { instant_remote_process(command: [ - "docker stop -t 30 $containerName", + "docker stop --time=$timeout $containerName", "docker rm -f $containerName", ], server: $server, throwError: false); } diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index 731a9b5da..c365bd187 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -26,6 +26,7 @@ class ApplicationSetting extends Model 'is_git_lfs_enabled' => 'boolean', 'is_git_shallow_clone_enabled' => 'boolean', 'docker_images_to_keep' => 'integer', + 'stop_grace_period' => 'integer', ]; protected $fillable = [ diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index bae2573de..ee6a3bc03 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -16,6 +16,7 @@ '@yearly' => '0 0 1 1 *', ]; const RESTART_MODE = 'unless-stopped'; +const DEFAULT_STOP_GRACE_PERIOD_SECONDS = 30; const DATABASE_DOCKER_IMAGES = [ 'bitnami/mariadb', diff --git a/database/migrations/2025_11_05_091558_add_stop_grace_period_to_application_settings.php b/database/migrations/2025_11_05_091558_add_stop_grace_period_to_application_settings.php new file mode 100644 index 000000000..cc702ce5c --- /dev/null +++ b/database/migrations/2025_11_05_091558_add_stop_grace_period_to_application_settings.php @@ -0,0 +1,31 @@ +integer('stop_grace_period') + ->nullable() + ->after('use_build_secrets') + ->comment('Seconds to wait for graceful shutdown before forcing container stop (1-3600). Null uses default of 30 seconds.'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('stop_grace_period'); + }); + } +}; diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php index a9f8c7233..362539a3c 100644 --- a/resources/views/livewire/project/application/advanced.blade.php +++ b/resources/views/livewire/project/application/advanced.blade.php @@ -86,7 +86,21 @@ helper="Readonly labels are disabled. You need to set the labels in the labels section." disabled instantSave id="isStripprefixEnabled" label="Strip Prefixes" canGate="update" :canResource="$application" /> @endif - +

Operations

+
+ + Save +

Logs

diff --git a/tests/Unit/ApplicationSettingStaticCastTest.php b/tests/Unit/ApplicationSettingStaticCastTest.php index 35ab7faaf..d06ee920d 100644 --- a/tests/Unit/ApplicationSettingStaticCastTest.php +++ b/tests/Unit/ApplicationSettingStaticCastTest.php @@ -103,3 +103,49 @@ ->and($casts[$field])->toBe('boolean'); } }); + +it('casts stop_grace_period to integer', function () { + $setting = new ApplicationSetting; + $casts = $setting->getCasts(); + + expect($casts)->toHaveKey('stop_grace_period') + ->and($casts['stop_grace_period'])->toBe('integer'); +}); + +it('handles null stop_grace_period for default behavior', function () { + $setting = new ApplicationSetting; + $setting->stop_grace_period = null; + + expect($setting->stop_grace_period)->toBeNull(); +}); + +it('casts stop_grace_period from string to integer', function () { + $setting = new ApplicationSetting; + $setting->stop_grace_period = '60'; + + expect($setting->stop_grace_period)->toBe(60) + ->and($setting->stop_grace_period)->toBeInt(); +}); + +it('casts stop_grace_period zero to integer (documents fallback trigger)', function () { + // Value of 0 is not a valid grace period — consumers guard with + // `($value > 0) ? $value : DEFAULT_STOP_GRACE_PERIOD_SECONDS`, so + // the cast itself must still round-trip cleanly without throwing. + $setting = new ApplicationSetting; + $setting->stop_grace_period = 0; + + expect($setting->stop_grace_period)->toBe(0) + ->and($setting->stop_grace_period)->toBeInt(); +}); + +it('casts stop_grace_period negative value to integer (documents fallback trigger)', function () { + // Negative values should never be persisted (UI validates `min=1`), + // but if one slips through (direct DB write, older data), the cast + // must not throw and consumers will treat it as the fallback via the + // `> 0` guard. + $setting = new ApplicationSetting; + $setting->stop_grace_period = -10; + + expect($setting->stop_grace_period)->toBe(-10) + ->and($setting->stop_grace_period)->toBeInt(); +}); From 9208ed102206a67b06ea3dfa978b996106c6500d Mon Sep 17 00:00:00 2001 From: Mohmmad Qunibi <82610649+MohmmadQunibi@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:25:16 +0300 Subject: [PATCH 010/158] Update EMQX image version to 6.2.0 --- templates/compose/emqx-enterprise.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/emqx-enterprise.yaml b/templates/compose/emqx-enterprise.yaml index e68c894bf..0f62c1983 100644 --- a/templates/compose/emqx-enterprise.yaml +++ b/templates/compose/emqx-enterprise.yaml @@ -7,7 +7,7 @@ services: emqx: - image: emqx/emqx-enterprise:latest + image: emqx/emqx-enterprise:6.2.0 environment: - SERVICE_URL_EMQX_18083 - EMQX_DASHBOARD__DEFAULT_PASSWORD=${SERVICE_PASSWORD_EMQX} From 71771c7d3a2265254e92568955596d0c59016e0d Mon Sep 17 00:00:00 2001 From: Mohmmad Qunibi <82610649+MohmmadQunibi@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:25:48 +0300 Subject: [PATCH 011/158] Add ports configuration for EMQX Enterprise --- templates/compose/emqx-enterprise.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/templates/compose/emqx-enterprise.yaml b/templates/compose/emqx-enterprise.yaml index 0f62c1983..01d063b6f 100644 --- a/templates/compose/emqx-enterprise.yaml +++ b/templates/compose/emqx-enterprise.yaml @@ -11,6 +11,11 @@ services: environment: - SERVICE_URL_EMQX_18083 - EMQX_DASHBOARD__DEFAULT_PASSWORD=${SERVICE_PASSWORD_EMQX} + ports: + - "1883:1883" + - "8083:8083" + - "8084:8084" + - "8883:8883" volumes: - emqx_data:/opt/emqx/data - emqx_log:/opt/emqx/log From 1942be9320ff11ae7721803cf04ba4ba3d7d1ea1 Mon Sep 17 00:00:00 2001 From: michalzard Date: Thu, 7 May 2026 16:45:12 +0200 Subject: [PATCH 012/158] feat: gitea runner template --- templates/compose/gitea-runner.yaml | 29 +++++++++++++++++++++++++ templates/service-templates-latest.json | 14 ++++++++++++ templates/service-templates.json | 14 ++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 templates/compose/gitea-runner.yaml diff --git a/templates/compose/gitea-runner.yaml b/templates/compose/gitea-runner.yaml new file mode 100644 index 000000000..3d12354f5 --- /dev/null +++ b/templates/compose/gitea-runner.yaml @@ -0,0 +1,29 @@ +# documentation: https://github.com/go-gitea/gitea +# category: devtools, runers +# slogan: Gitea Actions runner for docker +# tags: gitea,actions,runner,docker +# logo: svgs/gitea.svg + +services: + runner: + image: "docker.io/gitea/runner:1.0.0" + restart: unless-stopped + environment: + GITEA_INSTANCE_URL: "${GITEA_INSTANCE_URL}" + GITEA_RUNNER_REGISTRATION_TOKEN: "${GITEA_RUNNER_REGISTRATION_TOKEN}" + GITEA_RUNNER_NAME: "${GITEA_RUNNER_NAME:-gitea-runner}" + GITEA_RUNNER_LABELS: "${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://node:22}" + GITEA_TOKEN: "${GITEA_TOKEN}" + working_dir: /data + volumes: + - "runner-data:/data" + - "/var/run/docker.sock:/var/run/docker.sock" + healthcheck: + test: + - CMD-SHELL + - "ps aux | grep '[R]unner' > /dev/null || exit 1" + interval: 5s + timeout: 10s + retries: 15 +volumes: + runner-data: null diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index eb667fcb8..67c26e8f9 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -1634,6 +1634,20 @@ "minversion": "0.0.0", "port": "2368" }, + "gitea-runner": { + "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", + "slogan": "Gitea Actions runner for docker", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgY29udGFpbmVyX25hbWU6IGdpdGVhLXJ1bm5lcgogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICBHSVRFQV9JTlNUQU5DRV9VUkw6ICcke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIEdJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU46ICcke0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU59JwogICAgICBHSVRFQV9SVU5ORVJfTkFNRTogJyR7R0lURUFfUlVOTkVSX05BTUU6LUdpdGVhIFJ1bm5lcn0nCiAgICAgIEdJVEVBX1JVTk5FUl9MQUJFTFM6ICcke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIEdJVEVBX1RPS0VOOiAnJHtHSVRFQV9UT0tFTn0nCiAgICB3b3JraW5nX2RpcjogL2RhdGEKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3J1bm5lci1kYXRhOi9kYXRhJwogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKdm9sdW1lczoKICBydW5uZXItZGF0YTogbnVsbAo=", + "tags": [ + "gitea", + "actions", + "runner", + "docker" + ], + "category": "devtools, runers", + "logo": "svgs/gitea.svg", + "minversion": "0.0.0" + }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", diff --git a/templates/service-templates.json b/templates/service-templates.json index cc909dc68..acbd74941 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1634,6 +1634,20 @@ "minversion": "0.0.0", "port": "2368" }, + "gitea-runner": { + "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", + "slogan": "Gitea Actions runner for docker", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgY29udGFpbmVyX25hbWU6IGdpdGVhLXJ1bm5lcgogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICBHSVRFQV9JTlNUQU5DRV9VUkw6ICcke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIEdJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU46ICcke0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU59JwogICAgICBHSVRFQV9SVU5ORVJfTkFNRTogJyR7R0lURUFfUlVOTkVSX05BTUU6LUdpdGVhIFJ1bm5lcn0nCiAgICAgIEdJVEVBX1JVTk5FUl9MQUJFTFM6ICcke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIEdJVEVBX1RPS0VOOiAnJHtHSVRFQV9UT0tFTn0nCiAgICB3b3JraW5nX2RpcjogL2RhdGEKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3J1bm5lci1kYXRhOi9kYXRhJwogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKdm9sdW1lczoKICBydW5uZXItZGF0YTogbnVsbAo=", + "tags": [ + "gitea", + "actions", + "runner", + "docker" + ], + "category": "devtools, runers", + "logo": "svgs/gitea.svg", + "minversion": "0.0.0" + }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", From 7540b3b9f8eff77825d2af9182929b7465c7e216 Mon Sep 17 00:00:00 2001 From: michalzard Date: Thu, 7 May 2026 17:21:21 +0200 Subject: [PATCH 013/158] fix: category --- templates/compose/gitea-runner.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/gitea-runner.yaml b/templates/compose/gitea-runner.yaml index 3d12354f5..07a74a239 100644 --- a/templates/compose/gitea-runner.yaml +++ b/templates/compose/gitea-runner.yaml @@ -1,5 +1,5 @@ # documentation: https://github.com/go-gitea/gitea -# category: devtools, runers +# category: devtools # slogan: Gitea Actions runner for docker # tags: gitea,actions,runner,docker # logo: svgs/gitea.svg From 684e7c2388fcdddb7854f7ef5000834e053b41ba Mon Sep 17 00:00:00 2001 From: michalzard Date: Thu, 7 May 2026 17:37:32 +0200 Subject: [PATCH 014/158] fix: requested changes fix: changes fix: revert jsons revert:jsons fix: revert --- templates/compose/gitea-runner.yaml | 19 ++++++++----------- templates/service-templates-latest.json | 14 -------------- templates/service-templates.json | 14 -------------- 3 files changed, 8 insertions(+), 39 deletions(-) diff --git a/templates/compose/gitea-runner.yaml b/templates/compose/gitea-runner.yaml index 07a74a239..81cce4492 100644 --- a/templates/compose/gitea-runner.yaml +++ b/templates/compose/gitea-runner.yaml @@ -6,18 +6,17 @@ services: runner: - image: "docker.io/gitea/runner:1.0.0" - restart: unless-stopped + image: 'docker.io/gitea/runner:1.0.0' environment: - GITEA_INSTANCE_URL: "${GITEA_INSTANCE_URL}" - GITEA_RUNNER_REGISTRATION_TOKEN: "${GITEA_RUNNER_REGISTRATION_TOKEN}" - GITEA_RUNNER_NAME: "${GITEA_RUNNER_NAME:-gitea-runner}" - GITEA_RUNNER_LABELS: "${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://node:22}" - GITEA_TOKEN: "${GITEA_TOKEN}" + - 'GITEA_INSTANCE_URL=${GITEA_INSTANCE_URL}' + - 'GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN}' + - 'GITEA_RUNNER_NAME=${GITEA_RUNNER_NAME:-gitea-runner}' + - 'GITEA_RUNNER_LABELS=${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://node:22}' + - 'GITEA_TOKEN=${GITEA_TOKEN}' working_dir: /data volumes: - - "runner-data:/data" - - "/var/run/docker.sock:/var/run/docker.sock" + - 'runner-data:/data' + - '/var/run/docker.sock:/var/run/docker.sock' healthcheck: test: - CMD-SHELL @@ -25,5 +24,3 @@ services: interval: 5s timeout: 10s retries: 15 -volumes: - runner-data: null diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 67c26e8f9..eb667fcb8 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -1634,20 +1634,6 @@ "minversion": "0.0.0", "port": "2368" }, - "gitea-runner": { - "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", - "slogan": "Gitea Actions runner for docker", - "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgY29udGFpbmVyX25hbWU6IGdpdGVhLXJ1bm5lcgogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICBHSVRFQV9JTlNUQU5DRV9VUkw6ICcke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIEdJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU46ICcke0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU59JwogICAgICBHSVRFQV9SVU5ORVJfTkFNRTogJyR7R0lURUFfUlVOTkVSX05BTUU6LUdpdGVhIFJ1bm5lcn0nCiAgICAgIEdJVEVBX1JVTk5FUl9MQUJFTFM6ICcke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIEdJVEVBX1RPS0VOOiAnJHtHSVRFQV9UT0tFTn0nCiAgICB3b3JraW5nX2RpcjogL2RhdGEKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3J1bm5lci1kYXRhOi9kYXRhJwogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKdm9sdW1lczoKICBydW5uZXItZGF0YTogbnVsbAo=", - "tags": [ - "gitea", - "actions", - "runner", - "docker" - ], - "category": "devtools, runers", - "logo": "svgs/gitea.svg", - "minversion": "0.0.0" - }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", diff --git a/templates/service-templates.json b/templates/service-templates.json index acbd74941..cc909dc68 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1634,20 +1634,6 @@ "minversion": "0.0.0", "port": "2368" }, - "gitea-runner": { - "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", - "slogan": "Gitea Actions runner for docker", - "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgY29udGFpbmVyX25hbWU6IGdpdGVhLXJ1bm5lcgogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICBHSVRFQV9JTlNUQU5DRV9VUkw6ICcke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIEdJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU46ICcke0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU59JwogICAgICBHSVRFQV9SVU5ORVJfTkFNRTogJyR7R0lURUFfUlVOTkVSX05BTUU6LUdpdGVhIFJ1bm5lcn0nCiAgICAgIEdJVEVBX1JVTk5FUl9MQUJFTFM6ICcke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIEdJVEVBX1RPS0VOOiAnJHtHSVRFQV9UT0tFTn0nCiAgICB3b3JraW5nX2RpcjogL2RhdGEKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3J1bm5lci1kYXRhOi9kYXRhJwogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKdm9sdW1lczoKICBydW5uZXItZGF0YTogbnVsbAo=", - "tags": [ - "gitea", - "actions", - "runner", - "docker" - ], - "category": "devtools, runers", - "logo": "svgs/gitea.svg", - "minversion": "0.0.0" - }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", From 39a30b60a9bc05fc92f338894d2312d01be3b107 Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Mon, 11 May 2026 10:42:01 +0530 Subject: [PATCH 015/158] chore(service): disable litequeen Service not updated for 10 months and official website is available for sale (domain expired) --- templates/compose/litequeen.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/compose/litequeen.yaml b/templates/compose/litequeen.yaml index cf0c041c2..bda2d40c8 100644 --- a/templates/compose/litequeen.yaml +++ b/templates/compose/litequeen.yaml @@ -1,3 +1,4 @@ +# ignore: true # documentation: https://litequeen.com/ # slogan: Lite Queen is an open-source SQLite database management software that runs on your server. # category: database From 2253c40e01bd7e7d5248183eab1b591f099d175a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 11 May 2026 22:05:07 +0200 Subject: [PATCH 016/158] fix(deployment): include commit in preview image tags Generate pull request preview image tags with both the PR id and commit so different commits on the same PR do not reuse the same image tag. Sanitize and truncate generated tags to stay within Docker tag limits. --- app/Jobs/ApplicationDeploymentJob.php | 28 ++++- .../ApplicationPreviewImageNameTest.php | 107 ++++++++++++++++++ 2 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 tests/Feature/ApplicationPreviewImageNameTest.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 4f9481794..5e5606c30 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1154,12 +1154,15 @@ private function generate_image_names() $this->production_image_name = "{$this->dockerImage}:{$this->dockerImageTag}"; } } elseif ($this->pull_request_id !== 0) { + $previewImageTag = $this->previewImageTag(); + $previewBuildImageTag = $this->previewImageTag(build: true); + if ($this->application->docker_registry_image_name) { - $this->build_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build"; - $this->production_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}"; + $this->build_image_name = "{$this->application->docker_registry_image_name}:{$previewBuildImageTag}"; + $this->production_image_name = "{$this->application->docker_registry_image_name}:{$previewImageTag}"; } else { - $this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build"; - $this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}"; + $this->build_image_name = "{$this->application->uuid}:{$previewBuildImageTag}"; + $this->production_image_name = "{$this->application->uuid}:{$previewImageTag}"; } } else { $this->dockerImageTag = str($this->commit)->substr(0, 128); @@ -1176,6 +1179,23 @@ private function generate_image_names() } } + private function previewImageTag(bool $build = false): string + { + $prefix = "pr-{$this->pull_request_id}-"; + $suffix = $build ? '-build' : ''; + $maxCommitLength = max(1, 128 - strlen($prefix) - strlen($suffix)); + $commit = Str::of($this->commit ?: 'HEAD') + ->replaceMatches('/[^A-Za-z0-9_.-]/', '-') + ->substr(0, $maxCommitLength) + ->toString(); + + if ($commit === '') { + $commit = 'HEAD'; + } + + return "{$prefix}{$commit}{$suffix}"; + } + private function just_restart() { $this->application_deployment_queue->addLogEntry("Restarting {$this->customRepository}:{$this->application->git_branch} on {$this->server->name}."); diff --git a/tests/Feature/ApplicationPreviewImageNameTest.php b/tests/Feature/ApplicationPreviewImageNameTest.php new file mode 100644 index 000000000..6a3fb083b --- /dev/null +++ b/tests/Feature/ApplicationPreviewImageNameTest.php @@ -0,0 +1,107 @@ +newInstanceWithoutConstructor(); + + $application = new Application; + $application->uuid = 'preview-app'; + $application->build_pack = 'dockerfile'; + $application->dockerfile = null; + $application->docker_registry_image_name = $registryImageName; + + foreach ([ + 'application' => $application, + 'pull_request_id' => $pullRequestId, + 'commit' => $commit, + ] as $property => $value) { + $reflectionProperty = $reflection->getProperty($property); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($job, $value); + } + + return $job; +} + +function generatePreviewImageNames(object $job): array +{ + $reflection = new ReflectionClass(ApplicationDeploymentJob::class); + $method = $reflection->getMethod('generate_image_names'); + $method->setAccessible(true); + $method->invoke($job); + + $buildImageName = $reflection->getProperty('build_image_name'); + $buildImageName->setAccessible(true); + + $productionImageName = $reflection->getProperty('production_image_name'); + $productionImageName->setAccessible(true); + + return [ + 'build' => $buildImageName->getValue($job), + 'production' => $productionImageName->getValue($job), + ]; +} + +it('includes the pull request id and commit in preview image names', function () { + $names = generatePreviewImageNames(makePreviewImageNameJob( + commit: '111222333444555666777888999000aaabbbccc1', + pullRequestId: 123, + )); + + expect($names['production'])->toBe('preview-app:pr-123-111222333444555666777888999000aaabbbccc1') + ->and($names['build'])->toBe('preview-app:pr-123-111222333444555666777888999000aaabbbccc1-build'); +}); + +it('generates different preview image names for different commits on the same pull request', function () { + $firstCommitNames = generatePreviewImageNames(makePreviewImageNameJob( + commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + pullRequestId: 123, + )); + $secondCommitNames = generatePreviewImageNames(makePreviewImageNameJob( + commit: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + pullRequestId: 123, + )); + + expect($firstCommitNames['production'])->not->toBe($secondCommitNames['production']) + ->and($firstCommitNames['build'])->not->toBe($secondCommitNames['build']); +}); + +it('uses the configured registry image name for commit-specific preview tags', function () { + $names = generatePreviewImageNames(makePreviewImageNameJob( + commit: '111222333444555666777888999000aaabbbccc1', + pullRequestId: 123, + registryImageName: 'registry.example.com/team/app', + )); + + expect($names['production'])->toBe('registry.example.com/team/app:pr-123-111222333444555666777888999000aaabbbccc1') + ->and($names['build'])->toBe('registry.example.com/team/app:pr-123-111222333444555666777888999000aaabbbccc1-build'); +}); + +it('sanitizes and truncates preview image tags to docker tag limits', function () { + $names = generatePreviewImageNames(makePreviewImageNameJob( + commit: str_repeat('feature/add dockerfile changes/', 10), + pullRequestId: 123, + )); + + $productionTag = str($names['production'])->after(':')->toString(); + $buildTag = str($names['build'])->after(':')->toString(); + + expect(strlen($productionTag))->toBeLessThanOrEqual(128) + ->and(strlen($buildTag))->toBeLessThanOrEqual(128) + ->and($productionTag)->toMatch('/^pr-123-[A-Za-z0-9_.-]+$/') + ->and($buildTag)->toMatch('/^pr-123-[A-Za-z0-9_.-]+-build$/'); +}); + +it('keeps non-preview dockerfile image names commit based', function () { + $names = generatePreviewImageNames(makePreviewImageNameJob( + commit: '111222333444555666777888999000aaabbbccc1', + pullRequestId: 0, + )); + + expect($names['production'])->toBe('preview-app:111222333444555666777888999000aaabbbccc1') + ->and($names['build'])->toBe('preview-app:111222333444555666777888999000aaabbbccc1-build'); +}); From 9bb40f3ccb7f6e8f2ea6906ca37ac7e730d4ce99 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 11 May 2026 22:11:08 +0200 Subject: [PATCH 017/158] fix(deployment): avoid shared preview tags for HEAD commits Use the deployment UUID when preview deployments are built from HEAD so each deployment gets distinct production and build image tags. --- app/Jobs/ApplicationDeploymentJob.php | 6 +++- templates/service-templates-latest.json | 30 +++++++++---------- templates/service-templates.json | 30 +++++++++---------- .../ApplicationPreviewImageNameTest.php | 21 ++++++++++++- 4 files changed, 53 insertions(+), 34 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 5e5606c30..815d6c318 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1184,7 +1184,11 @@ private function previewImageTag(bool $build = false): string $prefix = "pr-{$this->pull_request_id}-"; $suffix = $build ? '-build' : ''; $maxCommitLength = max(1, 128 - strlen($prefix) - strlen($suffix)); - $commit = Str::of($this->commit ?: 'HEAD') + $commitSource = ($this->commit === 'HEAD' || blank($this->commit)) + ? $this->deployment_uuid + : $this->commit; + + $commit = Str::of($commitSource) ->replaceMatches('/[^A-Za-z0-9_.-]/', '-') ->substr(0, $maxCommitLength) ->toString(); diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index b57d9d29c..4b21a8798 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -1634,6 +1634,20 @@ "minversion": "0.0.0", "port": "2368" }, + "gitea-runner": { + "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", + "slogan": "Gitea Actions runner for docker", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "gitea", + "actions", + "runner", + "docker" + ], + "category": "devtools", + "logo": "svgs/gitea.svg", + "minversion": "0.0.0" + }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", @@ -2587,22 +2601,6 @@ "minversion": "0.0.0", "port": "4000" }, - "litequeen": { - "documentation": "https://litequeen.com/?utm_source=coolify.io", - "slogan": "Lite Queen is an open-source SQLite database management software that runs on your server.", - "compose": "c2VydmljZXM6CiAgbGl0ZXF1ZWVuOgogICAgaW1hZ2U6ICdraXZzZWdyb2IvbGl0ZS1xdWVlbjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9MSVRFUVVFRU5fODAwMAogICAgdm9sdW1lczoKICAgICAgLSAnbGl0ZXF1ZWVuLWRhdGE6L2hvbWUvbGl0ZXF1ZWVuL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2RhdGFiYXNlcwogICAgICAgIHRhcmdldDogL3NydgogICAgICAgIGlzX2RpcmVjdG9yeTogdHJ1ZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJiYXNoIC1jICc6PiAvZGV2L3RjcC8xMjcuMC4wLjEvODAwMCcgfHwgZXhpdCAxIgogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMwo=", - "tags": [ - "sqlite", - "sqlite-database-management", - "self-hosted", - "vps", - "database" - ], - "category": "database", - "logo": "svgs/litequeen.svg", - "minversion": "0.0.0", - "port": "8000" - }, "lobe-chat": { "documentation": "https://github.com/lobehub/lobe-chat?tab=readme-ov-file#b-deploying-with-docker?utm_source=coolify.io", "slogan": "An open-source, modern-design AI chat framework.", diff --git a/templates/service-templates.json b/templates/service-templates.json index ea9fd145a..8dd787f2e 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1634,6 +1634,20 @@ "minversion": "0.0.0", "port": "2368" }, + "gitea-runner": { + "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", + "slogan": "Gitea Actions runner for docker", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "gitea", + "actions", + "runner", + "docker" + ], + "category": "devtools", + "logo": "svgs/gitea.svg", + "minversion": "0.0.0" + }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", @@ -2587,22 +2601,6 @@ "minversion": "0.0.0", "port": "4000" }, - "litequeen": { - "documentation": "https://litequeen.com/?utm_source=coolify.io", - "slogan": "Lite Queen is an open-source SQLite database management software that runs on your server.", - "compose": "c2VydmljZXM6CiAgbGl0ZXF1ZWVuOgogICAgaW1hZ2U6ICdraXZzZWdyb2IvbGl0ZS1xdWVlbjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTElURVFVRUVOXzgwMDAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xpdGVxdWVlbi1kYXRhOi9ob21lL2xpdGVxdWVlbi9kYXRhJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kYXRhYmFzZXMKICAgICAgICB0YXJnZXQ6IC9zcnYKICAgICAgICBpc19kaXJlY3Rvcnk6IHRydWUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzgwMDAnIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMK", - "tags": [ - "sqlite", - "sqlite-database-management", - "self-hosted", - "vps", - "database" - ], - "category": "database", - "logo": "svgs/litequeen.svg", - "minversion": "0.0.0", - "port": "8000" - }, "lobe-chat": { "documentation": "https://github.com/lobehub/lobe-chat?tab=readme-ov-file#b-deploying-with-docker?utm_source=coolify.io", "slogan": "An open-source, modern-design AI chat framework.", diff --git a/tests/Feature/ApplicationPreviewImageNameTest.php b/tests/Feature/ApplicationPreviewImageNameTest.php index 6a3fb083b..a8d3c0e9d 100644 --- a/tests/Feature/ApplicationPreviewImageNameTest.php +++ b/tests/Feature/ApplicationPreviewImageNameTest.php @@ -3,7 +3,7 @@ use App\Jobs\ApplicationDeploymentJob; use App\Models\Application; -function makePreviewImageNameJob(string $commit, int $pullRequestId = 42, ?string $registryImageName = null): object +function makePreviewImageNameJob(string $commit, int $pullRequestId = 42, ?string $registryImageName = null, string $deploymentUuid = 'deployment-uuid'): object { $reflection = new ReflectionClass(ApplicationDeploymentJob::class); $job = $reflection->newInstanceWithoutConstructor(); @@ -18,6 +18,7 @@ function makePreviewImageNameJob(string $commit, int $pullRequestId = 42, ?strin 'application' => $application, 'pull_request_id' => $pullRequestId, 'commit' => $commit, + 'deployment_uuid' => $deploymentUuid, ] as $property => $value) { $reflectionProperty = $reflection->getProperty($property); $reflectionProperty->setAccessible(true); @@ -70,6 +71,24 @@ function generatePreviewImageNames(object $job): array ->and($firstCommitNames['build'])->not->toBe($secondCommitNames['build']); }); +it('uses the deployment uuid for preview image names when commit is HEAD', function () { + $firstDeploymentNames = generatePreviewImageNames(makePreviewImageNameJob( + commit: 'HEAD', + pullRequestId: 123, + deploymentUuid: 'deployment-one', + )); + $secondDeploymentNames = generatePreviewImageNames(makePreviewImageNameJob( + commit: 'HEAD', + pullRequestId: 123, + deploymentUuid: 'deployment-two', + )); + + expect($firstDeploymentNames['production'])->toBe('preview-app:pr-123-deployment-one') + ->and($firstDeploymentNames['build'])->toBe('preview-app:pr-123-deployment-one-build') + ->and($secondDeploymentNames['production'])->toBe('preview-app:pr-123-deployment-two') + ->and($secondDeploymentNames['build'])->toBe('preview-app:pr-123-deployment-two-build'); +}); + it('uses the configured registry image name for commit-specific preview tags', function () { $names = generatePreviewImageNames(makePreviewImageNameJob( commit: '111222333444555666777888999000aaabbbccc1', From a42613168de872b68cc19e326e3927155195ddba Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 11 May 2026 22:22:01 +0200 Subject: [PATCH 018/158] fix(applications): store custom nginx config from API correctly Decode base64 custom_nginx_configuration before model assignment so it is not double-encoded, and allow null values when clearing the setting. Add API coverage for create, update, invalid input, and clearing behavior. --- .../Api/ApplicationsController.php | 8 +- app/Models/Application.php | 4 +- templates/service-templates-latest.json | 30 ++-- templates/service-templates.json | 30 ++-- ...icationCustomNginxConfigurationApiTest.php | 150 ++++++++++++++++++ 5 files changed, 187 insertions(+), 35 deletions(-) create mode 100644 tests/Feature/ApplicationCustomNginxConfigurationApiTest.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 9919a8054..074269fa0 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -979,6 +979,9 @@ private function create_application(Request $request, $type) ], ], 422); } + $request->merge([ + 'custom_nginx_configuration' => $customNginxConfiguration, + ]); } $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); @@ -2397,7 +2400,7 @@ public function update_by_uuid(Request $request) } } } - if ($request->has('custom_nginx_configuration')) { + if ($request->has('custom_nginx_configuration') && ! is_null($request->custom_nginx_configuration)) { if (! isBase64Encoded($request->custom_nginx_configuration)) { return response()->json([ 'message' => 'Validation failed.', @@ -2415,6 +2418,9 @@ public function update_by_uuid(Request $request) ], ], 422); } + $request->merge([ + 'custom_nginx_configuration' => $customNginxConfiguration, + ]); } $return = $this->validateDataApplications($request, $server); if ($return instanceof JsonResponse) { diff --git a/app/Models/Application.php b/app/Models/Application.php index 2a364e13f..b5a0f0ea9 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -886,8 +886,8 @@ public function status(): Attribute public function customNginxConfiguration(): Attribute { return Attribute::make( - set: fn ($value) => base64_encode($value), - get: fn ($value) => base64_decode($value), + set: fn ($value) => is_null($value) ? null : base64_encode($value), + get: fn ($value) => is_null($value) ? null : base64_decode($value), ); } diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index b57d9d29c..4b21a8798 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -1634,6 +1634,20 @@ "minversion": "0.0.0", "port": "2368" }, + "gitea-runner": { + "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", + "slogan": "Gitea Actions runner for docker", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "gitea", + "actions", + "runner", + "docker" + ], + "category": "devtools", + "logo": "svgs/gitea.svg", + "minversion": "0.0.0" + }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", @@ -2587,22 +2601,6 @@ "minversion": "0.0.0", "port": "4000" }, - "litequeen": { - "documentation": "https://litequeen.com/?utm_source=coolify.io", - "slogan": "Lite Queen is an open-source SQLite database management software that runs on your server.", - "compose": "c2VydmljZXM6CiAgbGl0ZXF1ZWVuOgogICAgaW1hZ2U6ICdraXZzZWdyb2IvbGl0ZS1xdWVlbjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9MSVRFUVVFRU5fODAwMAogICAgdm9sdW1lczoKICAgICAgLSAnbGl0ZXF1ZWVuLWRhdGE6L2hvbWUvbGl0ZXF1ZWVuL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2RhdGFiYXNlcwogICAgICAgIHRhcmdldDogL3NydgogICAgICAgIGlzX2RpcmVjdG9yeTogdHJ1ZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJiYXNoIC1jICc6PiAvZGV2L3RjcC8xMjcuMC4wLjEvODAwMCcgfHwgZXhpdCAxIgogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMwo=", - "tags": [ - "sqlite", - "sqlite-database-management", - "self-hosted", - "vps", - "database" - ], - "category": "database", - "logo": "svgs/litequeen.svg", - "minversion": "0.0.0", - "port": "8000" - }, "lobe-chat": { "documentation": "https://github.com/lobehub/lobe-chat?tab=readme-ov-file#b-deploying-with-docker?utm_source=coolify.io", "slogan": "An open-source, modern-design AI chat framework.", diff --git a/templates/service-templates.json b/templates/service-templates.json index ea9fd145a..8dd787f2e 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1634,6 +1634,20 @@ "minversion": "0.0.0", "port": "2368" }, + "gitea-runner": { + "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", + "slogan": "Gitea Actions runner for docker", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "gitea", + "actions", + "runner", + "docker" + ], + "category": "devtools", + "logo": "svgs/gitea.svg", + "minversion": "0.0.0" + }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", @@ -2587,22 +2601,6 @@ "minversion": "0.0.0", "port": "4000" }, - "litequeen": { - "documentation": "https://litequeen.com/?utm_source=coolify.io", - "slogan": "Lite Queen is an open-source SQLite database management software that runs on your server.", - "compose": "c2VydmljZXM6CiAgbGl0ZXF1ZWVuOgogICAgaW1hZ2U6ICdraXZzZWdyb2IvbGl0ZS1xdWVlbjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTElURVFVRUVOXzgwMDAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xpdGVxdWVlbi1kYXRhOi9ob21lL2xpdGVxdWVlbi9kYXRhJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kYXRhYmFzZXMKICAgICAgICB0YXJnZXQ6IC9zcnYKICAgICAgICBpc19kaXJlY3Rvcnk6IHRydWUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzgwMDAnIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMK", - "tags": [ - "sqlite", - "sqlite-database-management", - "self-hosted", - "vps", - "database" - ], - "category": "database", - "logo": "svgs/litequeen.svg", - "minversion": "0.0.0", - "port": "8000" - }, "lobe-chat": { "documentation": "https://github.com/lobehub/lobe-chat?tab=readme-ov-file#b-deploying-with-docker?utm_source=coolify.io", "slogan": "An open-source, modern-design AI chat framework.", diff --git a/tests/Feature/ApplicationCustomNginxConfigurationApiTest.php b/tests/Feature/ApplicationCustomNginxConfigurationApiTest.php new file mode 100644 index 000000000..358003588 --- /dev/null +++ b/tests/Feature/ApplicationCustomNginxConfigurationApiTest.php @@ -0,0 +1,150 @@ + InstanceSettings::firstOrCreate(['id' => 0])); + + $this->team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + session(['currentTeam' => $this->team]); + + $plainTextToken = Str::random(40); + $token = $this->user->tokens()->create([ + 'name' => 'custom-nginx-api-test-'.Str::random(6), + 'token' => hash('sha256', $plainTextToken), + 'abilities' => ['*'], + 'team_id' => $this->team->id, + ]); + $this->bearerToken = $token->getKey().'|'.$plainTextToken; + + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->destination = StandaloneDocker::where('server_id', $this->server->id)->first(); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); +}); + +function customNginxApiHeaders(string $bearerToken): array +{ + return [ + 'Authorization' => 'Bearer '.$bearerToken, + 'Content-Type' => 'application/json', + ]; +} + +function customNginxConfig(): string +{ + return <<<'NGINX' +server { + listen 80; + location / { + try_files $uri $uri/ /index.html; + } +} +NGINX; +} + +function makeCustomNginxApplication(array $overrides = []): Application +{ + return Application::factory()->create(array_merge([ + 'environment_id' => test()->environment->id, + 'destination_id' => test()->destination->id, + 'destination_type' => test()->destination->getMorphClass(), + 'build_pack' => 'static', + ], $overrides)); +} + +describe('PATCH /api/v1/applications/{uuid} custom_nginx_configuration', function () { + test('decodes base64 custom nginx configuration before storing it', function () { + $application = makeCustomNginxApplication(); + $configuration = customNginxConfig(); + $encodedConfiguration = base64_encode($configuration); + + $response = $this->withHeaders(customNginxApiHeaders($this->bearerToken)) + ->patchJson("/api/v1/applications/{$application->uuid}", [ + 'custom_nginx_configuration' => $encodedConfiguration, + ]); + + $response->assertOk(); + + $application->refresh(); + expect($application->custom_nginx_configuration)->toBe($configuration); + + $storedConfiguration = DB::table('applications') + ->where('id', $application->id) + ->value('custom_nginx_configuration'); + + expect($storedConfiguration)->toBe(base64_encode($configuration)); + + $this->withHeaders(customNginxApiHeaders($this->bearerToken)) + ->getJson("/api/v1/applications/{$application->uuid}") + ->assertOk() + ->assertJsonPath('custom_nginx_configuration', $configuration); + }); + + test('rejects custom nginx configuration that is not base64 encoded', function () { + $application = makeCustomNginxApplication(); + + $response = $this->withHeaders(customNginxApiHeaders($this->bearerToken)) + ->patchJson("/api/v1/applications/{$application->uuid}", [ + 'custom_nginx_configuration' => customNginxConfig(), + ]); + + $response->assertUnprocessable() + ->assertJsonPath('errors.custom_nginx_configuration', 'The custom_nginx_configuration should be base64 encoded.'); + }); + + test('can clear custom nginx configuration with null', function () { + $application = makeCustomNginxApplication([ + 'custom_nginx_configuration' => customNginxConfig(), + ]); + + $response = $this->withHeaders(customNginxApiHeaders($this->bearerToken)) + ->patchJson("/api/v1/applications/{$application->uuid}", [ + 'custom_nginx_configuration' => null, + ]); + + $response->assertOk(); + + $application->refresh(); + expect($application->custom_nginx_configuration)->toBeNull(); + }); +}); + +describe('POST /api/v1/applications/public custom_nginx_configuration', function () { + test('decodes base64 custom nginx configuration before storing it on create', function () { + $configuration = customNginxConfig(); + + $response = $this->withHeaders(customNginxApiHeaders($this->bearerToken)) + ->postJson('/api/v1/applications/public', [ + 'project_uuid' => $this->project->uuid, + 'environment_uuid' => $this->environment->uuid, + 'server_uuid' => $this->server->uuid, + 'git_repository' => 'https://gitlab.com/coolify/test-static-app', + 'git_branch' => 'main', + 'build_pack' => 'static', + 'ports_exposes' => '80', + 'custom_nginx_configuration' => base64_encode($configuration), + 'autogenerate_domain' => false, + ]); + + $response->assertCreated(); + + $application = Application::where('uuid', $response->json('uuid'))->firstOrFail(); + + expect($application->custom_nginx_configuration)->toBe($configuration); + }); +}); From 63c2d31ca0628a9b0552906981a4f49d2efb3079 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 11 May 2026 23:43:53 +0200 Subject: [PATCH 019/158] feat(applications): add configurable stop grace period Add centralized stop grace period resolution for application settings and use it across manual stops, preview stops, and deployments. Validate the Livewire advanced setting against shared min/max constants and cover persistence, fillable creation, and fallback behavior with tests. --- app/Actions/Application/StopApplication.php | 4 +- .../Application/StopApplicationOneServer.php | 4 +- app/Jobs/ApplicationDeploymentJob.php | 8 +- app/Livewire/Project/Application/Advanced.php | 27 +++-- app/Livewire/Project/Application/Previews.php | 4 +- app/Models/ApplicationSetting.php | 22 +++++ bootstrap/helpers/constants.php | 2 + .../project/application/advanced.blade.php | 6 +- .../AdvancedStopGracePeriodTest.php | 98 +++++++++++++++++++ tests/Feature/ModelFillableCreationTest.php | 2 + .../Unit/ApplicationSettingStaticCastTest.php | 42 +++++--- 11 files changed, 173 insertions(+), 46 deletions(-) create mode 100644 tests/Feature/Livewire/Project/Application/AdvancedStopGracePeriodTest.php diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 2badcbb67..b79709c5a 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -36,9 +36,7 @@ public function handle(Application $application, bool $previewDeployments = fals : getCurrentApplicationContainerStatus($server, $application->id, 0); $containersToStop = $containers->pluck('Names')->toArray(); - $timeout = ($application->settings->stop_grace_period > 0) - ? $application->settings->stop_grace_period - : DEFAULT_STOP_GRACE_PERIOD_SECONDS; + $timeout = $application->settings->stopGracePeriodSeconds(); foreach ($containersToStop as $containerName) { instant_remote_process(command: [ diff --git a/app/Actions/Application/StopApplicationOneServer.php b/app/Actions/Application/StopApplicationOneServer.php index cb51bc865..09de9b628 100644 --- a/app/Actions/Application/StopApplicationOneServer.php +++ b/app/Actions/Application/StopApplicationOneServer.php @@ -20,9 +20,7 @@ public function handle(Application $application, Server $server) } try { $containers = getCurrentApplicationContainerStatus($server, $application->id, 0); - $timeout = ($application->settings->stop_grace_period > 0) - ? $application->settings->stop_grace_period - : DEFAULT_STOP_GRACE_PERIOD_SECONDS; + $timeout = $application->settings->stopGracePeriodSeconds(); if ($containers->count() > 0) { foreach ($containers as $container) { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 6d8cf059f..c2be064c4 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -3790,13 +3790,7 @@ private function build_image() private function graceful_shutdown_container(string $containerName, bool $skipRemove = false) { try { - if (isDev()) { - $timeout = 1; - } else { - $timeout = ($this->application->settings->stop_grace_period > 0) - ? $this->application->settings->stop_grace_period - : DEFAULT_STOP_GRACE_PERIOD_SECONDS; - } + $timeout = $this->application->settings->deploymentStopGracePeriodSeconds(); if ($skipRemove) { $this->execute_remote_command( diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index 862181ecf..618958140 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -4,6 +4,8 @@ use App\Models\Application; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; use Livewire\Attributes\Validate; use Livewire\Component; @@ -264,24 +266,21 @@ public function saveStopGracePeriod() try { $this->authorize('update', $this->application); - // Convert empty string to null, otherwise cast to integer - $value = ($this->stopGracePeriod === '' || $this->stopGracePeriod === null) + $validated = Validator::make( + ['stopGracePeriod' => $this->stopGracePeriod === '' ? null : $this->stopGracePeriod], + ['stopGracePeriod' => ['nullable', 'integer', 'min:'.MIN_STOP_GRACE_PERIOD_SECONDS, 'max:'.MAX_STOP_GRACE_PERIOD_SECONDS]], + [], + ['stopGracePeriod' => 'stop grace period'] + )->validate(); + + $this->application->settings->stop_grace_period = $validated['stopGracePeriod'] === null ? null - : (int) $this->stopGracePeriod; - - // Validate the integer value - if ($value !== null && ($value < 1 || $value > 3600)) { - $this->dispatch('error', 'Stop grace period must be between 1 and 3600 seconds.'); - - return; - } - - // Save to model - $this->application->settings->stop_grace_period = $value; + : (int) $validated['stopGracePeriod']; $this->application->settings->save(); - // User feedback $this->dispatch('success', 'Stop grace period updated.'); + } catch (ValidationException $e) { + throw $e; } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 9dd494f5c..59b52f557 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -338,9 +338,7 @@ public function addDockerImagePreview() private function stopContainers(array $containers, $server) { $containersToStop = collect($containers)->pluck('Names')->toArray(); - $timeout = ($this->application->settings->stop_grace_period > 0) - ? $this->application->settings->stop_grace_period - : DEFAULT_STOP_GRACE_PERIOD_SECONDS; + $timeout = $this->application->settings->stopGracePeriodSeconds(); foreach ($containersToStop as $containerName) { instant_remote_process(command: [ diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index c365bd187..ef09c0c48 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -65,8 +65,30 @@ class ApplicationSetting extends Model 'inject_build_args_to_dockerfile', 'include_source_commit_in_build', 'docker_images_to_keep', + 'stop_grace_period', ]; + public function stopGracePeriodSeconds(): int + { + if ( + $this->stop_grace_period >= MIN_STOP_GRACE_PERIOD_SECONDS && + $this->stop_grace_period <= MAX_STOP_GRACE_PERIOD_SECONDS + ) { + return $this->stop_grace_period; + } + + return DEFAULT_STOP_GRACE_PERIOD_SECONDS; + } + + public function deploymentStopGracePeriodSeconds(): int + { + if (isDev() && $this->stop_grace_period === null) { + return MIN_STOP_GRACE_PERIOD_SECONDS; + } + + return $this->stopGracePeriodSeconds(); + } + public function isStatic(): Attribute { return Attribute::make( diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index e395f3faf..79049e8c7 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -36,6 +36,8 @@ ]; const RESTART_MODE = 'unless-stopped'; const DEFAULT_STOP_GRACE_PERIOD_SECONDS = 30; +const MIN_STOP_GRACE_PERIOD_SECONDS = 1; +const MAX_STOP_GRACE_PERIOD_SECONDS = 3600; const DATABASE_DOCKER_IMAGES = [ 'bitnami/mariadb', diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php index 362539a3c..82ee31933 100644 --- a/resources/views/livewire/project/application/advanced.blade.php +++ b/resources/views/livewire/project/application/advanced.blade.php @@ -93,9 +93,9 @@ id="stopGracePeriod" label="Stop Grace Period (seconds)" placeholder="{{ DEFAULT_STOP_GRACE_PERIOD_SECONDS }}" - helper="How long to wait for graceful shutdown during rolling updates, manual stops, and restarts. Applies to all containers for this application. Default: {{ DEFAULT_STOP_GRACE_PERIOD_SECONDS }} seconds. Range: 1-3600 seconds (1 hour)." - min="1" - max="3600" + helper="How long to wait for graceful shutdown during rolling updates, manual stops, and restarts. Applies to all containers for this application. Default: {{ DEFAULT_STOP_GRACE_PERIOD_SECONDS }} seconds. Range: {{ MIN_STOP_GRACE_PERIOD_SECONDS }}-{{ MAX_STOP_GRACE_PERIOD_SECONDS }} seconds (1 hour)." + min="{{ MIN_STOP_GRACE_PERIOD_SECONDS }}" + max="{{ MAX_STOP_GRACE_PERIOD_SECONDS }}" canGate="update" :canResource="$application" /> diff --git a/tests/Feature/Livewire/Project/Application/AdvancedStopGracePeriodTest.php b/tests/Feature/Livewire/Project/Application/AdvancedStopGracePeriodTest.php new file mode 100644 index 000000000..8d8de1d47 --- /dev/null +++ b/tests/Feature/Livewire/Project/Application/AdvancedStopGracePeriodTest.php @@ -0,0 +1,98 @@ +create(); + $server = Server::factory()->create(['team_id' => $team->id]); + $project = Project::factory()->create(['team_id' => $team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + + return Application::create([ + 'name' => 'stop-grace-period-test-app', + 'git_repository' => 'https://github.com/coollabsio/coolify', + 'git_branch' => 'main', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'environment_id' => $environment->id, + 'destination_id' => $server->standaloneDockers()->firstOrFail()->id, + 'destination_type' => $server->standaloneDockers()->firstOrFail()->getMorphClass(), + ]); +} + +beforeEach(function () { + $this->actingAs(User::factory()->create()); +}); + +it('saves a valid stop grace period', function () { + $application = createApplicationForAdvancedStopGracePeriodTest(); + + Livewire::test(Advanced::class, ['application' => $application]) + ->set('stopGracePeriod', '300') + ->call('saveStopGracePeriod') + ->assertHasNoErrors() + ->assertDispatched('success'); + + expect($application->settings()->first()->stop_grace_period)->toBe(300); +}); + +it('clears the stop grace period when submitted empty', function () { + $application = createApplicationForAdvancedStopGracePeriodTest(); + $application->settings->update(['stop_grace_period' => 300]); + + Livewire::test(Advanced::class, ['application' => $application->fresh()]) + ->set('stopGracePeriod', '') + ->call('saveStopGracePeriod') + ->assertHasNoErrors() + ->assertDispatched('success'); + + expect($application->settings()->first()->stop_grace_period)->toBeNull(); +}); + +it('rejects invalid stop grace periods', function (string $value, string $rule) { + $application = createApplicationForAdvancedStopGracePeriodTest(); + + Livewire::test(Advanced::class, ['application' => $application]) + ->set('stopGracePeriod', $value) + ->call('saveStopGracePeriod') + ->assertHasErrors(['stopGracePeriod' => [$rule]]); + + expect($application->settings()->first()->stop_grace_period)->toBeNull(); +})->with([ + 'below minimum' => ['0', 'min'], + 'above maximum' => [(string) (MAX_STOP_GRACE_PERIOD_SECONDS + 1), 'max'], + 'malformed integer' => ['10abc', 'integer'], + 'decimal' => ['1.9', 'integer'], +]); + +it('uses one second deployment timeout in local only when stop grace period is unset', function () { + config(['app.env' => 'local']); + + $setting = new ApplicationSetting; + + expect($setting->deploymentStopGracePeriodSeconds())->toBe(MIN_STOP_GRACE_PERIOD_SECONDS); + + $setting->stop_grace_period = 10; + + expect($setting->deploymentStopGracePeriodSeconds())->toBe(10); +}); + +it('uses default deployment timeout outside local when stop grace period is unset', function () { + config(['app.env' => 'production']); + + $setting = new ApplicationSetting; + + expect($setting->deploymentStopGracePeriodSeconds())->toBe(DEFAULT_STOP_GRACE_PERIOD_SECONDS); +}); diff --git a/tests/Feature/ModelFillableCreationTest.php b/tests/Feature/ModelFillableCreationTest.php index b72e7381e..e6d340a4f 100644 --- a/tests/Feature/ModelFillableCreationTest.php +++ b/tests/Feature/ModelFillableCreationTest.php @@ -299,6 +299,7 @@ 'inject_build_args_to_dockerfile' => true, 'include_source_commit_in_build' => true, 'docker_images_to_keep' => 5, + 'stop_grace_period' => 300, ]); expect($setting->exists)->toBeTrue(); @@ -309,6 +310,7 @@ expect($setting->custom_internal_name)->toBe('my-custom-app'); expect($setting->is_spa)->toBeTrue(); expect($setting->docker_images_to_keep)->toBe(5); + expect($setting->stop_grace_period)->toBe(300); }); it('creates ServerSetting with all fillable attributes', function () { diff --git a/tests/Unit/ApplicationSettingStaticCastTest.php b/tests/Unit/ApplicationSettingStaticCastTest.php index d06ee920d..fe0eaec22 100644 --- a/tests/Unit/ApplicationSettingStaticCastTest.php +++ b/tests/Unit/ApplicationSettingStaticCastTest.php @@ -11,7 +11,7 @@ it('casts is_static to boolean when true', function () { $setting = new ApplicationSetting; - $setting->is_static = true; + $setting->setRawAttributes(['is_static' => true]); // Verify it's cast to boolean expect($setting->is_static)->toBeTrue() @@ -20,7 +20,7 @@ it('casts is_static to boolean when false', function () { $setting = new ApplicationSetting; - $setting->is_static = false; + $setting->setRawAttributes(['is_static' => false]); // Verify it's cast to boolean expect($setting->is_static)->toBeFalse() @@ -29,7 +29,7 @@ it('casts is_static from string "1" to boolean true', function () { $setting = new ApplicationSetting; - $setting->is_static = '1'; + $setting->setRawAttributes(['is_static' => '1']); // Should cast string to boolean expect($setting->is_static)->toBeTrue() @@ -38,7 +38,7 @@ it('casts is_static from string "0" to boolean false', function () { $setting = new ApplicationSetting; - $setting->is_static = '0'; + $setting->setRawAttributes(['is_static' => '0']); // Should cast string to boolean expect($setting->is_static)->toBeFalse() @@ -47,7 +47,7 @@ it('casts is_static from integer 1 to boolean true', function () { $setting = new ApplicationSetting; - $setting->is_static = 1; + $setting->setRawAttributes(['is_static' => 1]); // Should cast integer to boolean expect($setting->is_static)->toBeTrue() @@ -56,7 +56,7 @@ it('casts is_static from integer 0 to boolean false', function () { $setting = new ApplicationSetting; - $setting->is_static = 0; + $setting->setRawAttributes(['is_static' => 0]); // Should cast integer to boolean expect($setting->is_static)->toBeFalse() @@ -128,9 +128,6 @@ }); it('casts stop_grace_period zero to integer (documents fallback trigger)', function () { - // Value of 0 is not a valid grace period — consumers guard with - // `($value > 0) ? $value : DEFAULT_STOP_GRACE_PERIOD_SECONDS`, so - // the cast itself must still round-trip cleanly without throwing. $setting = new ApplicationSetting; $setting->stop_grace_period = 0; @@ -139,13 +136,32 @@ }); it('casts stop_grace_period negative value to integer (documents fallback trigger)', function () { - // Negative values should never be persisted (UI validates `min=1`), - // but if one slips through (direct DB write, older data), the cast - // must not throw and consumers will treat it as the fallback via the - // `> 0` guard. $setting = new ApplicationSetting; $setting->stop_grace_period = -10; expect($setting->stop_grace_period)->toBe(-10) ->and($setting->stop_grace_period)->toBeInt(); }); + +it('resolves valid stop grace periods', function (?int $storedValue, int $expectedValue) { + $setting = new ApplicationSetting; + $setting->stop_grace_period = $storedValue; + + expect($setting->stopGracePeriodSeconds())->toBe($expectedValue); +})->with([ + 'minimum' => [MIN_STOP_GRACE_PERIOD_SECONDS, MIN_STOP_GRACE_PERIOD_SECONDS], + 'custom' => [300, 300], + 'maximum' => [MAX_STOP_GRACE_PERIOD_SECONDS, MAX_STOP_GRACE_PERIOD_SECONDS], +]); + +it('falls back to default stop grace period for invalid stored values', function (?int $storedValue) { + $setting = new ApplicationSetting; + $setting->stop_grace_period = $storedValue; + + expect($setting->stopGracePeriodSeconds())->toBe(DEFAULT_STOP_GRACE_PERIOD_SECONDS); +})->with([ + 'null' => [null], + 'zero' => [0], + 'negative' => [-10], + 'above maximum' => [MAX_STOP_GRACE_PERIOD_SECONDS + 1], +]); From 26cdd6e198a76402cf1a282a52072f08d9460508 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 11 May 2026 23:51:20 +0200 Subject: [PATCH 020/158] style(teams): update switch team button styling --- resources/views/livewire/switch-team.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/switch-team.blade.php b/resources/views/livewire/switch-team.blade.php index 6a0705efc..b979647cb 100644 --- a/resources/views/livewire/switch-team.blade.php +++ b/resources/views/livewire/switch-team.blade.php @@ -26,7 +26,7 @@ }">
Date: Tue, 12 May 2026 08:34:57 +0200 Subject: [PATCH 021/158] Update ryot.yaml --- templates/compose/ryot.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/ryot.yaml b/templates/compose/ryot.yaml index 56190cd5e..41a9ccf0a 100644 --- a/templates/compose/ryot.yaml +++ b/templates/compose/ryot.yaml @@ -7,7 +7,7 @@ services: ryot: - image: ignisda/ryot:v8 + image: ignisda/ryot:v10.3.0 environment: - SERVICE_URL_RYOT_8000 - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgresql:5432/${POSTGRES_DB} From ea6c63edcf2594181f217216435561ee4c53b8f3 Mon Sep 17 00:00:00 2001 From: "Mr. Kiter" <107297392+kiterwork@users.noreply.github.com> Date: Tue, 12 May 2026 08:58:05 +0200 Subject: [PATCH 022/158] Update jellyfin.yaml --- templates/compose/jellyfin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/jellyfin.yaml b/templates/compose/jellyfin.yaml index d03b66571..59e36821d 100644 --- a/templates/compose/jellyfin.yaml +++ b/templates/compose/jellyfin.yaml @@ -7,7 +7,7 @@ services: jellyfin: - image: lscr.io/linuxserver/jellyfin:latest + image: lscr.io/linuxserver/jellyfin:10.11.8 environment: - SERVICE_URL_JELLYFIN_8096 - PUID=1000 From b678b5852473d2b55a7c7e5882f32ce696d4c907 Mon Sep 17 00:00:00 2001 From: "Mr. Kiter" <107297392+kiterwork@users.noreply.github.com> Date: Tue, 12 May 2026 09:09:36 +0200 Subject: [PATCH 023/158] Update audiobookshelf.yaml --- templates/compose/audiobookshelf.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/audiobookshelf.yaml b/templates/compose/audiobookshelf.yaml index d958f56ff..4bb54710d 100644 --- a/templates/compose/audiobookshelf.yaml +++ b/templates/compose/audiobookshelf.yaml @@ -7,7 +7,7 @@ services: audiobookshelf: - image: ghcr.io/advplyr/audiobookshelf:latest + image: ghcr.io/advplyr/audiobookshelf:2.34.0 environment: - SERVICE_URL_AUDIOBOOKSHELF_80 - TZ=${TIMEZONE:-America/Toronto} From 5267b0ad82152746d62be0a25734065a9e196fef Mon Sep 17 00:00:00 2001 From: "Mr. Kiter" <107297392+kiterwork@users.noreply.github.com> Date: Tue, 12 May 2026 09:10:44 +0200 Subject: [PATCH 024/158] Update grocy.yaml --- templates/compose/grocy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/grocy.yaml b/templates/compose/grocy.yaml index 8a014ce70..a78373fdf 100644 --- a/templates/compose/grocy.yaml +++ b/templates/compose/grocy.yaml @@ -6,7 +6,7 @@ services: grocy: - image: lscr.io/linuxserver/grocy:latest + image: lscr.io/linuxserver/grocy:4.6.0 environment: - SERVICE_URL_GROCY - PUID=1000 From dd19d81e491b60b7e8e51d40a82c6b4ebcd1588f Mon Sep 17 00:00:00 2001 From: "Mr. Kiter" <107297392+kiterwork@users.noreply.github.com> Date: Tue, 12 May 2026 09:22:15 +0200 Subject: [PATCH 025/158] Update mealie.yaml --- templates/compose/mealie.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/mealie.yaml b/templates/compose/mealie.yaml index 7f1121613..a2034bce5 100644 --- a/templates/compose/mealie.yaml +++ b/templates/compose/mealie.yaml @@ -7,7 +7,7 @@ services: mealie: - image: 'ghcr.io/mealie-recipes/mealie:latest' + image: 'ghcr.io/mealie-recipes/mealie:3.17.0' environment: - SERVICE_URL_MEALIE_9000 - ALLOW_SIGNUP=${ALLOW_SIGNUP:-true} From f098895abfa6a2d07984ffde7539b4ef4d29eafc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 12 May 2026 11:07:19 +0200 Subject: [PATCH 026/158] style(navbar): refine collapsed sidebar spacing Adjust sidebar icon sizing, collapsed menu dimensions, and main layout padding for improved alignment. Also tidy related view spacing and formatting. --- resources/css/utilities.css | 8 ++++++-- resources/views/components/navbar.blade.php | 8 ++++---- resources/views/layouts/app.blade.php | 4 ++-- resources/views/livewire/global-search.blade.php | 2 +- resources/views/livewire/project/index.blade.php | 2 +- resources/views/livewire/project/service/index.blade.php | 2 +- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/resources/css/utilities.css b/resources/css/utilities.css index 7eb926a36..170e6ac16 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -181,7 +181,7 @@ @utility menu-item { @apply flex gap-3 items-center px-2 py-1 w-full text-sm dark:hover:bg-coolgray-100 dark:hover:text-white hover:bg-neutral-300 rounded-sm truncate min-w-0; } @utility menu-item-icon { - @apply flex-shrink-0 w-6 h-6 dark:hover:text-white; + @apply shrink-0 size-4 dark:hover:text-white; } @utility menu-item-label { @@ -201,7 +201,7 @@ @utility sub-menu-item { } @utility sub-menu-item-icon { - @apply flex-shrink-0 w-4 h-4 dark:hover:text-white; + @apply shrink-0 size-4 dark:hover:text-white; } @utility heading-item-active { @@ -347,8 +347,12 @@ @utility log-info { @media (min-width: 1024px) { .sidebar-collapsed .menu-item { justify-content: center; + width: var(--button-h, 2rem); + height: var(--button-h, 2rem); + min-height: var(--button-h, 2rem); padding-left: 0; padding-right: 0; gap: 0; + margin-inline: auto; } } diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index c5b076a71..3b21a81d5 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -1,5 +1,5 @@
\ No newline at end of file + diff --git a/resources/views/livewire/project/index.blade.php b/resources/views/livewire/project/index.blade.php index b9ee20326..b5e16f597 100644 --- a/resources/views/livewire/project/index.blade.php +++ b/resources/views/livewire/project/index.blade.php @@ -2,7 +2,7 @@ Projects | Coolify -
+

Projects

@can('createAnyResource') diff --git a/resources/views/livewire/project/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php index b849143fb..8ff04cbc2 100644 --- a/resources/views/livewire/project/service/index.blade.php +++ b/resources/views/livewire/project/service/index.blade.php @@ -123,7 +123,7 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ @if ($showPortWarningModal)
+ :class="{ 'z-40': modalOpen }" class="relative">