Merge pull request #1659 from coollabsio/next

v4.0.0-beta.197
This commit is contained in:
Andras Bacsai 2024-01-18 12:14:50 +01:00 committed by GitHub
commit e2f959ce4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 686 additions and 484 deletions

View File

@ -93,6 +93,10 @@ Contact us [here](https://coolify.io/docs/contact).
<a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
# Repo Activity
![Alt](https://repobeats.axiom.co/api/embed/eab1c8066f9c59d0ad37b76c23ebb5ccac4278ae.svg "Repobeats analytics image")
# Star History # Star History
[![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date) [![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date)

View File

@ -2,6 +2,7 @@
namespace App\Actions\Proxy; namespace App\Actions\Proxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@ -13,7 +14,6 @@ class StartProxy
public function handle(Server $server, bool $async = true): string|Activity public function handle(Server $server, bool $async = true): string|Activity
{ {
try { try {
$proxyType = $server->proxyType(); $proxyType = $server->proxyType();
$commands = collect([]); $commands = collect([]);
$proxy_path = get_proxy_path(); $proxy_path = get_proxy_path();

View File

@ -10,8 +10,10 @@ class DeleteService
use AsAction; use AsAction;
public function handle(Service $service) public function handle(Service $service)
{ {
StopService::run($service);
$server = data_get($service, 'server'); $server = data_get($service, 'server');
if ($server->isFunctional()) {
StopService::run($service);
}
$storagesToDelete = collect([]); $storagesToDelete = collect([]);
$service->environment_variables()->delete(); $service->environment_variables()->delete();

View File

@ -60,10 +60,10 @@ class Kernel extends ConsoleKernel
$servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4'); $servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers; $own = Team::find(0)->servers;
$servers = $servers->merge($own); $servers = $servers->merge($own);
$containerServers = $servers->where('settings.is_swarm_worker', false); $containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
} else { } else {
$servers = Server::all()->where('ip', '!=', '1.2.3.4'); $servers = Server::all()->where('ip', '!=', '1.2.3.4');
$containerServers = $servers->where('settings.is_swarm_worker', false); $containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
} }
foreach ($containerServers as $server) { foreach ($containerServers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
@ -72,7 +72,7 @@ class Kernel extends ConsoleKernel
} }
} }
foreach ($servers as $server) { foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyFiveMinutes()->onOneServer(); $schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
} }
} }
private function instance_auto_update($schedule) private function instance_auto_update($schedule)
@ -111,7 +111,8 @@ class Kernel extends ConsoleKernel
} }
} }
private function check_scheduled_tasks($schedule) { private function check_scheduled_tasks($schedule)
{
$scheduled_tasks = ScheduledTask::all(); $scheduled_tasks = ScheduledTask::all();
if ($scheduled_tasks->isEmpty()) { if ($scheduled_tasks->isEmpty()) {
ray('no scheduled tasks'); ray('no scheduled tasks');
@ -134,7 +135,6 @@ class Kernel extends ConsoleKernel
task: $scheduled_task task: $scheduled_task
))->cron($scheduled_task->frequency)->onOneServer(); ))->cron($scheduled_task->frequency)->onOneServer();
} }
} }
protected function commands(): void protected function commands(): void

View File

@ -0,0 +1,34 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProxyStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception("Team id is null");
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
}

View File

@ -56,7 +56,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private GithubApp|GitlabApp|string $source = 'other'; private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination; private StandaloneDocker|SwarmDocker $destination;
// Deploy to Server
private Server $server; private Server $server;
// Build Server
private Server $build_server;
private bool $use_build_server = false;
// Save original server between phases
private Server $original_server;
private Server $mainServer; private Server $mainServer;
private ?ApplicationPreview $preview = null; private ?ApplicationPreview $preview = null;
private ?string $git_type = null; private ?string $git_type = null;
@ -196,6 +202,25 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Check custom port // Check custom port
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository(); ['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
if (data_get($this->application, 'settings.is_build_server_enabled')) {
$teamId = data_get($this->application, 'environment.project.team.id');
$buildServers = Server::buildServers($teamId)->get();
if ($buildServers->count() === 0) {
$this->application_deployment_queue->addLogEntry("Build server feature activated, but no suitable build server found. Using the deployment server.");
$this->build_server = $this->server;
$this->original_server = $this->server;
} else {
$this->application_deployment_queue->addLogEntry("Build server feature activated and found a suitable build server. Using it to build your application - if needed.");
$this->build_server = $buildServers->random();
$this->original_server = $this->server;
$this->use_build_server = true;
}
} else {
// Set build server & original_server to the same as deployment server
$this->build_server = $this->server;
$this->original_server = $this->server;
}
ray($this->build_server);
try { try {
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') { if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart(); $this->just_restart();
@ -225,7 +250,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server)); dispatch(new ContainerStatusJob($this->server));
} }
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage' && !$this->application->destination->server->isSwarm()) { // Otherwise built image needs to be pushed before from the build server.
if (!$this->use_build_server) {
$this->push_to_docker_registry(); $this->push_to_docker_registry();
} }
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
@ -234,23 +260,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->fail($e); $this->fail($e);
throw $e; throw $e;
} finally { } finally {
if (isset($this->docker_compose_base64)) { if ($this->use_build_server) {
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); $this->server = $this->build_server;
$composeFileName = "$this->configuration_dir/docker-compose.yml"; } else {
if ($this->pull_request_id !== 0) { $this->write_deployment_configurations();
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
}
$this->execute_remote_command(
[
"mkdir -p $this->configuration_dir"
],
[
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
],
[
"echo '{$readme}' > $this->configuration_dir/README.md",
]
);
} }
$this->execute_remote_command( $this->execute_remote_command(
[ [
@ -269,42 +282,72 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
} }
} }
private function push_to_docker_registry() private function write_deployment_configurations()
{ {
try { if (isset($this->docker_compose_base64)) {
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server); if ($this->use_build_server) {
$this->server = $this->original_server;
}
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml";
if ($this->pull_request_id !== 0) {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
}
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo '\n----------------------------------------'", "mkdir -p $this->configuration_dir"
], ],
["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"],
[ [
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true "echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
], ],
[
"echo '{$readme}' > $this->configuration_dir/README.md",
]
); );
if ($this->application->docker_registry_image_tag) { if ($this->use_build_server) {
// Tag image with latest $this->server = $this->build_server;
}
}
}
private function push_to_docker_registry($forceFail = false)
{
ray((str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()));
if (
$this->application->docker_registry_image_name &&
$this->application->build_pack !== 'dockerimage' &&
!$this->application->destination->server->isSwarm() &&
!$this->restart_only &&
!(str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged())
) {
try {
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
$this->application_deployment_queue->addLogEntry("----------------------------------------");
$this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name}).");
$this->execute_remote_command( $this->execute_remote_command(
['echo -n "Tagging and pushing image with latest tag."'],
[ [
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
],
[
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
], ],
); );
if ($this->application->docker_registry_image_tag) {
// Tag image with latest
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with latest tag.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
[
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
);
}
$this->application_deployment_queue->addLogEntry("Image pushed to docker registry.'");
} catch (Exception $e) {
$this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information.'");
if ($forceFail) {
throw $e;
}
ray($e);
} }
$this->execute_remote_command([
"echo -n 'Image pushed to docker registry.'"
]);
} catch (Exception $e) {
if ($this->application->destination->server->isSwarm()) {
throw $e;
}
$this->execute_remote_command(
["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"],
);
ray($e);
} }
} }
private function generate_image_names() private function generate_image_names()
@ -340,20 +383,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function just_restart() private function just_restart()
{ {
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'");
[
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
$this->generate_image_names(); $this->generate_image_names();
$this->check_image_locally_or_remotely(); $this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->execute_remote_command([ $this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.");
"echo 'Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.'",
]);
$this->create_workdir(); $this->create_workdir();
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
@ -397,16 +434,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function deploy_simple_dockerfile() private function deploy_simple_dockerfile()
{ {
if ($this->use_build_server) {
$this->server = $this->build_server;
}
$dockerfile_base64 = base64_encode($this->application->dockerfile); $dockerfile_base64 = base64_encode($this->application->dockerfile);
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
[
"echo 'Starting deployment of {$this->application->name}.'"
],
);
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir$this->dockerfile_location") executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > {$this->workdir}{$this->dockerfile_location}")
], ],
); );
$this->generate_image_names(); $this->generate_image_names();
@ -422,11 +458,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->dockerImage = $this->application->docker_registry_image_name; $this->dockerImage = $this->application->docker_registry_image_name;
$this->dockerImageTag = $this->application->docker_registry_image_tag; $this->dockerImageTag = $this->application->docker_registry_image_tag;
ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"); ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'");
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.");
[
"echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"
],
);
$this->generate_image_names(); $this->generate_image_names();
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->generate_compose_file(); $this->generate_compose_file();
@ -496,24 +528,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true "docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true
]); ]);
} }
if (isset($this->docker_compose_base64)) { $this->write_deployment_configurations();
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml";
if ($this->pull_request_id !== 0) {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
}
$this->execute_remote_command(
[
"mkdir -p $this->configuration_dir"
],
[
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
],
[
"echo '{$readme}' > $this->configuration_dir/README.md",
]
);
}
// Start compose file // Start compose file
if ($this->docker_compose_custom_start_command) { if ($this->docker_compose_custom_start_command) {
$this->execute_remote_command( $this->execute_remote_command(
@ -528,14 +543,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_dockerfile_buildpack() private function deploy_dockerfile_buildpack()
{ {
if ($this->use_build_server) {
$this->server = $this->build_server;
}
if (data_get($this->application, 'dockerfile_location')) { if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location; $this->dockerfile_location = $this->application->dockerfile_location;
} }
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.");
[
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->clone_repository(); $this->clone_repository();
@ -555,11 +569,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_nixpacks_buildpack() private function deploy_nixpacks_buildpack()
{ {
$this->execute_remote_command( if ($this->use_build_server) {
[ $this->server = $this->build_server;
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" }
], $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.");
);
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
@ -568,17 +581,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->check_image_locally_or_remotely(); $this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir(); $this->create_workdir();
$this->execute_remote_command([ $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
"echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'",
]);
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
return; return;
} }
if ($this->application->isConfigurationChanged()) { if ($this->application->isConfigurationChanged()) {
$this->execute_remote_command([ $this->application_deployment_queue->addLogEntry("Configuration changed. Rebuilding image.");
"echo 'Configuration changed. Rebuilding image.'",
]);
} }
} }
$this->clone_repository(); $this->clone_repository();
@ -592,11 +601,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_static_buildpack() private function deploy_static_buildpack()
{ {
$this->execute_remote_command( if ($this->use_build_server) {
[ $this->server = $this->build_server;
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" }
], $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.");
);
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
@ -619,18 +627,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); $nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
} }
if ($nixpacks_php_fallback_path?->value === '/index.php' && $nixpacks_php_root_dir?->value === '/app/public' && $this->newVersionIsHealthy === false) { if ($nixpacks_php_fallback_path?->value === '/index.php' && $nixpacks_php_root_dir?->value === '/app/public' && $this->newVersionIsHealthy === false) {
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("There was a change in how Laravel is deployed. Please update your environment variables to match the new deployment method. More details here: https://coolify.io/docs/frameworks/laravel#requirements", 'stderr');
[
"echo 'There was a change in how Laravel is deployed. Please update your environment variables to match the new deployment method. More details here: https://coolify.io/docs/frameworks/laravel#requirements'", 'type' => 'err'
],
);
} }
} }
private function rolling_update() private function rolling_update()
{ {
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
if ($this->build_pack !== 'dockerimage') { if ($this->build_pack !== 'dockerimage') {
$this->push_to_docker_registry(); $this->push_to_docker_registry(forceFail: true);
} }
$this->application_deployment_queue->addLogEntry("Rolling update started."); $this->application_deployment_queue->addLogEntry("Rolling update started.");
$this->execute_remote_command( $this->execute_remote_command(
@ -640,22 +644,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
); );
$this->application_deployment_queue->addLogEntry("Rolling update completed."); $this->application_deployment_queue->addLogEntry("Rolling update completed.");
} else { } else {
if ($this->use_build_server) {
$this->push_to_docker_registry(forceFail: true);
$this->write_deployment_configurations();
$this->server = $this->original_server;
}
if (count($this->application->ports_mappings_array) > 0) { if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("----------------------------------------");
[ $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported.");
"echo '\n----------------------------------------'",
],
["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
);
$this->stop_running_container(force: true); $this->stop_running_container(force: true);
$this->start_by_compose_file(); $this->start_by_compose_file();
} else { } else {
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("----------------------------------------");
[ $this->application_deployment_queue->addLogEntry("Rolling update started.");
"echo '\n----------------------------------------'",
],
["echo -n 'Rolling update started.'"],
);
$this->start_by_compose_file(); $this->start_by_compose_file();
$this->health_check(); $this->health_check();
$this->stop_running_container(); $this->stop_running_container();
@ -676,17 +677,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// ray('New container name: ', $this->container_name); // ray('New container name: ', $this->container_name);
if ($this->container_name) { if ($this->container_name) {
$counter = 1; $counter = 1;
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Waiting for healthcheck to pass on the new container.");
[
"echo 'Waiting for healthcheck to pass on the new container.'"
]
);
if ($this->full_healthcheck_url) { if ($this->full_healthcheck_url) {
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
[
"echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
]
);
} }
while ($counter < $this->application->health_check_retries) { while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command( $this->execute_remote_command(
@ -698,19 +691,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}");
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
],
);
if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') { if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
$this->newVersionIsHealthy = true; $this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']); $this->application->update(['status' => 'running']);
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("New container is healthy.");
[
"echo 'New container is healthy.'"
],
);
break; break;
} }
if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') { if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
@ -725,11 +710,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_pull_request() private function deploy_pull_request()
{ {
if ($this->use_build_server) {
$this->server = $this->build_server;
}
$this->newVersionIsHealthy = true; $this->newVersionIsHealthy = true;
$this->generate_image_names(); $this->generate_image_names();
$this->execute_remote_command([ $this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.'",
]);
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->clone_repository(); $this->clone_repository();
$this->set_base_dir(); $this->set_base_dir();
@ -754,10 +740,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
} else { } else {
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Starting preview deployment.");
["echo -n 'Starting preview deployment.'"], if ($this->use_build_server) {
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], $this->execute_remote_command(
); ["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
);
}
} }
} }
private function create_workdir() private function create_workdir()
@ -774,16 +766,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Get user home directory // Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server); $this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server); $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
if ($this->use_build_server) {
if ($this->dockerConfigFileExists === 'OK') { if ($this->dockerConfigFileExists === 'NOK') {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; throw new RuntimeException('Docker config file (~/.docker/config.json) not found on the build server. Please run "docker login" to login to the docker registry on the server.');
}
$runCommand = "docker run -d --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else { } else {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
} }
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
$this->execute_remote_command( $this->execute_remote_command(
[
"echo -n 'Preparing container with helper image: $helperImage.'",
],
[ [
$runCommand, $runCommand,
"hidden" => true, "hidden" => true,
@ -801,19 +797,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$destination = StandaloneDocker::find($destination_id); $destination = StandaloneDocker::find($destination_id);
$server = $destination->server; $server = $destination->server;
if ($server->team_id !== $this->mainServer->team_id) { if ($server->team_id !== $this->mainServer->team_id) {
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!");
[
"echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'",
],
);
continue; continue;
} }
$this->server = $server; $this->server = $server;
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Deploying to {$this->server->name}.");
[
"echo -n 'Deploying to {$this->server->name}.'",
],
);
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->generate_image_names(); $this->generate_image_names();
$this->rolling_update(); $this->rolling_update();
@ -821,11 +809,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function set_base_dir() private function set_base_dir()
{ {
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Setting base directory to {$this->workdir}.");
[
"echo -n 'Setting base directory to {$this->workdir}.'"
],
);
} }
private function check_git_if_build_needed() private function check_git_if_build_needed()
{ {
@ -898,26 +882,28 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_nixpacks_confs() private function generate_nixpacks_confs()
{ {
$nixpacks_command = $this->nixpacks_build_cmd(); $nixpacks_command = $this->nixpacks_build_cmd();
$this->application_deployment_queue->addLogEntry("Generating nixpacks configuration with: $nixpacks_command");
$this->execute_remote_command( $this->execute_remote_command(
[
"echo -n 'Generating nixpacks configuration with: $nixpacks_command'",
],
[executeInDocker($this->deployment_uuid, $nixpacks_command), "save" => "nixpacks_plan", "hidden" => true], [executeInDocker($this->deployment_uuid, $nixpacks_command), "save" => "nixpacks_plan", "hidden" => true],
[executeInDocker($this->deployment_uuid, "nixpacks detect {$this->workdir}"), "save" => "nixpacks_type", "hidden" => true], [executeInDocker($this->deployment_uuid, "nixpacks detect {$this->workdir}"), "save" => "nixpacks_type", "hidden" => true],
); );
if ($this->saved_outputs->get('nixpacks_type')) { if ($this->saved_outputs->get('nixpacks_type')) {
$this->nixpacks_type = $this->saved_outputs->get('nixpacks_type'); $this->nixpacks_type = $this->saved_outputs->get('nixpacks_type');
if (str($this->nixpacks_type)->isEmpty()) {
throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers');
}
} }
if ($this->saved_outputs->get('nixpacks_plan')) { if ($this->saved_outputs->get('nixpacks_plan')) {
$this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan'); $this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan');
if ($this->nixpacks_plan) { if ($this->nixpacks_plan) {
$this->application_deployment_queue->addLogEntry("Found application type: {$this->nixpacks_type}.");
$this->application_deployment_queue->addLogEntry("If you need further customization, please check the documentation of Nixpacks: https://nixpacks.com/docs/providers/{$this->nixpacks_type}");
$parsed = Toml::Parse($this->nixpacks_plan); $parsed = Toml::Parse($this->nixpacks_plan);
// Do any modifications here // Do any modifications here
$this->generate_env_variables(); $this->generate_env_variables();
$merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', []))); $merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', [])));
data_set($parsed, 'variables', $merged_envs->toArray()); data_set($parsed, 'variables', $merged_envs->toArray());
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT); $this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
ray($this->nixpacks_plan);
} }
} }
} }
@ -1234,9 +1220,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function pull_latest_image($image) private function pull_latest_image($image)
{ {
$this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry.");
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Pulling latest image ($image) from the registry.'"],
[ [
executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true
] ]
@ -1244,25 +1229,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function build_image() private function build_image()
{ {
$this->application_deployment_queue->addLogEntry("----------------------------------------");
if ($this->application->build_pack === 'static') { if ($this->application->build_pack === 'static') {
$this->execute_remote_command([ $this->application_deployment_queue->addLogEntry("Static deployment. Copying static assets to the image.");
"echo -n 'Static deployment. Copying static assets to the image.'",
]);
} else { } else {
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Building docker image started.");
[ $this->application_deployment_queue->addLogEntry("To check the current progress, click on Show Debug Logs.");
"echo -n 'Building docker image started.'",
],
["echo -n 'To check the current progress, click on Show Debug Logs.'"]
);
} }
if ($this->application->settings->is_static || $this->application->build_pack === 'static') { if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
if ($this->application->static_image) { if ($this->application->static_image) {
$this->pull_latest_image($this->application->static_image); $this->pull_latest_image($this->application->static_image);
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Continuing with the building process.");
["echo -n 'Continue with the building process.'"],
);
} }
if ($this->application->build_pack === 'static') { if ($this->application->build_pack === 'static') {
$dockerfile = base64_encode("FROM {$this->application->static_image} $dockerfile = base64_encode("FROM {$this->application->static_image}
@ -1405,9 +1383,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} }
} }
} }
$this->execute_remote_command([ $this->application_deployment_queue->addLogEntry("Building docker image completed.");
"echo -n 'Building docker image completed.'",
]);
} }
private function stop_running_container(bool $force = false) private function stop_running_container(bool $force = false)
@ -1463,13 +1439,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
); );
} else { } else {
if ($this->docker_compose_location) { if ($this->use_build_server) {
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true], ["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true],
); );
} else { } else {
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
); );
} }
} }
@ -1535,13 +1511,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void public function failed(Throwable $exception): void
{ {
$this->execute_remote_command( $this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr');
["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'], $this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
["echo '{$exception->getMessage()}'", 'type' => 'err'],
);
if ($this->application->build_pack !== 'dockercompose') { if ($this->application->build_pack !== 'dockercompose') {
$this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr');
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
); );
} }

View File

@ -44,8 +44,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle() public function handle()
{ {
if (!$this->server->isServerReady($this->tries)) { if (!$this->server->isFunctional()) {
return 'Server is not reachable.'; return 'Server is not ready.';
}; };
try { try {
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {

View File

@ -31,11 +31,16 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
{ {
try { try {
$server = $this->resource->destination->server; $server = $this->resource->destination->server;
$this->resource->delete();
if (!$server->isFunctional()) { if (!$server->isFunctional()) {
$this->resource->forceDelete(); if ($this->resource->type() === 'service') {
ray('dispatching delete service');
DeleteService::dispatch($this->resource);
} else {
$this->resource->forceDelete();
}
return 'Server is not functional'; return 'Server is not functional';
} }
$this->resource->delete();
switch ($this->resource->type()) { switch ($this->resource->type()) {
case 'application': case 'application':
StopApplication::run($this->resource); StopApplication::run($this->resource);

View File

@ -38,6 +38,9 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle() public function handle()
{ {
try { try {
if (!$this->server->isServerReady($this->tries)) {
throw new \RuntimeException('Server is not ready.');
};
if ($this->server->isFunctional()) { if ($this->server->isFunctional()) {
$this->cleanup(notify: false); $this->cleanup(notify: false);
} }

View File

@ -3,6 +3,7 @@
namespace App\Livewire; namespace App\Livewire;
use App\Enums\ProcessStatus; use App\Enums\ProcessStatus;
use App\Models\User;
use Livewire\Component; use Livewire\Component;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
@ -10,14 +11,16 @@ class ActivityMonitor extends Component
{ {
public ?string $header = null; public ?string $header = null;
public $activityId; public $activityId;
public $eventToDispatch = 'activityFinished';
public $isPollingActive = false; public $isPollingActive = false;
protected $activity; protected $activity;
protected $listeners = ['newMonitorActivity']; protected $listeners = ['newMonitorActivity'];
public function newMonitorActivity($activityId) public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
{ {
$this->activityId = $activityId; $this->activityId = $activityId;
$this->eventToDispatch = $eventToDispatch;
$this->hydrateActivity(); $this->hydrateActivity();
@ -35,13 +38,28 @@ class ActivityMonitor extends Component
// $this->setStatus(ProcessStatus::IN_PROGRESS); // $this->setStatus(ProcessStatus::IN_PROGRESS);
$exit_code = data_get($this->activity, 'properties.exitCode'); $exit_code = data_get($this->activity, 'properties.exitCode');
if ($exit_code !== null) { if ($exit_code !== null) {
if ($exit_code === 0) { // if ($exit_code === 0) {
// $this->setStatus(ProcessStatus::FINISHED); // // $this->setStatus(ProcessStatus::FINISHED);
} else { // } else {
// $this->setStatus(ProcessStatus::ERROR); // // $this->setStatus(ProcessStatus::ERROR);
} // }
$this->isPollingActive = false; $this->isPollingActive = false;
$this->dispatch('activityFinished'); if ($exit_code === 0) {
if ($this->eventToDispatch !== null) {
if (str($this->eventToDispatch)->startsWith('App\\Events\\')) {
$causer_id = data_get($this->activity, 'causer_id');
$user = User::find($causer_id);
if ($user) {
foreach($user->teams as $team) {
$teamId = $team->id;
$this->eventToDispatch::dispatch($teamId);
}
}
return;
}
$this->dispatch($this->eventToDispatch);
}
}
} }
} }

View File

@ -16,6 +16,7 @@ class Advanced extends Component
'application.settings.is_force_https_enabled' => 'boolean|required', 'application.settings.is_force_https_enabled' => 'boolean|required',
'application.settings.is_log_drain_enabled' => 'boolean|required', 'application.settings.is_log_drain_enabled' => 'boolean|required',
'application.settings.is_gpu_enabled' => 'boolean|required', 'application.settings.is_gpu_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.gpu_driver' => 'string|required', 'application.settings.gpu_driver' => 'string|required',
'application.settings.gpu_count' => 'string|required', 'application.settings.gpu_count' => 'string|required',
'application.settings.gpu_device_ids' => 'string|required', 'application.settings.gpu_device_ids' => 'string|required',

View File

@ -67,6 +67,7 @@ class General extends Component
'application.docker_compose_custom_start_command' => 'nullable', 'application.docker_compose_custom_start_command' => 'nullable',
'application.docker_compose_custom_build_command' => 'nullable', 'application.docker_compose_custom_build_command' => 'nullable',
'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',
@ -100,6 +101,7 @@ class General extends Component
'application.docker_compose_custom_start_command' => 'Docker compose custom start command', 'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build command', 'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
'application.settings.is_raw_compose_deployment_enabled' => 'Is raw compose deployment enabled', 'application.settings.is_raw_compose_deployment_enabled' => 'Is raw compose deployment enabled',
'application.settings.is_build_server_enabled' => 'Is build server enabled',
]; ];
public function mount() public function mount()
{ {
@ -227,7 +229,6 @@ class General extends Component
if ($this->ports_exposes !== $this->application->ports_exposes) { if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false); $this->resetDefaultLabels(false);
} }
if (data_get($this->application, 'build_pack') === 'dockerimage') { if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([ $this->validate([
'application.docker_registry_image_name' => 'required', 'application.docker_registry_image_name' => 'required',

View File

@ -74,7 +74,11 @@ class Heading extends Component
return; return;
} }
if ($this->application->destination->server->isSwarm() && is_null($this->application->docker_registry_image_name)) { if ($this->application->destination->server->isSwarm() && is_null($this->application->docker_registry_image_name)) {
$this->dispatch('error', 'Please set a Docker image name first.'); $this->dispatch('error', 'To deploy to a Swarm cluster you must set a Docker image name first.');
return;
}
if (data_get($this->application, 'settings.is_build_server_enabled') && is_null($this->application->docker_registry_image_name)) {
$this->dispatch('error', 'To use a build server you must set a Docker image name first.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/build-server">documentation</a>');
return; return;
} }
$this->setDeploymentUuid(); $this->setDeploymentUuid();

View File

@ -49,8 +49,9 @@ class Previews extends Component
queue_application_deployment( queue_application_deployment(
application_id: $this->application->id, application_id: $this->application->id,
deployment_uuid: $this->deployment_uuid, deployment_uuid: $this->deployment_uuid,
force_rebuild: true, force_rebuild: false,
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
git_type: $found->git_type ?? null,
); );
return redirect()->route('project.application.deployment.show', [ return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],

View File

@ -30,18 +30,22 @@ class PublicGitRepository extends Component
public GithubApp|GitlabApp|string $git_source = 'other'; public GithubApp|GitlabApp|string $git_source = 'other';
public string $git_host; public string $git_host;
public string $git_repository; public string $git_repository;
public $build_pack;
public bool $show_is_static = true;
protected $rules = [ protected $rules = [
'repository_url' => 'required|url', 'repository_url' => 'required|url',
'port' => 'required|numeric', 'port' => 'required|numeric',
'is_static' => 'required|boolean', 'is_static' => 'required|boolean',
'publish_directory' => 'nullable|string', 'publish_directory' => 'nullable|string',
'build_pack' => 'required|string',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'repository_url' => 'repository', 'repository_url' => 'repository',
'port' => 'port', 'port' => 'port',
'is_static' => 'static', 'is_static' => 'static',
'publish_directory' => 'publish directory', 'publish_directory' => 'publish directory',
'build_pack' => 'build pack',
]; ];
public function mount() public function mount()
@ -53,7 +57,18 @@ class PublicGitRepository extends Component
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query(); $this->query = request()->query();
} }
public function updatedBuildPack()
{
if ($this->build_pack === 'nixpacks') {
$this->show_is_static = true;
} else if ($this->build_pack === 'static') {
$this->show_is_static = false;
$this->is_static = false;
} else {
$this->show_is_static = false;
$this->is_static = false;
}
}
public function instantSave() public function instantSave()
{ {
if ($this->is_static) { if ($this->is_static) {
@ -157,6 +172,7 @@ class PublicGitRepository extends Component
'environment_id' => $environment->id, 'environment_id' => $environment->id,
'destination_id' => $destination->id, 'destination_id' => $destination->id,
'destination_type' => $destination_class, 'destination_type' => $destination_class,
'build_pack' => $this->build_pack,
]; ];
} else { } else {
$application_init = [ $application_init = [
@ -170,7 +186,8 @@ class PublicGitRepository extends Component
'destination_id' => $destination->id, 'destination_id' => $destination->id,
'destination_type' => $destination_class, 'destination_type' => $destination_class,
'source_id' => $this->git_source->id, 'source_id' => $this->git_source->id,
'source_type' => $this->git_source->getMorphClass() 'source_type' => $this->git_source->getMorphClass(),
'build_pack' => $this->build_pack,
]; ];
} }

View File

@ -104,7 +104,7 @@ class Select extends Component
if ($this->includeSwarm) { if ($this->includeSwarm) {
$this->servers = $this->allServers; $this->servers = $this->allServers;
} else { } else {
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false); $this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false)->where('settings.is_build_server', false);
} }
} }
public function setType(string $type) public function setType(string $type)
@ -120,13 +120,13 @@ class Select extends Component
case 'mongodb': case 'mongodb':
$this->isDatabase = true; $this->isDatabase = true;
$this->includeSwarm = false; $this->includeSwarm = false;
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false); $this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false)->where('settings.is_build_server', false);
break; break;
} }
if (str($type)->startsWith('one-click-service') || str($type)->startsWith('docker-compose-empty')) { if (str($type)->startsWith('one-click-service') || str($type)->startsWith('docker-compose-empty')) {
$this->isDatabase = true; $this->isDatabase = true;
$this->includeSwarm = false; $this->includeSwarm = false;
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false); $this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false)->where('settings.is_build_server', false);
} }
if ($type === "existing-postgresql") { if ($type === "existing-postgresql") {
$this->current_step = $type; $this->current_step = $type;

View File

@ -8,5 +8,5 @@ class Destination extends Component
{ {
public $resource; public $resource;
public $servers = []; public $servers = [];
public $additionalServers = []; public $additional_servers = [];
} }

View File

@ -26,6 +26,7 @@ class Form extends Component
'server.settings.is_reachable' => 'required', 'server.settings.is_reachable' => 'required',
'server.settings.is_swarm_manager' => 'required|boolean', 'server.settings.is_swarm_manager' => 'required|boolean',
'server.settings.is_swarm_worker' => 'required|boolean', 'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_build_server' => 'required|boolean',
'wildcard_domain' => 'nullable|url', 'wildcard_domain' => 'nullable|url',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@ -38,6 +39,7 @@ class Form extends Component
'server.settings.is_reachable' => 'Is reachable', 'server.settings.is_reachable' => 'Is reachable',
'server.settings.is_swarm_manager' => 'Swarm Manager', 'server.settings.is_swarm_manager' => 'Swarm Manager',
'server.settings.is_swarm_worker' => 'Swarm Worker', 'server.settings.is_swarm_worker' => 'Swarm Worker',
'server.settings.is_build_server' => 'Build Server',
]; ];
public function mount() public function mount()
@ -76,7 +78,7 @@ class Form extends Component
$this->server->settings->is_usable = true; $this->server->settings->is_usable = true;
$this->server->settings->save(); $this->server->settings->save();
} else { } else {
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.'); $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return; return;
} }
} }
@ -85,12 +87,12 @@ class Form extends Component
try { try {
$uptime = $this->server->validateConnection(); $uptime = $this->server->validateConnection();
if (!$uptime) { if (!$uptime) {
$install && $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.'); $install && $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return; return;
} }
$supported_os_type = $this->server->validateOS(); $supported_os_type = $this->server->validateOS();
if (!$supported_os_type) { if (!$supported_os_type) {
$install && $this->dispatch('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.'); $install && $this->dispatch('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.');
return; return;
} }
$dockerInstalled = $this->server->validateDockerEngine(); $dockerInstalled = $this->server->validateDockerEngine();

View File

@ -25,6 +25,8 @@ class ByIp extends Component
public bool $is_swarm_worker = false; public bool $is_swarm_worker = false;
public $selected_swarm_cluster = null; public $selected_swarm_cluster = null;
public bool $is_build_server = false;
public $swarm_managers = []; public $swarm_managers = [];
protected $rules = [ protected $rules = [
'name' => 'required|string', 'name' => 'required|string',
@ -34,6 +36,7 @@ class ByIp extends Component
'port' => 'required|integer', 'port' => 'required|integer',
'is_swarm_manager' => 'required|boolean', 'is_swarm_manager' => 'required|boolean',
'is_swarm_worker' => 'required|boolean', 'is_swarm_worker' => 'required|boolean',
'is_build_server' => 'required|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'name' => 'Name', 'name' => 'Name',
@ -43,6 +46,7 @@ class ByIp extends Component
'port' => 'Port', 'port' => 'Port',
'is_swarm_manager' => 'Swarm Manager', 'is_swarm_manager' => 'Swarm Manager',
'is_swarm_worker' => 'Swarm Worker', 'is_swarm_worker' => 'Swarm Worker',
'is_build_server' => 'Build Server',
]; ];
public function mount() public function mount()
@ -89,8 +93,14 @@ class ByIp extends Component
$payload['swarm_cluster'] = $this->selected_swarm_cluster; $payload['swarm_cluster'] = $this->selected_swarm_cluster;
} }
$server = Server::create($payload); $server = Server::create($payload);
$server->settings->is_swarm_manager = $this->is_swarm_manager; if ($this->is_build_server) {
$server->settings->is_swarm_worker = $this->is_swarm_worker; $this->is_swarm_manager = false;
$this->is_swarm_worker = false;
} else {
$server->settings->is_swarm_manager = $this->is_swarm_manager;
$server->settings->is_swarm_worker = $this->is_swarm_worker;
}
$server->settings->is_build_server = $this->is_build_server;
$server->settings->save(); $server->settings->save();
$server->addInitialNetwork(); $server->addInitialNetwork();
return redirect()->route('server.show', $server->uuid); return redirect()->route('server.show', $server->uuid);

View File

@ -33,7 +33,6 @@ class Proxy extends Component
{ {
$this->server->proxy = null; $this->server->proxy = null;
$this->server->save(); $this->server->save();
$this->dispatch('proxyStatusUpdated');
} }
public function select_proxy($proxy_type) public function select_proxy($proxy_type)

View File

@ -4,6 +4,7 @@ namespace App\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@ -14,7 +15,17 @@ class Deploy extends Component
public ?string $currentRoute = null; public ?string $currentRoute = null;
public ?string $serverIp = null; public ?string $serverIp = null;
protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated', "checkProxy", "startProxy"]; public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ProxyStatusChanged" => 'proxyStarted',
'proxyStatusUpdated',
'traefikDashboardAvailable',
'serverRefresh' => 'proxyStatusUpdated',
"checkProxy", "startProxy"
];
}
public function mount() public function mount()
{ {
@ -29,12 +40,22 @@ class Deploy extends Component
{ {
$this->traefikDashboardAvailable = $data; $this->traefikDashboardAvailable = $data;
} }
public function proxyStarted()
{
CheckProxy::run($this->server, true);
$this->dispatch('success', 'Proxy started.');
}
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {
$this->server->refresh(); $this->server->refresh();
} }
public function ip() public function restart() {
{ try {
$this->stop();
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function checkProxy() public function checkProxy()
{ {
@ -50,7 +71,7 @@ class Deploy extends Component
{ {
try { try {
$activity = StartProxy::run($this->server); $activity = StartProxy::run($this->server);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('newMonitorActivity', $activity->id, ProxyStatusChanged::class);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@ -77,6 +98,5 @@ class Deploy extends Component
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
} }

View File

@ -71,7 +71,7 @@ class Server extends BaseModel
static public function isUsable() static public function isUsable()
{ {
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false); return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false);
} }
static public function destinationsByServer(string $server_id) static public function destinationsByServer(string $server_id)
@ -112,7 +112,7 @@ class Server extends BaseModel
]); ]);
} else { } else {
StandaloneDocker::create([ StandaloneDocker::create([
'name' => 'coolify-overlay', 'name' => 'coolify',
'network' => 'coolify', 'network' => 'coolify',
'server_id' => $this->id, 'server_id' => $this->id,
]); ]);
@ -141,6 +141,10 @@ class Server extends BaseModel
{ {
return $this->ip === 'host.docker.internal' || $this->id === 0; return $this->ip === 'host.docker.internal' || $this->id === 0;
} }
static public function buildServers($teamId)
{
return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true);
}
public function skipServer() public function skipServer()
{ {
if ($this->ip === '1.2.3.4') { if ($this->ip === '1.2.3.4') {
@ -194,7 +198,7 @@ class Server extends BaseModel
foreach ($this->databases() as $database) { foreach ($this->databases() as $database) {
$database->update(['status' => 'exited']); $database->update(['status' => 'exited']);
} }
foreach ($this->services() as $service) { foreach ($this->services()->get() as $service) {
$apps = $service->applications()->get(); $apps = $service->applications()->get();
$dbs = $service->databases()->get(); $dbs = $service->databases()->get();
foreach ($apps as $app) { foreach ($apps as $app) {
@ -328,7 +332,7 @@ class Server extends BaseModel
} }
public function isProxyShouldRun() public function isProxyShouldRun()
{ {
if ($this->proxyType() === ProxyTypes::NONE->value) { if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
return false; return false;
} }
// foreach ($this->applications() as $application) { // foreach ($this->applications() as $application) {
@ -436,7 +440,7 @@ class Server extends BaseModel
} }
$this->settings->is_usable = true; $this->settings->is_usable = true;
$this->settings->save(); $this->settings->save();
$this->validateCoolifyNetwork(isSwarm: false); $this->validateCoolifyNetwork(isSwarm: false, isBuildServer: $this->settings->is_build_server);
return true; return true;
} }
public function validateDockerSwarm() public function validateDockerSwarm()
@ -466,8 +470,11 @@ class Server extends BaseModel
$this->settings->save(); $this->settings->save();
return true; return true;
} }
public function validateCoolifyNetwork($isSwarm = false) public function validateCoolifyNetwork($isSwarm = false, $isBuildServer = false)
{ {
if ($isBuildServer) {
return;
}
if ($isSwarm) { if ($isSwarm) {
return instant_remote_process(["docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false); return instant_remote_process(["docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false);
} else { } else {

View File

@ -52,13 +52,13 @@ class Unreachable extends Notification implements ShouldQueue
public function toDiscord(): string public function toDiscord(): string
{ {
$message = "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."; $message = "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.";
return $message; return $message;
} }
public function toTelegram(): array public function toTelegram(): array
{ {
return [ return [
"message" => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations." "message" => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations."
]; ];
} }
} }

View File

@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.196', 'release' => '4.0.0-beta.197',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.196'; return '4.0.0-beta.197';

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('is_build_server_enabled')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_build_server_enabled');
});
}
};

View File

@ -14,7 +14,7 @@ class ServerSettingSeeder extends Seeder
{ {
$server_2 = Server::find(0)->load(['settings']); $server_2 = Server::find(0)->load(['settings']);
$server_2->settings->wildcard_domain = 'http://127.0.0.1.sslip.io'; $server_2->settings->wildcard_domain = 'http://127.0.0.1.sslip.io';
$server_2->settings->is_build_server = true; $server_2->settings->is_build_server = false;
$server_2->settings->is_usable = true; $server_2->settings->is_usable = true;
$server_2->settings->is_reachable = true; $server_2->settings->is_reachable = true;
$server_2->settings->save(); $server_2->settings->save();

View File

@ -1,119 +0,0 @@
<div class="navbar-main">
<a class="{{ request()->routeIs('project.application.configuration') ? 'text-white' : '' }}"
href="{{ route('project.application.configuration', $parameters) }}">
<button>Configuration</button>
</a>
@if (!$application->destination->server->isSwarm())
<a class="{{ request()->routeIs('project.application.command') ? 'text-white' : '' }}"
href="{{ route('project.application.command', $parameters) }}">
<button>Execute Command</button>
</a>
@endif
<a class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button>
</a>
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'text-white' : '' }}"
href="{{ route('project.application.deployment.index', $parameters) }}">
<button>Deployments</button>
</a>
<x-applications.links :application="$application" />
<div class="flex-1"></div>
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div>
@else
@if (!$application->destination->server->isSwarm())
<x-applications.advanced :application="$application" />
@endif
@if ($application->status !== 'exited')
@if (!$application->destination->server->isSwarm())
<button title="With rolling update if possible" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
@endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->destination->server->isSwarm())
<button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Update Service
</button>
@else
<button title="Restart without rebuilding" wire:click='restart'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</button>
@endif
{{-- @if (isDev())
<button title="Restart without rebuilding" wire:click='restartNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart (new)
</button>
@endif --}}
@endif
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@else
<button wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" 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 d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
{{-- @if (isDev())
<button wire:click='deployNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" 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 d="M7 4v16l13 -8z" />
</svg>
Deploy (new)
</button>
@endif --}}
@endif
@endif
</div>

View File

@ -1,5 +1,5 @@
<div class="px-2 form-control min-w-fit hover:bg-coolgray-100"> <div class="flex flex-row items-center gap-4 px-2 form-control min-w-fit hover:bg-coolgray-100">
<label class="flex gap-4 px-0 cursor-pointer label"> <label class="flex gap-4 px-0 label">
<span class="flex gap-2 label-text min-w-fit"> <span class="flex gap-2 label-text min-w-fit">
@if ($label) @if ($label)
{!! $label !!} {!! $label !!}
@ -10,8 +10,9 @@
<x-helper :helper="$helper" /> <x-helper :helper="$helper" />
@endif @endif
</span> </span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
wire:model={{ $id }} @else wire:model={{ $value ?? $id }} @endif />
</label> </label>
<span class="flex-grow"></span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
wire:model={{ $id }} @else wire:model={{ $value ?? $id }} @endif />
</div> </div>

View File

@ -2,38 +2,42 @@
<livewire:server.proxy.modal :server="$server" /> <livewire:server.proxy.modal :server="$server" />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>Server</h1> <h1>Server</h1>
@if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker()) @if (
$server->proxyType() !== 'NONE' &&
$server->isFunctional() &&
!$server->isSwarmWorker() &&
!$server->settings->is_build_server)
<livewire:server.proxy.status :server="$server" /> <livewire:server.proxy.status :server="$server" />
@endif @endif
</div> </div>
<div class="subtitle ">{{ data_get($server, 'name') }}</div> <div class="subtitle ">{{ data_get($server, 'name') }}</div>
<nav class="navbar-main"> <nav class="navbar-main">
<a class="{{ request()->routeIs('server.show') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('server.show') ? 'text-white' : '' }}"
href="{{ route('server.show', [ href="{{ route('server.show', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}"> ]) }}">
<button>General</button> <button>General</button>
</a> </a>
<a class="{{ request()->routeIs('server.private-key') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('server.private-key') ? 'text-white' : '' }}"
href="{{ route('server.private-key', [ href="{{ route('server.private-key', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}"> ]) }}">
<button>Private Key</button> <button>Private Key</button>
</a> </a>
@if (!$server->isSwarmWorker()) @if (!$server->isSwarmWorker() && !$server->settings->is_build_server)
<a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('server.proxy') ? 'text-white' : '' }}"
href="{{ route('server.proxy', [ href="{{ route('server.proxy', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}"> ]) }}">
<button>Proxy</button> <button>Proxy</button>
</a> </a>
<a class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('server.destinations') ? 'text-white' : '' }}"
href="{{ route('server.destinations', [ href="{{ route('server.destinations', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}"> ]) }}">
<button>Destinations</button> <button>Destinations</button>
</a> </a>
<a class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}"
href="{{ route('server.log-drains', [ href="{{ route('server.log-drains', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}"> ]) }}">
@ -42,7 +46,7 @@
@endif @endif
<div class="flex-1"></div> <div class="flex-1"></div>
@if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker()) @if ($server->proxyType() !== 'NONE' && $server->isFunctional() && !$server->isSwarmWorker() && !$server->settings->is_build_server)
<livewire:server.proxy.deploy :server="$server" /> <livewire:server.proxy.deploy :server="$server" />
@endif @endif
</nav> </nav>

View File

@ -3,7 +3,7 @@ Coolify cannot connect to your server ({{ $name }}). Please check your server an
All automations & integrations are turned off! All automations & integrations are turned off!
IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn onall automations & integrations. IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.
If you have any questions, please contact us. If you have any questions, please contact us.
</x-emails.layout> </x-emails.layout>

View File

@ -1,3 +1,3 @@
<x-emails.layout> <x-emails.layout>
Your server ({{ $name }}) was offline for a while, but it is back online now. All automations & integrationsare turned on again. Your server ({{ $name }}) was offline for a while, but it is back online now. All automations & integrations are turned on again.
</x-emails.layout> </x-emails.layout>

View File

@ -57,7 +57,7 @@
'root' or skip the boarding process and add a new private key manually to Coolify and to the 'root' or skip the boarding process and add a new private key manually to Coolify and to the
server. server.
<br /> <br />
Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help. Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.
<x-forms.input readonly id="serverPublicKey"></x-forms.input> <x-forms.input readonly id="serverPublicKey"></x-forms.input>
<x-forms.button class="box" wire:target="setServerType('localhost')" <x-forms.button class="box" wire:target="setServerType('localhost')"
wire:click="setServerType('localhost')">Check again wire:click="setServerType('localhost')">Check again
@ -242,7 +242,7 @@
<p>This will install the latest Docker Engine on your server, configure a few things to be able <p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.<br><br>Minimum Docker Engine version is: 22<br><br>To manually install Docker to run optimal.<br><br>Minimum Docker Engine version is: 22<br><br>To manually install Docker
Engine, check <a target="_blank" class="underline text-warning" Engine, check <a target="_blank" class="underline text-warning"
href="https://coolify.io/docs/servers#install-docker-engine-manually">this href="https://docs.docker.com/engine/install/#server">this
documentation</a>.</p> documentation</a>.</p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>

View File

@ -13,7 +13,7 @@
@endif @endif
</div> </div>
<div class="w-48"> <div class="w-48">
<x-forms.checkbox instantSave id="team.discord_enabled" label="Notification Enabled" /> <x-forms.checkbox instantSave id="team.discord_enabled" label="Enabled" />
</div> </div>
<x-forms.input type="password" <x-forms.input type="password"
helper="Generate a webhook in Discord.<br>Example: https://discord.com/api/webhooks/...." required helper="Generate a webhook in Discord.<br>Example: https://discord.com/api/webhooks/...." required

View File

@ -13,7 +13,7 @@
@endif @endif
</div> </div>
<div class="w-48"> <div class="w-48">
<x-forms.checkbox instantSave id="team.telegram_enabled" label="Notification Enabled" /> <x-forms.checkbox instantSave id="team.telegram_enabled" label="Enabled" />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input type="password" <x-forms.input type="password"

View File

@ -4,32 +4,38 @@
<h2>Advanced</h2> <h2>Advanced</h2>
</div> </div>
<div>Advanced configuration for your application.</div> <div>Advanced configuration for your application.</div>
<div class="flex flex-col w-full pt-4"> <div class="flex flex-col pt-4 ">
@if (!$application->settings->is_raw_compose_deployment_enabled) <h4>General</h4>
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
instantSave id="application.settings.is_log_drain_enabled" label="Drain Logs" />
@endif
<x-forms.checkbox
helper="Your application will be available only on https if your domain starts with https://..."
instantSave id="application.settings.is_force_https_enabled" label="Force Https" />
@if ($application->git_based()) @if ($application->git_based())
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave <x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
id="application.settings.is_auto_deploy_enabled" label="Auto Deploy" /> id="application.settings.is_auto_deploy_enabled" label="Auto Deploy" />
<x-forms.checkbox <x-forms.checkbox
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments." helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
instantSave id="application.settings.is_preview_deployments_enabled" label="Preview Deployments" /> instantSave id="application.settings.is_preview_deployments_enabled" label="Preview Deployments" />
@endif
<x-forms.checkbox
helper="Your application will be available only on https if your domain starts with https://..."
instantSave id="application.settings.is_force_https_enabled" label="Force Https" />
<h4>Logs</h4>
@if (!$application->settings->is_raw_compose_deployment_enabled)
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
instantSave id="application.settings.is_log_drain_enabled" label="Drain Logs" />
@endif
<h4>Git</h4>
@if ($application->git_based())
<x-forms.checkbox instantSave id="application.settings.is_git_submodules_enabled" label="Git Submodules" <x-forms.checkbox instantSave id="application.settings.is_git_submodules_enabled" label="Git Submodules"
helper="Allow Git Submodules during build process." /> helper="Allow Git Submodules during build process." />
<x-forms.checkbox instantSave id="application.settings.is_git_lfs_enabled" label="Git LFS" <x-forms.checkbox instantSave id="application.settings.is_git_lfs_enabled" label="Git LFS"
helper="Allow Git LFS during build process." /> helper="Allow Git LFS during build process." />
@endif @endif
<h4>GPU</h4>
<form wire:submit="submit"> <form wire:submit="submit">
@if ($application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockercompose')
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.checkbox <x-forms.checkbox
helper="Enable GPU usage for this application. More info <a href='https://docs.docker.com/compose/gpu-support/' class='text-white underline' target='_blank'>here</a>." helper="Enable GPU usage for this application. More info <a href='https://docs.docker.com/compose/gpu-support/' class='text-white underline' target='_blank'>here</a>."
instantSave id="application.settings.is_gpu_enabled" label="GPU Enabled Application" /> instantSave id="application.settings.is_gpu_enabled" label="Attach GPU" />
@if ($application->settings->is_gpu_enabled) @if ($application->settings->is_gpu_enabled)
<x-forms.button type="submiot">Save</x-forms.button> <x-forms.button type="submiot">Save</x-forms.button>
@endif @endif

View File

@ -18,9 +18,11 @@
href="#">Environment href="#">Environment
Variables</a> Variables</a>
@endif @endif
<a :class="activeTab === 'scheduled-tasks' && 'text-white'" @if ($application->build_pack !== 'static' && $application->build_pack !== 'dockercompose')
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'" href="#">Scheduled Tasks <a :class="activeTab === 'storages' && 'text-white'"
</a> @click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
</a>
@endif
@if ($application->git_based()) @if ($application->git_based())
<a :class="activeTab === 'source' && 'text-white'" <a :class="activeTab === 'source' && 'text-white'"
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a> @click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@ -28,11 +30,12 @@
<a :class="activeTab === 'server' && 'text-white'" <a :class="activeTab === 'server' && 'text-white'"
@click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server @click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server
</a> </a>
@if ($application->build_pack !== 'static' && $application->build_pack !== 'dockercompose')
<a :class="activeTab === 'storages' && 'text-white'" <a :class="activeTab === 'scheduled-tasks' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages @click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
</a> href="#">Scheduled Tasks
@endif </a>
<a :class="activeTab === 'webhooks' && 'text-white'" <a :class="activeTab === 'webhooks' && 'text-white'"
@click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks @click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks
</a> </a>

View File

@ -43,7 +43,7 @@
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<x-forms.checkbox instantSave id="application.settings.is_raw_compose_deployment_enabled" <x-forms.checkbox instantSave id="application.settings.is_raw_compose_deployment_enabled"
label="Raw Compose Deployment" label="Raw Compose Deployment"
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a href='https://coolify.io/docs/docker-compose'>documentation.</a>" /> helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a href='https://coolify.io/docs/docker/compose#raw-docker-compose-deployment'>documentation.</a>" />
@if (count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled) @if (count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled)
@foreach (data_get($parsedServices, 'services') as $serviceName => $service) @foreach (data_get($parsedServices, 'services') as $serviceName => $service)
@if (!isDatabaseImage(data_get($service, 'image'))) @if (!isDatabaseImage(data_get($service, 'image')))
@ -64,26 +64,28 @@
</div> </div>
@endif @endif
@if ($application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockercompose')
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains" <x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " /> helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " />
<x-forms.button wire:click="getWildcardDomain">Generate Domain <x-forms.button wire:click="getWildcardDomain">Generate Domain
</x-forms.button> </x-forms.button>
</div> </div>
@endif @endif
@if ($application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockercompose')
<h3>Docker Registry</h3> <div class="flex items-center gap-2 pt-8">
<h3>Docker Registry</h3>
@if ($application->build_pack !== 'dockerimage' && !$application->destination->server->isSwarm())
<x-helper
helper='Push the built image to a docker registry. More info <a class="underline"
href="https://coolify.io/docs/docker/registry" target="_blank">here</a>' />
@endif
</div>
@if ($application->destination->server->isSwarm()) @if ($application->destination->server->isSwarm())
@if ($application->build_pack !== 'dockerimage') @if ($application->build_pack !== 'dockerimage')
<div>Docker Swarm requires the image to be available in a registry. More info <a <div>Docker Swarm requires the image to be available in a registry. More info <a
class="underline" href="https://coolify.io/docs/docker-registries" class="underline" href="https://coolify.io/docs/docker/registry"
target="_blank">here</a>.</div> target="_blank">here</a>.</div>
@endif @endif
@else
@if ($application->build_pack !== 'dockerimage')
<div>Push the built image to a docker registry. More info <a class="underline"
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div>
@endif
@endif @endif
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
@if ($application->build_pack === 'dockerimage') @if ($application->build_pack === 'dockerimage')
@ -117,12 +119,18 @@
@endif @endif
@if ($application->build_pack !== 'dockerimage') @if ($application->build_pack !== 'dockerimage')
<h3>Build</h3> <h3 class="pt-8">Build</h3>
@if ($application->build_pack !== 'dockercompose')
<div class="w-96">
<x-forms.checkbox
helper="Use a build server to build your application. You can configure your build server in the Server settings. This is experimental. For more info, check the <a href='https://coolify.io/docs/server/build-server' class='underline' target='_blank'>documentation</a>."
instantSave id="application.settings.is_build_server_enabled"
label="Use a Build Server? (experimental)" />
</div>
@endif
@if ($application->could_set_build_commands()) @if ($application->could_set_build_commands())
@if ($application->build_pack === 'nixpacks') @if ($application->build_pack === 'nixpacks')
<div>Nixpacks will detect the required configuration automatically.
<a class="underline" href="https://coolify.io/docs/frameworks/">Framework Specific Docs</a>
</div>
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml" <x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml"
id="application.install_command" label="Install Command" /> id="application.install_command" label="Install Command" />
@ -131,6 +139,9 @@
<x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml" <x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml"
id="application.start_command" label="Start Command" /> id="application.start_command" label="Start Command" />
</div> </div>
<div>Nixpacks will detect the required configuration automatically.
<a class="underline" href="https://coolify.io/docs/frameworks/">Framework Specific Docs</a>
</div>
@endif @endif
@endif @endif
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
@ -190,7 +201,8 @@
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button> <x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>
@if ($application->settings->is_raw_compose_deployment_enabled) @if ($application->settings->is_raw_compose_deployment_enabled)
<x-forms.textarea rows="10" readonly id="application.docker_compose_raw" <x-forms.textarea rows="10" readonly id="application.docker_compose_raw"
label="Docker Compose Content (applicationId: {{$application->id}})" helper="You need to modify the docker compose file." /> label="Docker Compose Content (applicationId: {{ $application->id }})"
helper="You need to modify the docker compose file." />
@else @else
<x-forms.textarea rows="10" readonly id="application.docker_compose" <x-forms.textarea rows="10" readonly id="application.docker_compose"
label="Docker Compose Content" helper="You need to modify the docker compose file." /> label="Docker Compose Content" helper="You need to modify the docker compose file." />
@ -203,7 +215,7 @@
<x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea> <x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea>
@endif @endif
@if ($application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockercompose')
<h3>Network</h3> <h3 class="pt-8">Network</h3>
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
@if ($application->settings->is_static || $application->build_pack === 'static') @if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly /> <x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly />

View File

@ -1,4 +1,124 @@
<nav wire:poll.30000ms="check_status"> <nav wire:poll.30000ms="check_status">
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" /> <x-resources.breadcrumbs :resource="$application" :parameters="$parameters" />
<x-applications.navbar :application="$application" :parameters="$parameters" /> <div class="navbar-main">
<a class="{{ request()->routeIs('project.application.configuration') ? 'text-white' : '' }}"
href="{{ route('project.application.configuration', $parameters) }}">
<button>Configuration</button>
</a>
@if (!$application->destination->server->isSwarm())
<a class="{{ request()->routeIs('project.application.command') ? 'text-white' : '' }}"
href="{{ route('project.application.command', $parameters) }}">
<button>Execute Command</button>
</a>
@endif
<a class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button>
</a>
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'text-white' : '' }}"
href="{{ route('project.application.deployment.index', $parameters) }}">
<button>Deployments</button>
</a>
<x-applications.links :application="$application" />
<div class="flex-1"></div>
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div>
@else
@if (!$application->destination->server->isSwarm())
<x-applications.advanced :application="$application" />
@endif
@if ($application->status !== 'exited')
@if (!$application->destination->server->isSwarm())
<button title="With rolling update if possible" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
@endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->destination->server->isSwarm())
<button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Update Service
</button>
@else
<button title="Restart without rebuilding" wire:click='restart'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</button>
@endif
{{-- @if (isDev())
<button title="Restart without rebuilding" wire:click='restartNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart (new)
</button>
@endif --}}
@endif
<button wire:click='stop'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@else
<button wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" 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 d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
{{-- @if (isDev())
<button wire:click='deployNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" 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 d="M7 4v16l13 -8z" />
</svg>
Deploy (new)
</button>
@endif --}}
@endif
@endif
</div>
</nav> </nav>

View File

@ -10,6 +10,15 @@
Check repository Check repository
</x-forms.button> </x-forms.button>
</div> </div>
@if (!$branch_found)
<div>
<p>Public repositories: <span class='text-helper'>https://...</span></p>
<p>Private repositories: <span class='text-helper'>git@...</span></p>
<p>Preselect branch: <span
class='text-helper'>https://github.com/coollabsio/coolify-examples/tree/static</span> to
select 'static' branch.</p>
</div>
@endif
@if ($branch_found) @if ($branch_found)
@if ($rate_limit_remaining && $rate_limit_reset) @if ($rate_limit_remaining && $rate_limit_reset)
<div class="flex gap-2 py-2"> <div class="flex gap-2 py-2">
@ -21,12 +30,20 @@
<div class="flex flex-col gap-2 pb-6"> <div class="flex flex-col gap-2 pb-6">
<div class="flex gap-2"> <div class="flex gap-2">
@if ($git_source === 'other') @if ($git_source === 'other')
<x-forms.input id="git_branch" label="Selected branch" <x-forms.input id="git_branch" label="Branch"
helper="You can select other branches after configuration is done." /> helper="You can select other branches after configuration is done." />
@else @else
<x-forms.input disabled id="git_branch" label="Selected branch" <x-forms.input disabled id="git_branch" label="Branch"
helper="You can select other branches after configuration is done." /> helper="You can select other branches after configuration is done." />
@endif @endif
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option>
<option value="static">Static</option>
<option value="dockerfile">Dockerfile</option>
<option value="dockercompose">Docker Compose</option>
</x-forms.select>
</div>
@if ($show_is_static)
@if ($is_static) @if ($is_static)
<x-forms.input id="publish_directory" label="Publish Directory" <x-forms.input id="publish_directory" label="Publish Directory"
helper="If there is a build process involved (like Svelte, React, Next, etc..), please specify the output directory for the build assets." /> helper="If there is a build process involved (like Svelte, React, Next, etc..), please specify the output directory for the build assets." />
@ -34,14 +51,14 @@
<x-forms.input type="number" id="port" label="Port" :readonly="$is_static" <x-forms.input type="number" id="port" label="Port" :readonly="$is_static"
helper="The port your application listens on." /> helper="The port your application listens on." />
@endif @endif
</div> <div class="w-52">
<div class="w-52"> <x-forms.checkbox instantSave id="is_static" label="Is it a static site?"
<x-forms.checkbox instantSave id="is_static" label="Is it a static site?" helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
helper="If your application is a static site or the final build assets should be served as a static site, enable this." /> </div>
</div> @endif
</div> </div>
<x-forms.button wire:click.prevent='submit'> <x-forms.button wire:click.prevent='submit'>
Save New Application Continue
</x-forms.button> </x-forms.button>
@endif @endif
</div> </div>

View File

@ -207,7 +207,7 @@
<div class="flex items-center justify-center pt-4"> <div class="flex items-center justify-center pt-4">
<x-forms.checkbox instantSave wire:model="includeSwarm" <x-forms.checkbox instantSave wire:model="includeSwarm"
helper="Swarm clusters are excluded from this list by default. For database, services or complex compose deployments with databases to work with Swarm, helper="Swarm clusters are excluded from this list by default. For database, services or complex compose deployments with databases to work with Swarm,
you need to set a few things on the server. Read more <a class='text-white underline' href='https://coolify.io/docs/swarm#database-requirements' target='_blank'>here</a>." you need to set a few things on the server. Read more <a class='text-white underline' href='https://coolify.io/docs/docker/swarm#database-requirements' target='_blank'>here</a>."
label="Include Swarm Clusters" /> label="Include Swarm Clusters" />
</div> </div>
@endif --}} @endif --}}

View File

@ -7,6 +7,14 @@
@click.prevent="activeTab = 'service-stack'; @click.prevent="activeTab = 'service-stack';
window.location.hash = 'service-stack'" window.location.hash = 'service-stack'"
href="#">Service Stack</a> href="#">Service Stack</a>
<a :class="activeTab === 'environment-variables' && 'text-white'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment
Variables</a>
<a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages';
window.location.hash = 'storages'"
href="#">Storages</a>
<a :class="activeTab === 'execute-command' && 'text-white'" <a :class="activeTab === 'execute-command' && 'text-white'"
@click.prevent="activeTab = 'execute-command'; @click.prevent="activeTab = 'execute-command';
window.location.hash = 'execute-command'" window.location.hash = 'execute-command'"
@ -15,17 +23,9 @@
@click.prevent="activeTab = 'logs'; @click.prevent="activeTab = 'logs';
window.location.hash = 'logs'" window.location.hash = 'logs'"
href="#">Logs</a> href="#">Logs</a>
<a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages';
window.location.hash = 'storages'"
href="#">Storages</a>
<a :class="activeTab === 'webhooks' && 'text-white'" <a :class="activeTab === 'webhooks' && 'text-white'"
@click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks @click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks
</a> </a>
<a :class="activeTab === 'environment-variables' && 'text-white'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment
Variables</a>
<a :class="activeTab === 'danger' && 'text-white'" <a :class="activeTab === 'danger' && 'text-white'"
@click.prevent="activeTab = 'danger'; @click.prevent="activeTab = 'danger';
window.location.hash = 'danger'" window.location.hash = 'danger'"

View File

@ -48,10 +48,5 @@
@endforeach @endforeach
</div> </div>
@endif @endif
@if (
$resource->persistentStorages()->get()->count() == 0 &&
$resource->fileStorages()->get()->count() == 0)
<div class="pt-4">No storages found.</div>
@endif
@endif @endif
</div> </div>

View File

@ -1,7 +1,8 @@
<div> <div>
<h2>Server</h2> <h2>Server</h2>
<div class="">The destination server where your application will be deployed to.</div> <div class="">Server related configurations.</div>
<div class="py-4 "> <h3 class="pt-4">Destination Server & Network</h3>
<div class="py-4">
<a class="box" <a class="box"
href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}">On href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}">On
server <span class="px-1 text-warning">{{ data_get($resource, 'destination.server.name') }}</span> server <span class="px-1 text-warning">{{ data_get($resource, 'destination.server.name') }}</span>

View File

@ -2,11 +2,11 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Webhooks</h2> <h2>Webhooks</h2>
<x-helper <x-helper
helper="For more details goto our <a class='text-white underline' href='https://coolify.io/docs/api-endpoints' target='_blank'>docs</a>." /> helper="For more details goto our <a class='text-white underline' href='https://coolify.io/docs/api/deploy-webhook' target='_blank'>docs</a>." />
</div> </div>
<div> <div>
<x-forms.input readonly <x-forms.input readonly
helper="See details in our <a target='_blank' class='text-white underline' href='https://coolify.io/docs/api-authentication'>documentation</a>." helper="See details in our <a target='_blank' class='text-white underline' href='https://coolify.io/docs/api/authentication'>documentation</a>."
label="Deploy Webhook (auth required)" id="deploywebhook"></x-forms.input> label="Deploy Webhook (auth required)" id="deploywebhook"></x-forms.input>
</div> </div>
@if ($resource->type() !== 'service') @if ($resource->type() !== 'service')

View File

@ -13,7 +13,7 @@
$wire.showNotification = true; $wire.showNotification = true;
@endif @endif
console.error( console.error(
'Coolify could not connect to the new realtime service introduced in beta.154. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/cloudflare-tunnels) or get help on Discord (https://coollabs.io/discord).)' 'Coolify could not connect to the new realtime service introduced in beta.154. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/cloudflare/tunnels) or get help on Discord (https://coollabs.io/discord).)'
); );
clearInterval(checkPusherInterval); clearInterval(checkPusherInterval);
} }
@ -26,7 +26,7 @@
$wire.showNotification = true; $wire.showNotification = true;
@endif @endif
console.error( console.error(
'Coolify could not connect to the new realtime service introduced in beta.154. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/cloudflare-tunnels) or get help on Discord (https://coollabs.io/discord).)' 'Coolify could not connect to the new realtime service introduced in beta.154. This will cause unusual problems on the UI if not fixed! Please check the related documentation (https://coolify.io/docs/cloudflare/tunnels) or get help on Discord (https://coollabs.io/discord).)'
); );
clearInterval(checkPusherInterval); clearInterval(checkPusherInterval);
} }
@ -38,7 +38,7 @@
<span><span class="font-bold text-left text-red-500">WARNING: </span>Coolify could not connect to the new <span><span class="font-bold text-left text-red-500">WARNING: </span>Coolify could not connect to the new
realtime service introduced in beta.154. <br>This will cause unusual problems on the UI if not realtime service introduced in beta.154. <br>This will cause unusual problems on the UI if not
fixed!<br><br>Please check the fixed!<br><br>Please check the
related <a href='https://coolify.io/docs/cloudflare-tunnels' target='_blank'>documentation</a> or get related <a href='https://coolify.io/docs/cloudflare/tunnels' target='_blank'>documentation</a> or get
help on <a href='https://coollabs.io/discord' target='_blank'>Discord</a>.</span> help on <a href='https://coollabs.io/discord' target='_blank'>Discord</a>.</span>
<x-forms.button class="bg-coolgray-400" wire:click='disable'>Acknowledge the problem and disable this <x-forms.button class="bg-coolgray-400" wire:click='disable'>Acknowledge the problem and disable this
popup</x-forms.button> popup</x-forms.button>

View File

@ -35,7 +35,7 @@
<div class="flex flex-col w-full gap-2 lg:flex-row"> <div class="flex flex-col w-full gap-2 lg:flex-row">
<x-forms.input id="server.name" label="Name" required /> <x-forms.input id="server.name" label="Name" required />
<x-forms.input id="server.description" label="Description" /> <x-forms.input id="server.description" label="Description" />
@if (!$server->settings->is_swarm_worker) @if (!$server->settings->is_swarm_worker && !$server->settings->is_build_server)
<x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain" <x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain"
helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example:</span><br>In case you set:<span class='text-helper'>https://example.com</span> your applications will get:<br> <span class='text-helper'>https://randomId.example.com</span>" /> helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example:</span><br>In case you set:<span class='text-helper'>https://example.com</span> your applications will get:<br> <span class='text-helper'>https://randomId.example.com</span>" />
@endif @endif
@ -51,29 +51,34 @@
</div> </div>
<div class="w-64"> <div class="w-64">
@if (!$server->isLocalhost()) @if (!$server->isLocalhost())
<x-forms.checkbox instantSave @if ($server->settings->is_build_server)
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<br><span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>" <x-forms.checkbox instantSave disabled id="server.settings.is_build_server"
id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" /> label="Use it as a build server?" />
@if ($server->isSwarm())
<div class="pt-6"> Swarm support is in alpha version. </div>
@endif
@if ($server->settings->is_swarm_worker)
<x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_manager"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>."
label="Is it a Swarm Manager?" />
@else @else
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_manager" <x-forms.checkbox instantSave
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>." helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<br><span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>"
label="Is it a Swarm Manager?" /> id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" />
@endif @if ($server->isSwarm())
@if ($server->settings->is_swarm_manager) <div class="pt-6"> Swarm support is experimental. </div>
<x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_worker" @endif
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>." @if ($server->settings->is_swarm_worker)
label="Is it a Swarm Worker?" /> <x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_manager"
@else helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/docker/swarm' target='_blank'>here</a>."
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_worker" label="Is it a Swarm Manager?" />
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>." @else
label="Is it a Swarm Worker?" /> <x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_manager"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/docker/swarm' target='_blank'>here</a>."
label="Is it a Swarm Manager?" />
@endif
@if ($server->settings->is_swarm_manager)
<x-forms.checkbox disabled instantSave type="checkbox" id="server.settings.is_swarm_worker"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/docker/swarm' target='_blank'>here</a>."
label="Is it a Swarm Worker?" />
@else
<x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_worker"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/docker/swarm' target='_blank'>here</a>."
label="Is it a Swarm Worker?" />
@endif
@endif @endif
@endif @endif
</div> </div>

View File

@ -26,23 +26,29 @@
@endforeach @endforeach
</x-forms.select> </x-forms.select>
<div class="w-96"> <div class="w-96">
<div class="pt-6"> Swarm support is in alpha version. Read the docs <a class='text-white' href='https://coolify.io/docs/swarm#deploy-with-persistent-storage' target='_blank'>here</a>.</div> <x-forms.checkbox instantSave type="checkbox" id="is_build_server" label="Use it as a build server?" />
@if ($is_swarm_worker) </div>
<div class="w-96">
<h3 class="pt-6">Swarm Support</h3>
<div> Swarm support is experimental. Read the docs <a class='text-white'
href='https://coolify.io/docs/docker/swarm#deploy-with-persistent-storage'
target='_blank'>here</a>.</div>
@if ($is_swarm_worker || $is_build_server)
<x-forms.checkbox disabled instantSave type="checkbox" id="is_swarm_manager" <x-forms.checkbox disabled instantSave type="checkbox" id="is_swarm_manager"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>." helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/docker/swarm' target='_blank'>here</a>."
label="Is it a Swarm Manager?" /> label="Is it a Swarm Manager?" />
@else @else
<x-forms.checkbox type="checkbox" instantSave id="is_swarm_manager" <x-forms.checkbox type="checkbox" instantSave id="is_swarm_manager"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>." helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/docker/swarm' target='_blank'>here</a>."
label="Is it a Swarm Manager?" /> label="Is it a Swarm Manager?" />
@endif @endif
@if ($is_swarm_manager) @if ($is_swarm_manager|| $is_build_server)
<x-forms.checkbox disabled instantSave type="checkbox" id="is_swarm_worker" <x-forms.checkbox disabled instantSave type="checkbox" id="is_swarm_worker"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>." helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/docker/swarm' target='_blank'>here</a>."
label="Is it a Swarm Worker?" /> label="Is it a Swarm Worker?" />
@else @else
<x-forms.checkbox type="checkbox" instantSave id="is_swarm_worker" <x-forms.checkbox type="checkbox" instantSave id="is_swarm_worker"
helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/swarm' target='_blank'>here</a>." helper="For more information, please read the documentation <a class='text-white' href='https://coolify.io/docs/docker/swarm' target='_blank'>here</a>."
label="Is it a Swarm Worker?" /> label="Is it a Swarm Worker?" />
@endif @endif
@if ($is_swarm_worker && count($swarm_managers) > 0) @if ($is_swarm_worker && count($swarm_managers) > 0)
@ -60,7 +66,7 @@
@endif @endif
</div> </div>
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Server Continue
</x-forms.button> </x-forms.button>
</form> </form>
@endif @endif

View File

@ -7,6 +7,15 @@
</p> </p>
</x-slot:modalBody> </x-slot:modalBody>
</x-modal> </x-modal>
<x-modal yesOrNo modalId="restartProxy" modalTitle="Restart Proxy" action="restart">
<x-slot:modalBody>
<p>This proxy will be stopped and started. It is not reversible. <br>All resources will be unavailable
during the restart.
<br>Please think
again.
</p>
</x-slot:modalBody>
</x-modal>
@if ($server->isFunctional() && data_get($server, 'proxy.type') !== 'NONE') @if ($server->isFunctional() && data_get($server, 'proxy.type') !== 'NONE')
@if (data_get($server, 'proxy.status') === 'running') @if (data_get($server, 'proxy.status') === 'running')
<div class="flex gap-4"> <div class="flex gap-4">
@ -18,6 +27,17 @@
</a> </a>
</button> </button>
@endif @endif
<x-forms.button isModal noStyle modalId="restartProxy"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart Proxy
</x-forms.button>
<x-forms.button isModal noStyle modalId="stopProxy" <x-forms.button isModal noStyle modalId="stopProxy"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"

View File

@ -3,7 +3,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<x-server.sidebar :server="$server" :parameters="$parameters" /> <x-server.sidebar :server="$server" :parameters="$parameters" />
<div class="w-full"> <div class="w-full">
@if ($server->proxyType() !== 'NONE' && $server->isFunctional()) @if ($server->isFunctional())
<livewire:server.proxy :server="$server" /> <livewire:server.proxy :server="$server" />
@endif @endif
</div> </div>

View File

@ -4,7 +4,7 @@
"version": "3.12.36" "version": "3.12.36"
}, },
"v4": { "v4": {
"version": "4.0.0-beta.196" "version": "4.0.0-beta.197"
} }
} }
} }