Improve upgrade process UX with better progress visibility (#7609)
This commit is contained in:
commit
2a21b18998
6 changed files with 770 additions and 109 deletions
|
|
@ -30,7 +30,6 @@ public function handle($manual_update = false)
|
|||
if (! $this->server) {
|
||||
return;
|
||||
}
|
||||
CleanupDocker::dispatch($this->server, false, false);
|
||||
|
||||
// Fetch fresh version from CDN instead of using cache
|
||||
try {
|
||||
|
|
@ -117,17 +116,12 @@ public function handle($manual_update = false)
|
|||
|
||||
private function update()
|
||||
{
|
||||
$helperImage = config('constants.coolify.helper_image');
|
||||
$latest_version = getHelperVersion();
|
||||
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
|
||||
|
||||
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
|
||||
instant_remote_process(["docker pull -q $image"], $this->server, false);
|
||||
|
||||
$latestHelperImageVersion = getHelperVersion();
|
||||
$upgradeScriptUrl = config('constants.coolify.upgrade_script_url');
|
||||
|
||||
remote_process([
|
||||
"curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh",
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion",
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion $latestHelperImageVersion",
|
||||
], $this->server);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Actions\Server\UpdateCoolify;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Upgrade extends Component
|
||||
|
|
@ -14,12 +15,20 @@ class Upgrade extends Component
|
|||
|
||||
public string $latestVersion = '';
|
||||
|
||||
public string $currentVersion = '';
|
||||
|
||||
protected $listeners = ['updateAvailable' => 'checkUpdate'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
}
|
||||
|
||||
public function checkUpdate()
|
||||
{
|
||||
try {
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
$this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false);
|
||||
if (isDev()) {
|
||||
$this->isUpgradeAvailable = true;
|
||||
|
|
@ -41,4 +50,71 @@ public function upgrade()
|
|||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function getUpgradeStatus(): array
|
||||
{
|
||||
// Only root team members can view upgrade status
|
||||
if (auth()->user()?->currentTeam()?->id !== 0) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
$server = Server::find(0);
|
||||
if (! $server) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
$statusFile = '/data/coolify/source/.upgrade-status';
|
||||
|
||||
try {
|
||||
$content = instant_remote_process(
|
||||
["cat {$statusFile} 2>/dev/null || echo ''"],
|
||||
$server,
|
||||
false
|
||||
);
|
||||
$content = trim($content ?? '');
|
||||
} catch (\Throwable $e) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
if (empty($content)) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
$parts = explode('|', $content);
|
||||
if (count($parts) < 3) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
[$step, $message, $timestamp] = $parts;
|
||||
|
||||
// Check if status is stale (older than 10 minutes)
|
||||
try {
|
||||
$statusTime = new \DateTime($timestamp);
|
||||
$now = new \DateTime;
|
||||
$diffMinutes = ($now->getTimestamp() - $statusTime->getTimestamp()) / 60;
|
||||
|
||||
if ($diffMinutes > 10) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
if ($step === 'error') {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'step' => 0,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
$stepInt = (int) $step;
|
||||
$status = $stepInt >= 6 ? 'complete' : 'in_progress';
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'step' => $stepInt,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,9 +64,45 @@ if [ -f /root/.docker/config.json ]; then
|
|||
DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json"
|
||||
fi
|
||||
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
echo "docker-compose.custom.yml detected." >>"$LOGFILE"
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
else
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
fi
|
||||
# Pull all required images before stopping containers
|
||||
# This ensures we don't take down the system if image pull fails (rate limits, network issues, etc.)
|
||||
echo "Pulling required Docker images..." >>"$LOGFILE"
|
||||
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify helper image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
docker pull postgres:15-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull PostgreSQL image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
docker pull redis:7-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull Redis image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
# Pull realtime image - version is hardcoded in docker-compose.prod.yml, extract it or use a known version
|
||||
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify realtime image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
echo "All images pulled successfully." >>"$LOGFILE"
|
||||
|
||||
# Stop and remove existing Coolify containers to prevent conflicts
|
||||
# This handles both old installations (project "source") and new ones (project "coolify")
|
||||
# Use nohup to ensure the script continues even if SSH connection is lost
|
||||
echo "Starting container restart sequence (detached)..." >>"$LOGFILE"
|
||||
|
||||
nohup bash -c "
|
||||
LOGFILE='$LOGFILE'
|
||||
DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT'
|
||||
REGISTRY_URL='$REGISTRY_URL'
|
||||
LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION'
|
||||
LATEST_IMAGE='$LATEST_IMAGE'
|
||||
|
||||
# Stop and remove containers
|
||||
echo 'Stopping existing Coolify containers...' >>\"\$LOGFILE\"
|
||||
for container in coolify coolify-db coolify-redis coolify-realtime; do
|
||||
if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then
|
||||
docker stop \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
|
||||
docker rm \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
|
||||
echo \" - Removed container: \$container\" >>\"\$LOGFILE\"
|
||||
fi
|
||||
done
|
||||
|
||||
# Start new containers
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
echo 'docker-compose.custom.yml detected.' >>\"\$LOGFILE\"
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
else
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
fi
|
||||
echo 'Upgrade completed.' >>\"\$LOGFILE\"
|
||||
" >>"$LOGFILE" 2>&1 &
|
||||
|
|
|
|||
152
resources/views/components/upgrade-progress.blade.php
Normal file
152
resources/views/components/upgrade-progress.blade.php
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
@props(['step' => 0])
|
||||
|
||||
{{--
|
||||
Step Mapping (Backend → UI):
|
||||
Backend steps 1-2 (config download, env update) → UI Step 1: Preparing
|
||||
Backend step 3 (pulling images) → UI Step 2: Helper + UI Step 3: Image
|
||||
Backend steps 4-5 (stop/start containers) → UI Step 4: Restart
|
||||
Backend step 6 (complete) → mapped in JS mapStepToUI() in upgrade.blade.php
|
||||
|
||||
The currentStep variable is inherited from parent Alpine component (upgradeModal).
|
||||
--}}
|
||||
<div class="w-full max-w-md mx-auto" x-data="{ activeStep: {{ $step }} }" x-effect="activeStep = $el.closest('[x-data]')?.__x?.$data?.currentStep ?? {{ $step }}">
|
||||
<div class="flex items-center justify-between">
|
||||
{{-- Step 1: Preparing --}}
|
||||
<div class="flex items-center flex-1">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-center justify-center size-8 rounded-full border-2 transition-all duration-300"
|
||||
:class="{
|
||||
'bg-success border-success': currentStep > 1,
|
||||
'bg-warning/20 border-warning': currentStep === 1,
|
||||
'border-neutral-400 dark:border-coolgray-300': currentStep < 1
|
||||
}">
|
||||
<template x-if="currentStep > 1">
|
||||
<svg class="size-4 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>
|
||||
</template>
|
||||
<template x-if="currentStep === 1">
|
||||
<svg class="size-4 text-warning animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="currentStep < 1">
|
||||
<span class="text-xs font-medium text-neutral-500 dark:text-neutral-400">1</span>
|
||||
</template>
|
||||
</div>
|
||||
<span class="mt-1.5 text-xs font-medium transition-colors duration-300"
|
||||
:class="{
|
||||
'text-success': currentStep > 1,
|
||||
'text-warning': currentStep === 1,
|
||||
'text-neutral-500 dark:text-neutral-400': currentStep < 1
|
||||
}">Preparing</span>
|
||||
</div>
|
||||
<div class="flex-1 h-0.5 mx-2 transition-all duration-300"
|
||||
:class="currentStep > 1 ? 'bg-success' : 'bg-neutral-300 dark:bg-coolgray-300'"></div>
|
||||
</div>
|
||||
|
||||
{{-- Step 2: Helper --}}
|
||||
<div class="flex items-center flex-1">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-center justify-center size-8 rounded-full border-2 transition-all duration-300"
|
||||
:class="{
|
||||
'bg-success border-success': currentStep > 2,
|
||||
'bg-warning/20 border-warning': currentStep === 2,
|
||||
'border-neutral-400 dark:border-coolgray-300': currentStep < 2
|
||||
}">
|
||||
<template x-if="currentStep > 2">
|
||||
<svg class="size-4 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>
|
||||
</template>
|
||||
<template x-if="currentStep === 2">
|
||||
<svg class="size-4 text-warning animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="currentStep < 2">
|
||||
<span class="text-xs font-medium text-neutral-500 dark:text-neutral-400">2</span>
|
||||
</template>
|
||||
</div>
|
||||
<span class="mt-1.5 text-xs font-medium transition-colors duration-300"
|
||||
:class="{
|
||||
'text-success': currentStep > 2,
|
||||
'text-warning': currentStep === 2,
|
||||
'text-neutral-500 dark:text-neutral-400': currentStep < 2
|
||||
}">Helper</span>
|
||||
</div>
|
||||
<div class="flex-1 h-0.5 mx-2 transition-all duration-300"
|
||||
:class="currentStep > 2 ? 'bg-success' : 'bg-neutral-300 dark:bg-coolgray-300'"></div>
|
||||
</div>
|
||||
|
||||
{{-- Step 3: Image --}}
|
||||
<div class="flex items-center flex-1">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-center justify-center size-8 rounded-full border-2 transition-all duration-300"
|
||||
:class="{
|
||||
'bg-success border-success': currentStep > 3,
|
||||
'bg-warning/20 border-warning': currentStep === 3,
|
||||
'border-neutral-400 dark:border-coolgray-300': currentStep < 3
|
||||
}">
|
||||
<template x-if="currentStep > 3">
|
||||
<svg class="size-4 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>
|
||||
</template>
|
||||
<template x-if="currentStep === 3">
|
||||
<svg class="size-4 text-warning animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="currentStep < 3">
|
||||
<span class="text-xs font-medium text-neutral-500 dark:text-neutral-400">3</span>
|
||||
</template>
|
||||
</div>
|
||||
<span class="mt-1.5 text-xs font-medium transition-colors duration-300"
|
||||
:class="{
|
||||
'text-success': currentStep > 3,
|
||||
'text-warning': currentStep === 3,
|
||||
'text-neutral-500 dark:text-neutral-400': currentStep < 3
|
||||
}">Image</span>
|
||||
</div>
|
||||
<div class="flex-1 h-0.5 mx-2 transition-all duration-300"
|
||||
:class="currentStep > 3 ? 'bg-success' : 'bg-neutral-300 dark:bg-coolgray-300'"></div>
|
||||
</div>
|
||||
|
||||
{{-- Step 4: Restart --}}
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-center justify-center size-8 rounded-full border-2 transition-all duration-300"
|
||||
:class="{
|
||||
'bg-success border-success': currentStep > 4,
|
||||
'bg-warning/20 border-warning': currentStep === 4,
|
||||
'border-neutral-400 dark:border-coolgray-300': currentStep < 4
|
||||
}">
|
||||
<template x-if="currentStep > 4">
|
||||
<svg class="size-4 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>
|
||||
</template>
|
||||
<template x-if="currentStep === 4">
|
||||
<svg class="size-4 text-warning animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="currentStep < 4">
|
||||
<span class="text-xs font-medium text-neutral-500 dark:text-neutral-400">4</span>
|
||||
</template>
|
||||
</div>
|
||||
<span class="mt-1.5 text-xs font-medium transition-colors duration-300"
|
||||
:class="{
|
||||
'text-success': currentStep > 4,
|
||||
'text-warning': currentStep === 4,
|
||||
'text-neutral-500 dark:text-neutral-400': currentStep < 4
|
||||
}">Restart</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,21 +1,22 @@
|
|||
<div @if ($isUpgradeAvailable) title="New version available" @else title="No upgrade available" @endif
|
||||
x-init="$wire.checkUpdate" x-data="upgradeModal">
|
||||
x-init="$wire.checkUpdate" x-data="upgradeModal({
|
||||
currentVersion: @js($currentVersion),
|
||||
latestVersion: @js($latestVersion)
|
||||
})">
|
||||
@if ($isUpgradeAvailable)
|
||||
<div :class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
|
||||
<button class="menu-item" @click="modalOpen=true" x-show="showProgress">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300 lds-heart" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
|
||||
</svg>
|
||||
In progress
|
||||
</button>
|
||||
<button class="menu-item cursor-pointer" @click="modalOpen=true" x-show="!showProgress">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
|
|
@ -28,10 +29,9 @@ class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300" viewBox="0 0
|
|||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
<div x-show="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in duration-100" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
<div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
|
|
@ -40,44 +40,131 @@ class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
|||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-fit bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between pb-3">
|
||||
<h3 class="text-lg font-semibold">Upgrade confirmation</h3>
|
||||
<button x-show="!showProgress" @click="modalOpen=false"
|
||||
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold"
|
||||
x-text="upgradeComplete ? 'Upgrade Complete!' : (showProgress ? 'Upgrading...' : 'Upgrade Available')">
|
||||
</h3>
|
||||
<div class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{{ $currentVersion }} <span class="mx-1">→</span> {{ $latestVersion }}
|
||||
</div>
|
||||
</div>
|
||||
<button x-show="!showProgress || upgradeError" @click="upgradeError ? closeErrorModal() : modalOpen=false"
|
||||
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-gray-600 rounded-full hover:text-gray-800 hover:bg-gray-50 dark:text-neutral-400 dark:hover:text-white dark:hover:bg-coolgray-300">
|
||||
<svg class="w-5 h-5" 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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative w-auto pb-8">
|
||||
<p>Are you sure you would like to upgrade your instance to {{ $latestVersion }}?</p>
|
||||
<br />
|
||||
|
||||
<x-callout type="warning" title="Caution">
|
||||
<p>Any deployments running during the update process will
|
||||
fail. Please ensure no deployments are in progress on any server before continuing.
|
||||
</p>
|
||||
</x-callout>
|
||||
<br />
|
||||
<p>You can review the changelogs <a class="font-bold underline dark:text-white"
|
||||
href="https://github.com/coollabsio/coolify/releases" target="_blank">here</a>.</p>
|
||||
<br />
|
||||
<p>If something goes wrong and you cannot upgrade your instance, You can check the following
|
||||
<a class="font-bold underline dark:text-white" href="https://coolify.io/docs/upgrade"
|
||||
target="_blank">guide</a> on what to do.
|
||||
</p>
|
||||
<div class="flex flex-col pt-4" x-show="showProgress">
|
||||
<h2>Progress <x-loading /></h2>
|
||||
<div x-html="currentStatus"></div>
|
||||
</div>
|
||||
{{-- Content --}}
|
||||
<div class="relative w-auto pb-6">
|
||||
{{-- Progress View --}}
|
||||
<template x-if="showProgress">
|
||||
<div class="space-y-6">
|
||||
{{-- Step Progress Indicator --}}
|
||||
<div class="pt-2">
|
||||
<x-upgrade-progress />
|
||||
</div>
|
||||
|
||||
{{-- Elapsed Time --}}
|
||||
<div class="text-center">
|
||||
<span class="text-sm text-neutral-500 dark:text-neutral-400">Elapsed time:</span>
|
||||
<span class="ml-2 font-mono text-sm" x-text="formatElapsedTime()"></span>
|
||||
</div>
|
||||
|
||||
{{-- Current Status Message --}}
|
||||
<div class="p-4 rounded-lg bg-neutral-200 dark:bg-coolgray-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<template x-if="!upgradeComplete && !upgradeError">
|
||||
<svg class="w-5 h-5 text-warning animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="upgradeComplete">
|
||||
<svg class="w-5 h-5 text-success" 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>
|
||||
</template>
|
||||
<template x-if="upgradeError">
|
||||
<svg class="w-5 h-5 text-error" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</template>
|
||||
<span x-text="currentStatus" class="text-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Success State with Countdown --}}
|
||||
<template x-if="upgradeComplete">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<p class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
Reloading in <span x-text="successCountdown"
|
||||
class="font-bold text-warning"></span> seconds...
|
||||
</p>
|
||||
<x-forms.button @click="reloadNow()" type="button">
|
||||
Reload Now
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{{-- Error State with Close Button --}}
|
||||
<template x-if="upgradeError">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Check the logs on the server at /data/coolify/source/upgrade*.
|
||||
</p>
|
||||
<x-forms.button @click="closeErrorModal()" type="button">
|
||||
Close
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{{-- Confirmation View --}}
|
||||
<template x-if="!showProgress">
|
||||
<div class="space-y-4">
|
||||
{{-- Warning --}}
|
||||
<x-callout type="warning" title="Caution">
|
||||
<p>Any deployments running during the update process will
|
||||
fail.
|
||||
</p>
|
||||
</x-callout>
|
||||
|
||||
{{-- Help Links --}}
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
If something goes wrong, check the
|
||||
<a class="font-medium underline dark:text-white hover:text-neutral-800 dark:hover:text-neutral-300"
|
||||
href="https://coolify.io/docs/upgrade" target="_blank">upgrade guide</a> or the
|
||||
logs on the server at /data/coolify/source/upgrade*.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Footer Actions --}}
|
||||
<div class="flex gap-4" x-show="!showProgress">
|
||||
<x-forms.button @click="modalOpen=false"
|
||||
class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel
|
||||
</x-forms.button>
|
||||
<div class="flex-1"></div>
|
||||
<x-forms.button @click="confirmed" class="w-24" isHighlighted type="button">Continue
|
||||
<x-forms.button @click="confirmed" class="w-32" isHighlighted type="button">
|
||||
Upgrade Now
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -89,23 +176,64 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel
|
|||
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('upgradeModal', () => ({
|
||||
Alpine.data('upgradeModal', (config) => ({
|
||||
modalOpen: false,
|
||||
showProgress: false,
|
||||
currentStatus: '',
|
||||
checkHealthInterval: null,
|
||||
checkIfIamDeadInterval: null,
|
||||
checkUpgradeStatusInterval: null,
|
||||
elapsedInterval: null,
|
||||
healthCheckAttempts: 0,
|
||||
startTime: null,
|
||||
elapsedTime: 0,
|
||||
currentStep: 0,
|
||||
upgradeComplete: false,
|
||||
upgradeError: false,
|
||||
successCountdown: 3,
|
||||
currentVersion: config.currentVersion || '',
|
||||
latestVersion: config.latestVersion || '',
|
||||
serviceDown: false,
|
||||
|
||||
confirmed() {
|
||||
this.showProgress = true;
|
||||
this.$wire.$call('upgrade')
|
||||
this.currentStep = 1;
|
||||
this.currentStatus = 'Starting upgrade...';
|
||||
this.startTimer();
|
||||
// Trigger server-side upgrade script via Livewire
|
||||
this.$wire.$call('upgrade');
|
||||
// Start client-side status polling
|
||||
this.upgrade();
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
// Prevent accidental navigation during upgrade
|
||||
this.beforeUnloadHandler = (event) => {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
});
|
||||
};
|
||||
window.addEventListener('beforeunload', this.beforeUnloadHandler);
|
||||
},
|
||||
|
||||
startTimer() {
|
||||
this.startTime = Date.now();
|
||||
this.elapsedInterval = setInterval(() => {
|
||||
this.elapsedTime = Math.floor((Date.now() - this.startTime) / 1000);
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
formatElapsedTime() {
|
||||
const minutes = Math.floor(this.elapsedTime / 60);
|
||||
const seconds = this.elapsedTime % 60;
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
mapStepToUI(apiStep) {
|
||||
// Map backend steps (1-6) to UI steps (1-4)
|
||||
// Backend: 1=config, 2=env, 3=pull, 4=stop, 5=start, 6=complete
|
||||
// UI: 1=prepare, 2=pull images, 3=pull coolify, 4=restart
|
||||
if (apiStep <= 2) return 1;
|
||||
if (apiStep === 3) return 2;
|
||||
if (apiStep <= 5) return 3;
|
||||
return 4;
|
||||
},
|
||||
|
||||
getReviveStatusMessage(elapsedMinutes, attempts) {
|
||||
if (elapsedMinutes === 0) {
|
||||
return `Waiting for Coolify to come back online... (attempt ${attempts})`;
|
||||
|
|
@ -119,68 +247,133 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel
|
|||
return `Still updating. If this takes longer than 15 minutes, please check server logs... (${elapsedMinutes} minutes elapsed)`;
|
||||
}
|
||||
},
|
||||
|
||||
revive() {
|
||||
if (this.checkHealthInterval) return true;
|
||||
this.healthCheckAttempts = 0;
|
||||
this.startTime = Date.now();
|
||||
console.log('Checking server\'s health...')
|
||||
this.currentStep = 4;
|
||||
console.log('Checking server\'s health...');
|
||||
this.checkHealthInterval = setInterval(() => {
|
||||
this.healthCheckAttempts++;
|
||||
const elapsedMinutes = Math.floor((Date.now() - this.startTime) / 60000);
|
||||
fetch('/api/health')
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
this.currentStatus =
|
||||
'Coolify is back online. Reloading this page in 5 seconds...';
|
||||
if (this.checkHealthInterval) {
|
||||
clearInterval(this.checkHealthInterval);
|
||||
this.checkHealthInterval = null;
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000)
|
||||
this.showSuccess();
|
||||
} else {
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this
|
||||
.healthCheckAttempts);
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Health check failed:', error);
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this
|
||||
.healthCheckAttempts);
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||
});
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
showSuccess() {
|
||||
if (this.checkHealthInterval) {
|
||||
clearInterval(this.checkHealthInterval);
|
||||
this.checkHealthInterval = null;
|
||||
}
|
||||
if (this.checkUpgradeStatusInterval) {
|
||||
clearInterval(this.checkUpgradeStatusInterval);
|
||||
this.checkUpgradeStatusInterval = null;
|
||||
}
|
||||
if (this.elapsedInterval) {
|
||||
clearInterval(this.elapsedInterval);
|
||||
this.elapsedInterval = null;
|
||||
}
|
||||
// Remove beforeunload handler now that upgrade is complete
|
||||
if (this.beforeUnloadHandler) {
|
||||
window.removeEventListener('beforeunload', this.beforeUnloadHandler);
|
||||
this.beforeUnloadHandler = null;
|
||||
}
|
||||
|
||||
this.upgradeComplete = true;
|
||||
this.currentStep = 5;
|
||||
this.currentStatus = `Successfully upgraded to ${this.latestVersion}`;
|
||||
this.successCountdown = 3;
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
this.successCountdown--;
|
||||
if (this.successCountdown <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
window.location.reload();
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
reloadNow() {
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
showError(message) {
|
||||
// Stop all intervals
|
||||
if (this.checkHealthInterval) {
|
||||
clearInterval(this.checkHealthInterval);
|
||||
this.checkHealthInterval = null;
|
||||
}
|
||||
if (this.checkUpgradeStatusInterval) {
|
||||
clearInterval(this.checkUpgradeStatusInterval);
|
||||
this.checkUpgradeStatusInterval = null;
|
||||
}
|
||||
if (this.elapsedInterval) {
|
||||
clearInterval(this.elapsedInterval);
|
||||
this.elapsedInterval = null;
|
||||
}
|
||||
// Remove beforeunload handler so user can close modal
|
||||
if (this.beforeUnloadHandler) {
|
||||
window.removeEventListener('beforeunload', this.beforeUnloadHandler);
|
||||
this.beforeUnloadHandler = null;
|
||||
}
|
||||
|
||||
this.upgradeError = true;
|
||||
this.currentStatus = `Error: ${message}`;
|
||||
},
|
||||
|
||||
closeErrorModal() {
|
||||
this.modalOpen = false;
|
||||
this.showProgress = false;
|
||||
this.upgradeError = false;
|
||||
this.currentStatus = '';
|
||||
this.currentStep = 0;
|
||||
},
|
||||
|
||||
upgrade() {
|
||||
if (this.checkIfIamDeadInterval || this.showProgress) return true;
|
||||
this.currentStatus = 'Update in progress. Pulling new images and preparing to restart Coolify...';
|
||||
this.checkIfIamDeadInterval = setInterval(() => {
|
||||
fetch('/api/health')
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
this.currentStatus =
|
||||
"Update in progress. Pulling new images and preparing to restart Coolify..."
|
||||
} else {
|
||||
this.currentStatus = "Coolify is restarting with the new version..."
|
||||
if (this.checkIfIamDeadInterval) {
|
||||
clearInterval(this.checkIfIamDeadInterval);
|
||||
this.checkIfIamDeadInterval = null;
|
||||
}
|
||||
this.revive();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Health check failed:', error);
|
||||
this.currentStatus = "Coolify is restarting with the new version..."
|
||||
if (this.checkIfIamDeadInterval) {
|
||||
clearInterval(this.checkIfIamDeadInterval);
|
||||
this.checkIfIamDeadInterval = null;
|
||||
if (this.checkUpgradeStatusInterval) return true;
|
||||
this.currentStep = 1;
|
||||
this.currentStatus = 'Starting upgrade...';
|
||||
this.serviceDown = false;
|
||||
|
||||
// Poll upgrade status via Livewire
|
||||
this.checkUpgradeStatusInterval = setInterval(async () => {
|
||||
try {
|
||||
const data = await this.$wire.getUpgradeStatus();
|
||||
if (data.status === 'in_progress') {
|
||||
this.currentStep = this.mapStepToUI(data.step);
|
||||
this.currentStatus = data.message;
|
||||
} else if (data.status === 'complete') {
|
||||
this.showSuccess();
|
||||
} else if (data.status === 'error') {
|
||||
this.showError(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
// Service is down - switch to health check mode
|
||||
console.log('Livewire unavailable, switching to health check mode');
|
||||
if (!this.serviceDown) {
|
||||
this.serviceDown = true;
|
||||
this.currentStep = 4;
|
||||
this.currentStatus = 'Coolify is restarting with the new version...';
|
||||
if (this.checkUpgradeStatusInterval) {
|
||||
clearInterval(this.checkUpgradeStatusInterval);
|
||||
this.checkUpgradeStatusInterval = null;
|
||||
}
|
||||
this.revive();
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
@ -7,27 +7,101 @@ LATEST_HELPER_VERSION=${2:-latest}
|
|||
REGISTRY_URL=${3:-ghcr.io}
|
||||
SKIP_BACKUP=${4:-false}
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
STATUS_FILE="/data/coolify/source/.upgrade-status"
|
||||
|
||||
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
||||
LOGFILE="/data/coolify/source/upgrade-${DATE}.log"
|
||||
|
||||
# Helper function to log with timestamp
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE"
|
||||
}
|
||||
|
||||
# Helper function to log section headers
|
||||
log_section() {
|
||||
echo "" >>"$LOGFILE"
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE"
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
}
|
||||
|
||||
# Helper function to write upgrade status for API polling
|
||||
write_status() {
|
||||
local step="$1"
|
||||
local message="$2"
|
||||
echo "${step}|${message}|$(date -Iseconds)" > "$STATUS_FILE"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Coolify Upgrade - ${DATE}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Initialize log file with header
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
echo "Coolify Upgrade Log" >>"$LOGFILE"
|
||||
echo "Started: $(date '+%Y-%m-%d %H:%M:%S')" >>"$LOGFILE"
|
||||
echo "Target Version: ${LATEST_IMAGE}" >>"$LOGFILE"
|
||||
echo "Helper Version: ${LATEST_HELPER_VERSION}" >>"$LOGFILE"
|
||||
echo "Registry URL: ${REGISTRY_URL}" >>"$LOGFILE"
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
|
||||
log_section "Step 1/6: Downloading configuration files"
|
||||
write_status "1" "Downloading configuration files"
|
||||
echo "1/6 Downloading latest configuration files..."
|
||||
log "Downloading docker-compose.yml from ${CDN}/docker-compose.yml"
|
||||
curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
||||
log "Downloading docker-compose.prod.yml from ${CDN}/docker-compose.prod.yml"
|
||||
curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
|
||||
log "Downloading .env.production from ${CDN}/.env.production"
|
||||
curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
log "Configuration files downloaded successfully"
|
||||
echo " Done."
|
||||
|
||||
# Extract all images from docker-compose configuration
|
||||
log "Extracting all images from docker-compose configuration..."
|
||||
COMPOSE_FILES="-f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml"
|
||||
|
||||
# Check if custom compose file exists
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
COMPOSE_FILES="$COMPOSE_FILES -f /data/coolify/source/docker-compose.custom.yml"
|
||||
log "Including custom docker-compose.yml in image extraction"
|
||||
fi
|
||||
|
||||
# Get all unique images from docker compose config
|
||||
# LATEST_IMAGE env var is needed for image substitution in compose files
|
||||
IMAGES=$(LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u)
|
||||
|
||||
if [ -z "$IMAGES" ]; then
|
||||
log "ERROR: Failed to extract images from docker-compose files"
|
||||
write_status "error" "Failed to parse docker-compose configuration"
|
||||
echo " ERROR: Failed to parse docker-compose configuration. Aborting upgrade."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Images to pull:"
|
||||
echo "$IMAGES" | while read img; do log " - $img"; done
|
||||
|
||||
# Backup existing .env file before making any changes
|
||||
if [ "$SKIP_BACKUP" != "true" ]; then
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "Creating backup of existing .env file to .env-$DATE" >>"$LOGFILE"
|
||||
echo " Creating backup of .env file..."
|
||||
log "Creating backup of .env file to .env-$DATE"
|
||||
cp "$ENV_FILE" "$ENV_FILE-$DATE"
|
||||
log "Backup created: ${ENV_FILE}-${DATE}"
|
||||
else
|
||||
echo "No existing .env file found to backup" >>"$LOGFILE"
|
||||
log "WARNING: No existing .env file found to backup"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Merging .env.production values into .env" >>"$LOGFILE"
|
||||
log_section "Step 2/6: Updating environment configuration"
|
||||
write_status "2" "Updating environment configuration"
|
||||
echo ""
|
||||
echo "2/6 Updating environment configuration..."
|
||||
log "Merging .env.production values into .env"
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE" /data/coolify/source/.env.production > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
echo ".env file merged successfully" >>"$LOGFILE"
|
||||
log "Environment file merged successfully"
|
||||
|
||||
update_env_var() {
|
||||
local key="$1"
|
||||
|
|
@ -36,37 +110,173 @@ update_env_var() {
|
|||
# If variable "key=" exists but has no value, update the value of the existing line
|
||||
if grep -q "^${key}=$" "$ENV_FILE"; then
|
||||
sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE"
|
||||
echo " - Updated value of ${key} as the current value was empty" >>"$LOGFILE"
|
||||
log "Updated ${key} (was empty)"
|
||||
# If variable "key=" doesn't exist, append it to the file with value
|
||||
elif ! grep -q "^${key}=" "$ENV_FILE"; then
|
||||
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
|
||||
echo " - Added ${key} with default value as the variable was missing" >>"$LOGFILE"
|
||||
log "Added ${key} (was missing)"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Checking and updating environment variables if necessary..." >>"$LOGFILE"
|
||||
log "Checking environment variables..."
|
||||
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
||||
log "Environment variables check complete"
|
||||
echo " Done."
|
||||
|
||||
# Make sure coolify network exists
|
||||
# It is created when starting Coolify with docker compose
|
||||
log "Checking Docker network 'coolify'..."
|
||||
if ! docker network inspect coolify >/dev/null 2>&1; then
|
||||
log "Network 'coolify' does not exist, creating..."
|
||||
if ! docker network create --attachable --ipv6 coolify 2>/dev/null; then
|
||||
echo "Failed to create coolify network with ipv6. Trying without ipv6..."
|
||||
log "Failed to create network with IPv6, trying without IPv6..."
|
||||
docker network create --attachable coolify 2>/dev/null
|
||||
log "Network 'coolify' created without IPv6"
|
||||
else
|
||||
log "Network 'coolify' created with IPv6 support"
|
||||
fi
|
||||
else
|
||||
log "Network 'coolify' already exists"
|
||||
fi
|
||||
|
||||
# Check if Docker config file exists
|
||||
DOCKER_CONFIG_MOUNT=""
|
||||
if [ -f /root/.docker/config.json ]; then
|
||||
DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json"
|
||||
log "Docker config mount enabled: /root/.docker/config.json"
|
||||
fi
|
||||
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
echo "docker-compose.custom.yml detected." >>"$LOGFILE"
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
log_section "Step 3/6: Pulling Docker images"
|
||||
write_status "3" "Pulling Docker images"
|
||||
echo ""
|
||||
echo "3/6 Pulling Docker images..."
|
||||
echo " This may take a few minutes depending on your connection."
|
||||
|
||||
# Also pull the helper image (not in compose files but needed for upgrade)
|
||||
HELPER_IMAGE="${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}"
|
||||
echo " - Pulling $HELPER_IMAGE..."
|
||||
log "Pulling image: $HELPER_IMAGE"
|
||||
if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then
|
||||
log "Successfully pulled $HELPER_IMAGE"
|
||||
else
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60" >>"$LOGFILE" 2>&1
|
||||
log "ERROR: Failed to pull $HELPER_IMAGE"
|
||||
write_status "error" "Failed to pull $HELPER_IMAGE"
|
||||
echo " ERROR: Failed to pull $HELPER_IMAGE. Aborting upgrade."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pull all images from compose config
|
||||
# Using a for loop to avoid subshell issues with exit
|
||||
for IMAGE in $IMAGES; do
|
||||
if [ -n "$IMAGE" ]; then
|
||||
echo " - Pulling $IMAGE..."
|
||||
log "Pulling image: $IMAGE"
|
||||
if docker pull "$IMAGE" >>"$LOGFILE" 2>&1; then
|
||||
log "Successfully pulled $IMAGE"
|
||||
else
|
||||
log "ERROR: Failed to pull $IMAGE"
|
||||
write_status "error" "Failed to pull $IMAGE"
|
||||
echo " ERROR: Failed to pull $IMAGE. Aborting upgrade."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log "All images pulled successfully"
|
||||
echo " All images pulled successfully."
|
||||
|
||||
log_section "Step 4/6: Stopping and restarting containers"
|
||||
write_status "4" "Stopping containers"
|
||||
echo ""
|
||||
echo "4/6 Stopping containers and starting new ones..."
|
||||
echo " This step will restart all Coolify containers."
|
||||
echo " Check the log file for details: ${LOGFILE}"
|
||||
|
||||
# From this point forward, we need to ensure the script continues even if
|
||||
# the SSH connection is lost (which happens when coolify container stops)
|
||||
# We use a subshell with nohup to ensure completion
|
||||
log "Starting container restart sequence (detached)..."
|
||||
|
||||
nohup bash -c "
|
||||
LOGFILE='$LOGFILE'
|
||||
STATUS_FILE='$STATUS_FILE'
|
||||
DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT'
|
||||
REGISTRY_URL='$REGISTRY_URL'
|
||||
LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION'
|
||||
LATEST_IMAGE='$LATEST_IMAGE'
|
||||
|
||||
log() {
|
||||
echo \"[\$(date '+%Y-%m-%d %H:%M:%S')] \$1\" >>\"\$LOGFILE\"
|
||||
}
|
||||
|
||||
write_status() {
|
||||
echo \"\$1|\$2|\$(date -Iseconds)\" > \"\$STATUS_FILE\"
|
||||
}
|
||||
|
||||
# Stop and remove containers
|
||||
for container in coolify coolify-db coolify-redis coolify-realtime; do
|
||||
if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then
|
||||
log \"Stopping container: \${container}\"
|
||||
docker stop \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
|
||||
log \"Removing container: \${container}\"
|
||||
docker rm \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
|
||||
log \"Container \${container} stopped and removed\"
|
||||
else
|
||||
log \"Container \${container} not found (skipping)\"
|
||||
fi
|
||||
done
|
||||
log \"Container cleanup complete\"
|
||||
|
||||
# Start new containers
|
||||
echo '' >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
log 'Step 5/6: Starting new containers'
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
write_status '5' 'Starting new containers'
|
||||
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
log 'Using custom docker-compose.yml'
|
||||
log 'Running docker compose up with custom configuration...'
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
else
|
||||
log 'Using standard docker-compose configuration'
|
||||
log 'Running docker compose up...'
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
fi
|
||||
log 'Docker compose up completed'
|
||||
|
||||
# Final log entry
|
||||
echo '' >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
log 'Step 6/6: Upgrade complete'
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
write_status '6' 'Upgrade complete'
|
||||
log 'Coolify upgrade completed successfully'
|
||||
log \"Version: \${LATEST_IMAGE}\"
|
||||
echo '' >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
echo \"Upgrade completed: \$(date '+%Y-%m-%d %H:%M:%S')\" >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
|
||||
# Clean up status file after a short delay to allow frontend to read completion
|
||||
sleep 10
|
||||
rm -f \"\$STATUS_FILE\"
|
||||
log 'Status file cleaned up'
|
||||
" >>"$LOGFILE" 2>&1 &
|
||||
|
||||
# Give the background process a moment to start
|
||||
sleep 2
|
||||
log "Container restart sequence started in background (PID: $!)"
|
||||
echo ""
|
||||
echo "5/6 Containers are being restarted in the background..."
|
||||
echo "6/6 Upgrade process initiated!"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Coolify upgrade to ${LATEST_IMAGE} in progress"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo " The upgrade will continue in the background."
|
||||
echo " Coolify will be available again shortly."
|
||||
echo " Log file: ${LOGFILE}"
|
||||
|
|
|
|||
Loading…
Reference in a new issue