Skip to content
24 changes: 24 additions & 0 deletions app/Models/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,21 @@ public function installedNodejsVersions(): array
return $versions;
}

/**
* @return array<int, string>
*/
public function installedBunVersions(): array
{
$versions = [];
$buns = $this->services()->where('type', 'bun')->get(['version']);
/** @var Service $bun */
foreach ($buns as $bun) {
$versions[] = $bun->version;
}

return $versions;
}

public function provider(): \App\ServerProviders\ServerProvider
{
$providerClass = config('server-provider.providers.'.$this->provider.'.handler');
Expand Down Expand Up @@ -449,6 +464,15 @@ public function nodejs(?string $version = null): ?Service
return $this->service('nodejs', $version);
}

public function bun(?string $version = null): ?Service
{
if ($version === null || $version === '' || $version === '0') {
return $this->defaultService('bun');
}

return $this->service('bun', $version);
}

public function memoryDatabase(?string $version = null): ?Service
{
if ($version === null || $version === '' || $version === '0') {
Expand Down
17 changes: 17 additions & 0 deletions app/Providers/ServiceTypeServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Providers;

use App\Plugins\RegisterServiceType;
use App\Services\Bun\Bun;
use App\Services\Database\Mariadb;
use App\Services\Database\Mysql;
use App\Services\Database\Postgresql;
Expand Down Expand Up @@ -31,6 +32,7 @@ public function boot(): void
$this->monitoring();
$this->php();
$this->node();
$this->bun();
}

private function webservers(): void
Expand Down Expand Up @@ -219,4 +221,19 @@ private function node(): void
])
->register();
}

private function bun(): void
{
RegisterServiceType::make(Bun::id())
->type(Bun::type())
->label('Bun')
->handler(Bun::class)
->versions([
'1.3.4',
'1.2.23',
'1.1.45',
'1.0.36',
])
->register();
}
}
29 changes: 29 additions & 0 deletions app/Providers/SiteTypeServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Plugins\RegisterSiteFeature;
use App\Plugins\RegisterSiteFeatureAction;
use App\Plugins\RegisterSiteType;
use App\SiteTypes\Bun;
use App\SiteTypes\Laravel;
use App\SiteTypes\LoadBalancer;
use App\SiteTypes\NodeJS;
Expand All @@ -27,6 +28,7 @@ public function boot(): void
$this->phpBlank();
$this->laravel();
$this->nodeJS();
$this->bun();
$this->loadBalancer();
$this->phpMyAdmin();
$this->wordpress();
Expand Down Expand Up @@ -159,6 +161,33 @@ private function nodeJS(): void
->register();
}

private function bun(): void
{
RegisterSiteType::make(Bun::id())
->label('Bun')
->handler(Bun::class)
->form(DynamicForm::make([
DynamicField::make('source_control')
->component()
->label('Source Control'),
DynamicField::make('port')
->text()
->label('Port')
->placeholder('3000')
->description('On which port your app will be running'),
DynamicField::make('repository')
->text()
->label('Repository')
->placeholder('organization/repository')
->description('Your package.json must have start and build scripts'),
DynamicField::make('branch')
->text()
->label('Branch')
->default('main'),
]))
->register();
}

public function loadBalancer(): void
{
RegisterSiteType::make(LoadBalancer::id())
Expand Down
101 changes: 101 additions & 0 deletions app/Services/Bun/Bun.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace App\Services\Bun;

use App\Exceptions\SSHError;
use App\Services\AbstractService;
use Closure;
use Illuminate\Validation\Rule;

class Bun extends AbstractService
{
public static function id(): string
{
return 'bun';
}

public static function type(): string
{
return 'bun';
}

public function unit(): string
{
return '';
}

public function creationRules(array $input): array
{
return [
'type' => [
function (string $attribute, mixed $value, Closure $fail): void {
$exists = $this->service->server->bun();
if ($exists) {
$fail('You already have Bun installed on the server.');
}
},
],
'version' => [
'required',
Rule::in(config('service.services.bun.versions')),
Rule::unique('services', 'version')
->where('type', 'bun')
->where('server_id', $this->service->server_id),
],
];
}

public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail): void {
$hasSite = $this->service->server->sites()
->where('type', 'bun')
->exists();
if ($hasSite) {
$fail('Some sites are using Bun.');
}
},
],
];
}

/**
* @throws SSHError
*/
public function install(): void
{
$server = $this->service->server;
$server->ssh()->exec(
view('ssh.services.bun.install-bun', [
'version' => $this->service->version,
]),
'install-bun-'.$this->service->version
);
event('service.installed', $this->service);
$this->service->server->os()->cleanup();
}

/**
* @throws SSHError
*/
public function uninstall(): void
{
$this->service->server->ssh()->exec(
view('ssh.services.bun.uninstall-bun'),
'uninstall-bun'
);
event('service.uninstalled', $this->service);
$this->service->server->os()->cleanup();
}

public function version(): string
{
$version = $this->service->server->ssh()->exec(
'bun --version'
);

return trim($version);
}
}
172 changes: 172 additions & 0 deletions app/SiteTypes/Bun.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php

namespace App\SiteTypes;

use App\Actions\Worker\CreateWorker;
use App\Actions\Worker\ManageWorker;
use App\Exceptions\FailedToDeployGitKey;
use App\Exceptions\SSHError;
use App\Models\Site;
use App\Models\Worker;
use App\SSH\OS\Git;
use Illuminate\Contracts\View\View;
use Illuminate\Validation\Rule;

class Bun extends AbstractSiteType
{
public static function id(): string
{
return 'bun';
}

public function language(): string
{
return 'bun';
}

public function requiredServices(): array
{
return [
'bun',
'webserver',
'process_manager',
];
}

public static function make(): self
{
return new self(new Site(['type' => self::id()]));
}

public function createRules(array $input): array
{
return [
'source_control' => [
'required',
Rule::exists('source_controls', 'id'),
],
'repository' => [
'required',
],
'branch' => [
'required',
],
'port' => [
'required',
'numeric',
'between:1,65535',
],
];
}

public function createFields(array $input): array
{
return [
'source_control_id' => $input['source_control'] ?? '',
'repository' => $input['repository'] ?? '',
'branch' => $input['branch'] ?? '',
'port' => $input['port'] ?? '',
];
}

public function data(array $input): array
{
return [];
}

/**
* @throws FailedToDeployGitKey
* @throws SSHError
*/
public function install(): void
{
$this->isolate();
$this->site->webserver()->createVHost($this->site);
$this->progress(15);
$this->deployKey();
$this->progress(30);
app(Git::class)->clone($this->site);
$this->site->server->ssh($this->site->user)->exec(
__('bun install --cwd :path', [
'path' => $this->site->path,
]),
'install-bun-dependencies',
$this->site->id
);
$this->site->server->ssh($this->site->user)->exec(
__('bun --bun run --cwd :path build', [
'path' => $this->site->path,
]),
'bun-build',
$this->site->id
);
$this->progress(65);
$command = __('bun --bun run --cwd :path start', [
Comment on lines +97 to +104
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --bun flag is typically unnecessary for modern Bun versions and may cause compatibility issues. Bun automatically uses its own runtime by default. Consider simplifying the commands to bun run build and bun run start unless there's a specific reason to force the Bun runtime (e.g., when Node compatibility mode might otherwise be triggered). This would also make the commands more consistent with the install command which doesn't use --bun.

Suggested change
__('bun --bun run --cwd :path build', [
'path' => $this->site->path,
]),
'bun-build',
$this->site->id
);
$this->progress(65);
$command = __('bun --bun run --cwd :path start', [
__('bun run --cwd :path build', [
'path' => $this->site->path,
]),
'bun-build',
$this->site->id
);
$this->progress(65);
$command = __('bun run --cwd :path start', [

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the --bun flag is necessary as stated by the bun team because it forces bun to use itself instead of Nodejs for serving the application, this is especially true for Nextjs.

'path' => $this->site->path,
]);
Comment on lines +104 to +106
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The worker command uses bun without the full path, but Bun is installed to $HOME/.bun/bin/bun which is not in supervisor's PATH. Unlike NodeJS which is installed system-wide via apt to /usr/bin/npm, Bun is installed per-user. Supervisor doesn't source shell configs where the Bun PATH is added. This is the root cause of workers getting stuck in "starting" state - supervisor cannot find the bun executable. The command should be changed to use the full path like /home/{user}/.bun/bin/bun --bun run --cwd :path start, where {user} is the site user. Alternatively, set the PATH environment variable in the supervisor config or install Bun system-wide.

Copilot uses AI. Check for mistakes.
$this->progress(80);
/** @var ?Worker $worker */
$worker = $this->site->workers()->where('name', 'app')->first();
if ($worker) {
app(ManageWorker::class)->restart($worker);
} else {
app(CreateWorker::class)->create(
$this->site->server,
[
'name' => 'app',
'command' => $command,
'user' => $this->site->user ?? $this->site->server->getSshUser(),
'auto_start' => true,
'auto_restart' => true,
'numprocs' => 1,
],
$this->site,
);
}
}

public function baseCommands(): array
{
return [
[
'name' => 'bun:install',
'command' => 'bun install',
],
[
'name' => 'bun:build',
'command' => 'bun --bun run build',
],
];
}

public function vhost(string $webserver): string|View
{
if ($webserver === 'nginx') {
return view('ssh.services.webserver.nginx.vhost', [
'header' => [
view('ssh.services.webserver.nginx.vhost-blocks.force-ssl', ['site' => $this->site]),
],
'main' => [
view('ssh.services.webserver.nginx.vhost-blocks.port', ['site' => $this->site]),
view('ssh.services.webserver.nginx.vhost-blocks.core', ['site' => $this->site]),
view('ssh.services.webserver.nginx.vhost-blocks.reverse-proxy', ['site' => $this->site]),
view('ssh.services.webserver.nginx.vhost-blocks.redirects', ['site' => $this->site]),
],
]);
}

if ($webserver === 'caddy') {
return view('ssh.services.webserver.caddy.vhost', [
'main' => [
view('ssh.services.webserver.caddy.vhost-blocks.force-ssl', ['site' => $this->site]),
view('ssh.services.webserver.caddy.vhost-blocks.port', ['site' => $this->site]),
view('ssh.services.webserver.caddy.vhost-blocks.core', ['site' => $this->site]),
view('ssh.services.webserver.caddy.vhost-blocks.reverse-proxy', ['site' => $this->site]),
view('ssh.services.webserver.caddy.vhost-blocks.redirects', ['site' => $this->site]),
],
]);
}

return '';
}
}
Loading