From a3cecff97b981d7dbf017a6292ddeb243515092f Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sat, 11 Oct 2025 13:49:19 +0200
Subject: [PATCH 62/73] refactor: remove debug sleep from global search modal
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Remove debug sleep(4) statement from openSearchModal method.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
app/Livewire/GlobalSearch.php | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
index 5fcedd94d..680ac7701 100644
--- a/app/Livewire/GlobalSearch.php
+++ b/app/Livewire/GlobalSearch.php
@@ -79,7 +79,6 @@ public function mount()
public function openSearchModal()
{
- sleep(4);
$this->isModalOpen = true;
$this->loadSearchableItems();
$this->loadCreatableItems();
From d93a13eeee237783b2c181b4ac20e1efc3517e98 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sat, 11 Oct 2025 13:56:55 +0200
Subject: [PATCH 63/73] feat: add YAML validation for cloud-init scripts
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add ValidCloudInitYaml validation rule to ensure cloud-init scripts
are properly formatted before saving. The validator supports:
- Cloud-config YAML (with or without #cloud-config header)
- Bash scripts (starting with #!)
- Empty/null values (optional field)
Uses Symfony YAML parser to validate YAML syntax and provides
detailed error messages when validation fails.
Added comprehensive unit tests covering:
- Valid cloud-config with/without header
- Valid bash scripts
- Invalid YAML syntax detection
- Complex multi-section cloud-config
Applied validation to:
- ByHetzner component (server creation)
- CloudInitScriptForm component (script management)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
app/Livewire/Security/CloudInitScriptForm.php | 2 +-
app/Livewire/Server/New/ByHetzner.php | 2 +-
app/Rules/ValidCloudInitYaml.php | 55 ++++++
.../security/cloud-init-script-form.blade.php | 2 +-
tests/Unit/Rules/ValidCloudInitYamlTest.php | 174 ++++++++++++++++++
5 files changed, 232 insertions(+), 3 deletions(-)
create mode 100644 app/Rules/ValidCloudInitYaml.php
create mode 100644 tests/Unit/Rules/ValidCloudInitYamlTest.php
diff --git a/app/Livewire/Security/CloudInitScriptForm.php b/app/Livewire/Security/CloudInitScriptForm.php
index ff670cd4f..33beff334 100644
--- a/app/Livewire/Security/CloudInitScriptForm.php
+++ b/app/Livewire/Security/CloudInitScriptForm.php
@@ -36,7 +36,7 @@ protected function rules(): array
{
return [
'name' => 'required|string|max:255',
- 'script' => 'required|string',
+ 'script' => ['required', 'string', new \App\Rules\ValidCloudInitYaml],
];
}
diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php
index 7d828b12e..abbe4c379 100644
--- a/app/Livewire/Server/New/ByHetzner.php
+++ b/app/Livewire/Server/New/ByHetzner.php
@@ -157,7 +157,7 @@ protected function rules(): array
'selectedHetznerSshKeyIds.*' => 'integer',
'enable_ipv4' => 'required|boolean',
'enable_ipv6' => 'required|boolean',
- 'cloud_init_script' => 'nullable|string',
+ 'cloud_init_script' => ['nullable', 'string', new \App\Rules\ValidCloudInitYaml],
'save_cloud_init_script' => 'boolean',
'cloud_init_script_name' => 'nullable|string|max:255',
'selected_cloud_init_script_id' => 'nullable|integer|exists:cloud_init_scripts,id',
diff --git a/app/Rules/ValidCloudInitYaml.php b/app/Rules/ValidCloudInitYaml.php
new file mode 100644
index 000000000..8116e1161
--- /dev/null
+++ b/app/Rules/ValidCloudInitYaml.php
@@ -0,0 +1,55 @@
+getMessage());
+ }
+
+ return;
+ }
+
+ // If it doesn't start with #! or #cloud-config, try to parse as YAML
+ // (some users might omit the #cloud-config header)
+ try {
+ Yaml::parse($script);
+ } catch (ParseException $e) {
+ $fail('The :attribute must be either a valid bash script (starting with #!) or valid cloud-config YAML. YAML parse error: '.$e->getMessage());
+ }
+ }
+}
diff --git a/resources/views/livewire/security/cloud-init-script-form.blade.php b/resources/views/livewire/security/cloud-init-script-form.blade.php
index 1632b48d3..83bedffab 100644
--- a/resources/views/livewire/security/cloud-init-script-form.blade.php
+++ b/resources/views/livewire/security/cloud-init-script-form.blade.php
@@ -2,7 +2,7 @@
+ helper="Enter your cloud-init script. Supports cloud-config YAML format." required />
@if ($modal_mode)
diff --git a/tests/Unit/Rules/ValidCloudInitYamlTest.php b/tests/Unit/Rules/ValidCloudInitYamlTest.php
new file mode 100644
index 000000000..f3ea906af
--- /dev/null
+++ b/tests/Unit/Rules/ValidCloudInitYamlTest.php
@@ -0,0 +1,174 @@
+validate('script', $script, function ($message) use (&$valid) {
+ $valid = false;
+ });
+
+ expect($valid)->toBeTrue();
+});
+
+it('accepts valid cloud-config YAML without header', function () {
+ $rule = new ValidCloudInitYaml;
+ $valid = true;
+
+ $script = <<<'YAML'
+users:
+ - name: demo
+ groups: sudo
+packages:
+ - nginx
+YAML;
+
+ $rule->validate('script', $script, function ($message) use (&$valid) {
+ $valid = false;
+ });
+
+ expect($valid)->toBeTrue();
+});
+
+it('accepts valid bash script with shebang', function () {
+ $rule = new ValidCloudInitYaml;
+ $valid = true;
+
+ $script = <<<'BASH'
+#!/bin/bash
+apt update
+apt install -y nginx
+systemctl start nginx
+BASH;
+
+ $rule->validate('script', $script, function ($message) use (&$valid) {
+ $valid = false;
+ });
+
+ expect($valid)->toBeTrue();
+});
+
+it('accepts empty or null script', function () {
+ $rule = new ValidCloudInitYaml;
+ $valid = true;
+
+ $rule->validate('script', '', function ($message) use (&$valid) {
+ $valid = false;
+ });
+
+ expect($valid)->toBeTrue();
+
+ $rule->validate('script', null, function ($message) use (&$valid) {
+ $valid = false;
+ });
+
+ expect($valid)->toBeTrue();
+});
+
+it('rejects invalid YAML format', function () {
+ $rule = new ValidCloudInitYaml;
+ $valid = true;
+ $errorMessage = '';
+
+ $script = <<<'YAML'
+#cloud-config
+users:
+ - name: demo
+ groups: sudo
+ invalid_indentation
+packages:
+ - nginx
+YAML;
+
+ $rule->validate('script', $script, function ($message) use (&$valid, &$errorMessage) {
+ $valid = false;
+ $errorMessage = $message;
+ });
+
+ expect($valid)->toBeFalse();
+ expect($errorMessage)->toContain('YAML');
+});
+
+it('rejects script that is neither bash nor valid YAML', function () {
+ $rule = new ValidCloudInitYaml;
+ $valid = true;
+ $errorMessage = '';
+
+ $script = <<<'INVALID'
+this is not valid YAML
+ and has invalid indentation:
+ - item
+ without proper structure {
+INVALID;
+
+ $rule->validate('script', $script, function ($message) use (&$valid, &$errorMessage) {
+ $valid = false;
+ $errorMessage = $message;
+ });
+
+ expect($valid)->toBeFalse();
+ expect($errorMessage)->toContain('bash script');
+});
+
+it('accepts complex cloud-config with multiple sections', function () {
+ $rule = new ValidCloudInitYaml;
+ $valid = true;
+
+ $script = <<<'YAML'
+#cloud-config
+users:
+ - name: coolify
+ groups: sudo, docker
+ shell: /bin/bash
+ sudo: ['ALL=(ALL) NOPASSWD:ALL']
+ ssh_authorized_keys:
+ - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ...
+
+packages:
+ - docker.io
+ - docker-compose
+ - git
+ - curl
+
+package_update: true
+package_upgrade: true
+
+runcmd:
+ - systemctl enable docker
+ - systemctl start docker
+ - usermod -aG docker coolify
+ - echo "Server setup complete"
+
+write_files:
+ - path: /etc/docker/daemon.json
+ content: |
+ {
+ "log-driver": "json-file",
+ "log-opts": {
+ "max-size": "10m",
+ "max-file": "3"
+ }
+ }
+YAML;
+
+ $rule->validate('script', $script, function ($message) use (&$valid) {
+ $valid = false;
+ });
+
+ expect($valid)->toBeTrue();
+});
From 9c21304ddbe39e1862632ea03b792b32a208eb4e Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sat, 11 Oct 2025 14:06:16 +0200
Subject: [PATCH 64/73] docs: clarify cloud-init script compatibility
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add note that cloud-init scripts currently work only with Hetzner
integration to set user expectations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
resources/views/livewire/security/cloud-init-scripts.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/resources/views/livewire/security/cloud-init-scripts.blade.php b/resources/views/livewire/security/cloud-init-scripts.blade.php
index aa7324e4b..e2013a4fb 100644
--- a/resources/views/livewire/security/cloud-init-scripts.blade.php
+++ b/resources/views/livewire/security/cloud-init-scripts.blade.php
@@ -8,7 +8,7 @@
@endcan
- Manage reusable cloud-init scripts for server initialization.
+ Manage reusable cloud-init scripts for server initialization. Currently working only with Hetzner's integration.
@forelse ($scripts as $script)
From 7ad7247284f257a3e3c58a5f8dd44c72e9763e26 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sat, 11 Oct 2025 18:51:29 +0200
Subject: [PATCH 65/73] feat: add clear button for cloud-init script dropdown
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add a 'Clear' button next to the cloud-init script dropdown that:
- Resets the dropdown to default (placeholder option)
- Clears the cloud-init script textarea
- Clears the script name input
- Unchecks the 'save script' checkbox
Improves UX by allowing users to quickly reset cloud-init fields
without manually clearing each field.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
app/Livewire/Server/New/ByHetzner.php | 8 ++++++++
.../livewire/server/new/by-hetzner.blade.php | 19 ++++++++++++-------
2 files changed, 20 insertions(+), 7 deletions(-)
diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php
index abbe4c379..f3368b4eb 100644
--- a/app/Livewire/Server/New/ByHetzner.php
+++ b/app/Livewire/Server/New/ByHetzner.php
@@ -407,6 +407,14 @@ public function updatedSelectedCloudInitScriptId($value)
}
}
+ public function clearCloudInitScript()
+ {
+ $this->selected_cloud_init_script_id = null;
+ $this->cloud_init_script = '';
+ $this->cloud_init_script_name = '';
+ $this->save_cloud_init_script = false;
+ }
+
private function createHetznerServer(string $token): array
{
$hetznerService = new HetznerService($token);
diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php
index 63f420f3f..b657f63e3 100644
--- a/resources/views/livewire/server/new/by-hetzner.blade.php
+++ b/resources/views/livewire/server/new/by-hetzner.blade.php
@@ -156,15 +156,20 @@ class="p-4 border border-yellow-500 dark:border-yellow-600 rounded bg-yellow-50
-
+
@if ($saved_cloud_init_scripts->count() > 0)
-
-
- @foreach ($saved_cloud_init_scripts as $script)
-
- @endforeach
-
+
+
+
+ @foreach ($saved_cloud_init_scripts as $script)
+
+ @endforeach
+
+
+ Clear
+
+
@endif
Date: Sat, 11 Oct 2025 18:55:43 +0200
Subject: [PATCH 66/73] refactor: reduce cloud-init label width for better
layout
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Reduce label width from w-64 to w-32 to prevent layout issues
when the clear button is added next to the dropdown.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
resources/views/livewire/server/new/by-hetzner.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php
index b657f63e3..4e9bcedc2 100644
--- a/resources/views/livewire/server/new/by-hetzner.blade.php
+++ b/resources/views/livewire/server/new/by-hetzner.blade.php
@@ -157,7 +157,7 @@ class="p-4 border border-yellow-500 dark:border-yellow-600 rounded bg-yellow-50
-
+
@if ($saved_cloud_init_scripts->count() > 0)
From 821aa6a5317f46c34934e5e89b6cbdf61f6c562d Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sat, 11 Oct 2025 18:57:36 +0200
Subject: [PATCH 67/73] dev container name
---
docker-compose.dev.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 164f0aea5..fee17dad6 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -65,6 +65,7 @@ services:
vite:
image: node:24-alpine
pull_policy: always
+ container_name: coolify-vite
working_dir: /var/www/html
environment:
VITE_HOST: "${VITE_HOST:-localhost}"
From 6e08af601646b8dfae1b5a505700b8973baf1e11 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sat, 11 Oct 2025 19:17:09 +0200
Subject: [PATCH 68/73] Update sponsors list in README
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add new sponsors:
- 23M - High-availability hosting solutions
- Brand.dev - API for product personalization
- ByteBase - Database CI/CD and Security
- Formbricks - Open source feedback platform
- Mobb - AI-generated code security
- Ramnode - High Performance Cloud VPS Hosting
Remove inactive sponsors (commented out in website):
- Cloudify.ro
- GlueOps
- MassiveGrid
- QuantCDN
- Trieve
- WZ-IT
Reorganize sponsors in alphabetical order for easier maintenance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
README.md | 56 +++++++++++++++++++++++++++----------------------------
1 file changed, 28 insertions(+), 28 deletions(-)
diff --git a/README.md b/README.md
index 1c88f4c54..9be4130c2 100644
--- a/README.md
+++ b/README.md
@@ -53,40 +53,40 @@ # Donations
## Big Sponsors
-* [CubePath](https://cubepath.com/?ref=coolify.io) - Dedicated Servers & Instant Deploy
-* [GlueOps](https://www.glueops.dev?ref=coolify.io) - DevOps automation and infrastructure management
+* [23M](https://23m.com?ref=coolify.io) - Your experts for high-availability hosting solutions!
* [Algora](https://algora.io?ref=coolify.io) - Open source contribution platform
-* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform
-* [LiquidWeb](https://liquidweb.com?ref=coolify.io) - Premium managed hosting solutions
-* [Convex](https://convex.link/coolify.io) - Open-source reactive database for web app developers
-* [Arcjet](https://arcjet.com?ref=coolify.io) - Advanced web security and performance solutions
-* [SaasyKit](https://saasykit.com?ref=coolify.io) - Complete SaaS starter kit for developers
-* [SupaGuide](https://supa.guide?ref=coolify.io) - Your comprehensive guide to Supabase
-* [Logto](https://logto.io?ref=coolify.io) - The better identity infrastructure for developers
-* [Trieve](https://trieve.ai?ref=coolify.io) - AI-powered search and analytics
-* [Supadata AI](https://supadata.ai/?ref=coolify.io) - Scrape YouTube, web, and files. Get AI-ready, clean data
-* [Darweb](https://darweb.nl/?ref=coolify.io) - Design. Develop. Deliver. Specialized in 3D CPQ Solutions
-* [Hetzner](http://htznr.li/CoolifyXHetzner) - Server, cloud, hosting, and data center solutions
-* [COMIT](https://comit.international?ref=coolify.io) - New York Times award–winning contractor
-* [Blacksmith](https://blacksmith.sh?ref=coolify.io) - Infrastructure automation platform
-* [WZ-IT](https://wz-it.com/?ref=coolify.io) - German agency for customised cloud solutions
-* [BC Direct](https://bc.direct?ref=coolify.io) - Your trusted technology consulting partner
-* [Tigris](https://www.tigrisdata.com?ref=coolify.io) - Modern developer data platform
-* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - Web hosting and VPS solutions
-* [QuantCDN](https://www.quantcdn.io?ref=coolify.io) - Enterprise-grade content delivery network
-* [PFGLabs](https://pfglabs.com?ref=coolify.io) - Build Real Projects with Golang
-* [JobsCollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - 30,000+ remote jobs for developers
-* [Juxtdigital](https://juxtdigital.com?ref=coolify.io) - Digital PR & AI Authority Building Agency
-* [Cloudify.ro](https://cloudify.ro?ref=coolify.io) - Cloud hosting solutions
-* [CodeRabbit](https://coderabbit.ai?ref=coolify.io) - Cut Code Review Time & Bugs in Half
* [American Cloud](https://americancloud.com?ref=coolify.io) - US-based cloud infrastructure services
-* [MassiveGrid](https://massivegrid.com?ref=coolify.io) - Enterprise cloud hosting solutions
-* [Syntax.fm](https://syntax.fm?ref=coolify.io) - Podcast for web developers
-* [Tolgee](https://tolgee.io?ref=coolify.io) - The open source localization platform
+* [Arcjet](https://arcjet.com?ref=coolify.io) - Advanced web security and performance solutions
+* [BC Direct](https://bc.direct?ref=coolify.io) - Your trusted technology consulting partner
+* [Blacksmith](https://blacksmith.sh?ref=coolify.io) - Infrastructure automation platform
+* [Brand.dev](https://brand.dev?ref=coolify.io) - API to personalize your product with logos, colors, and company info from any domain
+* [ByteBase](https://www.bytebase.com?ref=coolify.io) - Database CI/CD and Security at Scale
+* [CodeRabbit](https://coderabbit.ai?ref=coolify.io) - Cut Code Review Time & Bugs in Half
+* [COMIT](https://comit.international?ref=coolify.io) - New York Times award–winning contractor
* [CompAI](https://www.trycomp.ai?ref=coolify.io) - Open source compliance automation platform
+* [Convex](https://convex.link/coolify.io) - Open-source reactive database for web app developers
+* [CubePath](https://cubepath.com/?ref=coolify.io) - Dedicated Servers & Instant Deploy
+* [Darweb](https://darweb.nl/?ref=coolify.io) - Design. Develop. Deliver. Specialized in 3D CPQ Solutions
+* [Formbricks](https://formbricks.com?ref=coolify.io) - The open source feedback platform
* [GoldenVM](https://billing.goldenvm.com?ref=coolify.io) - Premium virtual machine hosting solutions
* [Gozunga](https://gozunga.com?ref=coolify.io) - Seriously Simple Cloud Infrastructure
+* [Hetzner](http://htznr.li/CoolifyXHetzner) - Server, cloud, hosting, and data center solutions
+* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - Web hosting and VPS solutions
+* [JobsCollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - 30,000+ remote jobs for developers
+* [Juxtdigital](https://juxtdigital.com?ref=coolify.io) - Digital PR & AI Authority Building Agency
+* [LiquidWeb](https://liquidweb.com?ref=coolify.io) - Premium managed hosting solutions
+* [Logto](https://logto.io?ref=coolify.io) - The better identity infrastructure for developers
* [Macarne](https://macarne.com?ref=coolify.io) - Best IP Transit & Carrier Ethernet Solutions for Simplified Network Connectivity
+* [Mobb](https://vibe.mobb.ai/?ref=coolify.io) - Secure Your AI-Generated Code to Unlock Dev Productivity
+* [PFGLabs](https://pfglabs.com?ref=coolify.io) - Build Real Projects with Golang
+* [Ramnode](https://ramnode.com/?ref=coolify.io) - High Performance Cloud VPS Hosting
+* [SaasyKit](https://saasykit.com?ref=coolify.io) - Complete SaaS starter kit for developers
+* [SupaGuide](https://supa.guide?ref=coolify.io) - Your comprehensive guide to Supabase
+* [Supadata AI](https://supadata.ai/?ref=coolify.io) - Scrape YouTube, web, and files. Get AI-ready, clean data
+* [Syntax.fm](https://syntax.fm?ref=coolify.io) - Podcast for web developers
+* [Tigris](https://www.tigrisdata.com?ref=coolify.io) - Modern developer data platform
+* [Tolgee](https://tolgee.io?ref=coolify.io) - The open source localization platform
+* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform
## Small Sponsors
From 6297ac6c88a712b8e867d6442ea81aa7abc8cb73 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sun, 12 Oct 2025 14:57:45 +0200
Subject: [PATCH 69/73] feat: replace terminal dropdown with searchable
datalist component
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Enhanced the terminal server/container selection with a new searchable datalist component:
**Terminal View Changes:**
- Replaced `x-forms.select` with `x-forms.datalist` for server/container selection
- Added search functionality for filtering servers and containers
- Fixed form validation by adding hidden input for proper HTML5 validation
- Prevented error messages when clearing selection (sets to 'default')
**Datalist Component (Single Selection):**
- Implemented Alpine.js-powered dropdown with search functionality
- Added visual dropdown arrow that rotates when opened
- Proper entangle binding for wire:model support
- Keyboard support (Escape to close)
- Click outside to close behavior
- Disabled options filtering (skips disabled options)
- Consistent styling with input/textarea components
**Styling Improvements:**
- Explicit background colors: `bg-white` (light) and `dark:bg-coolgray-100` (dark)
- Proper ring border: `ring-1 ring-inset ring-neutral-200 dark:ring-coolgray-300`
- Focus states: `focus-within:ring-2 focus-within:ring-coollabs dark:focus-within:ring-warning`
- Text colors: `text-black dark:text-white`
- Added custom scrollbar styling for dropdown lists
- Wire:dirty state support for visual feedback
- Proper padding and spacing (`py-1.5`, `px-1`, `px-2`)
**Multiple Selection Mode:**
- Also updated for consistent styling and scrollbar support
- Added proper background colors and focus states
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
app/Livewire/Terminal/Index.php | 4 +
.../views/components/forms/datalist.blade.php | 149 +++++++++++++++---
.../views/livewire/terminal/index.blade.php | 4 +-
3 files changed, 136 insertions(+), 21 deletions(-)
diff --git a/app/Livewire/Terminal/Index.php b/app/Livewire/Terminal/Index.php
index 03dbc1d91..6bb4c5e90 100644
--- a/app/Livewire/Terminal/Index.php
+++ b/app/Livewire/Terminal/Index.php
@@ -61,6 +61,10 @@ private function getAllActiveContainers()
public function updatedSelectedUuid()
{
+ if ($this->selected_uuid === 'default') {
+ // When cleared to default, do nothing (no error message)
+ return;
+ }
$this->connectToContainer();
}
diff --git a/resources/views/components/forms/datalist.blade.php b/resources/views/components/forms/datalist.blade.php
index 224ff82ab..7f9ffefec 100644
--- a/resources/views/components/forms/datalist.blade.php
+++ b/resources/views/components/forms/datalist.blade.php
@@ -98,11 +98,12 @@
{{-- Unified Input Container with Tags Inside --}}
+ wire:loading.class="opacity-50"
+ wire:dirty.class="dark:ring-warning ring-warning">
{{-- Selected Tags Inside Input --}}
@@ -124,13 +125,13 @@ class="inline-flex items-center gap-1.5 px-2 py-0.5 text-xs bg-coolgray-200 dark
@required($required) @readonly($readonly) @disabled($disabled) @if ($autofocus)
autofocus
@endif
- class="flex-1 min-w-[120px] text-sm border-0 outline-none bg-transparent p-0 focus:ring-0 placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
+ class="flex-1 min-w-[120px] text-sm border-0 outline-none bg-transparent p-0 focus:ring-0 placeholder:text-neutral-400 dark:placeholder:text-neutral-600 text-black dark:text-white"
/>
{{-- Dropdown Options --}}
+ class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
@@ -156,21 +157,131 @@ class="w-4 h-4 rounded border-neutral-300 dark:border-neutral-600 bg-white dark:
@else
-{{-- Single Selection Mode (Standard HTML5 Datalist) --}}
-merge(['class' => $defaultClass]) }} @required($required)
- @readonly($readonly) @disabled($disabled) wire:dirty.class="dark:ring-warning ring-warning"
- wire:loading.attr="disabled" name="{{ $id }}"
- @if ($value) value="{{ $value }}" @endif
- @if ($placeholder) placeholder="{{ $placeholder }}" @endif
- @if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }}
- @else
- wire:model="{{ $id }}" @endif
- @if ($instantSave) wire:change="{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}"
- wire:blur="{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}" @endif
- @if ($autofocus) x-ref="autofocusInput" @endif>
-
+{{-- Single Selection Mode with Alpine.js --}}
+
+
+ {{-- Hidden input for form validation --}}
+
+
+ {{-- Input Container --}}
+
+
+ {{-- Display Selected Value or Search Input --}}
+
+
+
+
+
+
+
+ {{-- Dropdown Arrow --}}
+
+
+
+ {{-- Dropdown Options --}}
+
+
+ {{-- Hidden datalist for options --}}
+
+
@endif
@error($id)
diff --git a/resources/views/livewire/terminal/index.blade.php b/resources/views/livewire/terminal/index.blade.php
index aed2ef55d..0d6e7c559 100644
--- a/resources/views/livewire/terminal/index.blade.php
+++ b/resources/views/livewire/terminal/index.blade.php
@@ -17,7 +17,7 @@
@if ($servers->count() > 0)
@else
From 7a008c859ad68332de72683ddb751e40a6487c38 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Sun, 12 Oct 2025 17:59:37 +0200
Subject: [PATCH 70/73] feat(onboarding): redesign user onboarding flow with
modern UI/UX
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add centered, card-based layout with clean design
- Implement 3-step progress indicator component
- Add proper dark/light mode support following Coolify design system
- Implement Livewire URL state persistence for browser navigation
- Separate private key textareas for "Generate" vs "Add your own" modes
- Consistent checkpoint styling across all onboarding phases
- Enhanced typography with prominent titles (semibold, white in dark mode)
- Fixed state restoration on page refresh and browser back/forward navigation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
app/Livewire/Boarding/Index.php | 83 +-
.../components/boarding-progress.blade.php | 47 ++
.../views/components/boarding-step.blade.php | 40 +-
resources/views/layouts/boarding.blade.php | 2 +-
.../views/livewire/boarding/index.blade.php | 717 +++++++++++++-----
5 files changed, 675 insertions(+), 214 deletions(-)
create mode 100644 resources/views/components/boarding-progress.blade.php
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index 430470fa0..e6f4c1d7b 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -16,14 +16,18 @@ class Index extends Component
{
protected $listeners = ['refreshBoardingIndex' => 'validateServer'];
+ #[\Livewire\Attributes\Url(as: 'step', history: true)]
public string $currentState = 'welcome';
+ #[\Livewire\Attributes\Url(keep: true)]
public ?string $selectedServerType = null;
public ?Collection $privateKeys = null;
+ #[\Livewire\Attributes\Url(keep: true)]
public ?int $selectedExistingPrivateKey = null;
+ #[\Livewire\Attributes\Url(keep: true)]
public ?string $privateKeyType = null;
public ?string $privateKey = null;
@@ -38,6 +42,7 @@ class Index extends Component
public ?Collection $servers = null;
+ #[\Livewire\Attributes\Url(keep: true)]
public ?int $selectedExistingServer = null;
public ?string $remoteServerName = null;
@@ -58,6 +63,7 @@ class Index extends Component
public Collection $projects;
+ #[\Livewire\Attributes\Url(keep: true)]
public ?int $selectedProject = null;
public ?Project $createdProject = null;
@@ -79,17 +85,63 @@ public function mount()
$this->minDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.');
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
- if (isDev()) {
- $this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY-----
-b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
-QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
-hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
-AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
-uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
------END OPENSSH PRIVATE KEY-----';
- $this->privateKeyDescription = 'Created by Coolify';
- $this->remoteServerDescription = 'Created by Coolify';
- $this->remoteServerHost = 'coolify-testing-host';
+
+ // Initialize collections to avoid null errors
+ if ($this->privateKeys === null) {
+ $this->privateKeys = collect();
+ }
+ if ($this->servers === null) {
+ $this->servers = collect();
+ }
+ if (! isset($this->projects)) {
+ $this->projects = collect();
+ }
+
+ // Restore state when coming from URL with query params
+ if ($this->selectedServerType === 'localhost' && $this->selectedExistingServer === 0) {
+ $this->createdServer = Server::find(0);
+ if ($this->createdServer) {
+ $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
+ }
+ }
+
+ if ($this->selectedServerType === 'remote') {
+ if ($this->privateKeys->isEmpty()) {
+ $this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
+ }
+ if ($this->servers->isEmpty()) {
+ $this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
+ }
+
+ if ($this->selectedExistingServer) {
+ $this->createdServer = Server::find($this->selectedExistingServer);
+ if ($this->createdServer) {
+ $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
+ $this->updateServerDetails();
+ }
+ }
+
+ if ($this->selectedExistingPrivateKey) {
+ $this->createdPrivateKey = PrivateKey::where('team_id', currentTeam()->id)
+ ->where('id', $this->selectedExistingPrivateKey)
+ ->first();
+ if ($this->createdPrivateKey) {
+ $this->privateKey = $this->createdPrivateKey->private_key;
+ $this->publicKey = $this->createdPrivateKey->getPublicKey();
+ }
+ }
+
+ // Auto-regenerate key pair for "Generate with Coolify" mode on page refresh
+ if ($this->privateKeyType === 'create' && empty($this->privateKey)) {
+ $this->createNewPrivateKey();
+ }
+ }
+
+ if ($this->selectedProject) {
+ $this->createdProject = Project::find($this->selectedProject);
+ if (! $this->createdProject) {
+ $this->projects = Project::ownedByCurrentTeam(['name'])->get();
+ }
}
}
@@ -129,11 +181,7 @@ public function setServerType(string $type)
return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') {
- if (isDev()) {
- $this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->get();
- } else {
- $this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
- }
+ $this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
}
@@ -202,6 +250,9 @@ public function setPrivateKey(string $type)
$this->privateKeyType = $type;
if ($type === 'create') {
$this->createNewPrivateKey();
+ } else {
+ $this->privateKey = null;
+ $this->publicKey = null;
}
$this->currentState = 'create-private-key';
}
diff --git a/resources/views/components/boarding-progress.blade.php b/resources/views/components/boarding-progress.blade.php
new file mode 100644
index 000000000..a946a7471
--- /dev/null
+++ b/resources/views/components/boarding-progress.blade.php
@@ -0,0 +1,47 @@
+@props(['currentStep' => 1, 'totalSteps' => 3])
+
+
+
+ @for ($i = 1; $i <= $totalSteps; $i++)
+
+
+
+ @if ($i < $currentStep)
+
+ @else
+
+ {{ $i }}
+
+ @endif
+
+
+ @if ($i === 1)
+ Server
+ @elseif ($i === 2)
+ Connection
+ @elseif ($i === 3)
+ Complete
+ @endif
+
+
+ @if ($i < $totalSteps)
+
+
+ @endif
+
+ @endfor
+
+
diff --git a/resources/views/components/boarding-step.blade.php b/resources/views/components/boarding-step.blade.php
index d963e55f0..3e6dc92e5 100644
--- a/resources/views/components/boarding-step.blade.php
+++ b/resources/views/components/boarding-step.blade.php
@@ -1,25 +1,29 @@
-
-
-
{{ $title }}
-
+
+
+
+
{{ $title }}
@isset($question)
-
+
{{ $question }}
-
+
@endisset
+
+ @if ($actions)
+
+ {{ $actions }}
+
+ @endif
- @if ($actions)
-
- {{ $actions }}
+
+ @isset($explanation)
+
+
+ Technical Details
+
+
+ {{ $explanation }}
+
- @endif
+ @endisset
- @isset($explanation)
-
-
Explanation
-
- {{ $explanation }}
-
-
- @endisset
diff --git a/resources/views/layouts/boarding.blade.php b/resources/views/layouts/boarding.blade.php
index 67c1cc998..e0b5f49a3 100644
--- a/resources/views/layouts/boarding.blade.php
+++ b/resources/views/layouts/boarding.blade.php
@@ -1,6 +1,6 @@
@extends('layouts.base')
@section('body')
-
+
{{ $slot }}
@parent
diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php
index 68b328b30..e396611a8 100644
--- a/resources/views/livewire/boarding/index.blade.php
+++ b/resources/views/livewire/boarding/index.blade.php
@@ -2,68 +2,160 @@
Onboarding | Coolify
-
-
+
+
@if ($currentState === 'welcome')
-
Welcome to Coolify
-
Let me help you set up the basics.
-
-
Get
- Started
-
+
+
+
Welcome to Coolify
+
+ Connect your first server and start deploying in minutes
+
+
+
+
+
+ What You'll Set Up
+
+
+
+
+
+
Server Connection
+
Connect via SSH to deploy your resources
+
+
+
+
+
+
Docker Environment
+
Automated installation and configuration
+
+
+
+
+
+
Project Structure
+
Organize your applications and resources
+
+
+
+
+
+
+
+ Start Setup
+
+
@elseif ($currentState === 'explanation')
-
+
+
- Coolify is an all-in-one application to automate tasks on your servers, deploy applications with
- Git
- integrations, deploy databases and services, monitor these resources with notifications and
- alerts
- without vendor lock-in.
- Coolify Home.
-
-
-
+ Coolify automates deployment and infrastructure management on your own servers. Deploy applications
+ from Git, manage databases, and monitor everything—without vendor lock-in.
- You don't need to manage your servers anymore.
- Coolify does
- it for you.
+ Coolify handles server configuration, Docker management, and
+ deployments automatically.
- All configurations are stored on your servers, so
- everything works without a connection to Coolify (except integrations and automations).
+ All data and configurations live on your infrastructure.
+ Works offline except for external integrations.
- You can get notified on your favourite platforms
- (Discord,
- Telegram, Email, etc.) when something goes wrong, or an action is needed from your side.
+ Get real-time notifications via Discord, Telegram,
+ Email, and other platforms.
- Next
+
+ Continue
@elseif ($currentState === 'select-server-type')
-
+
+
- Do you want to deploy your resources to your
-
- or to a
- ?
+ Select where to deploy your applications and databases. You can add more servers later.
- Localhost
-
+
+
-
Remote Server
-
+
+
@if (!$serverReachable)
@@ -111,51 +203,97 @@ class="bg-red-200 dark:bg-red-900 px-1 rounded-sm">~/.ssh/authorized_keys
@endif
- Servers are the main building blocks, as they will host your applications, databases,
- services, called resources. Any CPU intensive process will use the server's CPU where you
- are deploying your resources.
- is the server where Coolify is running on. It is not
- recommended to use one server
- for everything.
+ host your applications, databases, and services (collectively
+ called resources). All CPU-intensive operations run on the target server.
- is a server reachable through SSH. It can be hosted
- at home, or from any cloud
- provider.
+ The machine running Coolify. Not recommended for production
+ workloads due to resource contention.
+
+
+ Any SSH-accessible server—cloud providers (AWS, Hetzner,
+ DigitalOcean), bare metal, or self-hosted infrastructure.
@elseif ($currentState === 'private-key')
-
+
+
- Do you have your own SSH Private Key?
+ Configure SSH key-based authentication for secure server access.
- Yes
-
- No (create one for me)
-
- @if (count($privateKeys) > 0)
-
+ @if ($privateKeys && $privateKeys->count() > 0)
+
@endif
+
+
+
+
+
+
Use Existing Key
+
I have my own SSH key
+
+
+
+
+
+
+
+
Generate New Key
+
Create ED25519 key pair
+
+
+
+
- SSH Keys are used to connect to a remote server through a secure shell, called SSH.
- You can use your own ssh private key, or you can let Coolify to create one for you.
- In both ways, you need to add the public version of your ssh private key to the remote
- server's
- ~/.ssh/authorized_keys file.
+
+ Uses public-key cryptography for secure,
+ password-less server access.
+
+
+ Add the public key to your server's
+ ~/.ssh/authorized_keys
+ file.
+
+
+ Coolify generates ED25519 keys by default for optimal
+ security and performance.
@@ -237,164 +375,385 @@ class="bg-red-200 dark:bg-red-900 px-1 rounded-sm">~/.ssh/authorized_keys
@elseif ($currentState === 'create-private-key')
-
+
+
- Please let me know your key details.
+ Configure your SSH key for server authentication.
-
- Private Keys are used to connect to a remote server through a secure shell, called SSH.
- You can use your own private key, or you can let Coolify to create one for you.
- In both ways, you need to add the public version of your private key to the remote server's
- ~/.ssh/authorized_keys file.
+
+ Private keys are encrypted at rest in Coolify's database.
+
+
+ Deploy the public key to
+ ~/.ssh/authorized_keys
+ on your target server for the specified user.
+
+
+ Supports RSA, ED25519, ECDSA, and DSA key types in OpenSSH
+ format.
@elseif ($currentState === 'create-server')
-
+
+
- Please let me know your server details.
+ Provide connection details for your remote server.
-