diff --git a/README.md b/README.md index fe59db3d0..cef3fdc81 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,10 @@ Contact us [here](https://coolify.io/docs/contact). coollabsio%2Fcoolify | Trendshift +# Repo Activity + +![Alt](https://repobeats.axiom.co/api/embed/eab1c8066f9c59d0ad37b76c23ebb5ccac4278ae.svg "Repobeats analytics image") + # Star History [![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date) diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index a99e47b25..daee357d5 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -2,6 +2,7 @@ namespace App\Actions\Proxy; +use App\Events\ProxyStatusChanged; use App\Models\Server; use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; @@ -13,7 +14,6 @@ class StartProxy public function handle(Server $server, bool $async = true): string|Activity { try { - $proxyType = $server->proxyType(); $commands = collect([]); $proxy_path = get_proxy_path(); diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index 2d2c10ccb..c42a8ec4a 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -10,8 +10,10 @@ class DeleteService use AsAction; public function handle(Service $service) { - StopService::run($service); $server = data_get($service, 'server'); + if ($server->isFunctional()) { + StopService::run($service); + } $storagesToDelete = collect([]); $service->environment_variables()->delete(); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 7dbead831..50db6d681 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -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'); $own = Team::find(0)->servers; $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 { $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) { $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); @@ -72,7 +72,7 @@ class Kernel extends ConsoleKernel } } foreach ($servers as $server) { - $schedule->job(new ServerStatusJob($server))->everyFiveMinutes()->onOneServer(); + $schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer(); } } 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(); if ($scheduled_tasks->isEmpty()) { ray('no scheduled tasks'); @@ -134,7 +135,6 @@ class Kernel extends ConsoleKernel task: $scheduled_task ))->cron($scheduled_task->frequency)->onOneServer(); } - } protected function commands(): void diff --git a/app/Events/ProxyStatusChanged.php b/app/Events/ProxyStatusChanged.php new file mode 100644 index 000000000..42d276424 --- /dev/null +++ b/app/Events/ProxyStatusChanged.php @@ -0,0 +1,34 @@ +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}"), + ]; + } +} diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 5755b5a57..d9a7176a8 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -56,7 +56,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private GithubApp|GitlabApp|string $source = 'other'; private StandaloneDocker|SwarmDocker $destination; + // Deploy to 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 ?ApplicationPreview $preview = null; private ?string $git_type = null; @@ -196,6 +202,25 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // Check custom port ['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 { if ($this->restart_only && $this->application->build_pack !== 'dockerimage') { $this->just_restart(); @@ -225,7 +250,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted if ($this->server->isProxyShouldRun()) { 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->next(ApplicationDeploymentStatus::FINISHED->value); @@ -234,23 +260,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->fail($e); throw $e; } finally { - if (isset($this->docker_compose_base64)) { - $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", - ] - ); + if ($this->use_build_server) { + $this->server = $this->build_server; + } else { + $this->write_deployment_configurations(); } $this->execute_remote_command( [ @@ -269,42 +282,72 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); } } - private function push_to_docker_registry() + private function write_deployment_configurations() { - try { - instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server); + if (isset($this->docker_compose_base64)) { + 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( [ - "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) { - // Tag image with latest + if ($this->use_build_server) { + $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( - ['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->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 ], ); + 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() @@ -340,20 +383,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } private function just_restart() { - $this->execute_remote_command( - [ - "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->check_git_if_build_needed(); $this->set_base_dir(); $this->generate_image_names(); $this->check_image_locally_or_remotely(); if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { - $this->execute_remote_command([ - "echo 'Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.'", - ]); + $this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container."); $this->create_workdir(); $this->generate_compose_file(); $this->rolling_update(); @@ -397,16 +434,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private function deploy_simple_dockerfile() { + if ($this->use_build_server) { + $this->server = $this->build_server; + } $dockerfile_base64 = base64_encode($this->application->dockerfile); - $this->execute_remote_command( - [ - "echo 'Starting deployment of {$this->application->name}.'" - ], - ); + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}."); $this->prepare_builder_image(); $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(); @@ -422,11 +458,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->dockerImage = $this->application->docker_registry_image_name; $this->dockerImageTag = $this->application->docker_registry_image_tag; ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"); - $this->execute_remote_command( - [ - "echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'" - ], - ); + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}."); $this->generate_image_names(); $this->prepare_builder_image(); $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 ]); } - if (isset($this->docker_compose_base64)) { - $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", - ] - ); - } + $this->write_deployment_configurations(); // Start compose file if ($this->docker_compose_custom_start_command) { $this->execute_remote_command( @@ -528,14 +543,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } private function deploy_dockerfile_buildpack() { + if ($this->use_build_server) { + $this->server = $this->build_server; + } if (data_get($this->application, 'dockerfile_location')) { $this->dockerfile_location = $this->application->dockerfile_location; } - $this->execute_remote_command( - [ - "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->check_git_if_build_needed(); $this->clone_repository(); @@ -555,11 +569,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } private function deploy_nixpacks_buildpack() { - $this->execute_remote_command( - [ - "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" - ], - ); + if ($this->use_build_server) { + $this->server = $this->build_server; + } + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->prepare_builder_image(); $this->check_git_if_build_needed(); $this->set_base_dir(); @@ -568,17 +581,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->check_image_locally_or_remotely(); if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { $this->create_workdir(); - $this->execute_remote_command([ - "echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'", - ]); + $this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped."); $this->generate_compose_file(); $this->rolling_update(); return; } if ($this->application->isConfigurationChanged()) { - $this->execute_remote_command([ - "echo 'Configuration changed. Rebuilding image.'", - ]); + $this->application_deployment_queue->addLogEntry("Configuration changed. Rebuilding image."); } } $this->clone_repository(); @@ -592,11 +601,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } private function deploy_static_buildpack() { - $this->execute_remote_command( - [ - "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'" - ], - ); + if ($this->use_build_server) { + $this->server = $this->build_server; + } + $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->prepare_builder_image(); $this->check_git_if_build_needed(); $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(); } if ($nixpacks_php_fallback_path?->value === '/index.php' && $nixpacks_php_root_dir?->value === '/app/public' && $this->newVersionIsHealthy === false) { - $this->execute_remote_command( - [ - "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' - ], - ); + $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'); } } private function rolling_update() { if ($this->server->isSwarm()) { 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->execute_remote_command( @@ -640,22 +644,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ); $this->application_deployment_queue->addLogEntry("Rolling update completed."); } 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) { - $this->execute_remote_command( - [ - "echo '\n----------------------------------------'", - ], - ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"], - ); + $this->application_deployment_queue->addLogEntry("----------------------------------------"); + $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported."); $this->stop_running_container(force: true); $this->start_by_compose_file(); } else { - $this->execute_remote_command( - [ - "echo '\n----------------------------------------'", - ], - ["echo -n 'Rolling update started.'"], - ); + $this->application_deployment_queue->addLogEntry("----------------------------------------"); + $this->application_deployment_queue->addLogEntry("Rolling update started."); $this->start_by_compose_file(); $this->health_check(); $this->stop_running_container(); @@ -676,17 +677,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // ray('New container name: ', $this->container_name); if ($this->container_name) { $counter = 1; - $this->execute_remote_command( - [ - "echo 'Waiting for healthcheck to pass on the new container.'" - ] - ); + $this->application_deployment_queue->addLogEntry("Waiting for healthcheck to pass on the new container."); if ($this->full_healthcheck_url) { - $this->execute_remote_command( - [ - "echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'" - ] - ); + $this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}"); } while ($counter < $this->application->health_check_retries) { $this->execute_remote_command( @@ -698,19 +691,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ], ); - $this->execute_remote_command( - [ - "echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'" - ], - ); + $this->application_deployment_queue->addLogEntry("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') { $this->newVersionIsHealthy = true; $this->application->update(['status' => 'running']); - $this->execute_remote_command( - [ - "echo 'New container is healthy.'" - ], - ); + $this->application_deployment_queue->addLogEntry("New container is healthy."); break; } 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() { + if ($this->use_build_server) { + $this->server = $this->build_server; + } $this->newVersionIsHealthy = true; $this->generate_image_names(); - $this->execute_remote_command([ - "echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.'", - ]); + $this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->prepare_builder_image(); $this->clone_repository(); $this->set_base_dir(); @@ -754,10 +740,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted ], ); } else { - $this->execute_remote_command( - ["echo -n 'Starting preview deployment.'"], - [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], - ); + $this->application_deployment_queue->addLogEntry("Starting preview deployment."); + if ($this->use_build_server) { + $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() @@ -774,16 +766,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted // Get user home directory $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); - - 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}"; + if ($this->use_build_server) { + if ($this->dockerConfigFileExists === 'NOK') { + 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 { - $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( - [ - "echo -n 'Preparing container with helper image: $helperImage.'", - ], [ $runCommand, "hidden" => true, @@ -801,19 +797,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $destination = StandaloneDocker::find($destination_id); $server = $destination->server; if ($server->team_id !== $this->mainServer->team_id) { - $this->execute_remote_command( - [ - "echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'", - ], - ); + $this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!"); continue; } $this->server = $server; - $this->execute_remote_command( - [ - "echo -n 'Deploying to {$this->server->name}.'", - ], - ); + $this->application_deployment_queue->addLogEntry("Deploying to {$this->server->name}."); $this->prepare_builder_image(); $this->generate_image_names(); $this->rolling_update(); @@ -821,11 +809,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } private function set_base_dir() { - $this->execute_remote_command( - [ - "echo -n 'Setting base directory to {$this->workdir}.'" - ], - ); + $this->application_deployment_queue->addLogEntry("Setting base directory to {$this->workdir}."); } private function check_git_if_build_needed() { @@ -898,26 +882,28 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private function generate_nixpacks_confs() { $nixpacks_command = $this->nixpacks_build_cmd(); + $this->application_deployment_queue->addLogEntry("Generating nixpacks configuration with: $nixpacks_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 detect {$this->workdir}"), "save" => "nixpacks_type", "hidden" => true], ); if ($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')) { $this->nixpacks_plan = $this->saved_outputs->get('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); // Do any modifications here $this->generate_env_variables(); $merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', []))); data_set($parsed, 'variables', $merged_envs->toArray()); $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) { + $this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry."); $this->execute_remote_command( - ["echo -n 'Pulling latest image ($image) from the registry.'"], - [ executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true ] @@ -1244,25 +1229,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } private function build_image() { + $this->application_deployment_queue->addLogEntry("----------------------------------------"); if ($this->application->build_pack === 'static') { - $this->execute_remote_command([ - "echo -n 'Static deployment. Copying static assets to the image.'", - ]); + $this->application_deployment_queue->addLogEntry("Static deployment. Copying static assets to the image."); } else { - $this->execute_remote_command( - [ - "echo -n 'Building docker image started.'", - ], - ["echo -n 'To check the current progress, click on Show Debug Logs.'"] - ); + $this->application_deployment_queue->addLogEntry("Building docker image started."); + $this->application_deployment_queue->addLogEntry("To check the current progress, click on Show Debug Logs."); } if ($this->application->settings->is_static || $this->application->build_pack === 'static') { if ($this->application->static_image) { $this->pull_latest_image($this->application->static_image); - $this->execute_remote_command( - ["echo -n 'Continue with the building process.'"], - ); + $this->application_deployment_queue->addLogEntry("Continuing with the building process."); } if ($this->application->build_pack === 'static') { $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([ - "echo -n 'Building docker image completed.'", - ]); + $this->application_deployment_queue->addLogEntry("Building docker image completed."); } 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], ); } else { - if ($this->docker_compose_location) { + if ($this->use_build_server) { $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 { $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 { - $this->execute_remote_command( - ["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'], - ["echo '{$exception->getMessage()}'", 'type' => 'err'], - ); + $this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr'); + $this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr'); + 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( - ["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] ); } diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index c28e266dd..b9fa3443f 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -44,8 +44,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted public function handle() { - if (!$this->server->isServerReady($this->tries)) { - return 'Server is not reachable.'; + if (!$this->server->isFunctional()) { + return 'Server is not ready.'; }; try { if ($this->server->isSwarm()) { diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index cf7c6462a..a29c87c09 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -31,11 +31,16 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted { try { $server = $this->resource->destination->server; + $this->resource->delete(); 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'; } - $this->resource->delete(); switch ($this->resource->type()) { case 'application': StopApplication::run($this->resource); diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php index 2a3b41a2a..6272d442d 100644 --- a/app/Jobs/ServerStatusJob.php +++ b/app/Jobs/ServerStatusJob.php @@ -38,6 +38,9 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted public function handle() { try { + if (!$this->server->isServerReady($this->tries)) { + throw new \RuntimeException('Server is not ready.'); + }; if ($this->server->isFunctional()) { $this->cleanup(notify: false); } diff --git a/app/Livewire/ActivityMonitor.php b/app/Livewire/ActivityMonitor.php index 703899b65..8f887b3c5 100644 --- a/app/Livewire/ActivityMonitor.php +++ b/app/Livewire/ActivityMonitor.php @@ -3,6 +3,7 @@ namespace App\Livewire; use App\Enums\ProcessStatus; +use App\Models\User; use Livewire\Component; use Spatie\Activitylog\Models\Activity; @@ -10,14 +11,16 @@ class ActivityMonitor extends Component { public ?string $header = null; public $activityId; + public $eventToDispatch = 'activityFinished'; public $isPollingActive = false; protected $activity; protected $listeners = ['newMonitorActivity']; - public function newMonitorActivity($activityId) + public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished') { $this->activityId = $activityId; + $this->eventToDispatch = $eventToDispatch; $this->hydrateActivity(); @@ -35,13 +38,28 @@ class ActivityMonitor extends Component // $this->setStatus(ProcessStatus::IN_PROGRESS); $exit_code = data_get($this->activity, 'properties.exitCode'); if ($exit_code !== null) { - if ($exit_code === 0) { - // $this->setStatus(ProcessStatus::FINISHED); - } else { - // $this->setStatus(ProcessStatus::ERROR); - } + // if ($exit_code === 0) { + // // $this->setStatus(ProcessStatus::FINISHED); + // } else { + // // $this->setStatus(ProcessStatus::ERROR); + // } $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); + } + } } } diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index 9ad485f85..a2b7afa6a 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -16,6 +16,7 @@ class Advanced extends Component 'application.settings.is_force_https_enabled' => 'boolean|required', 'application.settings.is_log_drain_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_count' => 'string|required', 'application.settings.gpu_device_ids' => 'string|required', diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 0e93d389c..2e02aac60 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -67,6 +67,7 @@ class General extends Component 'application.docker_compose_custom_start_command' => 'nullable', 'application.docker_compose_custom_build_command' => 'nullable', 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', + 'application.settings.is_build_server_enabled' => 'boolean|required', ]; protected $validationAttributes = [ '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_build_command' => 'Docker compose custom build command', '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() { @@ -227,7 +229,6 @@ class General extends Component if ($this->ports_exposes !== $this->application->ports_exposes) { $this->resetDefaultLabels(false); } - if (data_get($this->application, 'build_pack') === 'dockerimage') { $this->validate([ 'application.docker_registry_image_name' => 'required', diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 4a138eaca..95dcbdf73 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -74,7 +74,11 @@ class Heading extends Component return; } 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.
More information here: documentation'); return; } $this->setDeploymentUuid(); diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index dffb9461c..57cb43302 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -49,8 +49,9 @@ class Previews extends Component queue_application_deployment( application_id: $this->application->id, deployment_uuid: $this->deployment_uuid, - force_rebuild: true, + force_rebuild: false, pull_request_id: $pull_request_id, + git_type: $found->git_type ?? null, ); return redirect()->route('project.application.deployment.show', [ 'project_uuid' => $this->parameters['project_uuid'], diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 6599d1188..840a5bd78 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -30,18 +30,22 @@ class PublicGitRepository extends Component public GithubApp|GitlabApp|string $git_source = 'other'; public string $git_host; public string $git_repository; + public $build_pack; + public bool $show_is_static = true; protected $rules = [ 'repository_url' => 'required|url', 'port' => 'required|numeric', 'is_static' => 'required|boolean', 'publish_directory' => 'nullable|string', + 'build_pack' => 'required|string', ]; protected $validationAttributes = [ 'repository_url' => 'repository', 'port' => 'port', 'is_static' => 'static', 'publish_directory' => 'publish directory', + 'build_pack' => 'build pack', ]; public function mount() @@ -53,7 +57,18 @@ class PublicGitRepository extends Component $this->parameters = get_route_parameters(); $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() { if ($this->is_static) { @@ -157,6 +172,7 @@ class PublicGitRepository extends Component 'environment_id' => $environment->id, 'destination_id' => $destination->id, 'destination_type' => $destination_class, + 'build_pack' => $this->build_pack, ]; } else { $application_init = [ @@ -170,7 +186,8 @@ class PublicGitRepository extends Component 'destination_id' => $destination->id, 'destination_type' => $destination_class, 'source_id' => $this->git_source->id, - 'source_type' => $this->git_source->getMorphClass() + 'source_type' => $this->git_source->getMorphClass(), + 'build_pack' => $this->build_pack, ]; } diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index 3133efc97..1c49ac7ad 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -104,7 +104,7 @@ class Select extends Component if ($this->includeSwarm) { $this->servers = $this->allServers; } 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) @@ -120,13 +120,13 @@ class Select extends Component case 'mongodb': $this->isDatabase = true; $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; } if (str($type)->startsWith('one-click-service') || str($type)->startsWith('docker-compose-empty')) { $this->isDatabase = true; $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") { $this->current_step = $type; diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index e6d2d5a33..5c99630ee 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -8,5 +8,5 @@ class Destination extends Component { public $resource; public $servers = []; - public $additionalServers = []; + public $additional_servers = []; } diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index d832c83d8..b8cabd58b 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -26,6 +26,7 @@ class Form extends Component 'server.settings.is_reachable' => 'required', 'server.settings.is_swarm_manager' => 'required|boolean', 'server.settings.is_swarm_worker' => 'required|boolean', + 'server.settings.is_build_server' => 'required|boolean', 'wildcard_domain' => 'nullable|url', ]; protected $validationAttributes = [ @@ -38,6 +39,7 @@ class Form extends Component 'server.settings.is_reachable' => 'Is reachable', 'server.settings.is_swarm_manager' => 'Swarm Manager', 'server.settings.is_swarm_worker' => 'Swarm Worker', + 'server.settings.is_build_server' => 'Build Server', ]; public function mount() @@ -76,7 +78,7 @@ class Form extends Component $this->server->settings->is_usable = true; $this->server->settings->save(); } else { - $this->dispatch('error', 'Server is not reachable.
Please validate your configuration and connection.

Check this documentation for further help.'); + $this->dispatch('error', 'Server is not reachable.
Please validate your configuration and connection.

Check this documentation for further help.'); return; } } @@ -85,12 +87,12 @@ class Form extends Component try { $uptime = $this->server->validateConnection(); if (!$uptime) { - $install && $this->dispatch('error', 'Server is not reachable.
Please validate your configuration and connection.

Check this documentation for further help.'); + $install && $this->dispatch('error', 'Server is not reachable.
Please validate your configuration and connection.

Check this documentation for further help.'); return; } $supported_os_type = $this->server->validateOS(); if (!$supported_os_type) { - $install && $this->dispatch('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.'); + $install && $this->dispatch('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.'); return; } $dockerInstalled = $this->server->validateDockerEngine(); diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php index 5238931a8..cd0166c54 100644 --- a/app/Livewire/Server/New/ByIp.php +++ b/app/Livewire/Server/New/ByIp.php @@ -25,6 +25,8 @@ class ByIp extends Component public bool $is_swarm_worker = false; public $selected_swarm_cluster = null; + public bool $is_build_server = false; + public $swarm_managers = []; protected $rules = [ 'name' => 'required|string', @@ -34,6 +36,7 @@ class ByIp extends Component 'port' => 'required|integer', 'is_swarm_manager' => 'required|boolean', 'is_swarm_worker' => 'required|boolean', + 'is_build_server' => 'required|boolean', ]; protected $validationAttributes = [ 'name' => 'Name', @@ -43,6 +46,7 @@ class ByIp extends Component 'port' => 'Port', 'is_swarm_manager' => 'Swarm Manager', 'is_swarm_worker' => 'Swarm Worker', + 'is_build_server' => 'Build Server', ]; public function mount() @@ -89,8 +93,14 @@ class ByIp extends Component $payload['swarm_cluster'] = $this->selected_swarm_cluster; } $server = Server::create($payload); - $server->settings->is_swarm_manager = $this->is_swarm_manager; - $server->settings->is_swarm_worker = $this->is_swarm_worker; + if ($this->is_build_server) { + $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->addInitialNetwork(); return redirect()->route('server.show', $server->uuid); diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 79ddb203b..5825cf3fb 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -33,7 +33,6 @@ class Proxy extends Component { $this->server->proxy = null; $this->server->save(); - $this->dispatch('proxyStatusUpdated'); } public function select_proxy($proxy_type) diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php index 4c748b7fa..8349325d0 100644 --- a/app/Livewire/Server/Proxy/Deploy.php +++ b/app/Livewire/Server/Proxy/Deploy.php @@ -4,6 +4,7 @@ namespace App\Livewire\Server\Proxy; use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; +use App\Events\ProxyStatusChanged; use App\Models\Server; use Livewire\Component; @@ -14,7 +15,17 @@ class Deploy extends Component public ?string $currentRoute = 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() { @@ -29,12 +40,22 @@ class Deploy extends Component { $this->traefikDashboardAvailable = $data; } + public function proxyStarted() + { + CheckProxy::run($this->server, true); + $this->dispatch('success', 'Proxy started.'); + } public function proxyStatusUpdated() { $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() { @@ -50,7 +71,7 @@ class Deploy extends Component { try { $activity = StartProxy::run($this->server); - $this->dispatch('newMonitorActivity', $activity->id); + $this->dispatch('newMonitorActivity', $activity->id, ProxyStatusChanged::class); } catch (\Throwable $e) { return handleError($e, $this); } @@ -77,6 +98,5 @@ class Deploy extends Component } catch (\Throwable $e) { return handleError($e, $this); } - } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 65b992b5a..dcbb0dde3 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -71,7 +71,7 @@ class Server extends BaseModel 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) @@ -112,7 +112,7 @@ class Server extends BaseModel ]); } else { StandaloneDocker::create([ - 'name' => 'coolify-overlay', + 'name' => 'coolify', 'network' => 'coolify', 'server_id' => $this->id, ]); @@ -141,6 +141,10 @@ class Server extends BaseModel { 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() { if ($this->ip === '1.2.3.4') { @@ -194,7 +198,7 @@ class Server extends BaseModel foreach ($this->databases() as $database) { $database->update(['status' => 'exited']); } - foreach ($this->services() as $service) { + foreach ($this->services()->get() as $service) { $apps = $service->applications()->get(); $dbs = $service->databases()->get(); foreach ($apps as $app) { @@ -328,7 +332,7 @@ class Server extends BaseModel } public function isProxyShouldRun() { - if ($this->proxyType() === ProxyTypes::NONE->value) { + if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) { return false; } // foreach ($this->applications() as $application) { @@ -436,7 +440,7 @@ class Server extends BaseModel } $this->settings->is_usable = true; $this->settings->save(); - $this->validateCoolifyNetwork(isSwarm: false); + $this->validateCoolifyNetwork(isSwarm: false, isBuildServer: $this->settings->is_build_server); return true; } public function validateDockerSwarm() @@ -466,8 +470,11 @@ class Server extends BaseModel $this->settings->save(); return true; } - public function validateCoolifyNetwork($isSwarm = false) + public function validateCoolifyNetwork($isSwarm = false, $isBuildServer = false) { + if ($isBuildServer) { + return; + } if ($isSwarm) { return instant_remote_process(["docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false); } else { diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index fe9b46a11..bfd862993 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -52,13 +52,13 @@ class Unreachable extends Notification implements ShouldQueue 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; } public function toTelegram(): array { 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." ]; } } diff --git a/config/sentry.php b/config/sentry.php index eedf1c992..f1f17f030 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // 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 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 5aa8bcb7a..32e5b5873 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ 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'); + }); + } +}; diff --git a/database/seeders/ServerSettingSeeder.php b/database/seeders/ServerSettingSeeder.php index 6d8c68188..af4a2694b 100644 --- a/database/seeders/ServerSettingSeeder.php +++ b/database/seeders/ServerSettingSeeder.php @@ -14,7 +14,7 @@ class ServerSettingSeeder extends Seeder { $server_2 = Server::find(0)->load(['settings']); $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_reachable = true; $server_2->settings->save(); diff --git a/resources/views/components/applications/navbar.blade.php b/resources/views/components/applications/navbar.blade.php deleted file mode 100644 index db69feb6b..000000000 --- a/resources/views/components/applications/navbar.blade.php +++ /dev/null @@ -1,119 +0,0 @@ - diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index 98141b044..1265dc132 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -1,5 +1,5 @@ -
-
- +
- +
Advanced
Advanced configuration for your application.
-
- @if (!$application->settings->is_raw_compose_deployment_enabled) - - @endif - +
+

General

@if ($application->git_based()) + @endif + +

Logs

+ @if (!$application->settings->is_raw_compose_deployment_enabled) + + @endif +

Git

+ @if ($application->git_based()) @endif +

GPU

@if ($application->build_pack !== 'dockercompose')
+ instantSave id="application.settings.is_gpu_enabled" label="Attach GPU" /> @if ($application->settings->is_gpu_enabled) Save @endif diff --git a/resources/views/livewire/project/application/configuration.blade.php b/resources/views/livewire/project/application/configuration.blade.php index dae96831d..3a43a7769 100644 --- a/resources/views/livewire/project/application/configuration.blade.php +++ b/resources/views/livewire/project/application/configuration.blade.php @@ -18,9 +18,11 @@ href="#">Environment Variables @endif - Scheduled Tasks - + @if ($application->build_pack !== 'static' && $application->build_pack !== 'dockercompose') + Storages + + @endif @if ($application->git_based()) Source @@ -28,11 +30,12 @@ Server - @if ($application->build_pack !== 'static' && $application->build_pack !== 'dockercompose') - Storages - - @endif + + Scheduled Tasks + + Webhooks diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 6df481e71..a16953c7e 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -43,7 +43,7 @@ @if ($application->build_pack === 'dockercompose') + 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 documentation." /> @if (count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled) @foreach (data_get($parsedServices, 'services') as $serviceName => $service) @if (!isDatabaseImage(data_get($service, 'image'))) @@ -64,26 +64,28 @@
@endif @if ($application->build_pack !== 'dockercompose') -
- - Generate Domain - -
- @endif +
+ + Generate Domain + +
+ @endif @if ($application->build_pack !== 'dockercompose') -

Docker Registry

+
+

Docker Registry

+ @if ($application->build_pack !== 'dockerimage' && !$application->destination->server->isSwarm()) + + @endif +
@if ($application->destination->server->isSwarm()) @if ($application->build_pack !== 'dockerimage')
Docker Swarm requires the image to be available in a registry. More info here.
@endif - @else - @if ($application->build_pack !== 'dockerimage') -
Push the built image to a docker registry. More info here.
- @endif @endif
@if ($application->build_pack === 'dockerimage') @@ -117,12 +119,18 @@ @endif @if ($application->build_pack !== 'dockerimage') -

Build

+

Build

+ @if ($application->build_pack !== 'dockercompose') +
+ +
+ @endif @if ($application->could_set_build_commands()) @if ($application->build_pack === 'nixpacks') -
Nixpacks will detect the required configuration automatically. - Framework Specific Docs -
+
@@ -131,6 +139,9 @@
+
Nixpacks will detect the required configuration automatically. + Framework Specific Docs +
@endif @endif @if ($application->build_pack === 'dockercompose') @@ -190,7 +201,8 @@ Reload Compose File @if ($application->settings->is_raw_compose_deployment_enabled) + label="Docker Compose Content (applicationId: {{ $application->id }})" + helper="You need to modify the docker compose file." /> @else @@ -203,7 +215,7 @@ @endif @if ($application->build_pack !== 'dockercompose') -

Network

+

Network

@if ($application->settings->is_static || $application->build_pack === 'static') diff --git a/resources/views/livewire/project/application/heading.blade.php b/resources/views/livewire/project/application/heading.blade.php index 2b9f189c2..aea5bb8b8 100644 --- a/resources/views/livewire/project/application/heading.blade.php +++ b/resources/views/livewire/project/application/heading.blade.php @@ -1,4 +1,124 @@ diff --git a/resources/views/livewire/project/new/public-git-repository.blade.php b/resources/views/livewire/project/new/public-git-repository.blade.php index 08a886777..5efeaa392 100644 --- a/resources/views/livewire/project/new/public-git-repository.blade.php +++ b/resources/views/livewire/project/new/public-git-repository.blade.php @@ -10,6 +10,15 @@ Check repository
+ @if (!$branch_found) +
+

Public repositories: https://...

+

Private repositories: git@...

+

Preselect branch: https://github.com/coollabsio/coolify-examples/tree/static to + select 'static' branch.

+
+ @endif @if ($branch_found) @if ($rate_limit_remaining && $rate_limit_reset)
@@ -21,12 +30,20 @@
@if ($git_source === 'other') - @else - @endif + + + + + + +
+ @if ($show_is_static) @if ($is_static) @@ -34,14 +51,14 @@ @endif -
-
- -
+
+ +
+ @endif
- Save New Application + Continue @endif
diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php index caff1fb15..ff2e22ae2 100644 --- a/resources/views/livewire/project/new/select.blade.php +++ b/resources/views/livewire/project/new/select.blade.php @@ -207,7 +207,7 @@
here." label="Include Swarm Clusters" />
@endif --}} diff --git a/resources/views/livewire/project/service/configuration.blade.php b/resources/views/livewire/project/service/configuration.blade.php index 62b02ec88..157a4fa16 100644 --- a/resources/views/livewire/project/service/configuration.blade.php +++ b/resources/views/livewire/project/service/configuration.blade.php @@ -7,6 +7,14 @@ @click.prevent="activeTab = 'service-stack'; window.location.hash = 'service-stack'" href="#">Service Stack + Environment + Variables + Storages Logs - Storages Webhooks - Environment - Variables @endif - @if ( - $resource->persistentStorages()->get()->count() == 0 && - $resource->fileStorages()->get()->count() == 0) -
No storages found.
- @endif @endif
diff --git a/resources/views/livewire/project/shared/destination.blade.php b/resources/views/livewire/project/shared/destination.blade.php index f06e806e0..469439254 100644 --- a/resources/views/livewire/project/shared/destination.blade.php +++ b/resources/views/livewire/project/shared/destination.blade.php @@ -1,7 +1,8 @@

Server

-
The destination server where your application will be deployed to.
-
+
Server related configurations.
+

Destination Server & Network

+
On server {{ data_get($resource, 'destination.server.name') }} diff --git a/resources/views/livewire/project/shared/webhooks.blade.php b/resources/views/livewire/project/shared/webhooks.blade.php index 927674f50..a90b01b58 100644 --- a/resources/views/livewire/project/shared/webhooks.blade.php +++ b/resources/views/livewire/project/shared/webhooks.blade.php @@ -2,11 +2,11 @@
@if ($resource->type() !== 'service') diff --git a/resources/views/livewire/realtime-connection.blade.php b/resources/views/livewire/realtime-connection.blade.php index 75cedea3e..e18c267ec 100644 --- a/resources/views/livewire/realtime-connection.blade.php +++ b/resources/views/livewire/realtime-connection.blade.php @@ -13,7 +13,7 @@ $wire.showNotification = true; @endif 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); } @@ -26,7 +26,7 @@ $wire.showNotification = true; @endif 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); } @@ -38,7 +38,7 @@ WARNING: 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 or get + related documentation or get help on Discord.
Acknowledge the problem and disable this popup diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index 858cfb5da..f801c6444 100644 --- a/resources/views/livewire/server/form.blade.php +++ b/resources/views/livewire/server/form.blade.php @@ -35,7 +35,7 @@
- @if (!$server->settings->is_swarm_worker) + @if (!$server->settings->is_swarm_worker && !$server->settings->is_build_server) @endif @@ -51,29 +51,34 @@
@if (!$server->isLocalhost()) - - @if ($server->isSwarm()) -
Swarm support is in alpha version.
- @endif - @if ($server->settings->is_swarm_worker) - + @if ($server->settings->is_build_server) + @else - - @endif - @if ($server->settings->is_swarm_manager) - - @else - + + @if ($server->isSwarm()) +
Swarm support is experimental.
+ @endif + @if ($server->settings->is_swarm_worker) + + @else + + @endif + @if ($server->settings->is_swarm_manager) + + @else + + @endif @endif @endif
diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php index d1a8f4c15..81d4b18c3 100644 --- a/resources/views/livewire/server/new/by-ip.blade.php +++ b/resources/views/livewire/server/new/by-ip.blade.php @@ -26,23 +26,29 @@ @endforeach
-
Swarm support is in alpha version. Read the docs here.
- @if ($is_swarm_worker) + +
+
+

Swarm Support

+
Swarm support is experimental. Read the docs here.
+ @if ($is_swarm_worker || $is_build_server) @else @endif - @if ($is_swarm_manager) + @if ($is_swarm_manager|| $is_build_server) @else @endif @if ($is_swarm_worker && count($swarm_managers) > 0) @@ -60,7 +66,7 @@ @endif
- Save Server + Continue @endif diff --git a/resources/views/livewire/server/proxy/deploy.blade.php b/resources/views/livewire/server/proxy/deploy.blade.php index d016c1c8e..0bc0ca1ba 100644 --- a/resources/views/livewire/server/proxy/deploy.blade.php +++ b/resources/views/livewire/server/proxy/deploy.blade.php @@ -7,6 +7,15 @@

+ + +

This proxy will be stopped and started. It is not reversible.
All resources will be unavailable + during the restart. +
Please think + again. +

+
+
@if ($server->isFunctional() && data_get($server, 'proxy.type') !== 'NONE') @if (data_get($server, 'proxy.status') === 'running')
@@ -18,6 +27,17 @@ @endif + + + + + + + + Restart Proxy +
- @if ($server->proxyType() !== 'NONE' && $server->isFunctional()) + @if ($server->isFunctional()) @endif
diff --git a/versions.json b/versions.json index 49da94bac..18d534d4f 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "version": "3.12.36" }, "v4": { - "version": "4.0.0-beta.196" + "version": "4.0.0-beta.197" } } }