Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s

Shared web hosting extracted from the platform monolith, with CI deploy
to /var/www/ladill-hosting matching Bird/Domains/Email.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-06 16:24:20 +00:00
co-authored by Cursor
commit e251a4cf60
367 changed files with 66268 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace App\Jobs;
use App\Models\HostingAccount;
use App\Models\HostingNode;
use App\Services\Hosting\NodeCapacityService;
use App\Services\Hosting\Providers\SharedNodeProvider;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ProvisionHostingAccountJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 300; // 5 minutes for SSH operations
public array $backoff = [30, 60, 120]; // Retry after 30s, 60s, 120s
public function __construct(
private int $accountId,
) {
}
public function handle(SharedNodeProvider $provider, NodeCapacityService $capacityService): void
{
$account = HostingAccount::with(['node', 'product'])->find($this->accountId);
if (! $account) {
Log::warning('ProvisionHostingAccountJob: Account not found', ['account_id' => $this->accountId]);
return;
}
// Skip if already provisioned or not in pending status
if ($account->status !== 'pending' && $account->provisioned_at !== null) {
Log::info('ProvisionHostingAccountJob: Already provisioned', ['account_id' => $this->accountId, 'status' => $account->status]);
return;
}
$node = $account->node;
if (! $node) {
Log::error('ProvisionHostingAccountJob: No node assigned', ['account_id' => $this->accountId]);
$account->update(['status' => 'failed', 'notes' => 'No hosting node assigned']);
return;
}
$product = $account->product;
if (! $product) {
Log::error('ProvisionHostingAccountJob: No product found', ['account_id' => $this->accountId]);
$account->update(['status' => 'failed', 'notes' => 'No hosting product found']);
return;
}
// Mark as provisioning
$account->update(['status' => 'provisioning']);
try {
$password = \Illuminate\Support\Str::password(16, true, true, false, false);
$domain = $account->primary_domain ?? $account->username . '.ladill.com';
$result = $provider->createAccountOnNode($node, [
'username' => $account->username,
'password' => $password,
'domain' => $domain,
'php_version' => '8.4',
'disk_quota_mb' => max((int) ($product->disk_gb ?? 0), 1) * 1024,
]);
$docRoot = $result['document_root'] ?? "/home/{$account->username}/public_html";
$account->update([
'status' => 'active',
'home_directory' => $result['home_directory'] ?? "/home/{$account->username}",
'provisioned_at' => now(),
'metadata' => array_merge((array) $account->metadata, [
'provisioned_password' => $password, // Store temporarily for admin reference
'provisioned_at' => now()->toISOString(),
]),
]);
// Apply resource limits
$provider->applyAccountResourceProfile($account->fresh(), false);
// Install WordPress if it's a WordPress hosting product
if ($product->type === 'wordpress') {
$wpResult = $provider->installWordPress($node, $account->username, $domain, [
'document_root' => $docRoot,
'admin_email' => $account->user?->email ?? 'admin@' . $domain,
'site_title' => $domain,
]);
// Store WordPress credentials in account metadata
$account->update([
'metadata' => array_merge((array) $account->metadata, [
'wp_admin_user' => $wpResult['admin_user'] ?? 'admin',
'wp_admin_password' => $wpResult['admin_password'] ?? null,
'wp_db_name' => $wpResult['db_name'] ?? null,
'wp_db_user' => $wpResult['db_user'] ?? null,
'wp_db_password' => $wpResult['db_password'] ?? null,
]),
]);
Log::info('ProvisionHostingAccountJob: WordPress installed', [
'account_id' => $this->accountId,
'domain' => $domain,
]);
}
// Refresh node capacity
$capacityService->refreshNodeResourceTotals($node);
Log::info('ProvisionHostingAccountJob: Successfully provisioned', [
'account_id' => $this->accountId,
'username' => $account->username,
'node_id' => $node->id,
]);
} catch (\Throwable $exception) {
Log::error('ProvisionHostingAccountJob: Failed to provision', [
'account_id' => $this->accountId,
'username' => $account->username,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
$account->update([
'status' => 'failed',
'notes' => 'Provisioning failed: ' . $exception->getMessage(),
]);
// Re-throw to trigger retry
throw $exception;
}
}
public function failed(\Throwable $exception): void
{
Log::error('ProvisionHostingAccountJob: Job failed after all retries', [
'account_id' => $this->accountId,
'error' => $exception->getMessage(),
]);
$account = HostingAccount::find($this->accountId);
if ($account) {
$account->update([
'status' => 'failed',
'notes' => 'Provisioning failed after retries: ' . $exception->getMessage(),
]);
}
}
}