mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-17 17:21:04 +00:00
Add real-time upgrade progress tracking via status file
- upgrade.sh now writes status to /data/coolify/source/.upgrade-status - New /api/upgrade-status endpoint reads status file for real progress - Frontend polls status API instead of simulating progress - Falls back to health check when service goes down during restart 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -186,4 +186,82 @@ class OtherController extends Controller
|
|||||||
{
|
{
|
||||||
return 'OK';
|
return 'OK';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
summary: 'Upgrade Status',
|
||||||
|
description: 'Get the current upgrade status. Returns the step and message from the upgrade process.',
|
||||||
|
path: '/upgrade-status',
|
||||||
|
operationId: 'upgrade-status',
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Returns upgrade status.',
|
||||||
|
content: new OA\JsonContent(
|
||||||
|
type: 'object',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'status', type: 'string', example: 'in_progress'),
|
||||||
|
new OA\Property(property: 'step', type: 'integer', example: 3),
|
||||||
|
new OA\Property(property: 'message', type: 'string', example: 'Pulling Docker images'),
|
||||||
|
new OA\Property(property: 'timestamp', type: 'string', example: '2024-01-15T10:30:45+00:00'),
|
||||||
|
]
|
||||||
|
)),
|
||||||
|
new OA\Response(
|
||||||
|
response: 400,
|
||||||
|
ref: '#/components/responses/400',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function upgradeStatus(Request $request)
|
||||||
|
{
|
||||||
|
$statusFile = '/data/coolify/source/.upgrade-status';
|
||||||
|
|
||||||
|
if (! file_exists($statusFile)) {
|
||||||
|
return response()->json(['status' => 'none']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = trim(file_get_contents($statusFile));
|
||||||
|
if (empty($content)) {
|
||||||
|
return response()->json(['status' => 'none']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode('|', $content);
|
||||||
|
if (count($parts) < 3) {
|
||||||
|
return response()->json(['status' => 'none']);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$step, $message, $timestamp] = $parts;
|
||||||
|
|
||||||
|
// Check if status is stale (older than 30 minutes)
|
||||||
|
try {
|
||||||
|
$statusTime = new \DateTime($timestamp);
|
||||||
|
$now = new \DateTime;
|
||||||
|
$diffMinutes = ($now->getTimestamp() - $statusTime->getTimestamp()) / 60;
|
||||||
|
|
||||||
|
if ($diffMinutes > 30) {
|
||||||
|
return response()->json(['status' => 'none']);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// If timestamp parsing fails, continue with the status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine status based on step
|
||||||
|
if ($step === 'error') {
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'error',
|
||||||
|
'step' => 0,
|
||||||
|
'message' => $message,
|
||||||
|
'timestamp' => $timestamp,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stepInt = (int) $step;
|
||||||
|
$status = $stepInt >= 6 ? 'complete' : 'in_progress';
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => $status,
|
||||||
|
'step' => $stepInt,
|
||||||
|
'message' => $message,
|
||||||
|
'timestamp' => $timestamp,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@
|
|||||||
showProgress: false,
|
showProgress: false,
|
||||||
currentStatus: '',
|
currentStatus: '',
|
||||||
checkHealthInterval: null,
|
checkHealthInterval: null,
|
||||||
checkIfIamDeadInterval: null,
|
checkUpgradeStatusInterval: null,
|
||||||
elapsedInterval: null,
|
elapsedInterval: null,
|
||||||
healthCheckAttempts: 0,
|
healthCheckAttempts: 0,
|
||||||
startTime: null,
|
startTime: null,
|
||||||
@@ -171,10 +171,12 @@
|
|||||||
successCountdown: 3,
|
successCountdown: 3,
|
||||||
currentVersion: config.currentVersion || '',
|
currentVersion: config.currentVersion || '',
|
||||||
latestVersion: config.latestVersion || '',
|
latestVersion: config.latestVersion || '',
|
||||||
|
serviceDown: false,
|
||||||
|
|
||||||
confirmed() {
|
confirmed() {
|
||||||
this.showProgress = true;
|
this.showProgress = true;
|
||||||
this.currentStep = 1;
|
this.currentStep = 1;
|
||||||
|
this.currentStatus = 'Starting upgrade...';
|
||||||
this.startTimer();
|
this.startTimer();
|
||||||
this.$wire.$call('upgrade');
|
this.$wire.$call('upgrade');
|
||||||
this.upgrade();
|
this.upgrade();
|
||||||
@@ -197,14 +199,14 @@
|
|||||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
getStepMessage(step) {
|
mapStepToUI(apiStep) {
|
||||||
const messages = {
|
// Map backend steps (1-6) to UI steps (1-4)
|
||||||
1: 'Preparing upgrade...',
|
// Backend: 1=config, 2=env, 3=pull, 4=stop, 5=start, 6=complete
|
||||||
2: 'Pulling helper image...',
|
// UI: 1=prepare, 2=pull images, 3=pull coolify, 4=restart
|
||||||
3: 'Pulling Coolify image...',
|
if (apiStep <= 2) return 1;
|
||||||
4: 'Restarting Coolify...'
|
if (apiStep === 3) return 2;
|
||||||
};
|
if (apiStep <= 5) return 3;
|
||||||
return messages[step] || 'Processing...';
|
return 4;
|
||||||
},
|
},
|
||||||
|
|
||||||
getReviveStatusMessage(elapsedMinutes, attempts) {
|
getReviveStatusMessage(elapsedMinutes, attempts) {
|
||||||
@@ -249,6 +251,10 @@
|
|||||||
clearInterval(this.checkHealthInterval);
|
clearInterval(this.checkHealthInterval);
|
||||||
this.checkHealthInterval = null;
|
this.checkHealthInterval = null;
|
||||||
}
|
}
|
||||||
|
if (this.checkUpgradeStatusInterval) {
|
||||||
|
clearInterval(this.checkUpgradeStatusInterval);
|
||||||
|
this.checkUpgradeStatusInterval = null;
|
||||||
|
}
|
||||||
if (this.elapsedInterval) {
|
if (this.elapsedInterval) {
|
||||||
clearInterval(this.elapsedInterval);
|
clearInterval(this.elapsedInterval);
|
||||||
this.elapsedInterval = null;
|
this.elapsedInterval = null;
|
||||||
@@ -273,52 +279,43 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
upgrade() {
|
upgrade() {
|
||||||
if (this.checkIfIamDeadInterval) return true;
|
if (this.checkUpgradeStatusInterval) return true;
|
||||||
this.currentStep = 1;
|
this.currentStep = 1;
|
||||||
this.currentStatus = this.getStepMessage(1);
|
this.currentStatus = 'Starting upgrade...';
|
||||||
|
this.serviceDown = false;
|
||||||
|
|
||||||
// Simulate step progression (since we can't get real-time feedback from Docker pulls)
|
// Poll upgrade status API for real progress
|
||||||
let stepTime = 0;
|
this.checkUpgradeStatusInterval = setInterval(() => {
|
||||||
const stepInterval = setInterval(() => {
|
fetch('/api/upgrade-status')
|
||||||
stepTime++;
|
|
||||||
// Progress through steps based on elapsed time
|
|
||||||
if (stepTime >= 3 && this.currentStep === 1) {
|
|
||||||
this.currentStep = 2;
|
|
||||||
this.currentStatus = this.getStepMessage(2);
|
|
||||||
} else if (stepTime >= 8 && this.currentStep === 2) {
|
|
||||||
this.currentStep = 3;
|
|
||||||
this.currentStatus = this.getStepMessage(3);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
this.checkIfIamDeadInterval = setInterval(() => {
|
|
||||||
fetch('/api/health')
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Still running, update status based on current step
|
return response.json();
|
||||||
this.currentStatus = this.getStepMessage(this.currentStep);
|
}
|
||||||
} else {
|
throw new Error('Service unavailable');
|
||||||
// Service is down, now waiting to revive
|
})
|
||||||
clearInterval(stepInterval);
|
.then(data => {
|
||||||
this.currentStep = 4;
|
if (data.status === 'in_progress') {
|
||||||
this.currentStatus = 'Coolify is restarting with the new version...';
|
this.currentStep = this.mapStepToUI(data.step);
|
||||||
if (this.checkIfIamDeadInterval) {
|
this.currentStatus = data.message;
|
||||||
clearInterval(this.checkIfIamDeadInterval);
|
} else if (data.status === 'complete') {
|
||||||
this.checkIfIamDeadInterval = null;
|
this.showSuccess();
|
||||||
}
|
} else if (data.status === 'error') {
|
||||||
this.revive();
|
this.currentStatus = `Error: ${data.message}`;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Health check failed:', error);
|
// Service is down - switch to health check mode
|
||||||
clearInterval(stepInterval);
|
console.log('Upgrade status API unavailable, switching to health check mode');
|
||||||
this.currentStep = 4;
|
if (!this.serviceDown) {
|
||||||
this.currentStatus = 'Coolify is restarting with the new version...';
|
this.serviceDown = true;
|
||||||
if (this.checkIfIamDeadInterval) {
|
this.currentStep = 4;
|
||||||
clearInterval(this.checkIfIamDeadInterval);
|
this.currentStatus = 'Coolify is restarting with the new version...';
|
||||||
this.checkIfIamDeadInterval = null;
|
if (this.checkUpgradeStatusInterval) {
|
||||||
|
clearInterval(this.checkUpgradeStatusInterval);
|
||||||
|
this.checkUpgradeStatusInterval = null;
|
||||||
|
}
|
||||||
|
this.revive();
|
||||||
}
|
}
|
||||||
this.revive();
|
|
||||||
});
|
});
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ use App\Models\Server;
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/health', [OtherController::class, 'healthcheck']);
|
Route::get('/health', [OtherController::class, 'healthcheck']);
|
||||||
|
Route::get('/upgrade-status', [OtherController::class, 'upgradeStatus']);
|
||||||
Route::group([
|
Route::group([
|
||||||
'prefix' => 'v1',
|
'prefix' => 'v1',
|
||||||
], function () {
|
], function () {
|
||||||
Route::get('/health', [OtherController::class, 'healthcheck']);
|
Route::get('/health', [OtherController::class, 'healthcheck']);
|
||||||
|
Route::get('/upgrade-status', [OtherController::class, 'upgradeStatus']);
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::post('/feedback', [OtherController::class, 'feedback']);
|
Route::post('/feedback', [OtherController::class, 'feedback']);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ LATEST_HELPER_VERSION=${2:-latest}
|
|||||||
REGISTRY_URL=${3:-ghcr.io}
|
REGISTRY_URL=${3:-ghcr.io}
|
||||||
SKIP_BACKUP=${4:-false}
|
SKIP_BACKUP=${4:-false}
|
||||||
ENV_FILE="/data/coolify/source/.env"
|
ENV_FILE="/data/coolify/source/.env"
|
||||||
|
STATUS_FILE="/data/coolify/source/.upgrade-status"
|
||||||
|
|
||||||
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
||||||
LOGFILE="/data/coolify/source/upgrade-${DATE}.log"
|
LOGFILE="/data/coolify/source/upgrade-${DATE}.log"
|
||||||
@@ -24,6 +25,13 @@ log_section() {
|
|||||||
echo "============================================================" >>"$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 "=========================================="
|
echo "=========================================="
|
||||||
echo " Coolify Upgrade - ${DATE}"
|
echo " Coolify Upgrade - ${DATE}"
|
||||||
@@ -40,6 +48,7 @@ echo "Registry URL: ${REGISTRY_URL}" >>"$LOGFILE"
|
|||||||
echo "============================================================" >>"$LOGFILE"
|
echo "============================================================" >>"$LOGFILE"
|
||||||
|
|
||||||
log_section "Step 1/6: Downloading configuration files"
|
log_section "Step 1/6: Downloading configuration files"
|
||||||
|
write_status "1" "Downloading configuration files"
|
||||||
echo "1/6 Downloading latest configuration files..."
|
echo "1/6 Downloading latest configuration files..."
|
||||||
log "Downloading docker-compose.yml from ${CDN}/docker-compose.yml"
|
log "Downloading docker-compose.yml from ${CDN}/docker-compose.yml"
|
||||||
curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
||||||
@@ -63,6 +72,7 @@ if [ "$SKIP_BACKUP" != "true" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
log_section "Step 2/6: Updating environment configuration"
|
log_section "Step 2/6: Updating environment configuration"
|
||||||
|
write_status "2" "Updating environment configuration"
|
||||||
echo ""
|
echo ""
|
||||||
echo "2/6 Updating environment configuration..."
|
echo "2/6 Updating environment configuration..."
|
||||||
log "Merging .env.production values into .env"
|
log "Merging .env.production values into .env"
|
||||||
@@ -115,6 +125,7 @@ if [ -f /root/.docker/config.json ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
log_section "Step 3/6: Pulling Docker images"
|
log_section "Step 3/6: Pulling Docker images"
|
||||||
|
write_status "3" "Pulling Docker images"
|
||||||
echo ""
|
echo ""
|
||||||
echo "3/6 Pulling Docker images..."
|
echo "3/6 Pulling Docker images..."
|
||||||
echo " This may take a few minutes depending on your connection."
|
echo " This may take a few minutes depending on your connection."
|
||||||
@@ -125,6 +136,7 @@ if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"
|
|||||||
log "Successfully pulled Coolify image"
|
log "Successfully pulled Coolify image"
|
||||||
else
|
else
|
||||||
log "ERROR: Failed to pull Coolify image"
|
log "ERROR: Failed to pull Coolify image"
|
||||||
|
write_status "error" "Failed to pull Coolify image"
|
||||||
echo " ERROR: Failed to pull Coolify image. Aborting upgrade."
|
echo " ERROR: Failed to pull Coolify image. Aborting upgrade."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -135,6 +147,7 @@ if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELP
|
|||||||
log "Successfully pulled Coolify helper image"
|
log "Successfully pulled Coolify helper image"
|
||||||
else
|
else
|
||||||
log "ERROR: Failed to pull Coolify helper image"
|
log "ERROR: Failed to pull Coolify helper image"
|
||||||
|
write_status "error" "Failed to pull Coolify helper image"
|
||||||
echo " ERROR: Failed to pull helper image. Aborting upgrade."
|
echo " ERROR: Failed to pull helper image. Aborting upgrade."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -145,6 +158,7 @@ if docker pull postgres:15-alpine >>"$LOGFILE" 2>&1; then
|
|||||||
log "Successfully pulled PostgreSQL image"
|
log "Successfully pulled PostgreSQL image"
|
||||||
else
|
else
|
||||||
log "ERROR: Failed to pull PostgreSQL image"
|
log "ERROR: Failed to pull PostgreSQL image"
|
||||||
|
write_status "error" "Failed to pull PostgreSQL image"
|
||||||
echo " ERROR: Failed to pull PostgreSQL image. Aborting upgrade."
|
echo " ERROR: Failed to pull PostgreSQL image. Aborting upgrade."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -155,6 +169,7 @@ if docker pull redis:7-alpine >>"$LOGFILE" 2>&1; then
|
|||||||
log "Successfully pulled Redis image"
|
log "Successfully pulled Redis image"
|
||||||
else
|
else
|
||||||
log "ERROR: Failed to pull Redis image"
|
log "ERROR: Failed to pull Redis image"
|
||||||
|
write_status "error" "Failed to pull Redis image"
|
||||||
echo " ERROR: Failed to pull Redis image. Aborting upgrade."
|
echo " ERROR: Failed to pull Redis image. Aborting upgrade."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -165,6 +180,7 @@ if docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"
|
|||||||
log "Successfully pulled Coolify realtime image"
|
log "Successfully pulled Coolify realtime image"
|
||||||
else
|
else
|
||||||
log "ERROR: Failed to pull Coolify realtime image"
|
log "ERROR: Failed to pull Coolify realtime image"
|
||||||
|
write_status "error" "Failed to pull Coolify realtime image"
|
||||||
echo " ERROR: Failed to pull realtime image. Aborting upgrade."
|
echo " ERROR: Failed to pull realtime image. Aborting upgrade."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -173,6 +189,7 @@ log "All images pulled successfully"
|
|||||||
echo " All images pulled successfully."
|
echo " All images pulled successfully."
|
||||||
|
|
||||||
log_section "Step 4/6: Stopping and restarting containers"
|
log_section "Step 4/6: Stopping and restarting containers"
|
||||||
|
write_status "4" "Stopping containers"
|
||||||
echo ""
|
echo ""
|
||||||
echo "4/6 Stopping containers and starting new ones..."
|
echo "4/6 Stopping containers and starting new ones..."
|
||||||
echo " This step will restart all Coolify containers."
|
echo " This step will restart all Coolify containers."
|
||||||
@@ -185,6 +202,7 @@ log "Starting container restart sequence (detached)..."
|
|||||||
|
|
||||||
nohup bash -c "
|
nohup bash -c "
|
||||||
LOGFILE='$LOGFILE'
|
LOGFILE='$LOGFILE'
|
||||||
|
STATUS_FILE='$STATUS_FILE'
|
||||||
DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT'
|
DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT'
|
||||||
REGISTRY_URL='$REGISTRY_URL'
|
REGISTRY_URL='$REGISTRY_URL'
|
||||||
LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION'
|
LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION'
|
||||||
@@ -194,6 +212,10 @@ nohup bash -c "
|
|||||||
echo \"[\$(date '+%Y-%m-%d %H:%M:%S')] \$1\" >>\"\$LOGFILE\"
|
echo \"[\$(date '+%Y-%m-%d %H:%M:%S')] \$1\" >>\"\$LOGFILE\"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
write_status() {
|
||||||
|
echo \"\$1|\$2|\$(date -Iseconds)\" > \"\$STATUS_FILE\"
|
||||||
|
}
|
||||||
|
|
||||||
# Stop and remove containers
|
# Stop and remove containers
|
||||||
for container in coolify coolify-db coolify-redis coolify-realtime; do
|
for container in coolify coolify-db coolify-redis coolify-realtime; do
|
||||||
if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then
|
if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then
|
||||||
@@ -213,6 +235,7 @@ nohup bash -c "
|
|||||||
echo '============================================================' >>\"\$LOGFILE\"
|
echo '============================================================' >>\"\$LOGFILE\"
|
||||||
log 'Step 5/6: Starting new containers'
|
log 'Step 5/6: Starting new containers'
|
||||||
echo '============================================================' >>\"\$LOGFILE\"
|
echo '============================================================' >>\"\$LOGFILE\"
|
||||||
|
write_status '5' 'Starting new containers'
|
||||||
|
|
||||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||||
log 'Using custom docker-compose.yml'
|
log 'Using custom docker-compose.yml'
|
||||||
@@ -230,6 +253,7 @@ nohup bash -c "
|
|||||||
echo '============================================================' >>\"\$LOGFILE\"
|
echo '============================================================' >>\"\$LOGFILE\"
|
||||||
log 'Step 6/6: Upgrade complete'
|
log 'Step 6/6: Upgrade complete'
|
||||||
echo '============================================================' >>\"\$LOGFILE\"
|
echo '============================================================' >>\"\$LOGFILE\"
|
||||||
|
write_status '6' 'Upgrade complete'
|
||||||
log 'Coolify upgrade completed successfully'
|
log 'Coolify upgrade completed successfully'
|
||||||
log \"Version: \${LATEST_IMAGE}\"
|
log \"Version: \${LATEST_IMAGE}\"
|
||||||
echo '' >>\"\$LOGFILE\"
|
echo '' >>\"\$LOGFILE\"
|
||||||
|
|||||||
Reference in New Issue
Block a user