mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 14:17:18 +00:00
feat(servers): add cross-instance server migration (#11075)
This commit is contained in:
1 parent
c15e3b35fd
commit
f9caa5250d
36 files changed
+7675
-15
No files matched your search
@@ -141,6 +141,14 @@
|
||||
['label' => 'Terminal Access', 'route' => 'server.security.terminal-access', 'active' => request()->routeIs('server.security.terminal-access'), 'icon' => 'browser-terminal', 'navigate' => false],
|
||||
],
|
||||
],
|
||||
[
|
||||
'label' => 'Transfer',
|
||||
'route' => 'server.transfer',
|
||||
'active' => $activeMenu === 'transfer',
|
||||
'icon' => 'arrow-right',
|
||||
'group' => 'Operations',
|
||||
'visible' => isDev() && ! $server->isLocalhost() && auth()->user()?->can('view', $server),
|
||||
],
|
||||
[
|
||||
'label' => 'Danger',
|
||||
'route' => 'server.delete',
|
||||
|
||||
@@ -5,27 +5,48 @@
|
||||
|
||||
<div class="mb-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h1 class="min-w-0 text-[24px]! leading-7! font-semibold! tracking-tight!">Servers</h1>
|
||||
@can('createAnyResource')
|
||||
<a href="{{ route('server.create') }}" {{ wireNavigate() }}
|
||||
class="button w-fit shrink-0 whitespace-nowrap button-highlighted">
|
||||
<x-reicon name="plus" class="size-3.5" />
|
||||
New server
|
||||
</a>
|
||||
@endcan
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@if (isDev())
|
||||
@can('create', App\Models\Server::class)
|
||||
<a href="{{ route('server.transfer.import') }}" {{ wireNavigate() }}
|
||||
class="button w-fit shrink-0 whitespace-nowrap">
|
||||
<x-reicon name="upload" class="size-3.5" />
|
||||
Import transfer
|
||||
<x-status-badge label="Dev" />
|
||||
</a>
|
||||
@endcan
|
||||
@endif
|
||||
@can('createAnyResource')
|
||||
<a href="{{ route('server.create') }}" {{ wireNavigate() }}
|
||||
class="button w-fit shrink-0 whitespace-nowrap button-highlighted">
|
||||
<x-reicon name="plus" class="size-3.5" />
|
||||
New server
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$serverRows = $servers->map(function ($server) {
|
||||
$isTransferredAway = $server->isTransferredAway();
|
||||
$isReady = $server->settings->is_reachable
|
||||
&& $server->settings->is_usable
|
||||
&& ! $server->settings->force_disabled;
|
||||
&& ! $server->settings->force_disabled
|
||||
&& ! $isTransferredAway;
|
||||
|
||||
$status = match (true) {
|
||||
$isTransferredAway => 'Transferred away',
|
||||
$server->settings->force_disabled => 'Disabled',
|
||||
$isReady => 'Ready',
|
||||
default => 'Validation required',
|
||||
};
|
||||
|
||||
return [
|
||||
'uuid' => $server->uuid,
|
||||
'name' => $server->name,
|
||||
'description' => $server->description ?: 'No description',
|
||||
'href' => route('server.show', ['server_uuid' => $server->uuid]),
|
||||
'status' => $isReady ? 'Ready' : ($server->settings->force_disabled ? 'Disabled' : 'Validation required'),
|
||||
'status' => $status,
|
||||
'statusType' => $isReady ? 'success' : 'error',
|
||||
'ready' => $isReady,
|
||||
];
|
||||
|
||||
@@ -89,8 +89,12 @@
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@endif
|
||||
<x-status-badge :label="$server->isFunctional() ? 'Ready' : 'Validation required'"
|
||||
:type="$server->isFunctional() ? 'success' : 'warning'" />
|
||||
@if ($server->isTransferredAway())
|
||||
<x-status-badge label="Transferred away" type="warning" />
|
||||
@else
|
||||
<x-status-badge :label="$server->isFunctional() ? 'Ready' : 'Validation required'"
|
||||
:type="$server->isFunctional() ? 'success' : 'warning'" />
|
||||
@endif
|
||||
</x-slot:actions>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
@@ -103,7 +107,9 @@
|
||||
{{ $server->name }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
|
||||
@if ($server->isFunctional())
|
||||
@if ($server->isTransferredAway())
|
||||
This server was migrated away from this Coolify instance and cannot be managed here.
|
||||
@elseif ($server->isFunctional())
|
||||
The server is reachable, validated, and ready to host resources.
|
||||
@else
|
||||
Validate the SSH connection before using this server.
|
||||
@@ -206,6 +212,14 @@
|
||||
</x-process-dialog>
|
||||
</x-slot:actions>
|
||||
|
||||
@if ($server->isTransferredAway())
|
||||
<x-callout type="warning" title="Transferred to another instance" class="mb-4">
|
||||
This server was migrated away from this Coolify instance. It cannot be revalidated or
|
||||
managed here. Use the target instance, or delete this server when you no longer need the
|
||||
archive.
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
@if ($this->limaStartCommand)
|
||||
<x-callout type="info" title="Start this Lima VM locally" class="mb-4">
|
||||
<code
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
Import server transfer | Coolify
|
||||
</x-slot>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h1>Import server transfer <x-status-badge label="Dev" /></h1>
|
||||
<a href="{{ route('server.index') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Back to servers</x-forms.button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="subtitle">
|
||||
Paste or upload a transfer bundle exported from another Coolify instance. This creates the server and its
|
||||
resources under the current team and <strong>claims</strong> the host for this instance
|
||||
(control-plane only — host data stays on the machine).
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 rounded-lg border border-neutral-200 p-4 dark:border-coolgray-200">
|
||||
<x-forms.input type="file" id="bundleFile" label="Bundle file (.json)" accept=".json,application/json" />
|
||||
<x-forms.textarea id="bundleJson" label="Or paste bundle JSON" rows="14" placeholder='{"schema_version":1,...}' />
|
||||
<x-forms.input id="passphrase" type="password" label="Passphrase (if encrypted)" placeholder="Optional" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.checkbox id="preserveUuids" label="Preserve UUIDs from the source instance" />
|
||||
<x-forms.checkbox id="adoptMode" label="Adopt mode (keep statuses; do not force exited redeploy)" />
|
||||
<x-forms.checkbox id="writeRemote" label="Also write ownership file on the host via SSH (optional)" />
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-forms.button wire:click="dryRun" wire:loading.attr="disabled" wire:target="dryRun,importBundle">
|
||||
<span wire:loading.remove wire:target="dryRun">Dry run</span>
|
||||
<span wire:loading wire:target="dryRun">Checking…</span>
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click="importBundle" wire:loading.attr="disabled"
|
||||
wire:target="dryRun,importBundle"
|
||||
wire:confirm="Import this server into the current team?">
|
||||
<span wire:loading.remove wire:target="importBundle">Import server</span>
|
||||
<span wire:loading wire:target="importBundle">Importing…</span>
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (count($lastWarnings) > 0)
|
||||
<div class="rounded-lg border border-warning/40 bg-warning/10 p-3 text-sm">
|
||||
<div class="mb-1 font-semibold text-warning">Warnings</div>
|
||||
<ul class="list-disc space-y-1 pl-5">
|
||||
@foreach ($lastWarnings as $warning)
|
||||
<li>{{ $warning }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($lastResult)
|
||||
<div class="rounded-lg border border-neutral-200 p-4 dark:border-coolgray-200">
|
||||
<div class="mb-2 font-semibold">
|
||||
{{ data_get($lastResult, 'dry_run') ? 'Dry-run result' : 'Import result' }}
|
||||
</div>
|
||||
@if ($importedServerUuid)
|
||||
<div class="mb-3 flex flex-wrap items-center gap-2 text-sm">
|
||||
<span>Server UUID: <code class="font-mono">{{ $importedServerUuid }}</code></span>
|
||||
@if (data_get($lastResult, 'claimed'))
|
||||
<span class="text-success">Claimed</span>
|
||||
@endif
|
||||
<a href="{{ route('server.show', ['server_uuid' => $importedServerUuid]) }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Open server</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ route('server.transfer', ['server_uuid' => $importedServerUuid]) }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Transfer details</x-forms.button>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
<pre class="max-h-80 overflow-auto rounded-lg bg-neutral-100 p-3 text-xs dark:bg-coolgray-100">{{ json_encode($lastResult, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,146 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
{{ data_get_str($server, 'name')->limit(10) }} > Transfer | Coolify
|
||||
</x-slot>
|
||||
|
||||
<livewire:server.navbar :server="$server" />
|
||||
|
||||
<div
|
||||
class="server-settings-workspace application-settings-workspace mt-4 grid w-full max-w-[1180px] min-w-0 gap-8 lg:mt-0 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
|
||||
<x-server.sidebar :server="$server" activeMenu="transfer" />
|
||||
|
||||
<div class="application-settings-form flex w-full flex-col gap-6">
|
||||
@if ($this->isLocalhost)
|
||||
<x-application.settings-section id="server-transfer-section" title="Transfer server (Dev)"
|
||||
helper="Move this server’s control-plane config to another Coolify instance (same physical host).">
|
||||
<x-callout type="warning" title="Localhost cannot be transferred">
|
||||
The Coolify host (localhost) cannot be transferred between instances.
|
||||
</x-callout>
|
||||
</x-application.settings-section>
|
||||
@else
|
||||
<x-application.settings-section id="server-transfer-section" title="Transfer server (Dev)"
|
||||
helper="Move this server’s control-plane config to another Coolify instance (same physical host).">
|
||||
<x-slot:actions>
|
||||
<x-status-badge
|
||||
:label="$this->transferStatus ?: 'ready'"
|
||||
type="neutral" />
|
||||
@if ($exportId)
|
||||
<span class="text-[11px] text-neutral-500 dark:text-fg-faint">export {{ $exportId }}</span>
|
||||
@endif
|
||||
</x-slot:actions>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-neutral-950 dark:text-fg">Transfer to another instance
|
||||
</h3>
|
||||
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
|
||||
Enter the target Coolify URL and an API token from that instance (root recommended).
|
||||
This exports the server, imports and claims it on the target, then disables automations
|
||||
here.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 md:max-w-xl">
|
||||
<x-forms.input id="targetUrl" label="Target instance URL" required
|
||||
placeholder="http://localhost:8001"
|
||||
helper="Base URL of the other Coolify instance (no trailing path)." />
|
||||
<x-forms.input id="targetToken" type="password" label="Target API token" required
|
||||
placeholder="Paste token from target instance" autocomplete="off"
|
||||
helper="Create a token on the target with root (or write + create servers). It is only used for this request." />
|
||||
<x-forms.checkbox id="writeRemote"
|
||||
label="Write ownership file on the host via SSH (optional)" />
|
||||
</div>
|
||||
<div>
|
||||
<x-forms.button canGate="update" :canResource="$server" wire:click="migrateServer"
|
||||
wire:loading.attr="disabled"
|
||||
wire:confirm="Transfer this server to the target instance? Automations will be disabled here.">
|
||||
<span wire:loading.remove wire:target="migrateServer">Transfer server</span>
|
||||
<span wire:loading wire:target="migrateServer">Transferring…</span>
|
||||
</x-forms.button>
|
||||
</div>
|
||||
@if (count($lastWarnings) > 0)
|
||||
<x-callout type="warning" title="Warnings">
|
||||
<ul class="mt-2 list-disc space-y-1 pl-5 text-sm">
|
||||
@foreach ($lastWarnings as $warning)
|
||||
<li>{{ $warning }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-callout>
|
||||
@endif
|
||||
@if ($lastResultJson)
|
||||
<div>
|
||||
<div class="mb-1 text-sm font-semibold">Result</div>
|
||||
<pre
|
||||
class="max-h-64 overflow-auto rounded-lg bg-neutral-100 p-3 text-xs dark:bg-coolgray-100">{{ $lastResultJson }}</pre>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
<x-application.settings-section id="server-transfer-advanced-section" title="Advanced"
|
||||
helper="Manual export, complete-only, and re-claim helpers for air-gapped or partial transfers.">
|
||||
<div class="flex flex-col gap-6" x-data="{ open: @entangle('showAdvanced') }">
|
||||
<button type="button"
|
||||
class="flex w-full items-center justify-between rounded-lg border border-neutral-200 px-4 py-3 text-left dark:border-white/[0.08]"
|
||||
@click="open = !open">
|
||||
<span class="text-sm font-semibold">Show advanced options</span>
|
||||
<span class="text-xs text-neutral-500 dark:text-fg-faint"
|
||||
x-text="open ? 'Hide' : 'Show'"></span>
|
||||
</button>
|
||||
|
||||
<div class="flex flex-col gap-6" x-show="open" x-cloak>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium">Download bundle</h4>
|
||||
<p class="mb-3 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
|
||||
Manual / air-gapped transfer. Import on the target via Servers → Import transfer.
|
||||
</p>
|
||||
<div class="mb-3 flex flex-col gap-3 md:max-w-xl">
|
||||
<x-forms.checkbox id="encryptBundle" label="Encrypt with passphrase" />
|
||||
<x-forms.input id="passphrase" type="password" label="Passphrase"
|
||||
placeholder="Used when encrypt is checked" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-forms.button canGate="view" :canResource="$server" wire:click="exportBundle"
|
||||
wire:loading.attr="disabled">
|
||||
<span wire:loading.remove wire:target="exportBundle">Download JSON</span>
|
||||
<span wire:loading wire:target="exportBundle">Exporting…</span>
|
||||
</x-forms.button>
|
||||
<a href="{{ route('server.transfer.import') }}" {{ wireNavigate() }}
|
||||
class="button">
|
||||
Import page (this instance)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-sm font-medium">Complete only</h4>
|
||||
<p class="mb-3 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
|
||||
Disable automations after a manual import on the target.
|
||||
</p>
|
||||
<x-forms.button canGate="update" :canResource="$server" wire:click="completeTransfer"
|
||||
wire:loading.attr="disabled"
|
||||
wire:confirm="Disable automations on this server?">
|
||||
Mark transferred & disable automations
|
||||
</x-forms.button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-sm font-medium">Re-claim only</h4>
|
||||
<p class="mb-3 text-xs leading-5 text-neutral-500 dark:text-fg-dim">
|
||||
Retry claim on this instance (after a local import).
|
||||
</p>
|
||||
<div class="mb-3 flex flex-col gap-2">
|
||||
<x-forms.checkbox id="writeRemoteOnClaim" label="Write ownership file via SSH" />
|
||||
<x-forms.checkbox id="rebindSentinelOnClaim" label="Rebind Sentinel" />
|
||||
</div>
|
||||
<x-forms.button canGate="update" :canResource="$server" wire:click="claimServer"
|
||||
wire:loading.attr="disabled">
|
||||
Re-claim
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in new issue
Block a user