diff --git a/.github/workflows/development-build.yml b/.github/workflows/development-build.yml
index 384ad1f09..7011fb1cc 100644
--- a/.github/workflows/development-build.yml
+++ b/.github/workflows/development-build.yml
@@ -9,7 +9,7 @@ env:
IMAGE_NAME: "coollabsio/coolify"
jobs:
- build:
+ amd64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -19,10 +19,6 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Get Version
- id: version
- run: |
- echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v4
with:
@@ -31,7 +27,50 @@ jobs:
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
- - uses: sarisia/actions-status-discord@v1
- if: always()
- with:
- webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
+ aarch64:
+ runs-on: [self-hosted, arm64]
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v3
+ - name: Login to ghcr.io
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Build image and push to registry
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ file: docker/prod-ssu/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64
+ merge-manifest:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [amd64, aarch64]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+ - name: Login to ghcr.io
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Create & publish manifest
+ run: |
+ docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
+ - uses: sarisia/actions-status-discord@v1
+ if: always()
+ with:
+ webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
\ No newline at end of file
diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml
index f77010aee..559145269 100644
--- a/.github/workflows/production-build.yml
+++ b/.github/workflows/production-build.yml
@@ -9,7 +9,7 @@ env:
IMAGE_NAME: "coollabsio/coolify"
jobs:
- build:
+ amd64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -31,6 +31,54 @@ jobs:
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ aarch64:
+ runs-on: [self-hosted, arm64]
+ steps:
+ - uses: actions/checkout@v3
+ - name: Login to ghcr.io
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
+ - name: Build image and push to registry
+ uses: docker/build-push-action@v4
+ with:
+ context: .
+ file: docker/prod-ssu/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ merge-manifest:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [amd64, aarch64]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+ - name: Login to ghcr.io
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
+ - name: Create & publish manifest
+ run: |
+ docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
- uses: sarisia/actions-status-discord@v1
if: always()
with:
diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php
index 70b40b48b..2a9fc63a0 100644
--- a/app/Actions/CoolifyTask/RunRemoteProcess.php
+++ b/app/Actions/CoolifyTask/RunRemoteProcess.php
@@ -39,7 +39,7 @@ class RunRemoteProcess
public function __construct(Activity $activity, bool $hide_from_output = false, bool $is_finished = false, bool $ignore_errors = false)
{
- if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value && $activity->getExtraProperty('type') !== ActivityTypes::DEPLOYMENT->value) {
+ if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value) {
throw new \RuntimeException('Incompatible Activity to run a remote command.');
}
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
new file mode 100644
index 000000000..32ff31827
--- /dev/null
+++ b/app/Console/Commands/Init.php
@@ -0,0 +1,31 @@
+cleanup_in_progress_application_deployments();
+ }
+ private function cleanup_in_progress_application_deployments()
+ {
+ // Cleanup any failed deployments
+
+ try {
+ $halted_deployments = ApplicationDeploymentQueue::where('status', '==', 'in_progress')->get();
+ foreach ($halted_deployments as $deployment) {
+ $deployment->status = ApplicationDeploymentStatus::FAILED->value;
+ $deployment->save();
+ }
+ } catch (\Exception $e) {
+ echo "Error: {$e->getMessage()}\n";
+ }
+ }
+}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index d8d5dabd5..3b320366f 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -19,6 +19,7 @@ class Kernel extends ConsoleKernel
} else {
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new InstanceDockerCleanupJob)->everyFiveMinutes();
+ $schedule->job(new InstanceProxyCheckJob)->everyFiveMinutes();
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes();
}
}
diff --git a/app/Enums/ActivityTypes.php b/app/Enums/ActivityTypes.php
index 47fec9424..e2536a7f0 100644
--- a/app/Enums/ActivityTypes.php
+++ b/app/Enums/ActivityTypes.php
@@ -5,5 +5,4 @@ namespace App\Enums;
enum ActivityTypes: string
{
case INLINE = 'inline';
- case DEPLOYMENT = 'deployment';
}
diff --git a/app/Enums/ApplicationDeploymentStatus.php b/app/Enums/ApplicationDeploymentStatus.php
new file mode 100644
index 000000000..6a934c75d
--- /dev/null
+++ b/app/Enums/ApplicationDeploymentStatus.php
@@ -0,0 +1,12 @@
+route('dashboard');
}
- return view('project.application.deployments', ['application' => $application]);
+ ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 8);
+ return view('project.application.deployments', ['application' => $application, 'deployments' => $deployments, 'deployments_count' => $count]);
}
public function deployment()
@@ -60,16 +61,16 @@ class ApplicationController extends Controller
if (!$application) {
return redirect()->route('dashboard');
}
- $activity = Activity::where('properties->type_uuid', '=', $deploymentUuid)->first();
- if (!$activity) {
- return redirect()->route('project.application.deployments', [
- 'project_uuid' => $project->uuid,
- 'environment_name' => $environment->name,
- 'application_uuid' => $application->uuid,
- ]);
- }
- $deployment = ApplicationDeploymentQueue::where('deployment_uuid', $deploymentUuid)->first();
- if (!$deployment) {
+ // $activity = Activity::where('properties->type_uuid', '=', $deploymentUuid)->first();
+ // if (!$activity) {
+ // return redirect()->route('project.application.deployments', [
+ // 'project_uuid' => $project->uuid,
+ // 'environment_name' => $environment->name,
+ // 'application_uuid' => $application->uuid,
+ // ]);
+ // }
+ $application_deployment_queue = ApplicationDeploymentQueue::where('deployment_uuid', $deploymentUuid)->first();
+ if (!$application_deployment_queue) {
return redirect()->route('project.application.deployments', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
@@ -78,8 +79,8 @@ class ApplicationController extends Controller
}
return view('project.application.deployment', [
'application' => $application,
- 'activity' => $activity,
- 'deployment' => $deployment,
+ // 'activity' => $activity,
+ 'application_deployment_queue' => $application_deployment_queue,
'deployment_uuid' => $deploymentUuid,
]);
}
diff --git a/app/Http/Livewire/Project/Application/Deploy.php b/app/Http/Livewire/Application/Heading.php
similarity index 54%
rename from app/Http/Livewire/Project/Application/Deploy.php
rename to app/Http/Livewire/Application/Heading.php
index 1e18772f4..99bc7d4f8 100644
--- a/app/Http/Livewire/Project/Application/Deploy.php
+++ b/app/Http/Livewire/Application/Heading.php
@@ -1,56 +1,39 @@
parameters = getRouteParameters();
- $this->application = Application::where('id', $this->applicationId)->first();
- $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
}
- public function applicationStatusChanged()
+
+ public function check_status()
{
+ dispatch_sync(new ApplicationContainerStatusJob(
+ application: $this->application,
+ container_name: generate_container_name($this->application->uuid),
+ ));
$this->application->refresh();
}
- protected function setDeploymentUuid()
+ public function deploy(bool $force_rebuild = false)
{
- $this->deploymentUuid = new Cuid2(7);
- $this->parameters['deployment_uuid'] = $this->deploymentUuid;
- }
- public function deploy(bool $force = false, bool|null $debug = null)
- {
- if ($debug && !$this->application->settings->is_debug_enabled) {
- $this->application->settings->is_debug_enabled = true;
- $this->application->settings->save();
- }
$this->setDeploymentUuid();
-
queue_application_deployment(
application_id: $this->application->id,
deployment_uuid: $this->deploymentUuid,
- force_rebuild: $force,
+ force_rebuild: $force_rebuild,
);
return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'],
@@ -59,20 +42,22 @@ class Deploy extends Component
'environment_name' => $this->parameters['environment_name'],
]);
}
-
+ public function force_deploy_without_cache()
+ {
+ $this->deploy(force_rebuild: true);
+ }
public function stop()
{
- instant_remote_process(["docker rm -f {$this->application->uuid}"], $this->application->destination->server);
+ remote_process(
+ ["docker rm -f {$this->application->uuid}"],
+ $this->application->destination->server
+ );
$this->application->status = 'stopped';
$this->application->save();
- $this->emit('applicationStatusChanged');
}
-
- public function pollStatus()
+ protected function setDeploymentUuid()
{
- dispatch(new ApplicationContainerStatusJob(
- application: $this->application,
- container_name: generate_container_name($this->application->uuid),
- ));
+ $this->deploymentUuid = new Cuid2(7);
+ $this->parameters['deployment_uuid'] = $this->deploymentUuid;
}
}
diff --git a/app/Http/Livewire/Project/Application/DeploymentLogs.php b/app/Http/Livewire/Project/Application/DeploymentLogs.php
index 3780608da..b39cc47f7 100644
--- a/app/Http/Livewire/Project/Application/DeploymentLogs.php
+++ b/app/Http/Livewire/Project/Application/DeploymentLogs.php
@@ -2,31 +2,23 @@
namespace App\Http\Livewire\Project\Application;
-use App\Enums\ActivityTypes;
-use App\Models\Application;
-use Illuminate\Support\Facades\Redis;
+use App\Models\ApplicationDeploymentQueue;
use Livewire\Component;
-use Spatie\Activitylog\Models\Activity;
class DeploymentLogs extends Component
{
- public Application $application;
- public $activity;
+ public ApplicationDeploymentQueue $application_deployment_queue;
public $isKeepAliveOn = true;
- public $deployment_uuid;
+ protected $listeners = ['refreshQueue'];
+ public function refreshQueue()
+ {
+ $this->application_deployment_queue->refresh();
+ }
public function polling()
{
$this->emit('deploymentFinished');
- if (is_null($this->activity) && isset($this->deployment_uuid)) {
- $this->activity = Activity::query()
- ->where('properties->type', '=', ActivityTypes::DEPLOYMENT->value)
- ->where('properties->type_uuid', '=', $this->deployment_uuid)
- ->first();
- } else {
- $this->activity?->refresh();
- }
-
- if (data_get($this->activity, 'properties.status') == 'finished' || data_get($this->activity, 'properties.status') == 'failed') {
+ $this->application_deployment_queue->refresh();
+ if (data_get($this->application_deployment_queue, 'status') == 'finished' || data_get($this->application_deployment_queue, 'status') == 'failed') {
$this->isKeepAliveOn = false;
}
}
diff --git a/app/Http/Livewire/Project/Application/DeploymentNavbar.php b/app/Http/Livewire/Project/Application/DeploymentNavbar.php
index 8f39ff762..5daa2f84f 100644
--- a/app/Http/Livewire/Project/Application/DeploymentNavbar.php
+++ b/app/Http/Livewire/Project/Application/DeploymentNavbar.php
@@ -2,41 +2,60 @@
namespace App\Http\Livewire\Project\Application;
-use App\Enums\ProcessStatus;
+use App\Enums\ApplicationDeploymentStatus;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\Process;
use Livewire\Component;
+use Illuminate\Support\Str;
class DeploymentNavbar extends Component
{
- public Application $application;
- public $activity;
- public string $deployment_uuid;
protected $listeners = ['deploymentFinished'];
+
+ public ApplicationDeploymentQueue $application_deployment_queue;
+ public bool $is_debug_enabled = false;
+
public function deploymentFinished()
{
- $this->activity->refresh();
+ $this->application_deployment_queue->refresh();
+ }
+ public function show_debug()
+ {
+ $application = Application::find($this->application_deployment_queue->application_id);
+ $application->settings->is_debug_enabled = !$application->settings->is_debug_enabled;
+ $application->settings->save();
+ $this->is_debug_enabled = $application->settings->is_debug_enabled;
+ $this->emit('refreshQueue');
}
public function cancel()
{
try {
- ray('Cancelling deployment: ' . $this->deployment_uuid . ' of application: ' . $this->application->uuid);
-
- // Update deployment queue
- $deployment = ApplicationDeploymentQueue::where('deployment_uuid', $this->deployment_uuid)->first();
- $deployment->status = 'cancelled by user';
- $deployment->save();
-
- // Update activity
- $this->activity->properties = $this->activity->properties->merge([
- 'exitCode' => 1,
- 'status' => ProcessStatus::CANCELLED->value,
- ]);
- $this->activity->save();
-
- // Remove builder container
- instant_remote_process(["docker rm -f {$this->deployment_uuid}"], $this->application->destination->server, throwError: false, repeat: 25);
- queue_next_deployment($this->application);
+ $kill_command = "kill -9 {$this->application_deployment_queue->current_process_id}";
+ $application = Application::find($this->application_deployment_queue->application_id);
+ $server = $application->destination->server;
+ if ($this->application_deployment_queue->current_process_id) {
+ $process = Process::run("ps -p {$this->application_deployment_queue->current_process_id} -o command --no-headers");
+ if (Str::of($process->output())->contains([$server->ip, 'EOF-COOLIFY-SSH'])) {
+ Process::run($kill_command);
+ }
+ $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
+ $new_log_entry = [
+ 'command' => $kill_command,
+ 'output' => "Deployment cancelled by user.",
+ 'type' => 'stderr',
+ 'order' => count($previous_logs) + 1,
+ 'timestamp' => Carbon::now('UTC'),
+ 'hidden' => false,
+ ];
+ $previous_logs[] = $new_log_entry;
+ $this->application_deployment_queue->update([
+ 'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR),
+ 'current_process_id' => null,
+ 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
+ ]);
+ }
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
diff --git a/app/Http/Livewire/Project/Application/Deployments.php b/app/Http/Livewire/Project/Application/Deployments.php
index 9dd6e9631..16b40895b 100644
--- a/app/Http/Livewire/Project/Application/Deployments.php
+++ b/app/Http/Livewire/Project/Application/Deployments.php
@@ -7,7 +7,7 @@ use Livewire\Component;
class Deployments extends Component
{
- public int $application_id;
+ public Application $application;
public $deployments = [];
public int $deployments_count = 0;
public string $current_url;
@@ -18,6 +18,17 @@ class Deployments extends Component
public function mount()
{
$this->current_url = url()->current();
+ $this->show_more();
+ }
+ private function show_more()
+ {
+ if (count($this->deployments) !== 0) {
+ $this->show_next = true;
+ if (count($this->deployments) < $this->default_take) {
+ $this->show_next = false;
+ }
+ return;
+ }
}
public function reload_deployments()
{
@@ -28,17 +39,11 @@ class Deployments extends Component
if ($take) {
$this->skip = $this->skip + $take;
}
-
$take = $this->default_take;
- ['deployments' => $deployments, 'count' => $count] = Application::find($this->application_id)->deployments($this->skip, $take);
+
+ ['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $take);
$this->deployments = $deployments;
$this->deployments_count = $count;
- if (count($this->deployments) !== 0) {
- $this->show_next = true;
- if (count($this->deployments) < $take) {
- $this->show_next = false;
- }
- return;
- }
+ $this->show_more();
}
}
diff --git a/app/Http/Livewire/Project/New/PublicGitRepository.php b/app/Http/Livewire/Project/New/PublicGitRepository.php
index 05793695e..9e24ed56d 100644
--- a/app/Http/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Http/Livewire/Project/New/PublicGitRepository.php
@@ -8,6 +8,7 @@ use App\Models\GitlabApp;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
+use Illuminate\Support\Facades\Log;
use Livewire\Component;
use Spatie\Url\Url;
@@ -21,15 +22,17 @@ class PublicGitRepository extends Component
public $parameters;
public $query;
- public $branches = [];
+ public bool $branch_found = false;
public string $selected_branch = 'main';
public bool $is_static = false;
public string|null $publish_directory = null;
+ public string $git_branch;
+ public int $rate_limit_remaining = 0;
+ public int $rate_limit_reset = 0;
private GithubApp|GitlabApp $git_source;
private string $git_host;
private string $git_repository;
- private string $git_branch;
protected $rules = [
'repository_url' => 'required|url',
@@ -64,16 +67,17 @@ class PublicGitRepository extends Component
}
$this->emit('success', 'Application settings updated!');
}
- public function load_branches()
+ public function load_branch()
{
+ $this->branch_found = false;
$this->validate([
'repository_url' => 'required|url'
]);
$this->get_git_source();
try {
- ['data' => $data] = git_api(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches");
- $this->branches = collect($data)->pluck('name')->toArray();
+ ['data' => $data, 'rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = git_api(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}");
+ $this->branch_found = true;
} catch (\Throwable $e) {
return general_error_handler(err: $e, that: $this);
}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 4f3911353..18b7dc6d1 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -2,281 +2,172 @@
namespace App\Jobs;
-use App\Actions\CoolifyTask\RunRemoteProcess;
-use App\Data\CoolifyTaskArgs;
-use App\Enums\ActivityTypes;
-use App\Enums\ProcessStatus;
+use App\Enums\ApplicationDeploymentStatus;
+use App\Enums\ProxyTypes;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
-use App\Notifications\Notifications\Application\DeployedSuccessfullyNotification;
-use App\Notifications\Notifications\Application\DeployedWithErrorNotification;
+use App\Models\GithubApp;
+use App\Models\GitlabApp;
+use App\Models\Server;
+use App\Models\StandaloneDocker;
+use App\Models\SwarmDocker;
+use App\Traits\ExecuteRemoteCommand;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\Notification;
-use Illuminate\Support\Facades\Storage;
-use Spatie\Activitylog\Models\Activity;
-use Symfony\Component\Yaml\Yaml;
-use Illuminate\Support\Str;
use Spatie\Url\Url;
+use Illuminate\Support\Str;
+use Symfony\Component\Yaml\Yaml;
+use Throwable;
use Visus\Cuid2\Cuid2;
class ApplicationDeploymentJob implements ShouldQueue
{
- use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
-
- private Application $application;
- private ApplicationDeploymentQueue $application_deployment_queue;
- private $destination;
- private $source;
- private Activity $activity;
-
- private string|null $git_commit = null;
- private string $workdir;
- private string $docker_compose;
- private $build_args;
- private $env_args;
- private string $build_image_name;
- private string $production_image_name;
- private string $container_name;
- private ApplicationPreview|null $preview = null;
+ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
public static int $batch_counter = 0;
- public $timeout = 10200;
- public function __construct(
- public int $application_deployment_queue_id,
- public string $deployment_uuid,
- public string $application_id,
- public bool $force_rebuild = false,
- public string $rollback_commit = 'HEAD',
- public int $pull_request_id = 0,
- ) {
- $this->application_deployment_queue = ApplicationDeploymentQueue::find($this->application_deployment_queue_id);
- $this->application_deployment_queue->update([
- 'status' => ProcessStatus::IN_PROGRESS->value,
- ]);
- if ($this->rollback_commit) {
- $this->git_commit = $this->rollback_commit;
- }
+ private int $application_deployment_queue_id;
- $this->application = Application::find($this->application_id);
+ private ApplicationDeploymentQueue $application_deployment_queue;
+ private Application $application;
+ private string $deployment_uuid;
+ private int $pull_request_id;
+ private string $commit;
+ private bool $force_rebuild;
- ray('pullrequestId: ' . $this->pull_request_id);
+ private GithubApp|GitlabApp $source;
+ private StandaloneDocker|SwarmDocker $destination;
+ private Server $server;
+ private string $private_key_location;
+ private ApplicationPreview|null $preview = null;
+
+ private string $container_name;
+ private string $workdir;
+ private string $build_image_name;
+ private string $production_image_name;
+ private bool $is_debug_enabled;
+ private $build_args;
+ private $env_args;
+ private $docker_compose;
+
+ private $log_model;
+ private Collection $saved_outputs;
+ public function __construct(int $application_deployment_queue_id)
+ {
+ ray()->clearScreen();
+ $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
+ $this->log_model = $this->application_deployment_queue;
+ $this->application = Application::find($this->application_deployment_queue->application_id);
+
+ $this->application_deployment_queue_id = $application_deployment_queue_id;
+ $this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
+ $this->pull_request_id = $this->application_deployment_queue->pull_request_id;
+ $this->commit = $this->application_deployment_queue->commit;
+ $this->force_rebuild = $this->application_deployment_queue->force_rebuild;
+
+ $this->source = $this->application->source->getMorphClass()::where('id', $this->application->source->id)->first();
+ $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
+ $this->server = $this->destination->server;
+ $this->private_key_location = save_private_key_for_server($this->server);
+
+ $this->workdir = "/artifacts/{$this->deployment_uuid}";
+ $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
+
+ $this->container_name = generate_container_name($this->application->uuid, $this->pull_request_id);
+ $this->private_key_location = save_private_key_for_server($this->server);
+ $this->saved_outputs = collect();
+
+ // Set preview fqdn
if ($this->pull_request_id !== 0) {
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
- ray($this->preview);
+ if ($this->application->fqdn) {
+ $preview_fqdn = data_get($this->preview, 'fqdn');
+ $template = $this->application->preview_url_template;
+ $url = Url::fromString($this->application->fqdn);
+ $host = $url->getHost();
+ $schema = $url->getScheme();
+ $random = new Cuid2(7);
+ $preview_fqdn = str_replace('{{random}}', $random, $template);
+ $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
+ $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
+ $preview_fqdn = "$schema://$preview_fqdn";
+ $this->preview->fqdn = $preview_fqdn;
+ $this->preview->save();
+ }
}
-
- $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
-
- $server = $this->destination->server;
-
- $private_key_location = save_private_key_for_server($server);
-
- $remoteProcessArgs = new CoolifyTaskArgs(
- server_ip: $server->ip,
- private_key_location: $private_key_location,
- command: 'overwritten-later',
- port: $server->port,
- user: $server->user,
- type: ActivityTypes::DEPLOYMENT->value,
- type_uuid: $this->deployment_uuid,
- );
-
- $this->activity = activity()
- ->performedOn($this->application)
- ->withProperties($remoteProcessArgs->toArray())
- ->event(ActivityTypes::DEPLOYMENT->value)
- ->log("[]");
}
+
public function handle(): void
{
+ $this->application_deployment_queue->update([
+ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
+ ]);
try {
- if ($this->application->deploymentType() === 'source') {
- $this->source = $this->application->source->getMorphClass()::where('id', $this->application->source->id)->first();
- }
-
- $this->workdir = "/artifacts/{$this->deployment_uuid}";
-
if ($this->pull_request_id !== 0) {
- ray('Deploying pull/' . $this->pull_request_id . '/head for application: ' . $this->application->name)->green();
- if ($this->application->fqdn) {
- $preview_fqdn = data_get($this->preview, 'fqdn');
- $template = $this->application->preview_url_template;
- $url = Url::fromString($this->application->fqdn);
- $host = $url->getHost();
- $schema = $url->getScheme();
- $random = new Cuid2(7);
- $preview_fqdn = str_replace('{{random}}', $random, $template);
- $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
- $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
- $preview_fqdn = "$schema://$preview_fqdn";
- $this->preview->fqdn = $preview_fqdn;
- $this->preview->save();
- }
$this->deploy_pull_request();
} else {
$this->deploy();
}
+ $this->next(ApplicationDeploymentStatus::FINISHED->value);
} catch (\Exception $e) {
- $this->execute_now([
- "echo '\nOops something is not okay, are you okay? 😢'",
- "echo '\n\n{$e->getMessage()}'",
+ ray($e);
+ $this->execute_remote_command([
+ ["echo '\nOops something is not okay, are you okay? 😢'"],
+ ["echo '\n\n{$e->getMessage()}'"]
]);
- $this->fail();
+ $this->fail($e->getMessage());
} finally {
- if (isset($this->docker_compose)) {
- Storage::disk('deployments')->put(Str::kebab($this->application->name) . '/docker-compose.yml', $this->docker_compose);
- }
- $this->execute_now(["docker rm -f {$this->deployment_uuid} >/dev/null 2>&1"], hideFromOutput: true);
+ // if (isset($this->docker_compose)) {
+ // Storage::disk('deployments')->put(Str::kebab($this->application->name) . '/docker-compose.yml', $this->docker_compose);
+ // }
+ $this->execute_remote_command(
+ [
+ "docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
+ "hidden" => true,
+ ]
+ );
}
}
-
- private function start_builder_image()
+ public function failed(Throwable $exception): void
{
- $this->execute_now([
- "echo -n 'Pulling latest version of the builder image (ghcr.io/coollabsio/coolify-builder)... '",
- ]);
- $this->execute_now([
- "docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-builder",
- ], isDebuggable: true);
- $this->execute_now([
- "echo 'Done.'"
- ]);
- $this->execute_now([
- $this->execute_in_builder("mkdir -p {$this->workdir}"),
- ]);
+ ray($exception);
+ $this->next(ApplicationDeploymentStatus::FAILED->value);
}
-
- private function clone_repository()
+ private function execute_in_builder(string $command)
{
- $this->execute_now([
- "echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} to {$this->workdir}... '"
- ]);
-
- $this->execute_now([
- ...$this->importing_git_repository(),
- ], 'importing_git_repository');
-
- $this->execute_now([
- "echo 'Done.'"
- ]);
- // Get git commit
- $this->execute_now([$this->execute_in_builder("cd {$this->workdir} && git rev-parse HEAD")], 'commit_sha', hideFromOutput: true);
- $this->git_commit = $this->activity->properties->get('commit_sha');
- }
-
- private function cleanup_git()
- {
- $this->execute_now([
- $this->execute_in_builder("rm -fr {$this->workdir}/.git")
- ], hideFromOutput: true);
- }
- private function generate_buildpack()
- {
- $this->execute_now([
- "echo -n 'Generating nixpacks configuration... '",
- ]);
- $this->execute_now([
- $this->nixpacks_build_cmd(),
- $this->execute_in_builder("cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile"),
- $this->execute_in_builder("rm -f {$this->workdir}/.nixpacks/Dockerfile"),
- ], isDebuggable: true);
- $this->execute_now([
- "echo 'Done... '",
- ]);
- }
- private function build_image()
- {
- $this->execute_now([
- "echo -n 'Building image... '",
- ]);
-
- if ($this->application->settings->is_static) {
- $this->execute_now([
- $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"),
- ], isDebuggable: true);
-
- $dockerfile = "FROM {$this->application->static_image}
-WORKDIR /usr/share/nginx/html/
-LABEL coolify.deploymentId={$this->deployment_uuid}
-COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .";
- $docker_file = base64_encode($dockerfile);
-
- $this->execute_now([
- $this->execute_in_builder("echo '{$docker_file}' | base64 -d > {$this->workdir}/Dockerfile-prod"),
- $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"),
- ], hideFromOutput: true);
- } else {
- $this->execute_now([
- $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"),
- ], isDebuggable: true);
- }
- $this->execute_now([
- "echo 'Done.'",
- ]);
- }
- private function deploy_pull_request()
- {
- dispatch(new ApplicationPullRequestUpdateJob(
- application_id: $this->application->id,
- pull_request_id: $this->pull_request_id,
- deployment_uuid: $this->deployment_uuid,
- status: 'in_progress'
- ));
- $this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build";
- $this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}";
- $this->container_name = generate_container_name($this->application->uuid, $this->pull_request_id);
- // Deploy pull request
- $this->execute_now([
- "echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch} PR#{$this->pull_request_id}...'",
- ]);
- $this->start_builder_image();
- $this->clone_repository();
- $this->cleanup_git();
- $this->generate_buildpack();
- $this->generate_compose_file();
- // Needs separate preview variables
- // $this->generate_build_env_variables();
- // $this->add_build_env_variables_to_dockerfile();
- $this->build_image();
- $this->stop_running_container();
- $this->start_by_compose_file();
- $this->next(ProcessStatus::FINISHED->value);
+ return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1'";
}
private function deploy()
{
- $this->container_name = generate_container_name($this->application->uuid);
- // Deploy normal commit
- $this->execute_now([
- "echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}...'",
- ]);
- $this->start_builder_image();
- ray('Rollback Commit: ' . $this->rollback_commit)->green();
- if ($this->rollback_commit === 'HEAD') {
- $this->clone_repository();
- }
- $this->build_image_name = "{$this->application->uuid}:{$this->git_commit}-build";
- $this->production_image_name = "{$this->application->uuid}:{$this->git_commit}";
- ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name:' . $this->production_image_name)->green();
+
+ $this->execute_remote_command(
+ [
+ "echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'"
+ ],
+ );
+ $this->prepare_builder_image();
+ $this->clone_repository();
+
+ $this->build_image_name = "{$this->application->git_repository}:{$this->commit}-build";
+ $this->production_image_name = "{$this->application->uuid}:{$this->commit}";
+ ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
+
if (!$this->force_rebuild) {
- $this->execute_now([
- "docker images -q {$this->application->uuid}:{$this->git_commit} 2>/dev/null",
- ], 'local_image_found', hideFromOutput: true, ignoreErrors: true);
- $image_found = Str::of($this->activity->properties->get('local_image_found'))->trim()->isNotEmpty();
- if ($image_found) {
- $this->execute_now([
- "echo 'Docker Image found locally with the same Git Commit SHA. Build skipped...'"
+ $this->execute_remote_command([
+ "docker images -q {$this->application->uuid}:{$this->commit} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
+ ]);
+ if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
+ $this->execute_remote_command([
+ "echo 'Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped...'"
]);
$this->generate_compose_file();
$this->stop_running_container();
$this->start_by_compose_file();
- $this->next(ProcessStatus::FINISHED->value);
return;
}
}
@@ -288,88 +179,103 @@ COPY --from=$this->build_image_name /app/{$this->application->publish_directory}
$this->build_image();
$this->stop_running_container();
$this->start_by_compose_file();
- $this->next(ProcessStatus::FINISHED->value);
}
-
- public function failed(): void
+ private function deploy_pull_request()
{
- $this->next(ProcessStatus::ERROR->value);
+ $this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build";
+ $this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}";
+ ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
+ $this->execute_remote_command([
+ "echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'",
+ ]);
+ $this->prepare_builder_image();
+ $this->clone_repository();
+ $this->cleanup_git();
+ $this->generate_buildpack();
+ $this->generate_compose_file();
+ // Needs separate preview variables
+ // $this->generate_build_env_variables();
+ // $this->add_build_env_variables_to_dockerfile();
+ $this->build_image();
+ $this->stop_running_container();
+ $this->start_by_compose_file();
}
private function next(string $status)
{
- if (!Str::of($this->application_deployment_queue->status)->startsWith('cancelled')) {
- ray('Next Status: ' . $status)->green();
+ // If the deployment is cancelled by the user, don't update the status
+ if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
$this->application_deployment_queue->update([
'status' => $status,
]);
- $this->activity->properties = $this->activity->properties->merge([
- 'status' => $status,
- ]);
- $this->activity->save();
- }
- if ($this->pull_request_id) {
- dispatch(new ApplicationPullRequestUpdateJob(
- application_id: $this->application->id,
- pull_request_id: $this->pull_request_id,
- deployment_uuid: $this->deployment_uuid,
- status: $status
- ));
- }
- if ($this->application->fqdn) {
- dispatch(new InstanceProxyCheckJob());
}
queue_next_deployment($this->application);
- if ($status === ProcessStatus::FINISHED->value) {
- $this->application->environment->project->team->notify(new DeployedSuccessfullyNotification($this->application, $this->deployment_uuid, $this->preview));
- }
- if ($status === ProcessStatus::ERROR->value) {
- $this->application->environment->project->team->notify(new DeployedWithErrorNotification($this->application, $this->deployment_uuid, $this->preview));
- }
}
- private function execute_in_builder(string $command)
+ private function start_by_compose_file()
{
- return "docker exec {$this->deployment_uuid} bash -c '{$command}'";
+ $this->execute_remote_command(
+ ["echo -n 'Starting new application... '"],
+ [$this->execute_in_builder("docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true],
+ ["echo 'Done. 🎉'"],
+ );
}
- private function generate_environment_variables($ports)
+ private function stop_running_container()
{
- $environment_variables = collect();
- ray('Generate Environment Variables')->green();
- if ($this->pull_request_id === 0) {
- ray($this->application->runtime_environment_variables)->green();
- foreach ($this->application->runtime_environment_variables as $env) {
- $environment_variables->push("$env->key=$env->value");
- }
- } else {
- ray($this->application->runtime_environment_variables_preview)->green();
- foreach ($this->application->runtime_environment_variables_preview as $env) {
- $environment_variables->push("$env->key=$env->value");
- }
- }
- // Add PORT if not exists, use the first port as default
- if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
- $environment_variables->push("PORT={$ports[0]}");
- }
- return $environment_variables->all();
+ $this->execute_remote_command(
+ ["echo -n 'Removing old running application.'"],
+ [$this->execute_in_builder("docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true],
+ );
}
- private function generate_env_variables()
+ private function build_image()
{
- $this->env_args = collect([]);
- if ($this->pull_request_id === 0) {
- foreach ($this->application->nixpacks_environment_variables as $env) {
- $this->env_args->push("--env {$env->key}={$env->value}");
- }
- } else {
- foreach ($this->application->nixpacks_environment_variables_preview as $env) {
- $this->env_args->push("--env {$env->key}={$env->value}");
- }
- }
+ $this->execute_remote_command([
+ "echo -n 'Building docker image.'",
+ ]);
- $this->env_args = $this->env_args->implode(' ');
+ if ($this->application->settings->is_static) {
+ $this->execute_remote_command([
+ $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
+ ]);
+
+ $dockerfile = "FROM {$this->application->static_image}
+WORKDIR /usr/share/nginx/html/
+LABEL coolify.deploymentId={$this->deployment_uuid}
+COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .";
+ $docker_file = base64_encode($dockerfile);
+
+ $this->execute_remote_command(
+ [
+ $this->execute_in_builder("echo '{$docker_file}' | base64 -d > {$this->workdir}/Dockerfile-prod")
+ ],
+ [
+ $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
+ ]
+ );
+ } else {
+ $this->execute_remote_command([
+ $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
+ ]);
+ }
+ }
+ private function add_build_env_variables_to_dockerfile()
+ {
+ $this->execute_remote_command([
+ $this->execute_in_builder("cat {$this->workdir}/Dockerfile"), "hidden" => true, "save" => 'dockerfile'
+ ]);
+ $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
+
+ foreach ($this->application->build_environment_variables as $env) {
+ $dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
+ }
+ $dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
+ $this->execute_remote_command([
+ $this->execute_in_builder("echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/Dockerfile"),
+ "hidden" => true
+ ]);
}
private function generate_build_env_variables()
{
- $this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->git_commit}"]);
+ $this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->commit}"]);
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
$this->build_args->push("--build-arg {$env->key}={$env->value}");
@@ -382,22 +288,8 @@ COPY --from=$this->build_image_name /app/{$this->application->publish_directory}
$this->build_args = $this->build_args->implode(' ');
}
- private function add_build_env_variables_to_dockerfile()
- {
- $this->execute_now([
- $this->execute_in_builder("cat {$this->workdir}/Dockerfile")
- ], propertyName: 'dockerfile', hideFromOutput: true);
- $dockerfile = collect(Str::of($this->activity->properties->get('dockerfile'))->trim()->explode("\n"));
- foreach ($this->application->build_environment_variables as $env) {
- $dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
- }
- $dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
- $this->execute_now([
- $this->execute_in_builder("echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/Dockerfile")
- ], hideFromOutput: true);
- }
- private function generate_docker_compose()
+ private function generate_compose_file()
{
$ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array;
@@ -454,7 +346,9 @@ COPY --from=$this->build_image_name /app/{$this->application->publish_directory}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- return Yaml::dump($docker_compose, 10);
+ $this->docker_compose = Yaml::dump($docker_compose, 10);
+ $docker_compose_base64 = base64_encode($this->docker_compose);
+ $this->execute_remote_command([$this->execute_in_builder("echo '{$docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
}
private function generate_local_persistent_volumes()
{
@@ -469,7 +363,6 @@ COPY --from=$this->build_image_name /app/{$this->application->publish_directory}
ray('local_persistent_volumes', $local_persistent_volumes)->green();
return $local_persistent_volumes;
}
-
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
@@ -490,6 +383,27 @@ COPY --from=$this->build_image_name /app/{$this->application->publish_directory}
}
return $local_persistent_volumes_names;
}
+ private function generate_environment_variables($ports)
+ {
+ $environment_variables = collect();
+ ray('Generate Environment Variables')->green();
+ if ($this->pull_request_id === 0) {
+ ray($this->application->runtime_environment_variables)->green();
+ foreach ($this->application->runtime_environment_variables as $env) {
+ $environment_variables->push("$env->key=$env->value");
+ }
+ } else {
+ ray($this->application->runtime_environment_variables_preview)->green();
+ foreach ($this->application->runtime_environment_variables_preview as $env) {
+ $environment_variables->push("$env->key=$env->value");
+ }
+ }
+ // Add PORT if not exists, use the first port as default
+ if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
+ $environment_variables->push("PORT={$ports[0]}");
+ }
+ return $environment_variables->all();
+ }
private function generate_healthcheck_commands()
{
if (!$this->application->health_check_port) {
@@ -506,7 +420,6 @@ COPY --from=$this->build_image_name /app/{$this->application->publish_directory}
}
return implode(' ', $generated_healthchecks_commands);
}
-
private function set_labels_for_applications()
{
$labels = [];
@@ -520,169 +433,66 @@ COPY --from=$this->build_image_name /app/{$this->application->publish_directory}
}
if ($this->application->fqdn) {
if ($this->pull_request_id !== 0) {
- $preview_fqdn = data_get($this->preview, 'fqdn');
- $template = $this->application->preview_url_template;
- $url = Url::fromString($this->application->fqdn);
- $host = $url->getHost();
- $schema = $url->getScheme();
- $random = new Cuid2(7);
- $preview_fqdn = str_replace('{{random}}', $random, $template);
- $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
- $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
- $preview_fqdn = "$schema://$preview_fqdn";
- $this->preview->fqdn = $preview_fqdn;
- $this->preview->save();
- $domains = Str::of($preview_fqdn)->explode(',');
+ $domains = Str::of(data_get($this->preview, 'fqdn'))->explode(',');
} else {
- $domains = Str::of($this->application->fqdn)->explode(',');
+ $domains = Str::of(data_get($this->application, 'fqdn'))->explode(',');
}
- $labels[] = 'traefik.enable=true';
- foreach ($domains as $domain) {
- $url = Url::fromString($domain);
- $host = $url->getHost();
- $path = $url->getPath();
- $schema = $url->getScheme();
- $slug = Str::slug($host . $path);
+ if ($this->application->destination->server->proxy->type === ProxyTypes::TRAEFIK_V2->value) {
+ $labels[] = 'traefik.enable=true';
+ foreach ($domains as $domain) {
+ $url = Url::fromString($domain);
+ $host = $url->getHost();
+ $path = $url->getPath();
+ $schema = $url->getScheme();
+ $slug = Str::slug($host . $path);
- $http_label = "{$this->application->uuid}-{$slug}-http";
- $https_label = "{$this->application->uuid}-{$slug}-https";
+ $http_label = "{$this->application->uuid}-{$slug}-http";
+ $https_label = "{$this->application->uuid}-{$slug}-https";
- if ($schema === 'https') {
- // Set labels for https
- $labels[] = "traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
- $labels[] = "traefik.http.routers.{$https_label}.entryPoints=https";
- $labels[] = "traefik.http.routers.{$https_label}.middlewares=gzip";
- if ($path !== '/') {
- $labels[] = "traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix";
- $labels[] = "traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}";
- }
+ if ($schema === 'https') {
+ // Set labels for https
+ $labels[] = "traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
+ $labels[] = "traefik.http.routers.{$https_label}.entryPoints=https";
+ $labels[] = "traefik.http.routers.{$https_label}.middlewares=gzip";
+ if ($path !== '/') {
+ $labels[] = "traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix";
+ $labels[] = "traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}";
+ }
- $labels[] = "traefik.http.routers.{$https_label}.tls=true";
- $labels[] = "traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt";
+ $labels[] = "traefik.http.routers.{$https_label}.tls=true";
+ $labels[] = "traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt";
- // Set labels for http (redirect to https)
- $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
- $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http";
- if ($this->application->settings->is_force_https_enabled) {
- $labels[] = "traefik.http.routers.{$http_label}.middlewares=redirect-to-https";
- }
- } else {
- // Set labels for http
- $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
- $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http";
- $labels[] = "traefik.http.routers.{$http_label}.middlewares=gzip";
- if ($path !== '/') {
- $labels[] = "traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix";
- $labels[] = "traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}";
+ // Set labels for http (redirect to https)
+ $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
+ $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http";
+ if ($this->application->settings->is_force_https_enabled) {
+ $labels[] = "traefik.http.routers.{$http_label}.middlewares=redirect-to-https";
+ }
+ } else {
+ // Set labels for http
+ $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
+ $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http";
+ $labels[] = "traefik.http.routers.{$http_label}.middlewares=gzip";
+ if ($path !== '/') {
+ $labels[] = "traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix";
+ $labels[] = "traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}";
+ }
}
}
}
}
return $labels;
}
-
- private function execute_now(
- array|Collection $command,
- string $propertyName = null,
- bool $isFinished = false,
- bool $hideFromOutput = false,
- bool $isDebuggable = false,
- bool $ignoreErrors = false
- ) {
- static::$batch_counter++;
-
- if ($command instanceof Collection) {
- $commandText = $command->implode("\n");
- } else {
- $commandText = collect($command)->implode("\n");
- }
- ray('Executing command: ' . $commandText)->green();
- $this->activity->properties = $this->activity->properties->merge([
- 'command' => $commandText,
- ]);
- $this->activity->save();
- if ($isDebuggable && !$this->application->settings->is_debug_enabled) {
- ray('Debugging is disabled for this application. Skipping command.')->green();
- $hideFromOutput = true;
- }
- $remote_process = resolve(RunRemoteProcess::class, [
- 'activity' => $this->activity,
- 'hide_from_output' => $hideFromOutput,
- 'is_finished' => $isFinished,
- 'ingore_errors' => $ignoreErrors,
- ]);
- $result = $remote_process();
- if ($propertyName) {
- $this->activity->properties = $this->activity->properties->merge([
- $propertyName => trim($result->output()),
- ]);
- $this->activity->save();
- }
-
- if ($result->exitCode() != 0 && $result->errorOutput() && !$ignoreErrors) {
- throw new \RuntimeException($result->errorOutput());
- }
- }
- private function set_git_import_settings($git_clone_command)
+ private function generate_buildpack()
{
- if ($this->application->git_commit_sha !== 'HEAD') {
- $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1";
- }
- if ($this->application->settings->is_git_submodules_enabled) {
- $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git submodule update --init --recursive";
- }
- if ($this->application->settings->is_git_lfs_enabled) {
- $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git lfs pull";
- }
- return $git_clone_command;
- }
- private function importing_git_repository()
- {
- $git_clone_command = "git clone -q -b {$this->application->git_branch}";
- if ($this->pull_request_id !== 0) {
- $pr_branch_name = "pr-{$this->pull_request_id}-coolify";
- }
-
- if ($this->application->deploymentType() === 'source') {
- $source_html_url = data_get($this->application, 'source.html_url');
- $url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
- $source_html_url_host = $url['host'];
- $source_html_url_scheme = $url['scheme'];
-
- if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
- if ($this->source->is_public) {
- $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->workdir}";
- $git_clone_command = $this->set_git_import_settings($git_clone_command);
-
- $commands = [$this->execute_in_builder($git_clone_command)];
-
- if ($this->pull_request_id !== 0) {
- $commands[] = $this->execute_in_builder("cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name >/dev/null 2>&1 && git checkout $pr_branch_name >/dev/null 2>&1");
- }
- return $commands;
- } else {
- $github_access_token = generate_github_installation_token($this->source);
- $commands = [
- $this->execute_in_builder("git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->workdir}")
- ];
- if ($this->pull_request_id !== 0) {
- $commands[] = $this->execute_in_builder("cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name");
- }
- return $commands;
- }
- }
- }
- if ($this->application->deploymentType() === 'deploy_key') {
- $private_key = base64_encode($this->application->private_key->private_key);
- $git_clone_command = "GIT_SSH_COMMAND=\"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_full_url} {$this->workdir}";
- $git_clone_command = $this->set_git_import_settings($git_clone_command);
- return [
- $this->execute_in_builder("mkdir -p /root/.ssh"),
- $this->execute_in_builder("echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
- $this->execute_in_builder("chmod 600 /root/.ssh/id_rsa"),
- $this->execute_in_builder($git_clone_command)
- ];
- }
+ $this->execute_remote_command(
+ [
+ "echo -n 'Generating nixpacks configuration.'",
+ ],
+ [$this->nixpacks_build_cmd()],
+ [$this->execute_in_builder("cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")],
+ [$this->execute_in_builder("rm -f {$this->workdir}/.nixpacks/Dockerfile")]
+ );
}
private function nixpacks_build_cmd()
{
@@ -700,32 +510,113 @@ COPY --from=$this->build_image_name /app/{$this->application->publish_directory}
$nixpacks_command .= " {$this->workdir}";
return $this->execute_in_builder($nixpacks_command);
}
- private function stop_running_container()
+ private function generate_env_variables()
{
- $this->execute_now([
- "echo -n 'Removing old instance... '",
- $this->execute_in_builder("docker rm -f $this->container_name >/dev/null 2>&1"),
- "echo 'Done.'",
- ]);
+ $this->env_args = collect([]);
+ if ($this->pull_request_id === 0) {
+ foreach ($this->application->nixpacks_environment_variables as $env) {
+ $this->env_args->push("--env {$env->key}={$env->value}");
+ }
+ } else {
+ foreach ($this->application->nixpacks_environment_variables_preview as $env) {
+ $this->env_args->push("--env {$env->key}={$env->value}");
+ }
+ }
+
+ $this->env_args = $this->env_args->implode(' ');
}
- private function start_by_compose_file()
+ private function cleanup_git()
{
- $this->execute_now([
- "echo -n 'Starting your application... '",
- ]);
- $this->execute_now([
- $this->execute_in_builder("docker compose --project-directory {$this->workdir} up -d >/dev/null"),
- ], isDebuggable: true);
- $this->execute_now([
- "echo 'Done. 🎉'",
- ], isFinished: true);
+ $this->execute_remote_command(
+ [$this->execute_in_builder("rm -fr {$this->workdir}/.git")],
+ );
}
- private function generate_compose_file()
+ private function prepare_builder_image()
{
- $this->docker_compose = $this->generate_docker_compose();
- $docker_compose_base64 = base64_encode($this->docker_compose);
- $this->execute_now([
- $this->execute_in_builder("echo '{$docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml")
- ], hideFromOutput: true);
+ $this->execute_remote_command(
+ [
+ "echo -n 'Pulling latest version of the builder image (ghcr.io/coollabsio/coolify-builder).'",
+ ],
+ [
+ "docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-builder",
+ "hidden" => true,
+ ],
+ [
+ "command" => $this->execute_in_builder("mkdir -p {$this->workdir}")
+ ],
+ );
+ }
+ private function set_git_import_settings($git_clone_command)
+ {
+ if ($this->application->git_commit_sha !== 'HEAD') {
+ $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1";
+ }
+ if ($this->application->settings->is_git_submodules_enabled) {
+ $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git submodule update --init --recursive";
+ }
+ if ($this->application->settings->is_git_lfs_enabled) {
+ $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git lfs pull";
+ }
+ return $git_clone_command;
+ }
+ private function importing_git_repository()
+ {
+ $commands = collect([]);
+ $git_clone_command = "git clone -q -b {$this->application->git_branch}";
+ if ($this->pull_request_id !== 0) {
+ $pr_branch_name = "pr-{$this->pull_request_id}-coolify";
+ }
+
+ if ($this->application->deploymentType() === 'source') {
+ $source_html_url = data_get($this->application, 'source.html_url');
+ $url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
+ $source_html_url_host = $url['host'];
+ $source_html_url_scheme = $url['scheme'];
+
+ if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
+ if ($this->source->is_public) {
+ $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->workdir}";
+ $git_clone_command = $this->set_git_import_settings($git_clone_command);
+
+ $commands->push($this->execute_in_builder($git_clone_command));
+ } else {
+ $github_access_token = generate_github_installation_token($this->source);
+ $commands->push($this->execute_in_builder("git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->workdir}"));
+ }
+ if ($this->pull_request_id !== 0) {
+ $commands->push($this->execute_in_builder("cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name"));
+ }
+ return $commands->implode(' && ');
+ }
+ }
+ if ($this->application->deploymentType() === 'deploy_key') {
+ $private_key = base64_encode($this->application->private_key->private_key);
+ $git_clone_command = "GIT_SSH_COMMAND=\"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_full_url} {$this->workdir}";
+ $git_clone_command = $this->set_git_import_settings($git_clone_command);
+ $commands = collect([
+ $this->execute_in_builder("mkdir -p /root/.ssh"),
+ $this->execute_in_builder("echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
+ $this->execute_in_builder("chmod 600 /root/.ssh/id_rsa"),
+ $this->execute_in_builder($git_clone_command)
+ ]);
+ return $commands->implode(' && ');
+ }
+ }
+ private function clone_repository()
+ {
+ $this->execute_remote_command(
+ [
+ "echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} to {$this->workdir}. '"
+ ],
+ [
+ $this->importing_git_repository()
+ ],
+ [
+ $this->execute_in_builder("cd {$this->workdir} && git rev-parse HEAD"),
+ "hidden" => true,
+ "save" => "git_commit_sha"
+ ],
+ );
+ $this->commit = $this->saved_outputs->get('git_commit_sha');
}
}
diff --git a/app/Jobs/ApplicationDeploymentJobOld.php b/app/Jobs/ApplicationDeploymentJobOld.php
new file mode 100644
index 000000000..87fca814c
--- /dev/null
+++ b/app/Jobs/ApplicationDeploymentJobOld.php
@@ -0,0 +1,738 @@
+application_deployment_queue = ApplicationDeploymentQueue::find($this->application_deployment_queue_id);
+ $this->application_deployment_queue->update([
+ 'status' => ProcessStatus::IN_PROGRESS->value,
+ ]);
+ if ($this->rollback_commit) {
+ $this->git_commit = $this->rollback_commit;
+ }
+
+ $this->application = Application::find($this->application_id);
+
+ ray('pullrequestId: ' . $this->pull_request_id);
+ if ($this->pull_request_id !== 0) {
+ $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
+ ray($this->preview);
+ }
+
+ $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
+
+ $server = $this->destination->server;
+
+ $private_key_location = save_private_key_for_server($server);
+
+ $remoteProcessArgs = new CoolifyTaskArgs(
+ server_ip: $server->ip,
+ private_key_location: $private_key_location,
+ command: 'overwritten-later',
+ port: $server->port,
+ user: $server->user,
+ type: ActivityTypes::DEPLOYMENT->value,
+ type_uuid: $this->deployment_uuid,
+ );
+
+ $this->activity = activity()
+ ->performedOn($this->application)
+ ->withProperties($remoteProcessArgs->toArray())
+ ->event(ActivityTypes::DEPLOYMENT->value)
+ ->log("[]");
+ }
+ public function handle(): void
+ {
+ try {
+ if ($this->application->deploymentType() === 'source') {
+ $this->source = $this->application->source->getMorphClass()::where('id', $this->application->source->id)->first();
+ }
+
+ $this->workdir = "/artifacts/{$this->deployment_uuid}";
+
+ if ($this->pull_request_id !== 0) {
+ ray('Deploying pull/' . $this->pull_request_id . '/head for application: ' . $this->application->name)->green();
+ if ($this->application->fqdn) {
+ $preview_fqdn = data_get($this->preview, 'fqdn');
+ $template = $this->application->preview_url_template;
+ $url = Url::fromString($this->application->fqdn);
+ $host = $url->getHost();
+ $schema = $url->getScheme();
+ $random = new Cuid2(7);
+ $preview_fqdn = str_replace('{{random}}', $random, $template);
+ $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
+ $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
+ $preview_fqdn = "$schema://$preview_fqdn";
+ $this->preview->fqdn = $preview_fqdn;
+ $this->preview->save();
+ }
+ $this->deploy_pull_request();
+ } else {
+ $this->deploy();
+ }
+ } catch (\Exception $e) {
+ $this->execute_now([
+ "echo '\nOops something is not okay, are you okay? 😢'",
+ "echo '\n\n{$e->getMessage()}'",
+ ]);
+ ray($e);
+ $this->fail($e->getMessage());
+ } finally {
+ if (isset($this->docker_compose)) {
+ Storage::disk('deployments')->put(Str::kebab($this->application->name) . '/docker-compose.yml', $this->docker_compose);
+ }
+ $this->execute_now(["docker rm -f {$this->deployment_uuid} >/dev/null 2>&1"], hideFromOutput: true);
+ }
+ }
+
+ private function start_builder_image()
+ {
+ $this->execute_now([
+ "echo -n 'Pulling latest version of the builder image (ghcr.io/coollabsio/coolify-builder)... '",
+ ]);
+ // if (isDev()) {
+ // $this->execute_now([
+ // "docker run -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock coolify-builder",
+ // ], isDebuggable: true);
+ // } else {
+ $this->execute_now([
+ "docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-builder",
+ ], isDebuggable: true);
+ // }
+ $this->execute_now([
+ "echo 'Done.'"
+ ]);
+ $this->execute_now([
+ $this->execute_in_builder("mkdir -p {$this->workdir}"),
+ ]);
+ }
+
+ private function clone_repository()
+ {
+ $this->execute_now([
+ "echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} to {$this->workdir}... '"
+ ]);
+
+ $this->execute_now([
+ ...$this->importing_git_repository(),
+ ], 'importing_git_repository');
+
+ $this->execute_now([
+ "echo 'Done.'"
+ ]);
+ // Get git commit
+ $this->execute_now([$this->execute_in_builder("cd {$this->workdir} && git rev-parse HEAD")], 'commit_sha', hideFromOutput: true);
+ $this->git_commit = $this->activity->properties->get('commit_sha');
+ }
+
+ private function cleanup_git()
+ {
+ $this->execute_now([
+ $this->execute_in_builder("rm -fr {$this->workdir}/.git")
+ ], hideFromOutput: true);
+ }
+ private function generate_buildpack()
+ {
+ $this->execute_now([
+ "echo -n 'Generating nixpacks configuration... '",
+ ]);
+ $this->execute_now([
+ $this->nixpacks_build_cmd(),
+ $this->execute_in_builder("cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile"),
+ $this->execute_in_builder("rm -f {$this->workdir}/.nixpacks/Dockerfile"),
+ ], isDebuggable: true);
+ $this->execute_now([
+ "echo 'Done... '",
+ ]);
+ }
+ private function build_image()
+ {
+ $this->execute_now([
+ "echo -n 'Building image... '",
+ ]);
+
+ if ($this->application->settings->is_static) {
+ $this->execute_now([
+ $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"),
+ ], isDebuggable: true);
+
+ $dockerfile = "FROM {$this->application->static_image}
+WORKDIR /usr/share/nginx/html/
+LABEL coolify.deploymentId={$this->deployment_uuid}
+COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .";
+ $docker_file = base64_encode($dockerfile);
+
+ $this->execute_now([
+ $this->execute_in_builder("echo '{$docker_file}' | base64 -d > {$this->workdir}/Dockerfile-prod"),
+ $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"),
+ ], hideFromOutput: true);
+ } else {
+ $this->execute_now([
+ $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"),
+ ], isDebuggable: true);
+ }
+ $this->execute_now([
+ "echo 'Done.'",
+ ]);
+ }
+ private function deploy_pull_request()
+ {
+ dispatch(new ApplicationPullRequestUpdateJob(
+ application_id: $this->application->id,
+ pull_request_id: $this->pull_request_id,
+ deployment_uuid: $this->deployment_uuid,
+ status: 'in_progress'
+ ));
+ $this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build";
+ $this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}";
+ $this->container_name = generate_container_name($this->application->uuid, $this->pull_request_id);
+ // Deploy pull request
+ $this->execute_now([
+ "echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch} PR#{$this->pull_request_id}...'",
+ ]);
+ $this->start_builder_image();
+ $this->clone_repository();
+ $this->cleanup_git();
+ $this->generate_buildpack();
+ $this->generate_compose_file();
+ // Needs separate preview variables
+ // $this->generate_build_env_variables();
+ // $this->add_build_env_variables_to_dockerfile();
+ $this->build_image();
+ $this->stop_running_container();
+ $this->start_by_compose_file();
+ $this->next(ProcessStatus::FINISHED->value);
+ }
+ private function deploy()
+ {
+ $this->container_name = generate_container_name($this->application->uuid);
+ // Deploy normal commit
+ $this->execute_now([
+ "echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}...'",
+ ]);
+ $this->start_builder_image();
+ ray('Rollback Commit: ' . $this->rollback_commit)->green();
+ if ($this->rollback_commit === 'HEAD') {
+ $this->clone_repository();
+ }
+ $this->build_image_name = "{$this->application->uuid}:{$this->git_commit}-build";
+ $this->production_image_name = "{$this->application->uuid}:{$this->git_commit}";
+ ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name:' . $this->production_image_name)->green();
+ if (!$this->force_rebuild) {
+ $this->execute_now([
+ "docker images -q {$this->application->uuid}:{$this->git_commit} 2>/dev/null",
+ ], 'local_image_found', hideFromOutput: true, ignoreErrors: true);
+ $image_found = Str::of($this->activity->properties->get('local_image_found'))->trim()->isNotEmpty();
+ if ($image_found) {
+ $this->execute_now([
+ "echo 'Docker Image found locally with the same Git Commit SHA. Build skipped...'"
+ ]);
+ $this->generate_compose_file();
+ $this->stop_running_container();
+ $this->start_by_compose_file();
+ $this->next(ProcessStatus::FINISHED->value);
+ return;
+ }
+ }
+ $this->cleanup_git();
+ $this->generate_buildpack();
+ $this->generate_compose_file();
+ $this->generate_build_env_variables();
+ $this->add_build_env_variables_to_dockerfile();
+ $this->build_image();
+ $this->stop_running_container();
+ $this->start_by_compose_file();
+ $this->next(ProcessStatus::FINISHED->value);
+ }
+
+ public function failed(Throwable $exception): void
+ {
+ $this->next(ProcessStatus::ERROR->value);
+ }
+
+ private function next(string $status)
+ {
+ ray('Next Status: ' . $status)->green();
+ $this->application_deployment_queue->update([
+ 'status' => $status,
+ ]);
+ ray($this->application_deployment_queue)->purple();
+ ray($this->activity)->purple();
+ $this->activity->properties = $this->activity->properties->merge([
+ 'status' => $status,
+ ]);
+ $this->activity->save();
+ if ($this->pull_request_id) {
+ dispatch(new ApplicationPullRequestUpdateJob(
+ application_id: $this->application->id,
+ pull_request_id: $this->pull_request_id,
+ deployment_uuid: $this->deployment_uuid,
+ status: $status
+ ));
+ }
+ if ($this->application->fqdn) {
+ dispatch(new InstanceProxyCheckJob());
+ }
+ queue_next_deployment($this->application);
+ if ($status === ProcessStatus::FINISHED->value) {
+ $this->application->environment->project->team->notify(new DeployedSuccessfullyNotification($this->application, $this->deployment_uuid, $this->preview));
+ }
+ if ($status === ProcessStatus::ERROR->value) {
+ $this->application->environment->project->team->notify(new DeployedWithErrorNotification($this->application, $this->deployment_uuid, $this->preview));
+ }
+ }
+ private function execute_in_builder(string $command)
+ {
+ return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1'";
+ }
+ private function generate_environment_variables($ports)
+ {
+ $environment_variables = collect();
+ ray('Generate Environment Variables')->green();
+ if ($this->pull_request_id === 0) {
+ ray($this->application->runtime_environment_variables)->green();
+ foreach ($this->application->runtime_environment_variables as $env) {
+ $environment_variables->push("$env->key=$env->value");
+ }
+ } else {
+ ray($this->application->runtime_environment_variables_preview)->green();
+ foreach ($this->application->runtime_environment_variables_preview as $env) {
+ $environment_variables->push("$env->key=$env->value");
+ }
+ }
+ // Add PORT if not exists, use the first port as default
+ if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
+ $environment_variables->push("PORT={$ports[0]}");
+ }
+ return $environment_variables->all();
+ }
+ private function generate_env_variables()
+ {
+ $this->env_args = collect([]);
+ if ($this->pull_request_id === 0) {
+ foreach ($this->application->nixpacks_environment_variables as $env) {
+ $this->env_args->push("--env {$env->key}={$env->value}");
+ }
+ } else {
+ foreach ($this->application->nixpacks_environment_variables_preview as $env) {
+ $this->env_args->push("--env {$env->key}={$env->value}");
+ }
+ }
+
+ $this->env_args = $this->env_args->implode(' ');
+ }
+ private function generate_build_env_variables()
+ {
+ $this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->git_commit}"]);
+ if ($this->pull_request_id === 0) {
+ foreach ($this->application->build_environment_variables as $env) {
+ $this->build_args->push("--build-arg {$env->key}={$env->value}");
+ }
+ } else {
+ foreach ($this->application->build_environment_variables_preview as $env) {
+ $this->build_args->push("--build-arg {$env->key}={$env->value}");
+ }
+ }
+
+ $this->build_args = $this->build_args->implode(' ');
+ }
+ private function add_build_env_variables_to_dockerfile()
+ {
+ $this->execute_now([
+ $this->execute_in_builder("cat {$this->workdir}/Dockerfile")
+ ], propertyName: 'dockerfile', hideFromOutput: true);
+ $dockerfile = collect(Str::of($this->activity->properties->get('dockerfile'))->trim()->explode("\n"));
+
+ foreach ($this->application->build_environment_variables as $env) {
+ $dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
+ }
+ $dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
+ $this->execute_now([
+ $this->execute_in_builder("echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/Dockerfile")
+ ], hideFromOutput: true);
+ }
+ private function generate_docker_compose()
+ {
+ $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array;
+
+ $persistent_storages = $this->generate_local_persistent_volumes();
+ $volume_names = $this->generate_local_persistent_volumes_only_volume_names();
+ $environment_variables = $this->generate_environment_variables($ports);
+
+ $docker_compose = [
+ 'version' => '3.8',
+ 'services' => [
+ $this->container_name => [
+ 'image' => $this->production_image_name,
+ 'container_name' => $this->container_name,
+ 'restart' => 'always',
+ 'environment' => $environment_variables,
+ 'labels' => $this->set_labels_for_applications(),
+ 'expose' => $ports,
+ 'networks' => [
+ $this->destination->network,
+ ],
+ 'healthcheck' => [
+ 'test' => [
+ 'CMD-SHELL',
+ $this->generate_healthcheck_commands()
+ ],
+ 'interval' => $this->application->health_check_interval . 's',
+ 'timeout' => $this->application->health_check_timeout . 's',
+ 'retries' => $this->application->health_check_retries,
+ 'start_period' => $this->application->health_check_start_period . 's'
+ ],
+ 'mem_limit' => $this->application->limits_memory,
+ 'memswap_limit' => $this->application->limits_memory_swap,
+ 'mem_swappiness' => $this->application->limits_memory_swappiness,
+ 'mem_reservation' => $this->application->limits_memory_reservation,
+ 'cpus' => $this->application->limits_cpus,
+ 'cpuset' => $this->application->limits_cpuset,
+ 'cpu_shares' => $this->application->limits_cpu_shares,
+ ]
+ ],
+ 'networks' => [
+ $this->destination->network => [
+ 'external' => false,
+ 'name' => $this->destination->network,
+ 'attachable' => true,
+ ]
+ ]
+ ];
+ if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
+ $docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array;
+ }
+ if (count($persistent_storages) > 0) {
+ $docker_compose['services'][$this->container_name]['volumes'] = $persistent_storages;
+ }
+ if (count($volume_names) > 0) {
+ $docker_compose['volumes'] = $volume_names;
+ }
+ return Yaml::dump($docker_compose, 10);
+ }
+ private function generate_local_persistent_volumes()
+ {
+ $local_persistent_volumes = [];
+ foreach ($this->application->persistentStorages as $persistentStorage) {
+ $volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
+ if ($this->pull_request_id !== 0) {
+ $volume_name = $volume_name . '-pr-' . $this->pull_request_id;
+ }
+ $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
+ }
+ ray('local_persistent_volumes', $local_persistent_volumes)->green();
+ return $local_persistent_volumes;
+ }
+
+ private function generate_local_persistent_volumes_only_volume_names()
+ {
+ $local_persistent_volumes_names = [];
+ foreach ($this->application->persistentStorages as $persistentStorage) {
+ if ($persistentStorage->host_path) {
+ continue;
+ }
+ $name = $persistentStorage->name;
+
+ if ($this->pull_request_id !== 0) {
+ $name = $name . '-pr-' . $this->pull_request_id;
+ }
+
+ $local_persistent_volumes_names[$name] = [
+ 'name' => $name,
+ 'external' => false,
+ ];
+ }
+ return $local_persistent_volumes_names;
+ }
+ private function generate_healthcheck_commands()
+ {
+ if (!$this->application->health_check_port) {
+ $this->application->health_check_port = $this->application->ports_exposes_array[0];
+ }
+ if ($this->application->health_check_path) {
+ $generated_healthchecks_commands = [
+ "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}{$this->application->health_check_path} > /dev/null"
+ ];
+ } else {
+ $generated_healthchecks_commands = [
+ "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$this->application->health_check_port}/"
+ ];
+ }
+ return implode(' ', $generated_healthchecks_commands);
+ }
+
+ private function set_labels_for_applications()
+ {
+ $labels = [];
+ $labels[] = 'coolify.managed=true';
+ $labels[] = 'coolify.version=' . config('version');
+ $labels[] = 'coolify.applicationId=' . $this->application->id;
+ $labels[] = 'coolify.type=application';
+ $labels[] = 'coolify.name=' . $this->application->name;
+ if ($this->pull_request_id !== 0) {
+ $labels[] = 'coolify.pullRequestId=' . $this->pull_request_id;
+ }
+ if ($this->application->fqdn) {
+ if ($this->pull_request_id !== 0) {
+ $preview_fqdn = data_get($this->preview, 'fqdn');
+ $template = $this->application->preview_url_template;
+ $url = Url::fromString($this->application->fqdn);
+ $host = $url->getHost();
+ $schema = $url->getScheme();
+ $random = new Cuid2(7);
+ $preview_fqdn = str_replace('{{random}}', $random, $template);
+ $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
+ $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
+ $preview_fqdn = "$schema://$preview_fqdn";
+ $this->preview->fqdn = $preview_fqdn;
+ $this->preview->save();
+ $domains = Str::of($preview_fqdn)->explode(',');
+ } else {
+ $domains = Str::of($this->application->fqdn)->explode(',');
+ }
+ $labels[] = 'traefik.enable=true';
+ foreach ($domains as $domain) {
+ $url = Url::fromString($domain);
+ $host = $url->getHost();
+ $path = $url->getPath();
+ $schema = $url->getScheme();
+ $slug = Str::slug($host . $path);
+
+ $http_label = "{$this->application->uuid}-{$slug}-http";
+ $https_label = "{$this->application->uuid}-{$slug}-https";
+
+ if ($schema === 'https') {
+ // Set labels for https
+ $labels[] = "traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
+ $labels[] = "traefik.http.routers.{$https_label}.entryPoints=https";
+ $labels[] = "traefik.http.routers.{$https_label}.middlewares=gzip";
+ if ($path !== '/') {
+ $labels[] = "traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix";
+ $labels[] = "traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}";
+ }
+
+ $labels[] = "traefik.http.routers.{$https_label}.tls=true";
+ $labels[] = "traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt";
+
+ // Set labels for http (redirect to https)
+ $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
+ $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http";
+ if ($this->application->settings->is_force_https_enabled) {
+ $labels[] = "traefik.http.routers.{$http_label}.middlewares=redirect-to-https";
+ }
+ } else {
+ // Set labels for http
+ $labels[] = "traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)";
+ $labels[] = "traefik.http.routers.{$http_label}.entryPoints=http";
+ $labels[] = "traefik.http.routers.{$http_label}.middlewares=gzip";
+ if ($path !== '/') {
+ $labels[] = "traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix";
+ $labels[] = "traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}";
+ }
+ }
+ }
+ }
+ return $labels;
+ }
+
+ private function execute_now(
+ array|Collection $command,
+ string $propertyName = null,
+ bool $isFinished = false,
+ bool $hideFromOutput = false,
+ bool $isDebuggable = false,
+ bool $ignoreErrors = false
+ ) {
+ static::$batch_counter++;
+
+ if ($command instanceof Collection) {
+ $commandText = $command->implode("\n");
+ } else {
+ $commandText = collect($command)->implode("\n");
+ }
+ ray('Executing command: ' . $commandText)->green();
+ $this->activity->properties = $this->activity->properties->merge([
+ 'command' => $commandText,
+ ]);
+ $this->activity->save();
+ if ($isDebuggable && !$this->application->settings->is_debug_enabled) {
+ ray('Debugging is disabled for this application. Skipping command.')->green();
+ $hideFromOutput = true;
+ }
+ $remote_process = resolve(RunRemoteProcess::class, [
+ 'activity' => $this->activity,
+ 'hide_from_output' => $hideFromOutput,
+ 'is_finished' => $isFinished,
+ 'ingore_errors' => $ignoreErrors,
+ ]);
+ $result = $remote_process();
+ if ($propertyName) {
+ $this->activity->properties = $this->activity->properties->merge([
+ $propertyName => trim($result->output()),
+ ]);
+ $this->activity->save();
+ }
+
+ if ($result->exitCode() != 0 && $result->errorOutput() && !$ignoreErrors) {
+ throw new \RuntimeException($result->errorOutput());
+ }
+ }
+ private function set_git_import_settings($git_clone_command)
+ {
+ if ($this->application->git_commit_sha !== 'HEAD') {
+ $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1";
+ }
+ if ($this->application->settings->is_git_submodules_enabled) {
+ $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git submodule update --init --recursive";
+ }
+ if ($this->application->settings->is_git_lfs_enabled) {
+ $git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git lfs pull";
+ }
+ return $git_clone_command;
+ }
+ private function importing_git_repository()
+ {
+ $git_clone_command = "git clone -q -b {$this->application->git_branch}";
+ if ($this->pull_request_id !== 0) {
+ $pr_branch_name = "pr-{$this->pull_request_id}-coolify";
+ }
+
+ if ($this->application->deploymentType() === 'source') {
+ $source_html_url = data_get($this->application, 'source.html_url');
+ $url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
+ $source_html_url_host = $url['host'];
+ $source_html_url_scheme = $url['scheme'];
+
+ if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
+ if ($this->source->is_public) {
+ $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->workdir}";
+ $git_clone_command = $this->set_git_import_settings($git_clone_command);
+
+ $commands = [$this->execute_in_builder($git_clone_command)];
+
+ if ($this->pull_request_id !== 0) {
+ $commands[] = $this->execute_in_builder("cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name >/dev/null 2>&1 && git checkout $pr_branch_name >/dev/null 2>&1");
+ }
+ return $commands;
+ } else {
+ $github_access_token = generate_github_installation_token($this->source);
+ $commands = [
+ $this->execute_in_builder("git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->workdir}")
+ ];
+ if ($this->pull_request_id !== 0) {
+ $commands[] = $this->execute_in_builder("cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name");
+ }
+ return $commands;
+ }
+ }
+ }
+ if ($this->application->deploymentType() === 'deploy_key') {
+ $private_key = base64_encode($this->application->private_key->private_key);
+ $git_clone_command = "GIT_SSH_COMMAND=\"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_full_url} {$this->workdir}";
+ $git_clone_command = $this->set_git_import_settings($git_clone_command);
+ return [
+ $this->execute_in_builder("mkdir -p /root/.ssh"),
+ $this->execute_in_builder("echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
+ $this->execute_in_builder("chmod 600 /root/.ssh/id_rsa"),
+ $this->execute_in_builder($git_clone_command)
+ ];
+ }
+ }
+ private function nixpacks_build_cmd()
+ {
+ $this->generate_env_variables();
+ $nixpacks_command = "nixpacks build -o {$this->workdir} {$this->env_args} --no-error-without-start";
+ if ($this->application->build_command) {
+ $nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
+ }
+ if ($this->application->start_command) {
+ $nixpacks_command .= " --start-cmd \"{$this->application->start_command}\"";
+ }
+ if ($this->application->install_command) {
+ $nixpacks_command .= " --install-cmd \"{$this->application->install_command}\"";
+ }
+ $nixpacks_command .= " {$this->workdir}";
+ return $this->execute_in_builder($nixpacks_command);
+ }
+ private function stop_running_container()
+ {
+ $this->execute_now([
+ "echo -n 'Removing old instance... '",
+ $this->execute_in_builder("docker rm -f $this->container_name >/dev/null 2>&1"),
+ "echo 'Done.'",
+ ]);
+ }
+ private function start_by_compose_file()
+ {
+ $this->execute_now([
+ "echo -n 'Starting your application... '",
+ ]);
+ $this->execute_now([
+ $this->execute_in_builder("docker compose --project-directory {$this->workdir} up -d >/dev/null"),
+ ], isDebuggable: true);
+ $this->execute_now([
+ "echo 'Done. 🎉'",
+ ], isFinished: true);
+ }
+ private function generate_compose_file()
+ {
+ $this->docker_compose = $this->generate_docker_compose();
+ $docker_compose_base64 = base64_encode($this->docker_compose);
+ $this->execute_now([
+ $this->execute_in_builder("echo '{$docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml")
+ ], hideFromOutput: true);
+ }
+}
diff --git a/app/Jobs/InstanceProxyCheckJob.php b/app/Jobs/InstanceProxyCheckJob.php
index 4387cd318..6d90f1be3 100755
--- a/app/Jobs/InstanceProxyCheckJob.php
+++ b/app/Jobs/InstanceProxyCheckJob.php
@@ -29,7 +29,7 @@ class InstanceProxyCheckJob implements ShouldQueue
{
try {
$container_name = 'coolify-proxy';
- $servers = Server::whereRelation('settings', 'is_reachable', true)->where('proxy->type', ProxyTypes::TRAEFIK_V2)->get();
+ $servers = Server::whereRelation('settings', 'is_usable', true)->where('proxy->type', ProxyTypes::TRAEFIK_V2)->get();
foreach ($servers as $server) {
$status = get_container_status(server: $server, container_id: $container_name);
diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php
index 474c38cc8..1b7ae4781 100644
--- a/app/Models/ApplicationDeploymentQueue.php
+++ b/app/Models/ApplicationDeploymentQueue.php
@@ -2,19 +2,9 @@
namespace App\Models;
-use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
-use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
class ApplicationDeploymentQueue extends Model
{
- protected $fillable = [
- 'application_id',
- 'deployment_uuid',
- 'pull_request_id',
- 'force_rebuild',
- 'commit',
- 'status',
- 'is_webhook',
- ];
+ protected $guarded = [];
}
diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php
index a1b116560..0a3d02ff7 100644
--- a/app/Providers/FortifyServiceProvider.php
+++ b/app/Providers/FortifyServiceProvider.php
@@ -52,6 +52,11 @@ class FortifyServiceProvider extends ServiceProvider
Fortify::loginView(function () {
$settings = InstanceSettings::get();
+ $users = User::count();
+ if ($users == 0) {
+ // If there are no users, redirect to registration
+ return redirect()->route('register');
+ }
return view('auth.login', [
'is_registration_enabled' => $settings->is_registration_enabled
]);
diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php
new file mode 100644
index 000000000..6e8c5b761
--- /dev/null
+++ b/app/Traits/ExecuteRemoteCommand.php
@@ -0,0 +1,83 @@
+server instanceof Server === false) {
+ throw new \RuntimeException('Server is not set or is not an instance of Server model');
+ }
+
+ $ip = data_get($this->server, 'ip');
+ $user = data_get($this->server, 'user');
+ $port = data_get($this->server, 'port');
+ $private_key_location = get_private_key_for_server($this->server);
+
+ $commandsText->each(function ($single_command) use ($private_key_location, $ip, $user, $port) {
+ $command = data_get($single_command, 'command') ?? $single_command[0] ?? null;
+ if ($command === null) {
+ throw new \RuntimeException('Command is not set');
+ }
+ $hidden = data_get($single_command, 'hidden', false);
+ $ignore_errors = data_get($single_command, 'ignore_errors', false);
+ $this->save = data_get($single_command, 'save');
+
+ $remote_command = generate_ssh_command($private_key_location, $ip, $user, $port, $command);
+ $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) {
+ $new_log_entry = [
+ 'command' => $command,
+ 'output' => $output,
+ 'type' => $type === 'err' ? 'stderr' : 'stdout',
+ 'timestamp' => Carbon::now('UTC'),
+ 'hidden' => $hidden,
+ 'batch' => static::$batch_counter,
+ ];
+
+ if (!$this->log_model->logs) {
+ $new_log_entry['order'] = 1;
+ } else {
+ $previous_logs = json_decode($this->log_model->logs, associative: true, flags: JSON_THROW_ON_ERROR);
+ $new_log_entry['order'] = count($previous_logs) + 1;
+ }
+
+ $previous_logs[] = $new_log_entry;
+ $this->log_model->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR);
+ $this->log_model->save();
+
+ if ($this->save) {
+ $this->saved_outputs[$this->save] = Str::of($output)->trim();
+ }
+ });
+ $this->log_model->update([
+ 'current_process_id' => $process->id(),
+ ]);
+
+ $process_result = $process->wait();
+ if ($process_result->exitCode() !== 0) {
+ if (!$ignore_errors) {
+ $status = ApplicationDeploymentStatus::FAILED->value;
+ $this->log_model->status = $status;
+ $this->log_model->save();
+ throw new \RuntimeException($process_result->errorOutput());
+ }
+ }
+ });
+ }
+}
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index bbf24ff00..fa57cebde 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -29,12 +29,7 @@ function queue_application_deployment(int $application_id, string $deployment_uu
}
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
- application_id: $application_id,
- deployment_uuid: $deployment_uuid,
- force_rebuild: $force_rebuild,
- rollback_commit: $commit,
- pull_request_id: $pull_request_id,
- ));
+ ))->onConnection('long-running')->onQueue('long-running');
}
function queue_next_deployment(Application $application)
@@ -43,10 +38,6 @@ function queue_next_deployment(Application $application)
if ($next_found) {
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id,
- application_id: $next_found->application_id,
- deployment_uuid: $next_found->deployment_uuid,
- force_rebuild: $next_found->force_rebuild,
- pull_request_id: $next_found->pull_request_id
- ));
+ ))->onConnection('long-running')->onQueue('long-running');
}
}
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index def788668..a8bd2a2f7 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -42,9 +42,9 @@ function get_container_status(Server $server, string $container_id, bool $all_da
return $container[0]['State']['Status'];
}
-function generate_container_name(string $uuid, int|null $pull_request_id = null)
+function generate_container_name(string $uuid, int $pull_request_id = 0)
{
- if ($pull_request_id) {
+ if ($pull_request_id !== 0) {
return $uuid . '-pr-' . $pull_request_id;
} else {
return $uuid;
diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php
index f282ee6f1..4e4fdd7fc 100644
--- a/bootstrap/helpers/github.php
+++ b/bootstrap/helpers/github.php
@@ -64,10 +64,11 @@ function git_api(GithubApp|GitlabApp $source, string $endpoint, string $method =
}
$json = $response->json();
if ($response->failed() && $throwError) {
- throw new \Exception("Failed to get data from {$source->name} with error: " . $json['message']);
+ throw new \Exception("Failed to get data from {$source->name} with error:
" . $json['message']);
}
return [
'rate_limit_remaining' => $response->header('X-RateLimit-Remaining'),
+ 'rate_limit_reset' => $response->header('X-RateLimit-Reset'),
'data' => collect($json)
];
}
diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php
index 00ffaa89e..fadd19255 100644
--- a/bootstrap/helpers/remoteProcess.php
+++ b/bootstrap/helpers/remoteProcess.php
@@ -3,8 +3,12 @@
use App\Actions\CoolifyTask\PrepareCoolifyTask;
use App\Data\CoolifyTaskArgs;
use App\Enums\ActivityTypes;
+use App\Models\Application;
+use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
+use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Sleep;
@@ -21,13 +25,16 @@ function remote_process(
string $type = ActivityTypes::INLINE->value,
?string $type_uuid = null,
?Model $model = null,
- bool $ignore_errors = false
+ bool $ignore_errors = false,
): Activity {
$command_string = implode("\n", $command);
-
- // @TODO: Check if the user has access to this server
- // checkTeam($server->team_id);
+ if (auth()->user()) {
+ $teams = auth()->user()->teams->pluck('id');
+ if (!$teams->contains($server->team_id) && !$teams->contains(0)) {
+ throw new \Exception("User is not part of the team that owns this server");
+ }
+ }
$private_key_location = save_private_key_for_server($server);
@@ -47,6 +54,11 @@ function remote_process(
),
])();
}
+function get_private_key_for_server(Server $server)
+{
+ $temp_file = "id.root@{$server->ip}";
+ return '/var/www/html/storage/app/ssh/keys/' . $temp_file;
+}
function save_private_key_for_server(Server $server)
{
if (data_get($server, 'privateKey.private_key') === null) {
@@ -106,3 +118,33 @@ function instant_remote_process(array $command, Server $server, $throwError = tr
}
return $output;
}
+
+function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection
+{
+ $application = Application::find(data_get($application_deployment_queue, 'application_id'));
+ $is_debug_enabled = data_get($application, 'settings.is_debug_enabled');
+ if (is_null($application_deployment_queue)) {
+ return collect([]);
+ }
+ try {
+ $decoded = json_decode(
+ data_get($application_deployment_queue, 'logs'),
+ associative: true,
+ flags: JSON_THROW_ON_ERROR
+ );
+ } catch (\JsonException $exception) {
+ return collect([]);
+ }
+ $formatted = collect($decoded);
+ if (!$is_debug_enabled) {
+ $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
+ }
+ $formatted = $formatted
+ ->sortBy(fn ($i) => $i['order'])
+ ->map(function ($i) {
+ $i['timestamp'] = Carbon::parse($i['timestamp'])->format('Y-M-d H:i:s.u');
+ return $i;
+ });
+
+ return $formatted;
+}
diff --git a/config/horizon.php b/config/horizon.php
index d9f841aa6..f35cbd731 100644
--- a/config/horizon.php
+++ b/config/horizon.php
@@ -190,7 +190,18 @@ return [
'maxJobs' => 0,
'memory' => 128,
'tries' => 1,
- 'timeout' => 3600,
+ 'timeout' => 300,
+ 'nice' => 0,
+ ],
+ 'long-running' => [
+ 'connection' => 'redis',
+ 'queue' => ['long-running'],
+ 'balance' => 'auto',
+ 'maxTime' => 0,
+ 'maxJobs' => 0,
+ 'memory' => 128,
+ 'tries' => 1,
+ 'timeout' => 3560,
'nice' => 0,
],
],
@@ -203,6 +214,12 @@ return [
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],
+ 'long-running' => [
+ 'autoScalingStrategy' => 'size',
+ 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 10),
+ 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
+ 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
+ ],
],
'local' => [
@@ -212,6 +229,12 @@ return [
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],
+ 'long-running' => [
+ 'autoScalingStrategy' => 'size',
+ 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 10),
+ 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
+ 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
+ ],
],
],
];
diff --git a/config/queue.php b/config/queue.php
index a7a3d46f8..7bb782605 100644
--- a/config/queue.php
+++ b/config/queue.php
@@ -33,7 +33,14 @@ return [
'sync' => [
'driver' => 'sync',
],
-
+ 'long-running' => [
+ 'driver' => 'redis',
+ 'connection' => 'default',
+ 'queue' => 'long-running',
+ 'retry_after' => 3600,
+ 'block_for' => null,
+ 'after_commit' => false,
+ ],
'database' => [
'driver' => 'database',
'table' => 'jobs',
@@ -66,7 +73,7 @@ return [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
- 'retry_after' => 90,
+ 'retry_after' => 300,
'block_for' => null,
'after_commit' => false,
],
diff --git a/config/version.php b/config/version.php
index b343fad81..6c54c29bf 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
text('logs')->default(null)->nullable();
+ $table->string('current_process_id')->default(null)->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('application_deployment_queues', function (Blueprint $table) {
+ $table->dropColumn('logs');
+ $table->dropColumn('current_process_id');
+ });
+ }
+};
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index cc22fad99..872480a6e 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -26,7 +26,8 @@ services:
- REDIS_PASSWORD
- SSL_MODE=off
- PHP_PM_CONTROL=dynamic
- - PHP_PM_START_SERVERS=5
+ - PHP_PM_START_SERVERS=1
+ - PHP_PM_MIN_SPARE_SERVERS=1
- PHP_PM_MAX_SPARE_SERVERS=10
ports:
- "${APP_PORT:-8000}:80"
diff --git a/docker/coolify-builder/Dockerfile b/docker/coolify-builder/Dockerfile
index d03e0196c..390ea38f9 100644
--- a/docker/coolify-builder/Dockerfile
+++ b/docker/coolify-builder/Dockerfile
@@ -35,5 +35,5 @@ RUN if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
;fi
ENTRYPOINT ["/sbin/tini", "--"]
-CMD ["sh", "-c", "while true; do sleep 3600 && exit 0; done"]
+CMD ["sh", "-c", "while true; do sleep 1; done"]
diff --git a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-seeder/dependencies.d/db-migration b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/dependencies.d/init-seeder
similarity index 100%
rename from docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-seeder/dependencies.d/db-migration
rename to docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/dependencies.d/init-seeder
diff --git a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/type b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/type
new file mode 100644
index 000000000..bdd22a185
--- /dev/null
+++ b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/type
@@ -0,0 +1 @@
+oneshot
diff --git a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up
new file mode 100644
index 000000000..09595f708
--- /dev/null
+++ b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up
@@ -0,0 +1,2 @@
+#!/command/execlineb -P
+php /var/www/html/artisan app:init
diff --git a/resources/css/app.css b/resources/css/app.css
index 8602c69a5..1ed610d1c 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -97,7 +97,7 @@ a {
}
}
.bg-coollabs-gradient {
- @apply text-transparent bg-clip-text bg-gradient-to-r from-purple-500 via-pink-500 to-red-500;
+ @apply text-transparent text-white bg-gradient-to-r from-purple-500 via-pink-500 to-red-500;
}
.text-helper {
@apply inline-block font-bold text-warning;
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php
index 1bb5c9b28..0a66be609 100644
--- a/resources/views/auth/login.blade.php
+++ b/resources/views/auth/login.blade.php
@@ -8,8 +8,7 @@