diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php
index 92829edd6..de79fcd24 100644
--- a/app/Livewire/Project/Service/StackForm.php
+++ b/app/Livewire/Project/Service/StackForm.php
@@ -101,6 +101,7 @@ class StackForm extends Component
$rules = data_get($field, 'rules', 'nullable');
$isPassword = data_get($field, 'isPassword', false);
$customHelper = data_get($field, 'customHelper', false);
+ $sortOrder = data_get($field, 'sortOrder');
$this->fields->put($key, [
'serviceName' => $serviceName,
'key' => $key,
@@ -109,6 +110,7 @@ class StackForm extends Component
'isPassword' => $isPassword,
'rules' => $rules,
'customHelper' => $customHelper,
+ 'sortOrder' => $sortOrder,
]);
$this->validationAttributes["fields.$key.value"] = $fieldKey;
@@ -116,7 +118,7 @@ class StackForm extends Component
}
$this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
return $group->sortBy(function ($field) {
- return data_get($field, 'isPassword') ? 1 : 0;
+ return data_get($field, 'sortOrder') ?? (data_get($field, 'isPassword') ? 1 : 0);
})->mapWithKeys(function ($field) {
return [$field['key'] => $field];
});
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 224507c4e..0da97b301 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -1180,6 +1180,27 @@ class Service extends BaseModel
}
$fields->put('Openclaw', $data->toArray());
break;
+ case $image->contains('coollabsio/jean-server'):
+ $data = collect([]);
+ $settings = [
+ 'Token' => ['key' => 'SERVICE_PASSWORD_64_JEAN', 'rules' => 'required', 'isPassword' => true, 'sortOrder' => 1, 'customHelper' => 'Token required to access Jean Server. Variable name: SERVICE_PASSWORD_64_JEAN'],
+ 'Allowed Origins' => ['key' => 'JEAN_ALLOWED_ORIGINS', 'rules' => 'nullable|string', 'sortOrder' => 2, 'customHelper' => 'Comma-separated additional browser origins. Same-origin access is always allowed. Variable name: JEAN_ALLOWED_ORIGINS'],
+ ];
+
+ foreach ($settings as $label => $setting) {
+ $variable = $this->environment_variables()->where('key', $setting['key'])->first();
+ if (! $variable) {
+ continue;
+ }
+
+ $data->put($label, [
+ ...$setting,
+ 'value' => data_get($variable, 'value'),
+ ]);
+ }
+
+ $fields->put('', $data->toArray());
+ break;
default:
$data = collect([]);
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
diff --git a/resources/views/livewire/project/service/stack-form.blade.php b/resources/views/livewire/project/service/stack-form.blade.php
index 89dfa0e5c..d6c55ecfe 100644
--- a/resources/views/livewire/project/service/stack-form.blade.php
+++ b/resources/views/livewire/project/service/stack-form.blade.php
@@ -66,7 +66,12 @@
@foreach ($fields as $serviceName => $field)
- {{ data_get($field, 'serviceName') }} · {{ data_get($field, 'name') }}
+
+ @if (filled(data_get($field, 'serviceName')))
+ {{ data_get($field, 'serviceName') }} ·
+ @endif
+ {{ data_get($field, 'name') }}
+
@if (data_get($field, 'customHelper'))
@else
diff --git a/svgs/jean.png b/svgs/jean.png
new file mode 100644
index 000000000..b5f15fdc2
Binary files /dev/null and b/svgs/jean.png differ
diff --git a/templates/compose/jean.yaml b/templates/compose/jean.yaml
new file mode 100644
index 000000000..4b24ee02d
--- /dev/null
+++ b/templates/compose/jean.yaml
@@ -0,0 +1,31 @@
+# documentation: https://github.com/coollabsio/jean/blob/main/docs/headless-server.md
+# slogan: Open-source desktop and server client for orchestrating AI coding agents.
+# category: development
+# tags: jean,ai,coding,agents,development,git,worktrees
+# logo: svgs/jean.png
+# port: 3456
+
+services:
+ jean:
+ image: 'ghcr.io/coollabsio/jean-server:${JEAN_VERSION:-latest}'
+ environment:
+ - SERVICE_URL_JEAN_3456
+ - JEAN_HEADLESS=${JEAN_HEADLESS:-1}
+ - JEAN_HOST=${JEAN_HOST:-0.0.0.0}
+ - JEAN_PORT=${JEAN_PORT:-3456}
+ - JEAN_TOKEN=${SERVICE_PASSWORD_64_JEAN}
+ - JEAN_NO_TOKEN=${JEAN_NO_TOKEN:-0}
+ - JEAN_ALLOW_UNSAFE_NO_TOKEN=${JEAN_ALLOW_UNSAFE_NO_TOKEN:-0}
+ - JEAN_ALLOW_NATIVE_OPEN=${JEAN_ALLOW_NATIVE_OPEN:-0}
+ - JEAN_ALLOWED_ORIGINS=${JEAN_ALLOWED_ORIGINS:-}
+ - JEAN_DATA_DIR=/home/jean/.local/share/com.jean.desktop
+ volumes:
+ - jean-data:/home/jean/.local/share/com.jean.desktop
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - 'curl -fsS http://127.0.0.1:3456/readyz >/dev/null || exit 1'
+ interval: 10s
+ timeout: 5s
+ retries: 12
+ start_period: 15s
diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json
index cda94584b..f90fa3790 100644
--- a/templates/service-templates-latest.json
+++ b/templates/service-templates-latest.json
@@ -2453,6 +2453,25 @@
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
"port": "80"
},
+ "jean": {
+ "documentation": "https://github.com/coollabsio/jean/blob/main/docs/headless-server.md?utm_source=coolify.io",
+ "slogan": "Open-source desktop and server client for orchestrating AI coding agents.",
+ "compose": "c2VydmljZXM6CiAgamVhbjoKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL2plYW4tc2VydmVyOiR7SkVBTl9WRVJTSU9OOi1sYXRlc3R9JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfSkVBTl8zNDU2CiAgICAgIC0gJ0pFQU5fSEVBRExFU1M9JHtKRUFOX0hFQURMRVNTOi0xfScKICAgICAgLSAnSkVBTl9IT1NUPSR7SkVBTl9IT1NUOi0wLjAuMC4wfScKICAgICAgLSAnSkVBTl9QT1JUPSR7SkVBTl9QT1JUOi0zNDU2fScKICAgICAgLSAnSkVBTl9UT0tFTj0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfSkVBTn0nCiAgICAgIC0gJ0pFQU5fTk9fVE9LRU49JHtKRUFOX05PX1RPS0VOOi0wfScKICAgICAgLSAnSkVBTl9BTExPV19VTlNBRkVfTk9fVE9LRU49JHtKRUFOX0FMTE9XX1VOU0FGRV9OT19UT0tFTjotMH0nCiAgICAgIC0gJ0pFQU5fQUxMT1dfTkFUSVZFX09QRU49JHtKRUFOX0FMTE9XX05BVElWRV9PUEVOOi0wfScKICAgICAgLSAnSkVBTl9BTExPV0VEX09SSUdJTlM9JHtKRUFOX0FMTE9XRURfT1JJR0lOUzotfScKICAgICAgLSBKRUFOX0RBVEFfRElSPS9ob21lL2plYW4vLmxvY2FsL3NoYXJlL2NvbS5qZWFuLmRlc2t0b3AKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2plYW4tZGF0YTovaG9tZS9qZWFuLy5sb2NhbC9zaGFyZS9jb20uamVhbi5kZXNrdG9wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdjdXJsIC1mc1MgaHR0cDovLzEyNy4wLjAuMTozNDU2L3JlYWR5eiA+L2Rldi9udWxsIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMgogICAgICBzdGFydF9wZXJpb2Q6IDE1cwo=",
+ "tags": [
+ "jean",
+ "ai",
+ "coding",
+ "agents",
+ "development",
+ "git",
+ "worktrees"
+ ],
+ "category": "development",
+ "logo": "svgs/jean.png",
+ "minversion": "0.0.0",
+ "template_last_updated_at": null,
+ "port": "3456"
+ },
"jellyfin": {
"documentation": "https://jellyfin.org?utm_source=coolify.io",
"slogan": "Jellyfin is a media server for hosting and streaming your media collection.",
diff --git a/templates/service-templates.json b/templates/service-templates.json
index 50f647315..5a4ad35be 100644
--- a/templates/service-templates.json
+++ b/templates/service-templates.json
@@ -2453,6 +2453,25 @@
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
"port": "80"
},
+ "jean": {
+ "documentation": "https://github.com/coollabsio/jean/blob/main/docs/headless-server.md?utm_source=coolify.io",
+ "slogan": "Open-source desktop and server client for orchestrating AI coding agents.",
+ "compose": "c2VydmljZXM6CiAgamVhbjoKICAgIGltYWdlOiAnZ2hjci5pby9jb29sbGFic2lvL2plYW4tc2VydmVyOiR7SkVBTl9WRVJTSU9OOi1sYXRlc3R9JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0pFQU5fMzQ1NgogICAgICAtICdKRUFOX0hFQURMRVNTPSR7SkVBTl9IRUFETEVTUzotMX0nCiAgICAgIC0gJ0pFQU5fSE9TVD0ke0pFQU5fSE9TVDotMC4wLjAuMH0nCiAgICAgIC0gJ0pFQU5fUE9SVD0ke0pFQU5fUE9SVDotMzQ1Nn0nCiAgICAgIC0gJ0pFQU5fVE9LRU49JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0pFQU59JwogICAgICAtICdKRUFOX05PX1RPS0VOPSR7SkVBTl9OT19UT0tFTjotMH0nCiAgICAgIC0gJ0pFQU5fQUxMT1dfVU5TQUZFX05PX1RPS0VOPSR7SkVBTl9BTExPV19VTlNBRkVfTk9fVE9LRU46LTB9JwogICAgICAtICdKRUFOX0FMTE9XX05BVElWRV9PUEVOPSR7SkVBTl9BTExPV19OQVRJVkVfT1BFTjotMH0nCiAgICAgIC0gJ0pFQU5fQUxMT1dFRF9PUklHSU5TPSR7SkVBTl9BTExPV0VEX09SSUdJTlM6LX0nCiAgICAgIC0gSkVBTl9EQVRBX0RJUj0vaG9tZS9qZWFuLy5sb2NhbC9zaGFyZS9jb20uamVhbi5kZXNrdG9wCiAgICB2b2x1bWVzOgogICAgICAtICdqZWFuLWRhdGE6L2hvbWUvamVhbi8ubG9jYWwvc2hhcmUvY29tLmplYW4uZGVza3RvcCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtZnNTIGh0dHA6Ly8xMjcuMC4wLjE6MzQ1Ni9yZWFkeXogPi9kZXYvbnVsbCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTIKICAgICAgc3RhcnRfcGVyaW9kOiAxNXMK",
+ "tags": [
+ "jean",
+ "ai",
+ "coding",
+ "agents",
+ "development",
+ "git",
+ "worktrees"
+ ],
+ "category": "development",
+ "logo": "svgs/jean.png",
+ "minversion": "0.0.0",
+ "template_last_updated_at": null,
+ "port": "3456"
+ },
"jellyfin": {
"documentation": "https://jellyfin.org?utm_source=coolify.io",
"slogan": "Jellyfin is a media server for hosting and streaming your media collection.",
diff --git a/tests/Feature/ServiceExtraFieldsTest.php b/tests/Feature/ServiceExtraFieldsTest.php
index 2b6ed0c0b..2a521f97e 100644
--- a/tests/Feature/ServiceExtraFieldsTest.php
+++ b/tests/Feature/ServiceExtraFieldsTest.php
@@ -45,3 +45,30 @@ it('only adds Grafana extra fields for Grafana server images', function (string
'promtail' => ['grafana/promtail:latest', false],
'tempo' => ['grafana/tempo:latest', false],
]);
+
+it('exposes Jean Server authentication and access settings', function () {
+ $service = serviceExtraFieldsTestServiceWithApplicationImage('ghcr.io/coollabsio/jean-server:latest');
+
+ $service->environment_variables()->createMany([
+ ['key' => 'SERVICE_PASSWORD_64_JEAN', 'value' => 'secret-token', 'is_preview' => false],
+ ['key' => 'JEAN_ALLOWED_ORIGINS', 'value' => 'https://jean.example.com', 'is_preview' => false],
+ ]);
+
+ $fields = $service->extraFields();
+
+ expect($fields)->toHaveKey('')
+ ->and($fields[''])->toMatchArray([
+ 'Token' => [
+ 'key' => 'SERVICE_PASSWORD_64_JEAN',
+ 'value' => 'secret-token',
+ 'rules' => 'required',
+ 'isPassword' => true,
+ 'sortOrder' => 1,
+ ],
+ 'Allowed Origins' => [
+ 'key' => 'JEAN_ALLOWED_ORIGINS',
+ 'value' => 'https://jean.example.com',
+ 'sortOrder' => 2,
+ ],
+ ]);
+});
diff --git a/tests/Unit/JeanServiceTemplateTest.php b/tests/Unit/JeanServiceTemplateTest.php
new file mode 100644
index 000000000..32de62ed3
--- /dev/null
+++ b/tests/Unit/JeanServiceTemplateTest.php
@@ -0,0 +1,39 @@
+toContain('ghcr.io/coollabsio/jean-server:${JEAN_VERSION:-latest}')
+ ->toContain('SERVICE_URL_JEAN_3456')
+ ->toContain('JEAN_HEADLESS=${JEAN_HEADLESS:-1}')
+ ->toContain('JEAN_HOST=${JEAN_HOST:-0.0.0.0}')
+ ->toContain('JEAN_PORT=${JEAN_PORT:-3456}')
+ ->toContain('JEAN_TOKEN=${SERVICE_PASSWORD_64_JEAN}')
+ ->toContain('JEAN_NO_TOKEN=${JEAN_NO_TOKEN:-0}')
+ ->toContain('JEAN_ALLOW_UNSAFE_NO_TOKEN=${JEAN_ALLOW_UNSAFE_NO_TOKEN:-0}')
+ ->toContain('JEAN_ALLOW_NATIVE_OPEN=${JEAN_ALLOW_NATIVE_OPEN:-0}')
+ ->toContain('JEAN_ALLOWED_ORIGINS=${JEAN_ALLOWED_ORIGINS:-}')
+ ->toContain('JEAN_DATA_DIR=/home/jean/.local/share/com.jean.desktop')
+ ->toContain('jean-data:/home/jean/.local/share/com.jean.desktop')
+ ->toContain('http://127.0.0.1:3456/readyz');
+
+ foreach (['service-templates.json', 'service-templates-latest.json'] as $templateFile) {
+ $templates = json_decode(
+ file_get_contents(__DIR__."/../../templates/{$templateFile}"),
+ associative: true,
+ flags: JSON_THROW_ON_ERROR,
+ );
+
+ expect($templates)->toHaveKey('jean');
+ expect($templates['jean']['port'] ?? null)->toBe('3456');
+ expect($templates['jean']['logo'] ?? null)->toBe('svgs/jean.png');
+ expect($templates['jean']['category'] ?? null)->toBe('development');
+
+ $generatedCompose = base64_decode($templates['jean']['compose'], strict: true);
+
+ expect($generatedCompose)
+ ->toContain('ghcr.io/coollabsio/jean-server:${JEAN_VERSION:-latest}')
+ ->toContain('JEAN_TOKEN=${SERVICE_PASSWORD_64_JEAN}');
+ }
+});