Run proxy restart as background job with real-time logs (#7475)

This commit is contained in:
Andras Bacsai
2025-12-04 14:59:50 +01:00
committed by GitHub
6 changed files with 378 additions and 18 deletions
+4 -1
View File
@@ -14,12 +14,15 @@ class ProxyStatusChangedUI implements ShouldBroadcast
public ?int $teamId = null;
public function __construct(?int $teamId = null)
public ?int $activityId = null;
public function __construct(?int $teamId = null, ?int $activityId = null)
{
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
$this->activityId = $activityId;
}
public function broadcastOn(): array
+122 -7
View File
@@ -2,9 +2,12 @@
namespace App\Jobs;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
use App\Actions\Proxy\GetProxyConfiguration;
use App\Actions\Proxy\SaveProxyConfiguration;
use App\Enums\ProxyTypes;
use App\Events\ProxyStatusChangedUI;
use App\Models\Server;
use App\Services\ProxyDashboardCacheService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -19,11 +22,13 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
public $tries = 1;
public $timeout = 60;
public $timeout = 120;
public ?int $activity_id = null;
public function middleware(): array
{
return [(new WithoutOverlapping('restart-proxy-'.$this->server->uuid))->expireAfter(60)->dontRelease()];
return [(new WithoutOverlapping('restart-proxy-'.$this->server->uuid))->expireAfter(120)->dontRelease()];
}
public function __construct(public Server $server) {}
@@ -31,15 +36,125 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
public function handle()
{
try {
StopProxy::run($this->server, restarting: true);
// Set status to restarting
$this->server->proxy->status = 'restarting';
$this->server->proxy->force_stop = false;
$this->server->save();
StartProxy::run($this->server, force: true, restarting: true);
// Build combined stop + start commands for a single activity
$commands = $this->buildRestartCommands();
// Create activity and dispatch immediately - returns Activity right away
// The remote_process runs asynchronously, so UI gets activity ID instantly
$activity = remote_process(
$commands,
$this->server,
callEventOnFinish: 'ProxyStatusChanged',
callEventData: $this->server->id
);
// Store activity ID and notify UI immediately with it
$this->activity_id = $activity->id;
ProxyStatusChangedUI::dispatch($this->server->team_id, $this->activity_id);
} catch (\Throwable $e) {
// Set error status
$this->server->proxy->status = 'error';
$this->server->save();
// Notify UI of error
ProxyStatusChangedUI::dispatch($this->server->team_id);
// Clear dashboard cache on error
ProxyDashboardCacheService::clearCache($this->server);
return handleError($e);
}
}
/**
* Build combined stop + start commands for proxy restart.
* This creates a single command sequence that shows all logs in one activity.
*/
private function buildRestartCommands(): array
{
$proxyType = $this->server->proxyType();
$containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
$proxy_path = $this->server->proxyPath();
$stopTimeout = 30;
// Get proxy configuration
$configuration = GetProxyConfiguration::run($this->server);
if (! $configuration) {
throw new \Exception('Configuration is not synced');
}
SaveProxyConfiguration::run($this->server, $configuration);
$docker_compose_yml_base64 = base64_encode($configuration);
$this->server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
$this->server->save();
$commands = collect([]);
// === STOP PHASE ===
$commands = $commands->merge([
"echo 'Stopping proxy...'",
"docker stop -t=$stopTimeout $containerName 2>/dev/null || true",
"docker rm -f $containerName 2>/dev/null || true",
'# Wait for container to be fully removed',
'for i in {1..15}; do',
" if ! docker ps -a --format \"{{.Names}}\" | grep -q \"^$containerName$\"; then",
" echo 'Container removed successfully.'",
' break',
' fi',
' echo "Waiting for container to be removed... ($i/15)"',
' sleep 1',
' # Force remove on each iteration in case it got stuck',
" docker rm -f $containerName 2>/dev/null || true",
'done',
'# Final verification and force cleanup',
"if docker ps -a --format \"{{.Names}}\" | grep -q \"^$containerName$\"; then",
" echo 'Container still exists after wait, forcing removal...'",
" docker rm -f $containerName 2>/dev/null || true",
' sleep 2',
'fi',
"echo 'Proxy stopped successfully.'",
]);
// === START PHASE ===
if ($this->server->isSwarmManager()) {
$commands = $commands->merge([
"echo 'Starting proxy (Swarm mode)...'",
"mkdir -p $proxy_path/dynamic",
"cd $proxy_path",
"echo 'Creating required Docker Compose file.'",
"echo 'Starting coolify-proxy.'",
'docker stack deploy --detach=true -c docker-compose.yml coolify-proxy',
"echo 'Successfully started coolify-proxy.'",
]);
} else {
if (isDev() && $proxyType === ProxyTypes::CADDY->value) {
$proxy_path = '/data/coolify/proxy/caddy';
}
$caddyfile = 'import /dynamic/*.caddy';
$commands = $commands->merge([
"echo 'Starting proxy...'",
"mkdir -p $proxy_path/dynamic",
"cd $proxy_path",
"echo '$caddyfile' > $proxy_path/dynamic/Caddyfile",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
]);
// Ensure required networks exist BEFORE docker compose up
$commands = $commands->merge(ensureProxyNetworksExist($this->server));
$commands = $commands->merge([
"echo 'Starting coolify-proxy.'",
'docker compose up -d --wait --remove-orphans',
"echo 'Successfully started coolify-proxy.'",
]);
$commands = $commands->merge(connectProxyToNetworks($this->server));
}
return $commands->toArray();
}
}
+47 -10
View File
@@ -6,6 +6,7 @@ use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Proxy\StopProxy;
use App\Enums\ProxyTypes;
use App\Jobs\RestartProxyJob;
use App\Models\Server;
use App\Services\ProxyDashboardCacheService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@@ -27,6 +28,10 @@ class Navbar extends Component
public ?string $proxyStatus = 'unknown';
public ?string $lastNotifiedStatus = null;
public bool $restartInitiated = false;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@@ -61,14 +66,19 @@ class Navbar extends Component
{
try {
$this->authorize('manageProxy', $this->server);
StopProxy::run($this->server, restarting: true);
$this->server->proxy->force_stop = false;
$this->server->save();
// Prevent duplicate restart calls
if ($this->restartInitiated) {
return;
}
$this->restartInitiated = true;
// Always use background job for all servers
RestartProxyJob::dispatch($this->server);
$activity = StartProxy::run($this->server, force: true, restarting: true);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
$this->restartInitiated = false;
return handleError($e, $this);
}
}
@@ -122,12 +132,27 @@ class Navbar extends Component
}
}
public function showNotification()
public function showNotification($event = null)
{
$previousStatus = $this->proxyStatus;
$this->server->refresh();
$this->proxyStatus = $this->server->proxy->status ?? 'unknown';
// If event contains activityId, open activity monitor
if ($event && isset($event['activityId'])) {
$this->dispatch('activityMonitor', $event['activityId']);
}
// Reset restart flag when proxy reaches a stable state
if (in_array($this->proxyStatus, ['running', 'exited', 'error'])) {
$this->restartInitiated = false;
}
// Skip notification if we already notified about this status (prevents duplicates)
if ($this->lastNotifiedStatus === $this->proxyStatus) {
return;
}
switch ($this->proxyStatus) {
case 'running':
$this->loadProxyConfiguration();
@@ -135,6 +160,7 @@ class Navbar extends Component
// Don't show during normal start/restart flows (starting, restarting, stopping)
if (in_array($previousStatus, ['exited', 'stopped', 'unknown', null])) {
$this->dispatch('success', 'Proxy is running.');
$this->lastNotifiedStatus = $this->proxyStatus;
}
break;
case 'exited':
@@ -142,19 +168,30 @@ class Navbar extends Component
// Don't show during normal stop/restart flows (stopping, restarting)
if (in_array($previousStatus, ['running'])) {
$this->dispatch('info', 'Proxy has exited.');
$this->lastNotifiedStatus = $this->proxyStatus;
}
break;
case 'stopping':
$this->dispatch('info', 'Proxy is stopping.');
// $this->dispatch('info', 'Proxy is stopping.');
$this->lastNotifiedStatus = $this->proxyStatus;
break;
case 'starting':
$this->dispatch('info', 'Proxy is starting.');
// $this->dispatch('info', 'Proxy is starting.');
$this->lastNotifiedStatus = $this->proxyStatus;
break;
case 'restarting':
// $this->dispatch('info', 'Proxy is restarting.');
$this->lastNotifiedStatus = $this->proxyStatus;
break;
case 'error':
$this->dispatch('error', 'Proxy restart failed. Check logs.');
$this->lastNotifiedStatus = $this->proxyStatus;
break;
case 'unknown':
$this->dispatch('info', 'Proxy status is unknown.');
// Don't notify for unknown status - too noisy
break;
default:
$this->dispatch('info', 'Proxy status updated.');
// Don't notify for other statuses
break;
}