feat(onboarding): redesign user onboarding flow with modern UI/UX

- 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 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2025-10-12 17:59:37 +02:00
parent 821aa6a531
commit 7a008c859a
5 changed files with 675 additions and 214 deletions

View file

@ -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';
}

View file

@ -0,0 +1,47 @@
@props(['currentStep' => 1, 'totalSteps' => 3])
<div class="w-full max-w-2xl mx-auto mb-8">
<div class="flex items-center justify-between">
@for ($i = 1; $i <= $totalSteps; $i++)
<div class="flex items-center {{ $i < $totalSteps ? 'flex-1' : '' }}">
<div class="flex flex-col items-center">
<div
class="flex items-center justify-center size-10 rounded-full border-2 transition-all duration-300
{{ $i < $currentStep ? 'bg-success border-success' : '' }}
{{ $i === $currentStep ? 'bg-white dark:bg-coolgray-100' : '' }}
">
@if ($i < $currentStep)
<svg class="size-5 text-white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
@else
<span
class="text-sm font-bold text-black dark:text-white">
{{ $i }}
</span>
@endif
</div>
<span
class="mt-2 text-xs font-medium text-black dark:text-white">
@if ($i === 1)
Server
@elseif ($i === 2)
Connection
@elseif ($i === 3)
Complete
@endif
</span>
</div>
@if ($i < $totalSteps)
<div
class="flex-1 h-0.5 mx-4 transition-all duration-300
{{ $i < $currentStep ? 'bg-success' : 'bg-coolgray-200 dark:bg-coolgray-600' }}">
</div>
@endif
</div>
@endfor
</div>
</div>

View file

@ -1,25 +1,29 @@
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="box-border col-span-2 lg:min-w-[24rem] lg:min-h-[21rem]">
<h1 class="text-2xl font-bold lg:text-5xl">{{ $title }}</h1>
<div class="py-6">
<div class="w-full max-w-4xl">
<div class="bg-white dark:bg-coolgray-100 rounded-lg shadow-sm border border-neutral-200 dark:border-coolgray-300 overflow-hidden">
<div class="p-8 lg:p-12">
<h1 class="text-3xl font-bold lg:text-4xl mb-4">{{ $title }}</h1>
@isset($question)
<p class="dark:text-neutral-400">
<div class="text-base lg:text-lg dark:text-neutral-400 mb-8">
{{ $question }}
</p>
</div>
@endisset
@if ($actions)
<div class="flex flex-col gap-4">
{{ $actions }}
</div>
@endif
</div>
@if ($actions)
<div class="flex flex-col flex-wrap gap-4 lg:items-center md:flex-row">
{{ $actions }}
@isset($explanation)
<div class="dark:bg-coolgray-200 border-t border-neutral-200 dark:border-coolgray-300 p-8 lg:p-12 bg-neutral-50">
<h3 class="text-sm font-bold uppercase tracking-wide mb-4 dark:text-neutral-400">
Technical Details
</h3>
<div class="space-y-3 text-sm dark:text-neutral-400">
{{ $explanation }}
</div>
</div>
@endif
@endisset
</div>
@isset($explanation)
<div class="col-span-1">
<h3 class="pb-8 font-bold">Explanation</h3>
<div class="space-y-4">
{{ $explanation }}
</div>
</div>
@endisset
</div>

View file

@ -1,6 +1,6 @@
@extends('layouts.base')
@section('body')
<main class="h-full">
<main class="min-h-screen flex items-center justify-center p-4">
{{ $slot }}
</main>
@parent

View file

@ -2,68 +2,160 @@
<x-slot:title>
Onboarding | Coolify
</x-slot>
<section class="flex flex-col h-full lg:items-center lg:justify-center">
<div
class="flex flex-col items-center justify-center p-10 mx-2 mt-10 bg-white border rounded-lg shadow-sm lg:p-20 dark:bg-transparent dark:border-none max-w-7xl border-neutral-200">
<section class="w-full">
<div class="flex flex-col items-center w-full space-y-8">
@if ($currentState === 'welcome')
<h1 class="text-3xl font-bold lg:text-5xl">Welcome to Coolify</h1>
<div class="py-6 text-center lg:text-xl">Let me help you set up the basics.</div>
<div class="flex justify-center ">
<x-forms.button class="justify-center w-64 box-boarding"
wire:click="$set('currentState','explanation')">Get
Started
</x-forms.button>
<div class="w-full max-w-2xl text-center space-y-8">
<div class="space-y-4">
<h1 class="text-4xl font-bold lg:text-6xl">Welcome to Coolify</h1>
<p class="text-lg lg:text-xl dark:text-neutral-400">
Connect your first server and start deploying in minutes
</p>
</div>
<div
class="bg-white dark:bg-coolgray-100 rounded-lg shadow-sm border border-neutral-200 dark:border-coolgray-300 p-8 text-left">
<h2 class="text-sm font-bold uppercase tracking-wide dark:text-neutral-400 mb-4">
What You'll Set Up
</h2>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Server Connection</div>
<div class="text-sm dark:text-neutral-400">Connect via SSH to deploy your resources</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Docker Environment</div>
<div class="text-sm dark:text-neutral-400">Automated installation and configuration</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Project Structure</div>
<div class="text-sm dark:text-neutral-400">Organize your applications and resources</div>
</div>
</div>
</div>
</div>
<div class="flex justify-center pt-4">
<x-forms.button class="justify-center px-12 py-4 text-lg font-bold box-boarding"
wire:click="explanation">
Start Setup
</x-forms.button>
</div>
</div>
@elseif ($currentState === 'explanation')
<x-boarding-step title="What is Coolify?">
<x-boarding-progress :currentStep="0" />
<x-boarding-step title="Platform Overview">
<x-slot:question>
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. <br />
<a href="https://coolify.io" class="dark:text-white hover:underline">Coolify Home</a>.
<br><br>
<span class="text-xl">
<x-highlighted text="Self-hosting with superpowers!" /></span>
Coolify automates deployment and infrastructure management on your own servers. Deploy applications
from Git, manage databases, and monitor everything—without vendor lock-in.
</x-slot:question>
<x-slot:explanation>
<p>
<x-highlighted text="Task automation:" /> You don't need to manage your servers anymore.
Coolify does
it for you.
<x-highlighted text="Automation:" /> Coolify handles server configuration, Docker management, and
deployments automatically.
</p>
<p>
<x-highlighted text="No vendor lock-in:" /> All configurations are stored on your servers, so
everything works without a connection to Coolify (except integrations and automations).
<x-highlighted text="Self-hosted:" /> All data and configurations live on your infrastructure.
Works offline except for external integrations.
</p>
<p>
<x-highlighted text="Monitoring:" />You can get notified on your favourite platforms
(Discord,
Telegram, Email, etc.) when something goes wrong, or an action is needed from your side.
<x-highlighted text="Monitoring & Alerts:" /> Get real-time notifications via Discord, Telegram,
Email, and other platforms.
</p>
</x-slot:explanation>
<x-slot:actions>
<x-forms.button class="justify-center w-64 box-boarding" wire:click="explanation">Next
<x-forms.button class="justify-center w-full lg:w-auto px-8 py-3 box-boarding"
wire:click="explanation">
Continue
</x-forms.button>
</x-slot:actions>
</x-boarding-step>
@elseif ($currentState === 'select-server-type')
<x-boarding-step title="Server">
<x-boarding-progress :currentStep="1" />
<x-boarding-step title="Choose Server Type">
<x-slot:question>
Do you want to deploy your resources to your
<x-highlighted text="Localhost" />
or to a
<x-highlighted text="Remote Server" />?
Select where to deploy your applications and databases. You can add more servers later.
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center w-64 box-boarding" wire:target="setServerType('localhost')"
wire:click="setServerType('localhost')">Localhost
</x-forms.button>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 w-full">
<button
class="group relative box-without-bg cursor-pointer hover:border-coollabs transition-all duration-200 p-6"
wire:target="setServerType('localhost')" wire:click="setServerType('localhost')">
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<svg class="size-10"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z" />
</svg>
<span
class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-neutral-100 dark:bg-coolgray-300 dark:text-neutral-400 rounded">
Quick Start
</span>
</div>
<div>
<h3 class="text-xl font-bold mb-2">This Machine</h3>
<p class="text-sm dark:text-neutral-400">
Deploy on the server running Coolify. Best for testing and single-server setups.
</p>
</div>
</div>
</button>
<x-forms.button class="justify-center w-64 box-boarding " wire:target="setServerType('remote')"
wire:click="setServerType('remote')">Remote Server
</x-forms.button>
<button
class="group relative box-without-bg cursor-pointer hover:border-coollabs transition-all duration-200 p-6"
wire:target="setServerType('remote')" wire:click="setServerType('remote')">
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<svg class="size-10 " xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z" />
</svg>
<span
class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-warning/10 dark:bg-warning/20 text-warning rounded">
Recommended
</span>
</div>
<div>
<h3 class="text-xl font-bold mb-2">Remote Server</h3>
<p class="text-sm dark:text-neutral-400">
Connect via SSH to any server—cloud VPS, bare metal, or home infrastructure.
</p>
</div>
</div>
</button>
</div>
@if (!$serverReachable)
<div class="mt-6 p-4 border border-error rounded-lg text-gray-800 dark:text-gray-200">
@ -111,51 +203,97 @@ class="bg-red-200 dark:bg-red-900 px-1 rounded-sm">~/.ssh/authorized_keys</code>
@endif
</x-slot:actions>
<x-slot:explanation>
<p>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.</p>
<p>
<x-highlighted text="Localhost" /> is the server where Coolify is running on. It is not
recommended to use one server
for everything.
<x-highlighted text="Servers" /> host your applications, databases, and services (collectively
called resources). All CPU-intensive operations run on the target server.
</p>
<p>
<x-highlighted text="A remote server" /> is a server reachable through SSH. It can be hosted
at home, or from any cloud
provider.
<x-highlighted text="Localhost:" /> The machine running Coolify. Not recommended for production
workloads due to resource contention.
</p>
<p>
<x-highlighted text="Remote Server:" /> Any SSH-accessible server—cloud providers (AWS, Hetzner,
DigitalOcean), bare metal, or self-hosted infrastructure.
</p>
</x-slot:explanation>
</x-boarding-step>
@elseif ($currentState === 'private-key')
<x-boarding-step title="SSH Key">
<x-boarding-progress :currentStep="2" />
<x-boarding-step title="SSH Authentication">
<x-slot:question>
Do you have your own SSH Private Key?
Configure SSH key-based authentication for secure server access.
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center lg:w-64 box-boarding" wire:target="setPrivateKey('own')"
wire:click="setPrivateKey('own')">Yes
</x-forms.button>
<x-forms.button class="justify-center lg:w-64 box-boarding" wire:target="setPrivateKey('create')"
wire:click="setPrivateKey('create')">No (create one for me)
</x-forms.button>
@if (count($privateKeys) > 0)
<form wire:submit='selectExistingPrivateKey' class="flex flex-col w-full gap-4 lg:pr-10">
<x-forms.select label="Existing SSH Keys" id='selectedExistingPrivateKey'>
@foreach ($privateKeys as $privateKey)
<option wire:key="{{ $loop->index }}" value="{{ $privateKey->id }}">
{{ $privateKey->name }}</option>
@endforeach
</x-forms.select>
<x-forms.button type="submit">Use this SSH Key</x-forms.button>
</form>
@if ($privateKeys && $privateKeys->count() > 0)
<div class="w-full space-y-4">
<div class="p-4 rounded-lg border border-neutral-200 dark:border-coolgray-400">
<form wire:submit='selectExistingPrivateKey' class="flex flex-col gap-4">
<x-forms.select label="Existing SSH Keys" id='selectedExistingPrivateKey'>
@foreach ($privateKeys as $privateKey)
<option wire:key="{{ $loop->index }}" value="{{ $privateKey->id }}">
{{ $privateKey->name }}</option>
@endforeach
</x-forms.select>
<x-forms.button type="submit" class="w-full lg:w-auto">Use Selected Key</x-forms.button>
</form>
</div>
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-neutral-300 dark:border-coolgray-400"></div>
</div>
<div class="relative flex justify-center text-sm">
<div
class="px-2 py-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-300 rounded text-xs font-bold text-neutral-500 dark:text-neutral-400">
OR
</div>
</div>
</div>
</div>
@endif
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 w-full">
<x-forms.button class="justify-center h-auto py-6 box-without-bg hover:border-coollabs transition-all duration-200" wire:target="setPrivateKey('own')"
wire:click="setPrivateKey('own')">
<div class="flex flex-col items-center gap-2">
<svg class="size-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
<div class="text-center">
<h3 class="text-xl font-bold mb-2">Use Existing Key</h3>
<p class="text-sm dark:text-neutral-400">I have my own SSH key</p>
</div>
</div>
</x-forms.button>
<x-forms.button class="justify-center h-auto py-6 box-without-bg hover:border-coollabs transition-all duration-200" wire:target="setPrivateKey('create')"
wire:click="setPrivateKey('create')">
<div class="flex flex-col items-center gap-2">
<svg class="size-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M19 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM4 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 0110.374 21c-2.331 0-4.512-.645-6.374-1.766z" />
</svg>
<div class="text-center">
<h3 class="text-xl font-bold mb-2">Generate New Key</h3>
<p class="text-sm dark:text-neutral-400">Create ED25519 key pair</p>
</div>
</div>
</x-forms.button>
</div>
</x-slot:actions>
<x-slot:explanation>
<p>SSH Keys are used to connect to a remote server through a secure shell, called SSH.</p>
<p>You can use your own ssh private key, or you can let Coolify to create one for you.</p>
<p>In both ways, you need to add the public version of your ssh private key to the remote
server's
<code class="dark:text-warning">~/.ssh/authorized_keys</code> file.
<p>
<x-highlighted text="SSH Key Authentication:" /> Uses public-key cryptography for secure,
password-less server access.
</p>
<p>
<x-highlighted text="Public Key Deployment:" /> Add the public key to your server's
<code class="text-xs bg-coolgray-300 dark:bg-coolgray-400 px-1 py-0.5 rounded">~/.ssh/authorized_keys</code>
file.
</p>
<p>
<x-highlighted text="Key Generation:" /> Coolify generates ED25519 keys by default for optimal
security and performance.
</p>
</x-slot:explanation>
</x-boarding-step>
@ -237,164 +375,385 @@ class="bg-red-200 dark:bg-red-900 px-1 rounded-sm">~/.ssh/authorized_keys</code>
</x-slot:explanation>
</x-boarding-step>
@elseif ($currentState === 'create-private-key')
<x-boarding-step title="Create Private Key">
<x-boarding-progress :currentStep="2" />
<x-boarding-step title="SSH Key Configuration">
<x-slot:question>
Please let me know your key details.
Configure your SSH key for server authentication.
</x-slot:question>
<x-slot:actions>
<form wire:submit='savePrivateKey' class="flex flex-col w-full gap-4 lg:pr-10">
<x-forms.input required placeholder="Choose a name for your Private Key. Could be anything."
label="Name" id="privateKeyName" />
<x-forms.input placeholder="Description, so others will know more about this."
<form wire:submit='savePrivateKey' class="flex flex-col w-full gap-4">
<x-forms.input required placeholder="e.g., production-server-key"
label="Key Name" id="privateKeyName" />
<x-forms.input placeholder="Optional: Note what this key is used for"
label="Description" id="privateKeyDescription" />
<x-forms.textarea required placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
label="Private Key" id="privateKey" />
@if ($privateKeyType === 'create')
<x-forms.textarea required readonly label="Private Key" id="privateKey" rows="8" />
<x-forms.textarea rows="7" readonly label="Public Key" id="publicKey" />
<span class="font-bold dark:text-warning">ACTION REQUIRED: Copy the 'Public Key' to your
server's
~/.ssh/authorized_keys
file.</span>
@else
<x-forms.textarea required placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
label="Private Key" id="privateKey" rows="8" />
@endif
<x-forms.button type="submit">Save</x-forms.button>
@if ($privateKeyType === 'create')
<div class="p-4 bg-warning/10 border border-warning rounded-lg">
<div class="flex gap-3">
<svg class="size-5 text-warning flex-shrink-0 mt-0.5" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd" />
</svg>
<div>
<p class="font-bold text-warning mb-1">Action Required</p>
<p class="text-sm dark:text-white text-black">
Copy the public key above and add it to your server's
<code
class="text-xs bg-coolgray-300 dark:bg-coolgray-400 px-1 py-0.5 rounded">~/.ssh/authorized_keys</code>
file.
</p>
</div>
</div>
</div>
@endif
<x-forms.button type="submit" class="w-full lg:w-auto">Save SSH Key</x-forms.button>
</form>
</x-slot:actions>
<x-slot:explanation>
<p>Private Keys are used to connect to a remote server through a secure shell, called SSH.</p>
<p>You can use your own private key, or you can let Coolify to create one for you.</p>
<p>In both ways, you need to add the public version of your private key to the remote server's
<code>~/.ssh/authorized_keys</code> file.
<p>
<x-highlighted text="Key Storage:" /> Private keys are encrypted at rest in Coolify's database.
</p>
<p>
<x-highlighted text="Public Key Distribution:" /> Deploy the public key to
<code class="text-xs bg-coolgray-300 dark:bg-coolgray-400 px-1 py-0.5 rounded">~/.ssh/authorized_keys</code>
on your target server for the specified user.
</p>
<p>
<x-highlighted text="Key Format:" /> Supports RSA, ED25519, ECDSA, and DSA key types in OpenSSH
format.
</p>
</x-slot:explanation>
</x-boarding-step>
@elseif ($currentState === 'create-server')
<x-boarding-step title="Create Server">
<x-boarding-progress :currentStep="2" />
<x-boarding-step title="Server Configuration">
<x-slot:question>
Please let me know your server details.
Provide connection details for your remote server.
</x-slot:question>
<x-slot:actions>
<form wire:submit='saveServer' class="flex flex-col w-full gap-4 lg:w-96">
<x-forms.input required placeholder="Choose a name for your Server. Could be anything."
label="Name" id="remoteServerName" wire:model="remoteServerName" />
<x-forms.input placeholder="Description, so others will know more about this."
<form wire:submit='saveServer' class="flex flex-col w-full gap-4">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<x-forms.input required placeholder="e.g., production-app-server"
label="Server Name" id="remoteServerName" wire:model="remoteServerName" />
<x-forms.input required placeholder="IP address or hostname" label="IP Address/Hostname"
id="remoteServerHost" wire:model="remoteServerHost" />
</div>
<x-forms.input placeholder="Optional: Note what this server hosts"
label="Description" id="remoteServerDescription" wire:model="remoteServerDescription" />
<x-forms.input required placeholder="127.0.0.1" label="IP Address" id="remoteServerHost"
wire:model="remoteServerHost" />
<div x-data="{ showAdvanced: false }" class="flex flex-col gap-2">
<div x-data="{ showAdvanced: false }" class="flex flex-col gap-4">
<button @click="showAdvanced = !showAdvanced" type="button"
class="text-left text-sm text-gray-600 dark:text-gray-300 hover:underline">
Advanced Settings
class="flex items-center gap-2 text-left text-sm font-medium hover:underline">
<svg x-show="!showAdvanced" class="size-4" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd" />
</svg>
<svg x-show="showAdvanced" class="size-4" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
clip-rule="evenodd" />
</svg>
Advanced Connection Settings
</button>
<div x-show="showAdvanced" class="flex flex-col gap-2">
<x-forms.input placeholder="Port number of your server. Default is 22." label="Port"
<div x-show="showAdvanced" x-cloak class="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4 rounded-lg border border-neutral-200 dark:border-coolgray-400">
<x-forms.input placeholder="Default: 22" label="SSH Port" type="number"
id="remoteServerPort" wire:model="remoteServerPort" />
<div>
<x-forms.input placeholder="Default is root." label="User"
<x-forms.input placeholder="Default: root" label="SSH User"
id="remoteServerUser" wire:model="remoteServerUser" />
<div class="text-xs text-gray-600 dark:text-gray-300">Non-root user is
experimental: <a class="font-bold underline" target="_blank"
href="https://coolify.io/docs/knowledge-base/server/non-root-user">docs</a>.
<p class="mt-1 text-xs dark:text-white text-black">
Non-root user support is experimental.
<a class="font-bold underline hover:text-coollabs" target="_blank"
href="https://coolify.io/docs/knowledge-base/server/non-root-user">Learn more</a>
</p>
</div>
</div>
</div>
<x-forms.button type="submit" class="w-full lg:w-auto">Validate Connection</x-forms.button>
</form>
</x-slot:actions>
<x-slot:explanation>
<p>
<x-highlighted text="Connection Requirements:" /> Server must be accessible via SSH on the
specified port (default 22).
</p>
<p>
<x-highlighted text="Hostname Resolution:" /> Use IP addresses for direct connections or ensure
DNS resolution is configured.
</p>
<p>
<x-highlighted text="User Permissions:" /> Root or sudo-enabled users recommended for full Docker
management capabilities.
</p>
</x-slot:explanation>
</x-boarding-step>
@elseif ($currentState === 'validate-server')
<x-boarding-progress :currentStep="2" />
<x-boarding-step title="Server Validation">
<x-slot:question>
Coolify will automatically install Docker {{ $minDockerVersion }}+ if not present.
</x-slot:question>
<x-slot:actions>
<div class="w-full space-y-6">
<div class="p-6 bg-neutral-50 dark:bg-coolgray-200 rounded-lg border border-neutral-200 dark:border-coolgray-400">
<h3 class="font-bold text-black dark:text-white mb-4">Validation Steps</h3>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Test SSH Connection</div>
<div class="text-sm dark:text-neutral-400">Verify key-based authentication</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Check OS Compatibility</div>
<div class="text-sm dark:text-neutral-400">Verify supported Linux distribution</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Install Docker Engine</div>
<div class="text-sm dark:text-neutral-400">Auto-install if version {{ $minDockerVersion }}+ not
found</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Configure Network</div>
<div class="text-sm dark:text-neutral-400">Set up Docker networks and proxy</div>
</div>
</div>
</div>
</div>
<x-forms.button type="submit">Continue</x-forms.button>
</form>
</x-slot:actions>
</x-boarding-step>
@elseif ($currentState === 'validate-server')
<x-boarding-step title="Validate & Configure Server">
<x-slot:question>
I need to validate your server (connection, Docker Engine, etc) and configure if something is
missing for me. Are you okay with this?
</x-slot:question>
<x-slot:actions>
<x-slide-over closeWithX fullScreen>
<x-slot:title>Validate & configure</x-slot:title>
<x-slot:content>
<livewire:server.validate-and-install :server="$this->createdServer" />
</x-slot:content>
<x-forms.button @click="slideOverOpen=true" class="w-full font-bold box-boarding lg:w-96"
wire:click.prevent='installServer' isHighlighted>
Let's do it!
</x-forms.button>
</x-slide-over>
<x-slide-over closeWithX fullScreen>
<x-slot:title>Server Validation</x-slot:title>
<x-slot:content>
<livewire:server.validate-and-install :server="$this->createdServer" />
</x-slot:content>
<x-forms.button @click="slideOverOpen=true" class="w-full font-bold py-4 box-boarding"
wire:click.prevent='installServer' isHighlighted>
Start Validation
</x-forms.button>
</x-slide-over>
</div>
</x-slot:actions>
<x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.<br><br>Minimum Docker Engine version is: {{ $minDockerVersion }}<br><br>To
manually install
Docker
Engine, check <a target="_blank" class="underline dark:text-warning"
href="https://docs.docker.com/engine/install/#server">this
documentation</a>.</p>
<p>
<x-highlighted text="Automated Setup:" /> Coolify installs Docker Engine, Docker Compose, and
configures system requirements automatically.
</p>
<p>
<x-highlighted text="Version Requirements:" /> Minimum Docker Engine {{ $minDockerVersion }}.x
required.
<a target="_blank" class="underline hover:text-coollabs"
href="https://docs.docker.com/engine/install/#server">Manual installation guide</a>
</p>
<p>
<x-highlighted text="System Configuration:" /> Sets up Docker networks, proxy configuration, and
resource monitoring.
</p>
</x-slot:explanation>
</x-boarding-step>
@elseif ($currentState === 'create-project')
<x-boarding-step title="Project">
<x-boarding-progress :currentStep="3" />
<x-boarding-step title="Project Setup">
<x-slot:question>
@if (count($projects) > 0)
You already have some projects. Do you want to use one of them or should I create a new one
for
you?
@if ($projects && $projects->count() > 0)
You have existing projects. Select one or create a new project to organize your resources.
@else
Let's create an initial project for you. You can change all the details later on.
Create your first project to organize applications, databases, and services.
@endif
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center w-64 box-boarding" wire:click="createNewProject">Create new
project!</x-forms.button>
<div>
@if (count($projects) > 0)
<form wire:submit='selectExistingProject' class="flex flex-col w-full gap-4 lg:w-96">
<x-forms.select label="Existing projects" class="w-96" id='selectedProject'>
<div class="w-full space-y-4">
<x-forms.button class="justify-center w-full py-4 font-bold box-boarding"
wire:click="createNewProject" isHighlighted>
Create "My First Project"
</x-forms.button>
@if ($projects && $projects->count() > 0)
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-neutral-300 dark:border-coolgray-400"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white dark:bg-coolgray-100 text-coolgray-400">Or use existing</span>
</div>
</div>
<form wire:submit='selectExistingProject' class="flex flex-col gap-4">
<x-forms.select label="Existing Projects" id='selectedProject'>
@foreach ($projects as $project)
<option wire:key="{{ $loop->index }}" value="{{ $project->id }}">
{{ $project->name }}</option>
@endforeach
</x-forms.select>
<x-forms.button type="submit">Use this Project</x-forms.button>
<x-forms.button type="submit" class="w-full lg:w-auto">Use Selected Project</x-forms.button>
</form>
@endif
</div>
</x-slot:actions>
<x-slot:explanation>
<p>Projects contain several resources combined into one virtual group. There are no
limitations on the number of projects you can add.</p>
<p>Each project should have at least one environment, this allows you to create a production &
staging version of the same application, but grouped separately.</p>
<p>
<x-highlighted text="Project Organization:" /> Group related resources (apps, databases, services)
into logical projects.
</p>
<p>
<x-highlighted text="Environments:" /> Each project includes a production environment by default.
Add staging, development, or custom environments as needed.
</p>
<p>
<x-highlighted text="Team Access:" /> Projects inherit team permissions and can be managed
collaboratively.
</p>
</x-slot:explanation>
</x-boarding-step>
@elseif ($currentState === 'create-resource')
<x-boarding-step title="Resources">
<x-slot:question>
Let's go to the new resource page, where you can create your first resource.
</x-slot:question>
<x-slot:actions>
<div class="items-center justify-center w-64 box-boarding" wire:click="showNewResource">Let's do
it!</div>
</x-slot:actions>
<x-slot:explanation>
<p>A resource could be an application, a database or a service (like WordPress).</p>
</x-slot:explanation>
</x-boarding-step>
<x-boarding-progress :currentStep="3" />
<div class="w-full max-w-2xl text-center space-y-8">
<div class="space-y-4">
<div class="flex justify-center">
<svg class="size-16 text-success" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h1 class="text-4xl font-bold lg:text-5xl">Setup Complete!</h1>
<p class="text-lg dark:text-neutral-400">
Your server is connected and ready. Start deploying your first resource.
</p>
</div>
<div
class="bg-white dark:bg-coolgray-100 rounded-lg shadow-sm border border-neutral-200 dark:border-coolgray-300 p-8 text-left">
<h2 class="text-sm font-bold uppercase tracking-wide dark:text-neutral-400 mb-4">
What's Configured
</h2>
<div class="space-y-3">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Server: {{ $createdServer->name }}</div>
<div class="text-sm dark:text-neutral-400">{{ $createdServer->ip }}</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Project: {{ $createdProject->name }}</div>
<div class="text-sm dark:text-neutral-400">Production environment ready</div>
</div>
</div>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="size-5 text-success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div>
<div class="font-semibold text-base dark:text-white">Docker Engine</div>
<div class="text-sm dark:text-neutral-400">Installed and running</div>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-3">
<x-forms.button class="justify-center w-full py-4 text-lg font-bold box-boarding"
wire:click="showNewResource" isHighlighted>
Deploy Your First Resource
</x-forms.button>
<button wire:click="skipBoarding"
class="text-sm dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning hover:underline transition-colors">
Go to Dashboard
</button>
</div>
</div>
@endif
</div>
<div class="flex flex-col justify-center gap-4 pt-4 lg:gap-2 lg:flex">
<div class="flex justify-center w-full gap-2">
<div class="cursor-pointer hover:underline dark:hover:text-white" wire:click='skipBoarding'>Skip
onboarding</div>
<div class="cursor-pointer hover:underline dark:hover:text-white" wire:click='restartBoarding'>Restart
onboarding</div>
@if ($currentState !== 'welcome' && $currentState !== 'create-resource')
<div class="flex flex-col items-center gap-4 pt-8 mt-8 border-t border-neutral-200 dark:border-coolgray-400">
<div class="flex justify-center gap-6 text-sm">
<button wire:click='skipBoarding'
class="dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning hover:underline transition-colors">
Skip Setup
</button>
<button wire:click='restartBoarding'
class="dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning hover:underline transition-colors">
Restart
</button>
</div>
<x-modal-input title="Need Help?">
<x-slot:content>
<button
class="text-sm dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning hover:underline transition-colors">
Contact Support
</button>
</x-slot:content>
<livewire:help />
</x-modal-input>
</div>
<x-modal-input title="How can we help?">
<x-slot:content>
<div class="w-full text-center cursor-pointer hover:underline dark:hover:text-white"
title="Send us feedback or get help!">
Feedback
</div>
</x-slot:content>
<livewire:help />
</x-modal-input>
</div>
@endif
</section>