Merge pull request #1480 from coollabsio/next

v4.0.0-beta.147
This commit is contained in:
Andras Bacsai 2023-11-28 13:39:34 +01:00 committed by GitHub
commit 5f8b8bd730
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 2615 additions and 2358 deletions

View File

@ -25,5 +25,15 @@ class StopApplication
// TODO: make notification for application
// $application->environment->project->team->notify(new StatusChanged($application));
}
// Delete Preview Deployments
$previewDeployments = $application->previews;
foreach ($previewDeployments as $previewDeployment) {
$containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
}
}
}
}

View File

@ -15,7 +15,7 @@ class InstallDocker
if (!$supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
}
ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS: ' . $supported_os_type);
ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS type: ' . $supported_os_type);
$dockerVersion = '24.0';
$config = base64_encode('{
"log-driver": "json-file",
@ -44,17 +44,23 @@ class InstallDocker
"ls -l /tmp"
]);
} else {
if ($supported_os_type === 'debian') {
if ($supported_os_type->contains('debian')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || apt-get update",
"command -v jq >/dev/null || apt install -y jq",
"command -v jq >/dev/null || apt-get update -y",
"command -v jq >/dev/null || apt install -y curl wget git jq",
]);
} else if ($supported_os_type === 'rhel') {
} else if ($supported_os_type->contains('rhel')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || dnf install -y jq",
"command -v jq >/dev/null || dnf install -y curl wget git jq",
]);
} else if ($supported_os_type->contains('sles')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || zypper update -y",
"command -v jq >/dev/null || zypper install -y curl wget git jq",
]);
} else {
throw new \Exception('Unsupported OS');

View File

@ -16,13 +16,13 @@ class StartService
$commands[] = "cd " . $service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null || true";
$commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null 2>&1 || true";
$commands[] = "echo 'Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo 'Pulling images.'";
$commands[] = "docker compose pull";
$commands[] = "echo 'Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate";
$commands[] = "docker network connect $service->uuid coolify-proxy || true";
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
$compose = data_get($service,'docker_compose',[]);
$serviceNames = data_get(Yaml::parse($compose),'services',[]);
foreach($serviceNames as $serviceName => $serviceConfig){

View File

@ -26,7 +26,7 @@ class ServicesGenerate extends Command
*/
public function handle()
{
ray()->clearAll();
// ray()->clearAll();
$files = array_diff(scandir(base_path('templates/compose')), ['.', '..']);
$files = array_filter($files, function ($file) {
return strpos($file, '.yaml') !== false;

View File

@ -71,6 +71,15 @@ class SyncBunny extends Command
]);
});
try {
if (!$only_template && !$only_version) {
$this->info('About to sync files (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
}
if ($only_template) {
$this->info('About to sync service-templates.json to BunnyCDN.');
}
if ($only_version) {
$this->info('About to sync versions.json to BunnyCDN.');
}
$confirmed = confirm('Are you sure you want to sync?');
if (!$confirmed) {
return;

View File

@ -67,7 +67,7 @@ class ProjectController extends Controller
$database = create_standalone_mongodb($environment->id, $destination_uuid);
} else if ($type->value() === 'mysql') {
$database = create_standalone_mysql($environment->id, $destination_uuid);
}else if ($type->value() === 'mariadb') {
} else if ($type->value() === 'mariadb') {
$database = create_standalone_mariadb($environment->id, $destination_uuid);
}
return redirect()->route('project.database.configuration', [
@ -104,27 +104,7 @@ class ProjectController extends Controller
$generatedValue = $value;
if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
// TODO: make it shared with Service.php
switch ($command->value()) {
case 'PASSWORD':
$generatedValue = Str::password(symbols: false);
break;
case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false);
break;
case 'BASE64_64':
$generatedValue = Str::random(64);
break;
case 'BASE64_128':
$generatedValue = Str::random(128);
break;
case 'BASE64':
$generatedValue = Str::random(32);
break;
case 'USER':
$generatedValue = Str::random(16);
break;
}
$generatedValue = generateEnvValue($command->value());
}
EnvironmentVariable::create([
'key' => $key,
@ -137,7 +117,7 @@ class ProjectController extends Controller
}
$service->parse(isNew: true);
return redirect()->route('project.service', [
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,

View File

@ -6,6 +6,7 @@ use App\Models\Application;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class General extends Component
{
@ -25,8 +26,14 @@ class General extends Component
public bool $labelsChanged = false;
public bool $isConfigurationChanged = false;
public ?string $initialDockerComposeLocation = null;
public ?string $initialDockerComposePrLocation = null;
public bool $is_static;
public $parsedServices = [];
public $parsedServiceDomains = [];
protected $listeners = [
'resetDefaultLabels'
];
@ -50,6 +57,12 @@ class General extends Component
'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable',
'application.docker_compose_location' => 'nullable',
'application.docker_compose_pr_location' => 'nullable',
'application.docker_compose' => 'nullable',
'application.docker_compose_pr' => 'nullable',
'application.docker_compose_raw' => 'nullable',
'application.docker_compose_pr_raw' => 'nullable',
'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
'application.settings.is_static' => 'boolean|required',
@ -74,6 +87,12 @@ class General extends Component
'application.docker_registry_image_name' => 'Docker registry image name',
'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location',
'application.docker_compose_location' => 'Docker compose location',
'application.docker_compose_pr_location' => 'Docker compose location',
'application.docker_compose' => 'Docker compose',
'application.docker_compose_pr' => 'Docker compose',
'application.docker_compose_raw' => 'Docker compose raw',
'application.docker_compose_pr_raw' => 'Docker compose raw',
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
'application.settings.is_static' => 'Is static',
@ -81,6 +100,13 @@ class General extends Component
public function mount()
{
try {
$this->parsedServices = $this->application->parseCompose();
} catch (\Throwable $e) {
$this->emit('error', $e->getMessage());
}
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes;
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->application->isConfigurationChanged(true);
@ -91,6 +117,7 @@ class General extends Component
} else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
}
$this->initialDockerComposeLocation = $this->application->docker_compose_location;
$this->checkLabelUpdates();
}
public function instantSave()
@ -98,12 +125,44 @@ class General extends Component
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function loadComposeFile($isInit = false)
{
try {
if ($isInit && $this->application->docker_compose_raw) {
return;
}
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
$this->emit('success', 'Docker compose file loaded.');
} catch (\Throwable $e) {
$this->application->docker_compose_location = $this->initialDockerComposeLocation;
$this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation;
$this->application->save();
return handleError($e, $this);
}
}
public function generateDomain(string $serviceName)
{
$domain = $this->parsedServiceDomains[$serviceName]['domain'] ?? null;
if (!$domain) {
$uuid = new Cuid2(7);
$domain = generateFqdn($this->application->destination->server, $uuid);
$this->parsedServiceDomains[$serviceName]['domain'] = $domain;
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->application->save();
$this->emit('success', 'Domain generated.');
}
return $domain;
}
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
$this->application->settings->is_static = $this->is_static = false;
$this->application->settings->save();
}
if ($this->application->build_pack === 'dockercompose') {
$this->application->fqdn = null;
$this->application->settings->save();
}
$this->submit();
}
public function checkLabelUpdates()
@ -140,6 +199,9 @@ class General extends Component
public function submit($showToaster = true)
{
try {
if ($this->application->build_pack === 'dockercompose' && ($this->initialDockerComposeLocation !== $this->application->docker_compose_location || $this->initialDockerComposePrLocation !== $this->application->docker_compose_pr_location)) {
$this->loadComposeFile();
}
$this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false);
@ -172,6 +234,10 @@ class General extends Component
$this->customLabels = str($this->customLabels)->replace(',', "\n");
}
$this->application->custom_labels = $this->customLabels->explode("\n")->implode(',');
if ($this->application->build_pack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->parsedServices = $this->application->parseCompose();
}
$this->application->save();
$showToaster && $this->emit('success', 'Application settings updated!');
} catch (\Throwable $e) {

View File

@ -41,6 +41,10 @@ class Heading extends Component
public function deploy(bool $force_rebuild = false)
{
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
$this->emit('error', 'Please load a Compose file first.');
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
@ -68,7 +72,8 @@ class Heading extends Component
$this->application->save();
$this->application->refresh();
}
public function restart() {
public function restart()
{
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,

View File

@ -72,10 +72,12 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
try {
$container_name = generateApplicationContainerName($this->application, $pull_request_id);
instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false);
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->delete();
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
}
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
$this->application->refresh();
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@ -129,7 +129,7 @@ class DockerCompose extends Component
$service->parse(isNew: true);
return redirect()->route('project.service', [
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,

View File

@ -47,7 +47,7 @@ class Select extends Component
}
public function render()
{
$this->loadServices();
if ($this->search) $this->loadServices();
return view('livewire.project.new.select');
}
@ -69,10 +69,10 @@ class Select extends Component
// }
// }
public function loadServices(bool $force = false)
public function loadServices()
{
try {
if (count($this->allServices) > 0 && !$force) {
if (count($this->allServices) > 0) {
if (!$this->search) {
$this->services = $this->allServices;
return;

View File

@ -41,7 +41,7 @@ class Application extends Component
try {
$this->application->delete();
$this->emit('success', 'Application deleted successfully.');
return redirect()->route('project.service', $this->parameters);
return redirect()->route('project.service.configuration', $this->parameters);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@ -23,7 +23,7 @@ class StackForm extends Component
foreach ($fields as $fieldKey => $field) {
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$rules = data_get($field, 'rules');
$rules = data_get($field, 'rules', 'nullable');
$isPassword = data_get($field, 'isPassword');
$this->fields[$key] = [
"serviceName" => $serviceName,
@ -31,6 +31,7 @@ class StackForm extends Component
"name" => $fieldKey,
"value" => $value,
"isPassword" => $isPassword,
"rules" => $rules
];
$this->rules["fields.$key.value"] = $rules;
$this->validationAttributes["fields.$key.value"] = $fieldKey;

View File

@ -59,6 +59,7 @@ class Show extends Component
{
$this->validate();
$this->env->save();
ray($this->env);
$this->emit('success', 'Environment variable updated successfully.');
$this->emit('refreshEnvs');
}

View File

@ -17,13 +17,15 @@ class Logs extends Component
public ?string $type = null;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
public Server $server;
public ?string $container = null;
public $container = [];
public $containers;
public $parameters;
public $query;
public $status;
public function mount()
{
$this->containers = collect();
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (data_get($this->parameters, 'application_uuid')) {
@ -33,7 +35,9 @@ class Logs extends Component
$this->server = $this->resource->destination->server;
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
if ($containers->count() > 0) {
$this->container = data_get($containers[0], 'Names');
$containers->each(function ($container) {
$this->containers->push(str_replace('/', '', $container['Names']));
});
}
} else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database';

View File

@ -19,6 +19,7 @@ class Show extends Component
if (is_null($this->server)) {
return redirect()->route('server.all');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@ -1,111 +0,0 @@
<?php
namespace App\Jobs;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Traits\ExecuteRemoteCommandNew;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;
class ApplicationDeployDockerImageJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommandNew;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle()
{
ray()->clearAll();
ray('Deploying Docker Image');
try {
$applicationDeploymentQueue = ApplicationDeploymentQueue::find($this->applicationDeploymentQueueId);
$application = Application::find($applicationDeploymentQueue->application_id);
$deploymentUuid = data_get($applicationDeploymentQueue, 'deployment_uuid');
$dockerImage = data_get($application, 'docker_registry_image_name');
$dockerImageTag = data_get($application, 'docker_registry_image_tag');
$productionImageName = str("{$dockerImage}:{$dockerImageTag}");
$destination = $application->destination->getMorphClass()::where('id', $application->destination->id)->first();
$pullRequestId = data_get($applicationDeploymentQueue, 'pull_request_id');
$server = data_get($destination, 'server');
$network = data_get($destination, 'network');
$containerName = generateApplicationContainerName($application, $pullRequestId);
savePrivateKeyToFs($server);
ray("echo 'Starting deployment of {$productionImageName}.'");
$applicationDeploymentQueue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: prepareHelperContainer($server, $network, $deploymentUuid)
);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: generateComposeFile(
deploymentUuid: $deploymentUuid,
server: $server,
network: $network,
application: $application,
containerName: $containerName,
imageName: $productionImageName,
pullRequestId: $pullRequestId
)
);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: rollingUpdate(application: $application, deploymentUuid: $deploymentUuid)
);
} catch (Throwable $e) {
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: [
"echo 'Oops something is not okay, are you okay? 😢'",
"echo '{$e->getMessage()}'",
"echo -n 'Deployment failed. Removing the new version of your application.'",
executeInDocker($deploymentUuid, "docker rm -f $containerName >/dev/null 2>&1"),
]
);
// $this->next(ApplicationDeploymentStatus::FAILED->value);
throw $e;
}
}
// private function next(string $status)
// {
// // 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,
// ]);
// }
// queue_next_deployment($this->application);
// if ($status === ApplicationDeploymentStatus::FINISHED->value) {
// $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
// }
// if ($status === ApplicationDeploymentStatus::FAILED->value) {
// $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
// }
// }
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Jobs;
use App\Traits\ExecuteRemoteCommand;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ApplicationDeploySimpleDockerfileJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle() {
ray('Deploying Simple Dockerfile');
}
}

View File

@ -73,9 +73,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose;
private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile';
private string $docker_compose_location = '/docker-compose.yml';
private ?string $addHosts = null;
private ?string $buildTarget = null;
private $log_model;
private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
@ -92,9 +92,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public $tries = 1;
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->build_pack = data_get($this->application, 'build_pack');
@ -114,7 +112,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user;
$this->basedir = "/artifacts/{$this->deployment_uuid}";
$this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
$this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
@ -183,15 +181,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
// Check custom port
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
if (count($matches) === 1) {
$this->customPort = $matches[0];
$gitHost = str($this->application->git_repository)->before(':');
$gitRepo = str($this->application->git_repository)->after('/');
$this->customRepository = "$gitHost:$gitRepo";
} else {
$this->customRepository = $this->application->git_repository;
}
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
try {
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart();
@ -203,6 +194,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return;
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockercompose') {
$this->deploy_docker_compose_buildpack();
} else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') {
@ -397,19 +390,27 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]);
}
}
// private function save_environment_variables()
// {
// $envs = collect([]);
// foreach ($this->application->environment_variables as $env) {
// $envs->push($env->key . '=' . $env->value);
// }
// $envs_base64 = base64_encode($envs->implode("\n"));
// $this->execute_remote_command(
// [
// executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
// ],
// );
// }
private function save_environment_variables()
{
$envs = collect([]);
if ($this->pull_request_id !== 0) {
foreach ($this->application->environment_variables_preview as $env) {
$envs->push($env->key . '=' . $env->value);
}
} else {
foreach ($this->application->environment_variables as $env) {
$envs->push($env->key . '=' . $env->value);
}
}
ray($envs);
$envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
],
);
}
private function deploy_simple_dockerfile()
{
$dockerfile_base64 = base64_encode($this->application->dockerfile);
@ -447,7 +448,45 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_compose_file();
$this->rolling_update();
}
private function deploy_docker_compose_buildpack()
{
if (data_get($this->application, 'docker_compose_location')) {
$this->docker_compose_location = $this->application->docker_compose_location;
}
if ($this->pull_request_id === 0) {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
} else {
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
}
$this->server->executeRemoteCommand(
commands: $this->application->prepareHelperImage($this->deployment_uuid),
loggingModel: $this->application_deployment_queue
);
$this->check_git_if_build_needed();
$this->clone_repository();
$this->generate_image_names();
$this->cleanup_git();
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id);
$yaml = Yaml::dump($composeFile->toArray(), 10);
$this->docker_compose_base64 = base64_encode($yaml);
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yaml"), "hidden" => true
]);
$this->save_environment_variables();
$this->stop_running_container(force: true);
$networkId = $this->application->uuid;
if ($this->pull_request_id !== 0) {
$networkId = "{$this->application->uuid}-{$this->pull_request_id}";
}
$this->execute_remote_command([
"docker network create --attachable '{$networkId}' >/dev/null || true", "hidden" => true, "ignore_errors" => true
], [
"docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true
]);
$this->start_by_compose_file();
$this->application->loadComposeFile(isInit: false);
}
private function deploy_dockerfile_buildpack()
{
if (data_get($this->application, 'dockerfile_location')) {
@ -472,7 +511,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// $this->push_to_docker_registry();
// $this->deploy_to_additional_destinations();
// } else {
$this->rolling_update();
$this->rolling_update();
// }
}
private function deploy_nixpacks_buildpack()
@ -618,8 +657,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
$this->generate_compose_file();
// Needs separate preview variables
// $this->generate_build_env_variables();
// $this->add_build_env_variables_to_dockerfile();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->stop_running_container();
$this->execute_remote_command(
@ -725,13 +764,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function clone_repository()
{
$importCommands = $this->generate_git_import_commands();
$this->application_deployment_queue->addLogEntry("\n----------------------------------------");
$this->application_deployment_queue->addLogEntry("Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}.");
if ($this->pull_request_id !== 0) {
$this->application_deployment_queue->addLogEntry("Checking out tag pull/{$this->pull_request_id}/head.");
}
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
[
"echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
],
[
$importCommands, "hidden" => true
]
@ -740,90 +778,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_git_import_commands()
{
$this->branch = $this->application->git_branch;
$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) {
$this->fullRepoUrl = "{$this->source->html_url}/{$this->customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
} else {
$github_access_token = generate_github_installation_token($this->source);
$commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git {$this->basedir}"));
$this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git";
}
if ($this->pull_request_id !== 0) {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin $this->branch && git checkout $pr_branch_name"));
}
return $commands->implode(' && ');
}
}
if ($this->application->deploymentType() === 'deploy_key') {
$this->fullRepoUrl = $this->customRepository;
$private_key = data_get($this->application, 'private_key.private_key');
if (is_null($private_key)) {
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command_base);
$commands = collect([
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
]);
if ($this->pull_request_id !== 0) {
ray($this->git_type);
if ($this->git_type === 'gitlab') {
$this->branch = "merge-requests/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
}
if ($this->git_type === 'github') {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
}
}
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
return $commands->implode(' && ');
}
if ($this->application->deploymentType() === 'other') {
$this->fullRepoUrl = $this->customRepository;
$git_clone_command = "{$git_clone_command} {$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
return $commands->implode(' && ');
}
['commands' => $commands, 'branch' => $this->branch, 'fullRepoUrl' => $this->fullRepoUrl] = $this->application->generateGitImportCommands($this->deployment_uuid, $this->pull_request_id, $this->git_type);
return $commands;
}
private function set_git_import_settings($git_clone_command)
{
if ($this->application->git_commit_sha !== 'HEAD') {
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && 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->basedir} && git submodule update --init --recursive";
}
if ($this->application->settings->is_git_lfs_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git lfs pull";
}
return $git_clone_command;
return $this->application->setGitImportSettings($this->deployment_uuid, $git_clone_command);
}
private function cleanup_git()
@ -849,7 +810,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function nixpacks_build_cmd()
{
$this->generate_env_variables();
$nixpacks_command = "nixpacks build --cache-key '{$this->application->uuid}' -o {$this->workdir} {$this->env_args} --no-error-without-start";
$cacheKey = $this->application->uuid;
if ($this->pull_request_id !== 0) {
$cacheKey = "{$this->application->uuid}-pr-{$this->pull_request_id}";
}
$nixpacks_command = "nixpacks build --cache-key '{$cacheKey}' -o {$this->workdir} {$this->env_args} --no-error-without-start";
if ($this->application->build_command) {
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
}
@ -879,6 +844,26 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->env_args = $this->env_args->implode(' ');
}
private function modify_compose_file()
{
// ray("{$this->workdir}{$this->docker_compose_location}");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->docker_compose_location}"), "hidden" => true, "save" => 'compose_file']);
if ($this->saved_outputs->get('compose_file')) {
$compose = $this->saved_outputs->get('compose_file');
}
try {
$yaml = Yaml::parse($compose);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
$services = data_get($yaml, 'services');
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
$definedNetwork = collect([$this->application->uuid]);
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelNetworks, $definedNetwork) {
$serviceNetworks = collect(data_get($service, 'networks', []));
});
}
private function generate_compose_file()
{
$ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array;
@ -967,7 +952,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
];
}
if ($this->application->settings->is_gpu_enabled) {
ray('asd');
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'),
@ -1206,14 +1190,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function stop_running_container(bool $force = false)
{
$this->execute_remote_command(["echo -n 'Removing old container.'"]);
$this->application_deployment_queue->addLogEntry("Removing old containers.");
if ($this->newVersionIsHealthy || $force) {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id !== 0) {
$containers = $containers->filter(function ($container) {
return data_get($container, 'Names') === $this->container_name;
});
} else {
if ($this->pull_request_id === 0) {
$containers = $containers->filter(function ($container) {
return data_get($container, 'Names') !== $this->container_name;
});
@ -1224,14 +1204,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
});
$this->execute_remote_command(
[
"echo 'Rolling update completed.'"
],
);
$this->application_deployment_queue->addLogEntry("Rolling update completed.");
} else {
$this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
$this->execute_remote_command(
["echo -n 'New container is not healthy, rolling back to the old container.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
}
@ -1240,8 +1216,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function start_by_compose_file()
{
if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
$this->execute_remote_command(
["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
@ -1274,10 +1250,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "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}");
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
}
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
}
}
ray($dockerfile->implode("\n"));
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}{$this->dockerfile_location}"),

View File

@ -35,17 +35,13 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return $this->server->id;
}
public function handle(): void
public function handle()
{
// ray("checking container statuses for {$this->server->id}");
try {
if (!$this->server->isServerReady()) {
return;
};
$containers = instant_remote_process(["docker container ls -q"], $this->server);
if (!$containers) {
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
$containers = format_docker_command_output_to_json($containers);
$applications = $this->server->applications();
@ -167,7 +163,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} else {
$url = null;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']);
}
@ -194,7 +190,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$url = null;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) {
@ -219,7 +215,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$url = null;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
foreach ($notRunningDatabases as $database) {
@ -243,7 +239,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} else {
$url = null;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
// Check if proxy is running
@ -256,7 +252,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
} else {
ray('Proxy could not be started.');
}
@ -272,7 +268,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} catch (\Throwable $e) {
send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
ray($e->getMessage());
handleError($e);
return handleError($e);
}
}
}

View File

@ -3,8 +3,6 @@
namespace App\Jobs;
use App\Models\Server;
use App\Notifications\Server\HighDiskUsage;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -13,6 +11,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use RuntimeException;
class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
{
@ -35,7 +34,7 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
}
});
if ($isInprogress) {
throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
}
if (!$this->server->isFunctional()) {
return;

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,9 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
class Application extends BaseModel
{
@ -45,7 +48,14 @@ class Application extends BaseModel
$application->environment_variables_preview()->delete();
});
}
public function link()
{
return route('project.application.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'application_uuid' => $this->uuid
]);
}
public function settings()
{
return $this->hasOne(ApplicationSetting::class);
@ -123,6 +133,36 @@ class Application extends BaseModel
}
);
}
public function dockerComposeLocation(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return '/docker-compose.yaml';
} else {
if ($value !== '/') {
return Str::start(Str::replaceEnd('/', '', $value), '/');
}
return Str::start($value, '/');
}
}
);
}
public function dockerComposePrLocation(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return '/docker-compose.yaml';
} else {
if ($value !== '/') {
return Str::start(Str::replaceEnd('/', '', $value), '/');
}
return Str::start($value, '/');
}
}
);
}
public function baseDirectory(): Attribute
{
return Attribute::make(
@ -157,7 +197,16 @@ class Application extends BaseModel
: explode(',', $this->ports_exposes)
);
}
public function serviceType()
{
$found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) {
return str($this->image)->before(':')->value() === $service;
})->first());
if ($found->isNotEmpty()) {
return $found;
}
return null;
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
@ -224,7 +273,6 @@ class Application extends BaseModel
{
return $this->morphTo();
}
public function isDeploymentInprogress()
{
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
@ -342,4 +390,289 @@ class Application extends BaseModel
}
return false;
}
public function healthCheckUrl()
{
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
return null;
}
if (!$this->health_check_port) {
$health_check_port = $this->ports_exposes_array[0];
} else {
$health_check_port = $this->health_check_port;
}
if ($this->health_check_path) {
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}";
} else {
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/";
}
return $full_healthcheck_url;
}
function customRepository()
{
preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches);
$port = 22;
if (count($matches) === 1) {
$port = $matches[0];
$gitHost = str($this->git_repository)->before(':');
$gitRepo = str($this->git_repository)->after('/');
$repository = "$gitHost:$gitRepo";
} else {
$repository = $this->git_repository;
}
return [
'repository' => $repository,
'port' => $port
];
}
function generateBaseDir(string $uuid)
{
return "/artifacts/{$uuid}";
}
function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
{
$baseDir = $this->generateBaseDir($deployment_uuid);
if ($this->git_commit_sha !== 'HEAD') {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git -c advice.detachedHead=false checkout {$this->git_commit_sha} >/dev/null 2>&1";
}
if ($this->settings->is_git_submodules_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git submodule update --init --recursive";
}
if ($this->settings->is_git_lfs_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git lfs pull";
}
return $git_clone_command;
}
function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null)
{
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
$baseDir = $custom_base_dir ?? $this->generateBaseDir($deployment_uuid);
$commands = collect([]);
$git_clone_command = "git clone -b {$this->git_branch}";
if ($only_checkout) {
$git_clone_command = "git clone --no-checkout -b {$this->git_branch}";
}
if ($pull_request_id !== 0) {
$pr_branch_name = "pr-{$pull_request_id}-coolify";
}
if ($this->deploymentType() === 'source') {
$source_html_url = data_get($this, '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) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
if (!$only_checkout) {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command);
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
} else {
$github_access_token = generate_github_installation_token($this->source);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}"));
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
} else {
$commands->push("{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}");
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
}
}
if ($pull_request_id !== 0) {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name"));
} else {
$commands->push("cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name");
}
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl
];
}
}
if ($this->deploymentType() === 'deploy_key') {
$fullRepoUrl = $customRepository;
$private_key = data_get($this, 'private_key.private_key');
if (is_null($private_key)) {
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$customRepository} {$baseDir}";
if (!$only_checkout) {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base);
}
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, "mkdir -p /root/.ssh"),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
executeInDocker($deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
]);
} else {
$commands = collect([
"mkdir -p /root/.ssh",
"echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa",
"chmod 600 /root/.ssh/id_rsa",
]);
}
if ($pull_request_id !== 0) {
if ($git_type === 'gitlab') {
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
}
if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
}
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl
];
}
if ($this->deploymentType() === 'other') {
$fullRepoUrl = $customRepository;
$git_clone_command = "{$git_clone_command} {$customRepository} {$baseDir}";
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl
];
}
}
public function prepareHelperImage(string $deploymentUuid)
{
$basedir = $this->generateBaseDir($deploymentUuid);
$helperImage = config('coolify.helper_image');
$server = data_get($this, 'destination.server');
$network = data_get($this, 'destination.network');
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
$commands = collect([]);
if ($dockerConfigFileExists === 'OK') {
$commands->push([
"command" => "docker run -d --network $network -v /:/host --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
"hidden" => true,
]);
} else {
$commands->push([
"command" => "docker run -d --network {$network} -v /:/host --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
"hidden" => true,
]);
}
$commands->push([
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
"hidden" => true,
]);
return $commands;
}
function parseCompose(int $pull_request_id = 0)
{
if ($this->docker_compose_raw) {
$mainCompose = parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id);
if ($this->getMorphClass() === 'App\Models\Application' && $this->docker_compose_pr_raw) {
parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, is_pr: true);
}
return $mainCompose;
} else {
return collect([]);
}
}
function loadComposeFile($isInit = false)
{
$initialDockerComposeLocation = $this->docker_compose_location;
$initialDockerComposePrLocation = $this->docker_compose_pr_location;
if ($this->build_pack === 'dockercompose') {
if ($isInit && $this->docker_compose_raw) {
return;
}
$uuid = new Cuid2();
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.');
$workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location;
$prComposeFile = $this->docker_compose_pr_location;
$fileList = collect([".$composeFile"]);
if ($composeFile !== $prComposeFile) {
$fileList->push(".$prComposeFile");
}
$commands = collect([
"mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}",
$cloneCommand,
"git sparse-checkout init --cone",
"git sparse-checkout set {$fileList->implode(' ')}",
"git read-tree -mu HEAD",
"cat .$workdir$composeFile",
]);
$composeFileContent = instant_remote_process($commands, $this->destination->server, false);
if (!$composeFileContent) {
$this->docker_compose_location = $initialDockerComposeLocation;
$this->save();
throw new \Exception("Could not load base compose file from $workdir$composeFile");
} else {
$this->docker_compose_raw = $composeFileContent;
$this->save();
}
if ($composeFile === $prComposeFile) {
$this->docker_compose_pr_raw = $composeFileContent;
$this->save();
} else {
$commands = collect([
"cd /tmp/{$uuid}",
"cat .$workdir$prComposeFile",
]);
$composePrFileContent = instant_remote_process($commands, $this->destination->server, false);
if (!$composePrFileContent) {
$this->docker_compose_pr_location = $initialDockerComposePrLocation;
$this->save();
throw new \Exception("Could not load compose file from $workdir$prComposeFile");
} else {
$this->docker_compose_pr_raw = $composePrFileContent;
$this->save();
}
}
$commands = collect([
"rm -rf /tmp/{$uuid}",
]);
instant_remote_process($commands, $this->destination->server, false);
return [
'parsedServices' => $this->parseCompose(),
'initialDockerComposeLocation' => $this->docker_compose_location,
'initialDockerComposePrLocation' => $this->docker_compose_pr_location,
];
}
}
}

View File

@ -3,8 +3,48 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
class ApplicationDeploymentQueue extends Model
{
protected $guarded = [];
public function getOutput($name)
{
if (!$this->logs) {
return null;
}
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
}
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
{
if ($type === 'error') {
$type = 'stderr';
}
$message = str($message)->trim();
if ($message->startsWith('╔')) {
$message = "\n" . $message;
}
$newLogEntry = [
'command' => null,
'output' => remove_iip($message),
'type' => $type,
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => 1,
];
if ($this->logs) {
$previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
$previousLogs[] = $newLogEntry;
$this->update([
'logs' => json_encode($previousLogs, flags: JSON_THROW_ON_ERROR),
]);
} else {
$this->update([
'logs' => json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR),
]);
}
}
}

View File

@ -5,7 +5,26 @@ namespace App\Models;
class ApplicationPreview extends BaseModel
{
protected $guarded = [];
protected static function booted()
{
static::deleting(function ($preview) {
if ($preview->application->build_pack === 'dockercompose') {
$server = $preview->application->destination->server;
$composeFile = $preview->application->parseCompose(pull_request_id: $preview->pull_request_id);
$volumes = data_get($composeFile, 'volumes');
$networks = data_get($composeFile, 'networks');
$networkKeys = collect($networks)->keys();
$volumeKeys = collect($volumes)->keys();
$volumeKeys->each(function ($key) use ($server) {
instant_remote_process(["docker volume rm -f $key"], $server, false);
});
$networkKeys->each(function ($key) use ($server) {
instant_remote_process(["docker network disconnect $key coolify-proxy"], $server, false);
instant_remote_process(["docker network rm $key"], $server, false);
});
}
});
}
static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
{
return self::where('application_id', $application_id)->where('pull_request_id', $pull_request_id)->firstOrFail();

View File

@ -3,8 +3,10 @@
namespace App\Models;
use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Spatie\Url\Url;
class InstanceSettings extends Model implements SendsEmail
{
@ -16,6 +18,18 @@ class InstanceSettings extends Model implements SendsEmail
'smtp_password' => 'encrypted',
];
public function fqdn(): Attribute
{
return Attribute::make(
set: function ($value) {
if ($value) {
$url = Url::fromString($value);
$host = $url->getHost();
return $url->getScheme() . '://' . $host;
}
}
);
}
public static function get()
{
return InstanceSettings::findOrFail(0);

View File

@ -4,20 +4,26 @@ namespace App\Models;
use App\Actions\Server\InstallLogDrain;
use App\Actions\Server\InstallNewRelic;
use App\Enums\ApplicationDeploymentStatus;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str;
use Stringable;
class Server extends BaseModel
{
use SchemalessAttributesTrait;
public static $batch_counter = 0;
protected static function booted()
{
@ -189,6 +195,13 @@ class Server extends BaseModel
{
return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
}
public function definedResources()
{
$applications = $this->applications();
$databases = $this->databases();
$services = $this->services();
return $applications->concat($databases)->concat($services->get());
}
public function hasDefinedResources()
{
$applications = $this->applications()->count() > 0;
@ -217,6 +230,23 @@ class Server extends BaseModel
return $standaloneDocker->applications;
})->flatten();
}
public function dockerComposeBasedApplications()
{
return $this->applications()->filter(function ($application) {
return data_get($application, 'build_pack') === 'dockercompose';
});
}
public function dockerComposeBasedPreviewDeployments()
{
return $this->previews()->filter(function ($preview) {
$applicationId = data_get($preview, 'application_id');
$application = Application::find($applicationId);
if (!$application) {
return false;
}
return data_get($application, 'build_pack') === 'dockercompose';
});
}
public function services()
{
return $this->hasMany(Service::class);
@ -304,7 +334,7 @@ class Server extends BaseModel
{
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled;
}
public function validateOS()
public function validateOS(): bool | Stringable
{
$os_release = instant_remote_process(['cat /etc/os-release'], $this);
$datas = collect(explode("\n", $os_release));
@ -314,12 +344,16 @@ class Server extends BaseModel
$collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
}
$ID = data_get($collectedData, 'ID');
$ID_LIKE = data_get($collectedData, 'ID_LIKE');
$VERSION_ID = data_get($collectedData, 'VERSION_ID');
// ray($ID, $ID_LIKE, $VERSION_ID);
if (collect(SUPPORTED_OS)->contains($ID_LIKE)) {
// $ID_LIKE = data_get($collectedData, 'ID_LIKE');
// $VERSION_ID = data_get($collectedData, 'VERSION_ID');
$supported = collect(SUPPORTED_OS)->filter(function ($supportedOs) use ($ID) {
if (str($supportedOs)->contains($ID)) {
return str($ID);
}
});
if ($supported->count() === 1) {
ray('supported');
return str($ID_LIKE)->explode(' ')->first();
return str($supported->first());
} else {
ray('not supported');
return false;
@ -387,4 +421,83 @@ class Server extends BaseModel
{
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
}
public function executeRemoteCommand(Collection $commands, ?ApplicationDeploymentQueue $loggingModel = null)
{
static::$batch_counter++;
foreach ($commands as $command) {
$realCommand = data_get($command, 'command');
if (is_null($realCommand)) {
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($command, 'hidden', false);
$ignoreErrors = data_get($command, 'ignoreErrors', false);
$customOutputType = data_get($command, 'customOutputType');
$name = data_get($command, 'name');
$remoteCommand = generateSshCommand($this, $realCommand);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remoteCommand, function (string $type, string $output) use ($realCommand, $hidden, $customOutputType, $loggingModel, $name) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$newLogEntry = [
'command' => remove_iip($realCommand),
'output' => remove_iip($output),
'type' => $customOutputType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if ($loggingModel) {
if (!$loggingModel->logs) {
$newLogEntry['order'] = 1;
} else {
$previousLogs = json_decode($loggingModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
}
if ($name) {
$newLogEntry['name'] = $name;
}
$previousLogs[] = $newLogEntry;
$loggingModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
$loggingModel->save();
}
});
if ($loggingModel) {
$loggingModel->update([
'current_process_id' => $process->id(),
]);
}
$processResult = $process->wait();
if ($processResult->exitCode() !== 0) {
if (!$ignoreErrors) {
if ($loggingModel) {
$status = ApplicationDeploymentStatus::FAILED->value;
$loggingModel->status = $status;
$loggingModel->save();
}
throw new \RuntimeException($processResult->errorOutput());
}
}
}
}
public function stopApplicationRelatedRunningContainers(string $applicationId, string $containerName)
{
$containers = getCurrentApplicationContainerStatus($this, $applicationId, 0);
$containers = $containers->filter(function ($container) use ($containerName) {
return data_get($container, 'Names') !== $containerName;
});
$containers->each(function ($container) {
$removableContainer = data_get($container, 'Names');
$this->server->executeRemoteCommand(
commands: collect([
'command' => "docker rm -f $removableContainer >/dev/null 2>&1",
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $this->deploymentQueueEntry
);
});
}
}

View File

@ -52,7 +52,7 @@ class Service extends BaseModel
foreach ($applications as $application) {
$image = str($application->image)->before(':')->value();
switch ($image) {
case str($image)->contains('minio'):
case str($image)?->contains('minio'):
$data = collect([]);
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
@ -105,7 +105,7 @@ class Service extends BaseModel
$fields->put('MinIO', $data->toArray());
break;
case str($image)->contains('weblate'):
case str($image)?->contains('weblate'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
@ -130,6 +130,67 @@ class Service extends BaseModel
]);
}
$fields->put('Weblate', $data);
break;
case str($image)?->contains('ghost'):
$data = collect([]);
$MAIL_OPTIONS_AUTH_PASS = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_PASS')->first();
$MAIL_OPTIONS_AUTH_USER = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_USER')->first();
$MAIL_OPTIONS_SECURE = $this->environment_variables()->where('key', 'MAIL_OPTIONS_SECURE')->first();
$MAIL_OPTIONS_PORT = $this->environment_variables()->where('key', 'MAIL_OPTIONS_PORT')->first();
$MAIL_OPTIONS_SERVICE = $this->environment_variables()->where('key', 'MAIL_OPTIONS_SERVICE')->first();
$MAIL_OPTIONS_HOST = $this->environment_variables()->where('key', 'MAIL_OPTIONS_HOST')->first();
if ($MAIL_OPTIONS_AUTH_PASS) {
$data = $data->merge([
'Mail Password' => [
'key' => data_get($MAIL_OPTIONS_AUTH_PASS, 'key'),
'value' => data_get($MAIL_OPTIONS_AUTH_PASS, 'value'),
'isPassword' => true,
],
]);
}
if ($MAIL_OPTIONS_AUTH_USER) {
$data = $data->merge([
'Mail User' => [
'key' => data_get($MAIL_OPTIONS_AUTH_USER, 'key'),
'value' => data_get($MAIL_OPTIONS_AUTH_USER, 'value'),
],
]);
}
if ($MAIL_OPTIONS_SECURE) {
$data = $data->merge([
'Mail Secure' => [
'key' => data_get($MAIL_OPTIONS_SECURE, 'key'),
'value' => data_get($MAIL_OPTIONS_SECURE, 'value'),
],
]);
}
if ($MAIL_OPTIONS_PORT) {
$data = $data->merge([
'Mail Port' => [
'key' => data_get($MAIL_OPTIONS_PORT, 'key'),
'value' => data_get($MAIL_OPTIONS_PORT, 'value'),
],
]);
}
if ($MAIL_OPTIONS_SERVICE) {
$data = $data->merge([
'Mail Service' => [
'key' => data_get($MAIL_OPTIONS_SERVICE, 'key'),
'value' => data_get($MAIL_OPTIONS_SERVICE, 'value'),
],
]);
}
if ($MAIL_OPTIONS_HOST) {
$data = $data->merge([
'Mail Host' => [
'key' => data_get($MAIL_OPTIONS_HOST, 'key'),
'value' => data_get($MAIL_OPTIONS_HOST, 'value'),
],
]);
}
$fields->put('Ghost', $data);
break;
}
}
$databases = $this->databases()->get();
@ -300,6 +361,14 @@ class Service extends BaseModel
}
}
}
public function link()
{
return route('project.service.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'service_uuid' => $this->uuid
]);
}
public function documentation()
{
$services = getServiceTemplates();
@ -371,521 +440,12 @@ class Service extends BaseModel
public function parse(bool $isNew = false): Collection
{
// ray()->clearAll();
if ($this->docker_compose_raw) {
try {
$yaml = Yaml::parse($this->docker_compose_raw);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
$topLevelVolumes = collect(data_get($yaml, 'volumes', []));
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services');
$generatedServiceFQDNS = collect([]);
if (is_null($this->destination)) {
$destination = $this->server->destinations()->first();
if ($destination) {
$this->destination()->associate($destination);
$this->save();
}
}
$definedNetwork = collect([$this->uuid]);
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS) {
$serviceVolumes = collect(data_get($service, 'volumes', []));
$servicePorts = collect(data_get($service, 'ports', []));
$serviceNetworks = collect(data_get($service, 'networks', []));
$serviceVariables = collect(data_get($service, 'environment', []));
$serviceLabels = collect(data_get($service, 'labels', []));
if ($serviceLabels->count() > 0) {
$removedLabels = collect([]);
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
if (!str($serviceLabel)->contains('=')) {
$removedLabels->put($serviceLabelName, $serviceLabel);
return false;
}
return $serviceLabel;
});
foreach($removedLabels as $removedLabelName =>$removedLabel) {
$serviceLabels->push("$removedLabelName=$removedLabel");
}
}
$containerName = "$serviceName-{$this->uuid}";
// Decide if the service is a database
$isDatabase = false;
$image = data_get_str($service, 'image');
if ($image->contains(':')) {
$image = Str::of($image);
} else {
$image = Str::of($image)->append(':latest');
}
$imageName = $image->before(':');
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
$isDatabase = true;
}
data_set($service, 'is_database', $isDatabase);
// Create new serviceApplication or serviceDatabase
if ($isDatabase) {
if ($isNew) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceDatabase::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
}
} else {
if ($isNew) {
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceApplication::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
}
}
if (is_null($savedService)) {
if ($isDatabase) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
}
}
// Check if image changed
if ($savedService->image !== $image) {
$savedService->image = $image;
$savedService->save();
}
// Collect/create/update networks
if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) {
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName;
});
if (!$networkExists) {
$topLevelNetworks->put($networkDetails, null);
}
}
}
// Collect/create/update ports
$collectedPorts = collect([]);
if ($servicePorts->count() > 0) {
foreach ($servicePorts as $sport) {
if (is_string($sport) || is_numeric($sport)) {
$collectedPorts->push($sport);
}
if (is_array($sport)) {
$target = data_get($sport, 'target');
$published = data_get($sport, 'published');
$protocol = data_get($sport, 'protocol');
$collectedPorts->push("$target:$published/$protocol");
}
}
}
$savedService->ports = $collectedPorts->implode(',');
$savedService->save();
// Add Coolify specific networks
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork;
});
if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [
'name' => $network,
'external' => true
]);
}
}
$networks = collect();
foreach ($serviceNetworks as $key => $serviceNetwork) {
if (gettype($serviceNetwork) === 'string') {
// networks:
// - appwrite
$networks->put($serviceNetwork, null);
} else if (gettype($serviceNetwork) === 'array') {
// networks:
// default:
// ipv4_address: 192.168.203.254
// $networks->put($serviceNetwork, null);
ray($key);
$networks->put($key, $serviceNetwork);
}
}
foreach ($definedNetwork as $key => $network) {
$networks->put($network, null);
}
data_set($service, 'networks', $networks->toArray());
// Collect/create/update volumes
if ($serviceVolumes->count() > 0) {
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($savedService, $topLevelVolumes) {
$type = null;
$source = null;
$target = null;
$content = null;
$isDirectory = false;
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
$type = Str::of('bind');
} else {
$type = Str::of('volume');
}
} else if (is_array($volume)) {
$type = data_get_str($volume, 'type');
$source = data_get_str($volume, 'source');
$target = data_get_str($volume, 'target');
$content = data_get($volume, 'content');
$isDirectory = (bool) data_get($volume, 'isDirectory', false);
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
if ($foundConfig) {
$contentNotNull = data_get($foundConfig, 'content');
if ($contentNotNull) {
$content = $contentNotNull;
}
$isDirectory = (bool) data_get($foundConfig, 'is_directory');
}
}
if ($type->value() === 'bind') {
if ($source->value() === "/var/run/docker.sock") {
return $volume;
}
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
return $volume;
}
LocalFileVolume::updateOrCreate(
[
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'fs_path' => $source,
'mount_path' => $target,
'content' => $content,
'is_directory' => $isDirectory,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
} else if ($type->value() === 'volume') {
$slugWithoutUuid = Str::slug($source, '-');
$name = "{$savedService->service->uuid}_{$slugWithoutUuid}";
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
$source = $name;
$volume = "$source:$target";
} else if (is_array($volume)) {
data_set($volume, 'source', $name);
}
$topLevelVolumes->put($name, [
'name' => $name,
]);
LocalPersistentVolume::updateOrCreate(
[
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'name' => $name,
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
}
$savedService->getFilesFromServer(isInit: true);
return $volume;
});
data_set($service, 'volumes', $serviceVolumes->toArray());
}
// Add env_file with at least .env to the service
// $envFile = collect(data_get($service, 'env_file', []));
// if ($envFile->count() > 0) {
// if (!$envFile->contains('.env')) {
// $envFile->push('.env');
// }
// } else {
// $envFile = collect(['.env']);
// }
// data_set($service, 'env_file', $envFile->toArray());
// Get variables from the service
foreach ($serviceVariables as $variableName => $variable) {
if (is_numeric($variableName)) {
$variable = Str::of($variable);
if ($variable->contains('=')) {
// - SESSION_SECRET=123
// - SESSION_SECRET=
$key = $variable->before('=');
$value = $variable->after('=');
} else {
// - SESSION_SECRET
$key = $variable;
$value = null;
}
} else {
// SESSION_SECRET: 123
// SESSION_SECRET:
$key = Str::of($variableName);
$value = Str::of($variable);
}
// TODO: here is the problem
if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
$fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}");
if (substr_count($key->value(), '_') === 3) {
// SERVICE_FQDN_UMAMI_1000
$port = $key->afterLast('_');
} else {
// SERVICE_FQDN_UMAMI
$port = null;
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if (substr_count($key->value(), '_') >= 2) {
if (is_null($value)) {
$value = Str::of('/');
}
$path = $value->value();
if ($generatedServiceFQDNS->count() > 0) {
$alreadyGenerated = $generatedServiceFQDNS->has($key->value());
if ($alreadyGenerated) {
$fqdn = $generatedServiceFQDNS->get($key->value());
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
}
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
}
$fqdn = "$fqdn$path";
}
if (!$isDatabase) {
if ($savedService->fqdn) {
$fqdn = $savedService->fqdn . ',' . $fqdn;
} else {
$fqdn = $fqdn;
}
$savedService->fqdn = $fqdn;
$savedService->save();
}
}
// data_forget($service, "environment.$variableName");
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
// if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) {
// $yaml = data_forget($yaml, "services.$serviceName.environment");
// }
continue;
}
if ($value?->startsWith('$')) {
$value = Str::of(replaceVariables($value));
$key = $value;
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'service_id' => $this->id,
])->first();
if ($value->startsWith('SERVICE_')) {
// Count _ in $value
$count = substr_count($value->value(), '_');
if ($count === 2) {
// SERVICE_FQDN_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
$forService = $value->afterLast('_');
$generatedValue = null;
$port = null;
}
if ($count === 3) {
// SERVICE_FQDN_UMAMI_1000
$command = $value->after('SERVICE_')->before('_');
$forService = $value->after('SERVICE_')->after('_')->before('_');
$generatedValue = null;
$port = $value->afterLast('_');
}
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($this->server, $containerName);
} else {
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value');
} else {
if ($command->value() === 'URL') {
$fqdn = Str::of($fqdn)->after('://')->value();
}
EnvironmentVariable::create([
'key' => $key,
'value' => $fqdn,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
if (!$isDatabase) {
if ($command->value() === 'FQDN' && is_null($savedService->fqdn)) {
$savedService->fqdn = $fqdn;
$savedService->save();
}
}
} else {
switch ($command) {
case 'PASSWORD':
$generatedValue = Str::password(symbols: false);
break;
case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false);
break;
case 'BASE64_64':
$generatedValue = Str::random(64);
break;
case 'BASE64_128':
$generatedValue = Str::random(128);
break;
case 'BASE64':
$generatedValue = Str::random(32);
break;
case 'USER':
$generatedValue = Str::random(16);
break;
}
if (!$foundEnv) {
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
}
} else {
if ($value->contains(':-')) {
$key = $value->before(':');
$defaultValue = $value->after(':-');
} else if ($value->contains('-')) {
$key = $value->before('-');
$defaultValue = $value->after('-');
} else if ($value->contains(':?')) {
$key = $value->before(':');
$defaultValue = $value->after(':?');
} else if ($value->contains('?')) {
$key = $value->before('?');
$defaultValue = $value->after('?');
} else {
$key = $value;
$defaultValue = null;
}
if ($foundEnv) {
$defaultValue = data_get($foundEnv, 'value');
}
EnvironmentVariable::updateOrCreate([
'key' => $key,
'service_id' => $this->id,
], [
'value' => $defaultValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
}
}
// Add labels to the service
if ($savedService->serviceType()) {
$fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true);
} else {
$fqdns = collect(data_get($savedService, 'fqdns'));
}
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
$serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
}
}
if ($this->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
data_set($service, 'logging', [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
]);
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE);
data_set($service, 'container_name', $containerName);
data_forget($service, 'volumes.*.content');
data_forget($service, 'volumes.*.isDirectory');
// Remove unnecessary variables from service.environment
// $withoutServiceEnvs = collect([]);
// collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
// ray($key, $value);
// if (!Str::of($key)->startsWith('$SERVICE_') && !Str::of($value)->startsWith('SERVICE_')) {
// $k = Str::of($value)->before("=");
// $v = Str::of($value)->after("=");
// $withoutServiceEnvs->put($k->value(), $v->value());
// }
// });
// ray($withoutServiceEnvs);
// data_set($service, 'environment', $withoutServiceEnvs->toArray());
return $service;
});
$finalServices = [
'version' => $dockerComposeVersion,
'services' => $services->toArray(),
'volumes' => $topLevelVolumes->toArray(),
'networks' => $topLevelNetworks->toArray(),
];
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->save();
$this->saveComposeConfigs();
return collect([]);
} else {
return collect([]);
}
return parseDockerComposeFile($this, $isNew);
}
public function networks()
{
$networks = getTopLevelNetworks($this);
// ray($networks);
return $networks;
}
}

View File

@ -41,6 +41,14 @@ class StandaloneMariadb extends BaseModel
$database->environment_variables()->delete();
});
}
public function link()
{
return route('project.database.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'database_uuid' => $this->uuid
]);
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);

View File

@ -48,6 +48,14 @@ class StandaloneMongodb extends BaseModel
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function link()
{
return route('project.database.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'database_uuid' => $this->uuid
]);
}
public function mongoInitdbRootPassword(): Attribute
{
return Attribute::make(

View File

@ -41,6 +41,14 @@ class StandaloneMysql extends BaseModel
$database->environment_variables()->delete();
});
}
public function link()
{
return route('project.database.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'database_uuid' => $this->uuid
]);
}
public function type(): string
{
return 'standalone-mysql';

View File

@ -41,7 +41,14 @@ class StandalonePostgresql extends BaseModel
$database->environment_variables()->delete();
});
}
public function link()
{
return route('project.database.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'database_uuid' => $this->uuid
]);
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);

View File

@ -36,7 +36,14 @@ class StandaloneRedis extends BaseModel
$database->environment_variables()->delete();
});
}
public function link()
{
return route('project.database.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'database_uuid' => $this->uuid
]);
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);

View File

@ -12,6 +12,7 @@ use Illuminate\Support\Str;
trait ExecuteRemoteCommand
{
public ?string $save = null;
public static int $batch_counter = 0;
public function execute_remote_command(...$commands)
{
static::$batch_counter++;
@ -23,8 +24,6 @@ trait ExecuteRemoteCommand
if ($this->server instanceof Server === false) {
throw new \RuntimeException('Server is not set or is not an instance of Server model');
}
$commandsText->each(function ($single_command) {
$command = data_get($single_command, 'command') ?? $single_command[0] ?? null;
if ($command === null) {
@ -49,32 +48,29 @@ trait ExecuteRemoteCommand
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if (!$this->log_model->logs) {
if (!$this->application_deployment_queue->logs) {
$new_log_entry['order'] = 1;
} else {
$previous_logs = json_decode($this->log_model->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$previous_logs = json_decode($this->application_deployment_queue->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();
$this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR);
$this->application_deployment_queue->save();
if ($this->save) {
$this->saved_outputs[$this->save] = Str::of($output)->trim();
}
});
$this->log_model->update([
$this->application_deployment_queue->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();
$this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value;
$this->application_deployment_queue->save();
throw new \RuntimeException($process_result->errorOutput());
}
}

View File

@ -36,8 +36,6 @@ function queue_application_deployment(int $application_id, string $deployment_uu
if ($running_deployments->count() > 0) {
return;
}
// New deployment
// dispatchDeploymentJob($deployment->id);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
))->onConnection('long-running')->onQueue('long-running');
@ -48,34 +46,11 @@ function queue_next_deployment(Application $application)
{
$next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first();
if ($next_found) {
// New deployment
// dispatchDeploymentJob($next_found->id);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id,
))->onConnection('long-running')->onQueue('long-running');
}
}
function dispatchDeploymentJob($id)
{
$applicationQueue = ApplicationDeploymentQueue::find($id);
$application = Application::find($applicationQueue->application_id);
$isRestartOnly = data_get($applicationQueue, 'restart_only');
$isSimpleDockerFile = data_get($application, 'dockerfile');
$isDockerImage = data_get($application, 'build_pack') === 'dockerimage';
if ($isRestartOnly) {
ApplicationRestartJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else if ($isSimpleDockerFile) {
ApplicationDeploySimpleDockerfileJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else if ($isDockerImage) {
ApplicationDeployDockerImageJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else {
throw new Exception('Unknown build pack');
}
}
// Deployment things
function generateHostIpMapping(Server $server, string $network)
{
@ -201,7 +176,6 @@ function generateComposeFile(string $deploymentUuid, Server $server, string $net
];
}
if ($application->settings->is_gpu_enabled) {
ray('asd');
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'),
@ -302,39 +276,37 @@ function generateEnvironmentVariables(Application $application, $ports, int $pul
return $environment_variables->all();
}
function rollingUpdate(Application $application, string $deploymentUuid)
function startNewApplication(Application $application, string $deploymentUuid, ApplicationDeploymentQueue $loggingModel)
{
$commands = collect([]);
$workDir = generateWorkdir($deploymentUuid, $application);
if (count($application->ports_mappings_array) > 0) {
// $this->execute_remote_command(
// [
// "echo '\n----------------------------------------'",
// ],
// ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
// );
// $this->stop_running_container(force: true);
// $this->start_by_compose_file();
if ($application->build_pack === 'dockerimage') {
$loggingModel->addLogEntry('Pulling latest images from the registry.');
$commands->push(
[
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"),
"hidden" => true
],
[
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
"hidden" => true
],
);
} else {
$commands->push(
[
"command" => "echo '\n----------------------------------------'"
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
"hidden" => true
],
[
"command" => "echo -n 'Rolling update started.'"
]
);
if ($application->build_pack === 'dockerimage') {
$commands->push(
["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"), "hidden" => true],
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
);
} else {
$commands->push(
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
);
}
return $commands;
}
return $commands;
}
function removeOldDeployment(string $containerName)
{
$commands = collect([]);
$commands->push(
["docker rm -f $containerName >/dev/null 2>&1"],
);
return $commands;
}

View File

@ -28,7 +28,9 @@ const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
];
// Based on /etc/os-release
const SUPPORTED_OS = [
'debian',
'rhel centos fedora'
'ubuntu debian raspbian',
'centos fedora rhel ol rocky',
'sles opensuse-leap opensuse-tumbleweed'
];

View File

@ -24,7 +24,7 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
}
return StandalonePostgresql::create([
'name' => generate_database_name('postgresql'),
'postgres_password' => \Illuminate\Support\Str::password(symbols: false),
'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
@ -39,7 +39,7 @@ function create_standalone_redis($environment_id, $destination_uuid): Standalone
}
return StandaloneRedis::create([
'name' => generate_database_name('redis'),
'redis_password' => \Illuminate\Support\Str::password(symbols: false),
'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
@ -54,7 +54,7 @@ function create_standalone_mongodb($environment_id, $destination_uuid): Standalo
}
return StandaloneMongodb::create([
'name' => generate_database_name('mongodb'),
'mongo_initdb_root_password' => \Illuminate\Support\Str::password(symbols: false),
'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
@ -68,8 +68,8 @@ function create_standalone_mysql($environment_id, $destination_uuid): Standalone
}
return StandaloneMysql::create([
'name' => generate_database_name('mysql'),
'mysql_root_password' => \Illuminate\Support\Str::password(symbols: false),
'mysql_password' => \Illuminate\Support\Str::password(symbols: false),
'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'mysql_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
@ -83,8 +83,8 @@ function create_standalone_mariadb($environment_id, $destination_uuid): Standalo
}
return StandaloneMariadb::create([
'name' => generate_database_name('mariadb'),
'mariadb_root_password' => \Illuminate\Support\Str::password(symbols: false),
'mariadb_password' => \Illuminate\Support\Str::password(symbols: false),
'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'mariadb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),

View File

@ -3,6 +3,9 @@
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\Url\Url;
@ -137,18 +140,28 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
$labels->push('coolify.name=' . $name);
$labels->push('coolify.pullRequestId=' . $pull_request_id);
if ($type === 'service') {
$labels->push('coolify.service.subId=' . $subId);
$labels->push('coolify.service.subType=' . $subType);
$subId && $labels->push('coolify.service.subId=' . $subId);
$subType && $labels->push('coolify.service.subType=' . $subType);
}
return $labels;
}
function generateServiceSpecificFqdns($service, $forTraefik = false)
function generateServiceSpecificFqdns(ServiceApplication|Application $resource, $forTraefik = false)
{
$variables = collect($service->service->environment_variables);
$type = $service->serviceType();
if ($resource->getMorphClass() === 'App\Models\ServiceApplication') {
$uuid = $resource->uuid;
$server = $resource->service->server;
$environment_variables = $resource->service->environment_variables;
$type = $resource->serviceType();
} else if ($resource->getMorphClass() === 'App\Models\Application') {
$uuid = $resource->uuid;
$server = $resource->destination->server;
$environment_variables = $resource->environment_variables;
$type = $resource->serviceType();
}
$variables = collect($environment_variables);
$payload = collect([]);
switch ($type) {
case $type->contains('minio'):
case $type?->contains('minio'):
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
@ -156,12 +169,12 @@ function generateServiceSpecificFqdns($service, $forTraefik = false)
}
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
$MINIO_BROWSER_REDIRECT_URL?->update([
"value" => generateFqdn($service->service->server, 'console-' . $service->uuid)
"value" => generateFqdn($server, 'console-' . $uuid)
]);
}
if (is_null($MINIO_SERVER_URL?->value)) {
$MINIO_SERVER_URL?->update([
"value" => generateFqdn($service->service->server, 'minio-' . $service->uuid)
"value" => generateFqdn($server, 'minio-' . $uuid)
]);
}
if ($forTraefik) {
@ -175,10 +188,11 @@ function generateServiceSpecificFqdns($service, $forTraefik = false)
$MINIO_SERVER_URL->value,
]);
}
break;
}
return $payload;
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
@ -260,3 +274,18 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
}
return $labels->all();
}
function isDatabaseImage(string $image)
{
$image = str($image);
if ($image->contains(':')) {
$image = str($image);
} else {
$image = str($image)->append(':latest');
}
$imageName = $image->before(':');
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
return true;
}
return false;
}

View File

@ -1,6 +1,7 @@
<?php
use App\Actions\Proxy\SaveConfiguration;
use App\Models\Application;
use App\Models\Server;
use Symfony\Component\Yaml\Yaml;
@ -12,9 +13,32 @@ function get_proxy_path()
}
function connectProxyToNetworks(Server $server)
{
// Standalone networks
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
});
// Service networks
foreach ($server->services()->get() as $service) {
$networks->push($service->networks());
}
// Docker compose based apps
$docker_compose_apps = $server->dockerComposeBasedApplications();
foreach ($docker_compose_apps as $app) {
$networks->push($app->uuid);
}
// Docker compose based preview deployments
$docker_compose_previews = $server->dockerComposeBasedPreviewDeployments();
foreach ($docker_compose_previews as $preview) {
$pullRequestId = $preview->pull_request_id;
$applicationId = $preview->application_id;
$application = Application::find($applicationId);
if (!$application) {
continue;
}
$network = "{$application->uuid}-{$pullRequestId}";
$networks->push($network);
}
$networks = collect($networks)->flatten()->unique();
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}

View File

@ -151,6 +151,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
if (is_null($application_deployment_queue)) {
return collect([]);
}
// ray(data_get($application_deployment_queue, 'logs'));
try {
$decoded = json_decode(
data_get($application_deployment_queue, 'logs'),
@ -160,14 +161,15 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
} catch (\JsonException $exception) {
return collect([]);
}
// ray($decoded );
$formatted = collect($decoded);
if (!$is_debug_enabled) {
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
}
$formatted = $formatted
->sortBy(fn ($i) => $i['order'])
->sortBy(fn ($i) => data_get($i, 'order'))
->map(function ($i) {
$i['timestamp'] = Carbon::parse($i['timestamp'])->format('Y-M-d H:i:s.u');
data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u'));
return $i;
});
return $formatted;

File diff suppressed because it is too large Load Diff

View File

@ -26,9 +26,9 @@ return [
'server' => [
'zero' => 0,
'self-hosted' => 999999999999,
'basic' => 1,
'pro' => 10,
'ultimate' => 25,
'basic' => env('LIMIT_SERVER_BASIC', 2),
'pro' => env('LIMIT_SERVER_PRO', 10),
'ultimate' => env('LIMIT_SERVER_ULTIMATE', 25),
],
'email' => [
'zero' => true,

View File

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

View File

@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.146';
return '4.0.0-beta.147';

View File

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('docker_compose_location')->nullable()->default('/docker-compose.yaml')->after('dockerfile_location');
$table->string('docker_compose_pr_location')->nullable()->default('/docker-compose.yaml')->after('docker_compose_location');
$table->longText('docker_compose')->nullable()->after('docker_compose_location');
$table->longText('docker_compose_pr')->nullable()->after('docker_compose_location');
$table->longText('docker_compose_raw')->nullable()->after('docker_compose');
$table->longText('docker_compose_pr_raw')->nullable()->after('docker_compose');
$table->text('docker_compose_domains')->nullable()->after('docker_compose_raw');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('docker_compose_location');
$table->dropColumn('docker_compose_pr_location');
$table->dropColumn('docker_compose');
$table->dropColumn('docker_compose_pr');
$table->dropColumn('docker_compose_raw');
$table->dropColumn('docker_compose_pr_raw');
$table->dropColumn('docker_compose_domains');
});
}
};

View File

@ -28,6 +28,8 @@ services:
- REDIS_HOST
- REDIS_PASSWORD
- HORIZON_MAX_PROCESSES
- HORIZON_BALANCE_MAX_SHIFT
- HORIZON_BALANCE_COOLDOWN
- SSL_MODE=off
- PHP_PM_CONTROL=dynamic
- PHP_PM_START_SERVERS=1

View File

@ -1,44 +1,50 @@
<div class="group">
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
<x-chevron-down />
</label>
@if (data_get($application, 'fqdn') ||
collect(json_decode($this->application->docker_compose_domains))->count() > 0 ||
data_get($application, 'previews', collect([]))->count() > 0 ||
data_get($application, 'ports_mappings_array'))
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
<x-chevron-down />
</label>
<div class="absolute z-50 hidden group-hover:block">
<ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
@if (data_get($application, 'gitBrancLocation'))
<li>
<a target="_blank"
class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
href="{{ $application->gitBranchLocation }}">
<x-git-icon git="{{ $application->source?->getMorphClass() }}" />
Git Repository
</a>
</li>
@endif
@if (data_get($application, 'fqdn'))
@foreach (Str::of(data_get($application, 'fqdn'))->explode(',') as $fqdn)
<div class="absolute z-50 hidden group-hover:block">
<ul tabindex="0"
class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
@if (data_get($application, 'gitBrancLocation'))
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ getFqdnWithoutPort($fqdn) }}
<a target="_blank"
class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
href="{{ $application->gitBranchLocation }}">
<x-git-icon git="{{ $application->source?->getMorphClass() }}" />
Git Repository
</a>
</li>
@endforeach
@endif
@if (data_get($application, 'previews', collect([]))->count() > 0)
@foreach (data_get($application, 'previews') as $preview)
@if (data_get($preview, 'fqdn'))
@endif
@if (data_get($application, 'build_pack') === 'dockercompose')
@foreach (collect(json_decode($this->application->docker_compose_domains)) as $fqdn)
@if (data_get($fqdn, 'domain'))
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getFqdnWithoutPort(data_get($fqdn, 'domain')) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ getFqdnWithoutPort(data_get($fqdn, 'domain')) }}
</a>
</li>
@endif
@endforeach
@endif
@if (data_get($application, 'fqdn'))
@foreach (str(data_get($application, 'fqdn'))->explode(',') as $fqdn)
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">
target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
@ -47,52 +53,72 @@
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
PR{{ data_get($preview, 'pull_request_id') }} |
{{ data_get($preview, 'fqdn') }}
</svg>{{ getFqdnWithoutPort($fqdn) }}
</a>
</li>
@endif
@endforeach
@endif
@if (data_get($application, 'ports_mappings_array'))
@foreach ($application->ports_mappings_array as $port)
@if (isDev())
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
Port {{ $port }}
</a>
</li>
@else
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank"
href="http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
Port {{ $port }}
</a>
</li>
@endif
@endforeach
@endif
</ul>
</div>
@endforeach
@endif
@if (data_get($application, 'previews', collect([]))->count() > 0)
@foreach (data_get($application, 'previews') as $preview)
@if (data_get($preview, 'fqdn'))
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
PR{{ data_get($preview, 'pull_request_id') }} |
{{ data_get($preview, 'fqdn') }}
</a>
</li>
@endif
@endforeach
@endif
@if (data_get($application, 'ports_mappings_array'))
@foreach ($application->ports_mappings_array as $port)
@if (isDev())
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
Port {{ $port }}
</a>
</li>
@else
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank"
href="http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
Port {{ $port }}
</a>
</li>
@endif
@endforeach
@endif
</ul>
</div>
@endif
</div>

View File

@ -13,46 +13,57 @@
</a>
<x-applications.links :application="$application" />
<div class="flex-1"></div>
<x-applications.advanced :application="$application" />
@if ($application->status !== 'exited')
<button title="With rolling update if possible" wire:click='deploy' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
<button title="Restart without rebuilding" wire:click='restart' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747"/>
<path d="M20 4v5h-5"/>
</g>
</svg>
Restart
</button>
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div>
@else
<button wire:click='deploy' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
<x-applications.advanced :application="$application" />
@if ($application->status !== 'exited')
<button title="With rolling update if possible" wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
@if ($application->build_pack !== 'dockercompose')
<button title="Restart without rebuilding" wire:click='restart'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</button>
@endif
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@else
<button wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
@endif
@endif
</div>

View File

@ -81,7 +81,7 @@
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" />
</svg>
1 server <x-helper helper="Bring Your Own Server." />
2 servers <x-helper helper="Bring Your Own Server." />
</li>
<li class="flex gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"

View File

@ -1,6 +1,6 @@
<div class="navbar-main">
<a class="{{ request()->routeIs('project.service') ? 'text-white' : '' }}"
href="{{ route('project.service', $parameters) }}">
<a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button>
</a>
<x-services.links />

View File

@ -24,13 +24,16 @@
helper="Allow Git LFS during build process." />
@endif
<form wire:submit.prevent="submit">
<div class="flex gap-2">
<x-forms.checkbox helper="Enable GPU usage for this application. More info <a href='https://docs.docker.com/compose/gpu-support/' class='text-white underline' target='_blank'>here</a>." instantSave
id="application.settings.is_gpu_enabled" label="GPU Enabled Application" />
@if ($application->settings->is_gpu_enabled)
<x-forms.button type="submiot">Save</x-forms.button>
@endif
</div>
@if ($application->build_pack !== 'dockercompose')
<div class="flex gap-2">
<x-forms.checkbox
helper="Enable GPU usage for this application. More info <a href='https://docs.docker.com/compose/gpu-support/' class='text-white underline' target='_blank'>here</a>."
instantSave id="application.settings.is_gpu_enabled" label="GPU Enabled Application" />
@if ($application->settings->is_gpu_enabled)
<x-forms.button type="submiot">Save</x-forms.button>
@endif
</div>
@endif
@if ($application->settings->is_gpu_enabled)
<div class="flex flex-col w-full gap-2 p-2 xl:flex-row">
<x-forms.input label="GPU Driver" id="application.settings.gpu_driver"> </x-forms.input>

View File

@ -20,7 +20,7 @@
<a :class="activeTab === 'server' && 'text-white'"
@click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server
</a>
@if ($application->build_pack !== 'static')
@if ($application->build_pack !== 'static' && $application->build_pack !== 'dockercompose')
<a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
</a>
@ -34,7 +34,7 @@
Deployments
</a>
@endif
@if ($application->build_pack !== 'static')
@if ($application->build_pack !== 'static' && $application->build_pack !== 'dockercompose')
<a :class="activeTab === 'health' && 'text-white'"
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Healthchecks
</a>
@ -42,10 +42,12 @@
<a :class="activeTab === 'rollback' && 'text-white'"
@click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback
</a>
<a :class="activeTab === 'resource-limits' && 'text-white'"
@click.prevent="activeTab = 'resource-limits'; window.location.hash = 'resource-limits'"
href="#">Resource Limits
</a>
@if ($application->build_pack !== 'dockercompose')
<a :class="activeTab === 'resource-limits' && 'text-white'"
@click.prevent="activeTab = 'resource-limits'; window.location.hash = 'resource-limits'"
href="#">Resource Limits
</a>
@endif
<a :class="activeTab === 'danger' && 'text-white'"
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone
</a>

View File

@ -49,7 +49,7 @@
<div @class([
'font-mono whitespace-pre-line',
'text-warning' => $line['hidden'],
'text-error' => $line['type'] == 'stderr',
'text-red-500' => $line['type'] == 'stderr',
])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
<br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT:
@endif{{ $line['output'] }}@if ($line['hidden'])

View File

@ -1,5 +1,5 @@
<div class="flex items-center gap-2 pb-4">
<h2>Logs</h2>
<h2>Deployment Log</h2>
@if ($is_debug_enabled)
<x-forms.button wire:click.prevent="show_debug">Hide Debug Logs</x-forms.button>
@else

View File

@ -16,20 +16,22 @@
<x-forms.input id="application.name" label="Name" required />
<x-forms.input id="application.description" label="Description" />
</div>
<div class="flex items-end gap-2">
<x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " />
<x-forms.button wire:click="getWildcardDomain">Generate Domain
</x-forms.button>
</div>
@if (!$application->dockerfile)
@if ($application->build_pack !== 'dockercompose')
<div class="flex items-end gap-2">
<x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " />
<x-forms.button wire:click="getWildcardDomain">Generate Domain
</x-forms.button>
</div>
@endif
@if (!$application->dockerfile && $application->build_pack !== 'dockerimage')
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<x-forms.select wire:model="application.build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option>
<option value="static">Static</option>
<option value="dockerfile">Dockerfile</option>
<option value="dockerimage">Docker Image</option>
<option value="dockercompose">Docker Compose</option>
</x-forms.select>
@if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.select id="application.static_image" label="Static Image" required>
@ -45,28 +47,47 @@
helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
</div>
@endif
@if ($application->build_pack === 'dockercompose')
@if (count($parsedServices) > 0)
@foreach (data_get($parsedServices, 'services') as $serviceName => $service)
@if (!isDatabaseImage(data_get($service, 'image')))
<div class="flex items-end gap-2">
<x-forms.input
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
label="Domains for {{ str($serviceName)->headline() }}"
id="parsedServiceDomains.{{ $serviceName }}.domain"></x-forms.input>
@if (!data_get($parsedServiceDomains, "$serviceName.domain"))
<x-forms.button wire:click="generateDomain('{{ $serviceName }}')">Generate
Domain</x-forms.button>
@endif
</div>
@endif
@endforeach
@endif
@endif
</div>
@endif
<h3>Docker Registry</h3>
@if ($application->build_pack !== 'dockerimage')
@if ($application->build_pack !== 'dockerimage' && $application->build_pack !== 'dockercompose')
<h3>Docker Registry</h3>
<div>Push the built image to a docker registry. More info <a class="underline"
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div>
@endif
<div class="flex flex-col gap-2 xl:flex-row">
@if ($application->build_pack === 'dockerimage')
<x-forms.input id="application.docker_registry_image_name" label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
@else
<x-forms.input id="application.docker_registry_image_name"
helper="Empty means it won't push the image to a docker registry."
placeholder="Empty means it won't push the image to a docker registry." label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag"
placeholder="Empty means only push commit sha tag."
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
label="Docker Image Tag" />
@endif
<div class="flex flex-col gap-2 xl:flex-row">
@if ($application->build_pack === 'dockerimage')
<x-forms.input id="application.docker_registry_image_name" label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
@else
<x-forms.input id="application.docker_registry_image_name"
helper="Empty means it won't push the image to a docker registry."
placeholder="Empty means it won't push the image to a docker registry."
label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag"
placeholder="Empty means only push commit sha tag."
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
label="Docker Image Tag" />
@endif
</div>
</div>
@endif
@if ($application->build_pack !== 'dockerimage')
<h3>Build</h3>
@ -91,7 +112,16 @@
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
label="Dockerfile Location"
helper="It is calculated together with the Base Directory: {{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}" />
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>" />
@endif
@if ($application->build_pack === 'dockercompose')
<span wire:init='loadComposeFile(true)'></span>
<x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_location"
label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
{{-- <x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_pr_location"
label="Docker Compose Location For Pull Requests"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_pr_location, '/') }}</span>" /> --}}
@endif
@if ($application->build_pack === 'dockerfile')
<x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target"
@ -108,23 +138,33 @@
@endif
</div>
@endif
@if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>
<x-forms.textarea rows="10" readonly id="application.docker_compose" label="Docker Compose Content"
helper="You need to modify the docker compose file." />
{{-- <x-forms.textarea rows="10" readonly id="application.docker_compose_pr"
label="Docker PR Compose Content" helper="You need to modify the docker compose file." /> --}}
@endif
@if ($application->dockerfile)
<x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea>
@endif
<h3>Network</h3>
<div class="flex flex-col gap-2 xl:flex-row">
@if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly />
@else
<x-forms.input placeholder="3000,3001" id="application.ports_exposes" label="Ports Exposes" required
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly." />
@endif
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
</div>
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea>
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>
@if ($application->build_pack !== 'dockercompose')
<h3>Network</h3>
<div class="flex flex-col gap-2 xl:flex-row">
@if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly />
@else
<x-forms.input placeholder="3000,3001" id="application.ports_exposes" label="Ports Exposes"
required
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly." />
@endif
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
</div>
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea>
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>
@endif
</div>
</form>
</div>

View File

@ -71,15 +71,16 @@
</a>
</div>
<div class="flex items-center gap-2 pt-6">
<x-forms.button class="bg-coolgray-500" wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
<x-forms.button class="bg-coolgray-500"
wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
@if (data_get($preview, 'status') === 'exited')
Deploy
@else
Redeploy
@endif
</x-forms.button>
<x-forms.button class="bg-coolgray-500" wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove
Preview
<x-forms.button class="bg-coolgray-500"
wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove Preview
</x-forms.button>
<a
href="{{ route('project.application.deployments', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">

View File

@ -67,7 +67,7 @@
Based on a Docker Compose
</div>
<div class="description">
You can deploy complex application easily with Docker Compose.
You can deploy complex application easily with Docker Compose, without Git.
</div>
</div>
</div>
@ -77,7 +77,7 @@
Based on an existing Docker Image
</div>
<div class="description">
You can deploy an existing Docker Image form any Registry.
You can deploy an existing Docker Image form any Registry, without Git.
</div>
</div>
</div>
@ -145,9 +145,9 @@
</div>
</div> --}}
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2" wire:init='loadServices'>
<h2 class="py-4">Services</h2>
<x-forms.button wire:click='loadServices(true)'>Reload Services List</x-forms.button>
<x-forms.button wire:click='loadServices'>Reload Services List</x-forms.button>
<input
class="w-full text-white rounded input input-sm bg-coolgray-200 disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
wire:model.debounce.200ms="search" placeholder="Search..."></input>

View File

@ -1,4 +1,4 @@
<dialog id="composeModal" class="modal" x-data="{ raw: true }">
<dialog id="composeModal" class="modal" x-data="{ raw: true }" wire:ignore.self>
<form method="dialog" class="flex flex-col gap-2 rounded max-w-7xl modal-box" wire:submit.prevent='submit'>
<div class="flex items-end gap-2">
<h1>Docker Compose</h1>

View File

@ -2,8 +2,8 @@
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex h-full pt-6">
<div class="flex flex-col gap-4 min-w-fit">
<a class="{{ request()->routeIs('project.service') ? 'text-white' : '' }}"
href="{{ route('project.service', [...$parameters, 'service_name' => null]) }}">
<a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
href="{{ route('project.service.configuration', [...$parameters, 'service_name' => null]) }}">
<button><- Back</button>
</a>
<a :class="activeTab === 'general' && 'text-white'"

View File

@ -18,7 +18,8 @@
</div>
<div class="grid grid-cols-2 gap-2">
@foreach ($fields as $serviceName => $field)
<x-forms.input type="{{ data_get($field, 'isPassword') ? 'password' : 'text' }}" required
<x-forms.input type="{{ data_get($field, 'isPassword') ? 'password' : 'text' }}"
required="{{ str(data_get($field, 'rules'))?->contains('required') }}"
helper="Variable name: {{ $serviceName }}"
label="{{ data_get($field, 'serviceName') }} {{ data_get($field, 'name') }}"
id="fields.{{ $serviceName }}.value"></x-forms.input>

View File

@ -1,6 +1,6 @@
<div x-init="$wire.getLogs">
<div class="flex gap-2">
<h2>Logs</h2>
<h4>Container: {{$container}}</h4>
@if ($streamLogs)
<span wire:poll.2000ms='getLogs(true)' class="loading loading-xs text-warning loading-spinner"></span>
@endif
@ -13,7 +13,7 @@
<x-forms.input label="Only Show Number of Lines" placeholder="1000" required id="numberOfLines"></x-forms.input>
<x-forms.button type="submit">Refresh</x-forms.button>
</form>
<div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'container w-full pt-4 mx-auto'">
<div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'container w-full py-4 mx-auto'">
<div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300"
:class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'">
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg

View File

@ -3,7 +3,14 @@
<h1>Logs</h1>
<livewire:project.application.heading :application="$resource" />
<div class="pt-4">
<livewire:project.shared.get-logs :server="$server" :container="$container" />
@forelse ($containers as $container)
@if ($loop->first)
<h2 class="pb-4">Logs</h2>
@endif
<livewire:project.shared.get-logs :server="$server" :container="$container" />
@empty
<div>No containers are not running.</div>
@endforelse
</div>
@elseif ($type === 'database')
<h1>Logs</h1>

View File

@ -11,12 +11,39 @@
<div class="pb-4">This will remove this server from Coolify. Beware! There is no coming
back!
</div>
@if ($server->hasDefinedResources())
<div class="text-warning">Please delete all resources before deleting this server.</div>
@if ($server->definedResources()->count() > 0)
<x-forms.button disabled isError isModal modalId="deleteServer">
Delete
</x-forms.button>
@else
<x-forms.button isError isModal modalId="deleteServer">
Delete
</x-forms.button>
@endif
<div class="flex flex-col">
@forelse ($server->definedResources() as $resource)
@if ($loop->first)
<h3 class="pt-4">Defined resources</h3>
@endif
<a class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline" href="{{ $resource->link() }}">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
</a>
@empty
@endforelse
</div>
@else
<div class="flex flex-col">
@forelse ($server->definedResources() as $resource)
@if ($loop->first)
<h3 class="pt-4">Defined resources</h3>
@endif
<a class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline" href="{{ $resource->link() }}">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
</a>
@empty
@endforelse
</div>
@endif
</div>

View File

@ -24,24 +24,7 @@
</x-forms.button>
</div>
</form>
{{-- <h3>Highlight.io</h3>
<div class="w-32">
<x-forms.checkbox instantSave='instantSave("highlight")'
id="server.settings.is_logdrain_highlight_enabled" label="Enabled" />
</div>
<form wire:submit.prevent='submit("highlight")' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" required id="server.settings.logdrain_highlight_project_id"
label="Project Id" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form> --}}
<h3>Axiom</h3>
<div class="w-32">
<x-forms.checkbox instantSave='instantSave("axiom")' id="server.settings.is_logdrain_axiom_enabled"
@ -61,6 +44,24 @@
</x-forms.button>
</div>
</form>
{{-- <h3>Highlight.io</h3>
<div class="w-32">
<x-forms.checkbox instantSave='instantSave("highlight")'
id="server.settings.is_logdrain_highlight_enabled" label="Enabled" />
</div>
<form wire:submit.prevent='submit("highlight")' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" required id="server.settings.logdrain_highlight_project_id"
label="Project Id" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form> --}}
</div>
</div>
</div>

View File

@ -79,7 +79,7 @@
@endforeach
@foreach ($environment->services->sortBy('name') as $service)
<a class="relative box group"
href="{{ route('project.service', [$project->uuid, $environment->name, $service->uuid]) }}">
href="{{ route('project.service.configuration', [$project->uuid, $environment->name, $service->uuid]) }}">
<div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $service->name }}</div>
<div class="description">{{ $service->description }}</div>

View File

@ -120,7 +120,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
// Services
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}', ServiceIndex::class)->name('project.service');
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}', ServiceIndex::class)->name('project.service.configuration');
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}', ServiceShow::class)->name('project.service.show');
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}/logs', Logs::class)->name('project.service.logs');
});

View File

@ -225,13 +225,9 @@ Route::post('/source/github/events/manual', function () {
return response("Nothing to do. No applications found with branch '$base_branch'.");
}
}
ray($applications);
foreach ($applications as $application) {
ray($application);
$webhook_secret = data_get($application, 'manual_webhook_secret_github');
ray($webhook_secret);
$hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret);
ray($hmac, $x_hub_signature_256);
if (!hash_equals($x_hub_signature_256, $hmac)) {
ray('Invalid signature');
continue;
@ -317,11 +313,13 @@ Route::post('/source/github/events', function () {
// Installation handled by setup redirect url. Repositories queried on-demand.
return response('cool');
}
$github_app = GithubApp::where('app_id', $x_github_hook_installation_target_id)->firstOrFail();
$github_app = GithubApp::where('app_id', $x_github_hook_installation_target_id)->first();
if (is_null($github_app)) {
return response('Nothing to do. No GitHub App found.');
}
$webhook_secret = data_get($github_app, 'webhook_secret');
$hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret);
ray($hmac, $x_hub_signature_256)->blue();
if (config('app.env') !== 'local') {
if (!hash_equals($x_hub_signature_256, $hmac)) {
return response('not cool');
@ -658,12 +656,10 @@ Route::post('/payments/paddle/events', function () {
$h1 = Str::of($signature)->after('h1=');
$signedPayload = $ts->value . ':' . request()->getContent();
$verify = hash_hmac('sha256', $signedPayload, config('subscription.paddle_webhook_secret'));
ray($verify, $h1->value, hash_equals($verify, $h1->value));
if (!hash_equals($verify, $h1->value)) {
return response('Invalid signature.', 400);
}
$eventType = data_get($payload, 'event_type');
ray($eventType);
$webhook = Webhook::create([
'type' => 'paddle',
'payload' => $payload,

View File

@ -1,11 +1,7 @@
#!/bin/bash
## Do not modify this file. You will lose the ability to install and auto-update!
###########
## Always run "php artisan app:sync-to-bunny-cdn --env=secrets" or "scripts/run sync-bunny" if you update this file.
###########
VERSION="1.0.3"
VERSION="1.1.0"
DOCKER_VERSION="24.0"
CDN="https://cdn.coollabs.io/coolify"
@ -18,10 +14,14 @@ if [ $EUID != 0 ]; then
echo "Please run as root"
exit
fi
if [ $OS_TYPE != "ubuntu" ] && [ $OS_TYPE != "debian" ] && [ $OS_TYPE != "raspbian" ]; then
echo "This script only supports Ubuntu and Debian for now."
case "$OS_TYPE" in
ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed) ;;
*)
echo "This script only supports Debian, Redhat or Sles based operating systems for now."
exit
fi
;;
esac
# Ovewrite LATEST_VERSION if user pass a version number
if [ "$1" != "" ]; then
@ -40,8 +40,23 @@ echo "Coolify version: $LATEST_VERSION"
echo -e "-------------"
echo "Installing required packages..."
apt update -y >/dev/null 2>&1
apt install -y curl wget git jq jc >/dev/null 2>&1
case "$OS_TYPE" in
ubuntu | debian | raspbian)
apt update -y >/dev/null 2>&1
apt install -y curl wget git jq >/dev/null 2>&1
;;
centos | fedora | rhel | ol | rocky)
dnf install -y curl wget git jq >/dev/null 2>&1
;;
sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null 2>&1
zypper install -y curl wget git jq >/dev/null 2>&1
;;
*)
echo "This script only supports Debian, Redhat or Sles based operating systems for now."
exit
;;
esac
if ! [ -x "$(command -v docker)" ]; then
echo "Docker is not installed. Installing Docker..."
@ -53,7 +68,7 @@ if ! [ -x "$(command -v docker)" ]; then
echo "Maybe your OS is not supported."
echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
fi
fi
echo -e "-------------"
echo -e "Check Docker Configuration..."
@ -93,7 +108,6 @@ else
systemctl restart docker
fi
echo -e "-------------"
mkdir -p /data/coolify/ssh/keys

View File

@ -0,0 +1,60 @@
# documentation: https://formbricks.com/docs/self-hosting/docker
# slogan: Open Source Experience Management
# tags: form, builder, forms, open source, experience, management, self-hosted, docker
services:
formbricks:
image: formbricks/formbricks:latest
environment:
- SERVICE_FQDN_FORMBRICKS
- WEBAPP_URL=$SERVICE_FQDN_FORMBRICKS
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgresql:5432/${POSTGRESQL_DATABASE:-formbricks}
- NEXTAUTH_SECRET=$SERVICE_BASE64_64_NEXTAUTH
- NEXTAUTH_URL=$SERVICE_FQDN_FORMBRICKS
- ENCRYPTION_KEY=$SERVICE_BASE64_64_ENCRYPTION
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
- MAIL_FROM=${MAIL_FROM:-test@example.com}
- SMTP_HOST=${SMTP_HOST:-test.example.com}
- SMTP_PORT=${SMTP_PORT:-587}
- SMTP_USER=${SMTP_USER:-test}
- SMTP_PASSWORD=${SMTP_PASSWORD:-test}
- SMTP_SECURE_ENABLED=${SMTP_SECURE_ENABLED:-0}
- SHORT_URL_BASE=${SHORT_URL_BASE}
- EMAIL_VERIFICATION_DISABLED=${EMAIL_VERIFICATION_DISABLED:-1}
- PASSWORD_RESET_DISABLED=${PASSWORD_RESET_DISABLED:-1}
- SIGNUP_DISABLED=${SIGNUP_DISABLED:-0}
- INVITE_DISABLED=${INVITE_DISABLED:-0}
- PRIVACY_URL=${PRIVACY_URL}
- TERMS_URL=${TERMS_URL}
- IMPRINT_URL=${IMPRINT_URL}
- GITHUB_AUTH_ENABLED=${GITHUB_AUTH_ENABLED:-0}
- GITHUB_ID=${GITHUB_ID}
- GITHUB_SECRET=${GITHUB_SECRET}
- GOOGLE_AUTH_ENABLED=${GOOGLE_AUTH_ENABLED:-0}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- ASSET_PREFIX_URL=${ASSET_PREFIX_URL}
volumes:
- formbricks-uploads:/apps/web/uploads/
depends_on:
postgresql:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000"]
interval: 2s
timeout: 10s
retries: 15
postgresql:
image: postgres:16-alpine
volumes:
- formbricks-postgresql-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${SERVICE_USER_POSTGRESQL}
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
- POSTGRES_DB=${POSTGRESQL_DATABASE:-formbricks}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 20s
retries: 10

View File

@ -14,6 +14,13 @@ services:
- database__connection__user=$SERVICE_USER_MYSQL
- database__connection__password=$SERVICE_PASSWORD_MYSQL
- database__connection__database=${MYSQL_DATABASE-ghost}
- mail__transport=SMTP
- mail__options__auth__pass=${MAIL_OPTIONS_AUTH_PASS}
- mail__options__auth__user=${MAIL_OPTIONS_AUTH_USER}
- mail__options__secure=${MAIL_OPTIONS_SECURE:-true}
- mail__options__port=${MAIL_OPTIONS_PORT:-465}
- mail__options__service=${MAIL_OPTIONS_SERVICE:-Mailgun}
- mail__options__host=${MAIL_OPTIONS_HOST}
depends_on:
mysql:
condition: service_healthy

View File

@ -0,0 +1,25 @@
# documentation: https://trigger.dev/docs/documentation/guides/self-hosting
# slogan: The open source Background Jobs framework for TypeScript
# tags: trigger.dev, background jobs, typescript, trigger, jobs, cron, scheduler
services:
trigger:
image: ghcr.io/triggerdotdev/trigger.dev:latest
environment:
- SERVICE_FQDN_TRIGGER
- LOGIN_ORIGIN=$SERVICE_FQDN_TRIGGER
- APP_ORIGIN=$SERVICE_FQDN_TRIGGER
- MAGIC_LINK_SECRET=$SERVICE_PASSWORD_64_MAGIC
- ENCRYPTION_KEY=$SERVICE_PASSWORD_64_ENCRYPTION
- SESSION_SECRET=$SERVICE_PASSWORD_64_SESSION
- DATABASE_URL=${DATABASE_URL}
- DIRECT_URL=${DATABASE_URL}
- RUNTIME_PLATFORM=docker-compose
- NODE_ENV=production
- AUTH_GITHUB_CLIENT_ID=${AUTH_GITHUB_CLIENT_ID}
- AUTH_GITHUB_CLIENT_SECRET=${AUTH_GITHUB_CLIENT_SECRET}
- RESEND_API_KEY=${RESEND_API_KEY}
- FROM_EMAIL=${FROM_EMAIL}
- REPLY_TO_EMAIL=${REPLY_TO_EMAIL}
healthcheck:
test: ["NONE"]

View File

@ -0,0 +1,46 @@
# documentation: https://trigger.dev/docs/documentation/guides/self-hosting
# slogan: The open source Background Jobs framework for TypeScript
# tags: trigger.dev, background jobs, typescript, trigger, jobs, cron, scheduler
services:
trigger:
image: ghcr.io/triggerdotdev/trigger.dev:latest
environment:
- SERVICE_FQDN_TRIGGER
- LOGIN_ORIGIN=$SERVICE_FQDN_TRIGGER
- APP_ORIGIN=$SERVICE_FQDN_TRIGGER
- MAGIC_LINK_SECRET=$SERVICE_PASSWORD_64_MAGIC
- ENCRYPTION_KEY=$SERVICE_PASSWORD_64_ENCRYPTION
- SESSION_SECRET=$SERVICE_PASSWORD_64_SESSION
- POSTGRES_USER=$SERVICE_USER_POSTGRES
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
- POSTGRES_DB=${POSTGRES_DB:-trigger}
- POSTGRES_HOST=postgres
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/$POSTGRES_DB
- DIRECT_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/$POSTGRES_DB
- RUNTIME_PLATFORM=docker-compose
- NODE_ENV=production
- AUTH_GITHUB_CLIENT_ID=${AUTH_GITHUB_CLIENT_ID}
- AUTH_GITHUB_CLIENT_SECRET=${AUTH_GITHUB_CLIENT_SECRET}
- RESEND_API_KEY=${RESEND_API_KEY}
- FROM_EMAIL=${FROM_EMAIL}
- REPLY_TO_EMAIL=${REPLY_TO_EMAIL}
depends_on:
postgresql:
condition: service_healthy
healthcheck:
test: ["NONE"]
postgresql:
image: postgres:15-alpine
volumes:
- postgresql-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=$SERVICE_USER_POSTGRES
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
- POSTGRES_DB=${POSTGRES_DB:-trigger}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 20s
retries: 10

View File

@ -18,15 +18,18 @@ services:
- POSTGRES_HOST=postgresql
- POSTGRES_PORT=5432
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=$SERVICE_PASSWORD_REDIS
volumes:
- weblate-data:/app/data
- weblate-cache:/app/cache
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080"]
interval: 2s
timeout: 10s
retries: 15
retries: 30
postgresql:
image: postgres:15-alpine
image: postgres:16-alpine
volumes:
- postgresql-data:/var/lib/postgresql/data
environment:
@ -40,7 +43,10 @@ services:
retries: 10
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
command: >
--appendonly yes --requirepass ${SERVICE_PASSWORD_REDIS}
environment:
- REDIS_PASSWORD=$SERVICE_PASSWORD_REDIS
volumes:
- weblate-redis-data:/data
healthcheck:

View File

@ -152,10 +152,25 @@
"administration-tool"
]
},
"formbricks": {
"documentation": "https:\/\/formbricks.com\/docs\/self-hosting\/docker",
"slogan": "Open Source Experience Management",
"compose": "c2VydmljZXM6CiAgZm9ybWJyaWNrczoKICAgIGltYWdlOiAnZm9ybWJyaWNrcy9mb3JtYnJpY2tzOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GT1JNQlJJQ0tTCiAgICAgIC0gV0VCQVBQX1VSTD0kU0VSVklDRV9GUUROX0ZPUk1CUklDS1MKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUxAcG9zdGdyZXNxbDo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZm9ybWJyaWNrc30nCiAgICAgIC0gTkVYVEFVVEhfU0VDUkVUPSRTRVJWSUNFX0JBU0U2NF82NF9ORVhUQVVUSAogICAgICAtIE5FWFRBVVRIX1VSTD0kU0VSVklDRV9GUUROX0ZPUk1CUklDS1MKICAgICAgLSBFTkNSWVBUSU9OX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfRU5DUllQVElPTgogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ01BSUxfRlJPTT0ke01BSUxfRlJPTTotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ1NNVFBfSE9TVD0ke1NNVFBfSE9TVDotdGVzdC5leGFtcGxlLmNvbX0nCiAgICAgIC0gJ1NNVFBfUE9SVD0ke1NNVFBfUE9SVDotNTg3fScKICAgICAgLSAnU01UUF9VU0VSPSR7U01UUF9VU0VSOi10ZXN0fScKICAgICAgLSAnU01UUF9QQVNTV09SRD0ke1NNVFBfUEFTU1dPUkQ6LXRlc3R9JwogICAgICAtICdTTVRQX1NFQ1VSRV9FTkFCTEVEPSR7U01UUF9TRUNVUkVfRU5BQkxFRDotMH0nCiAgICAgIC0gJ1NIT1JUX1VSTF9CQVNFPSR7U0hPUlRfVVJMX0JBU0V9JwogICAgICAtICdFTUFJTF9WRVJJRklDQVRJT05fRElTQUJMRUQ9JHtFTUFJTF9WRVJJRklDQVRJT05fRElTQUJMRUQ6LTF9JwogICAgICAtICdQQVNTV09SRF9SRVNFVF9ESVNBQkxFRD0ke1BBU1NXT1JEX1JFU0VUX0RJU0FCTEVEOi0xfScKICAgICAgLSAnU0lHTlVQX0RJU0FCTEVEPSR7U0lHTlVQX0RJU0FCTEVEOi0wfScKICAgICAgLSAnSU5WSVRFX0RJU0FCTEVEPSR7SU5WSVRFX0RJU0FCTEVEOi0wfScKICAgICAgLSAnUFJJVkFDWV9VUkw9JHtQUklWQUNZX1VSTH0nCiAgICAgIC0gJ1RFUk1TX1VSTD0ke1RFUk1TX1VSTH0nCiAgICAgIC0gJ0lNUFJJTlRfVVJMPSR7SU1QUklOVF9VUkx9JwogICAgICAtICdHSVRIVUJfQVVUSF9FTkFCTEVEPSR7R0lUSFVCX0FVVEhfRU5BQkxFRDotMH0nCiAgICAgIC0gJ0dJVEhVQl9JRD0ke0dJVEhVQl9JRH0nCiAgICAgIC0gJ0dJVEhVQl9TRUNSRVQ9JHtHSVRIVUJfU0VDUkVUfScKICAgICAgLSAnR09PR0xFX0FVVEhfRU5BQkxFRD0ke0dPT0dMRV9BVVRIX0VOQUJMRUQ6LTB9JwogICAgICAtICdHT09HTEVfQ0xJRU5UX0lEPSR7R09PR0xFX0NMSUVOVF9JRH0nCiAgICAgIC0gJ0dPT0dMRV9DTElFTlRfU0VDUkVUPSR7R09PR0xFX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdBU1NFVF9QUkVGSVhfVVJMPSR7QVNTRVRfUFJFRklYX1VSTH0nCiAgICB2b2x1bWVzOgogICAgICAtICdmb3JtYnJpY2tzLXVwbG9hZHM6L2FwcHMvd2ViL3VwbG9hZHMvJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdmb3JtYnJpY2tzLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWZvcm1icmlja3N9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
"tags": [
"form",
"builder",
"forms",
"open source",
"experience",
"management",
"self-hosted",
"docker"
]
},
"ghost": {
"documentation": "https:\/\/ghost.org\/docs",
"slogan": "Ghost is a popular open-source content management system (CMS) and blogging platform, known for its simplicity and focus on content creation.",
"compose": "c2VydmljZXM6CiAgZ2hvc3Q6CiAgICBpbWFnZTogJ2dob3N0OjUnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1jb250ZW50LWRhdGE6L3Zhci9saWIvZ2hvc3QvY29udGVudCcKICAgIGVudmlyb25tZW50OgogICAgICAtIHVybD0kU0VSVklDRV9GUUROX0dIT1NUCiAgICAgIC0gZGF0YWJhc2VfX2NsaWVudD1teXNxbAogICAgICAtIGRhdGFiYXNlX19jb25uZWN0aW9uX19ob3N0PW15c3FsCiAgICAgIC0gZGF0YWJhc2VfX2Nvbm5lY3Rpb25fX3VzZXI9JFNFUlZJQ0VfVVNFUl9NWVNRTAogICAgICAtIGRhdGFiYXNlX19jb25uZWN0aW9uX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgICAtICdkYXRhYmFzZV9fY29ubmVjdGlvbl9fZGF0YWJhc2U9JHtNWVNRTF9EQVRBQkFTRS1naG9zdH0nCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo4LjAnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFfScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSBsb2NhbGhvc3QKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
"compose": "c2VydmljZXM6CiAgZ2hvc3Q6CiAgICBpbWFnZTogJ2dob3N0OjUnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1jb250ZW50LWRhdGE6L3Zhci9saWIvZ2hvc3QvY29udGVudCcKICAgIGVudmlyb25tZW50OgogICAgICAtIHVybD0kU0VSVklDRV9GUUROX0dIT1NUCiAgICAgIC0gZGF0YWJhc2VfX2NsaWVudD1teXNxbAogICAgICAtIGRhdGFiYXNlX19jb25uZWN0aW9uX19ob3N0PW15c3FsCiAgICAgIC0gZGF0YWJhc2VfX2Nvbm5lY3Rpb25fX3VzZXI9JFNFUlZJQ0VfVVNFUl9NWVNRTAogICAgICAtIGRhdGFiYXNlX19jb25uZWN0aW9uX19wYXNzd29yZD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgICAtICdkYXRhYmFzZV9fY29ubmVjdGlvbl9fZGF0YWJhc2U9JHtNWVNRTF9EQVRBQkFTRS1naG9zdH0nCiAgICAgIC0gbWFpbF9fdHJhbnNwb3J0PVNNVFAKICAgICAgLSAnbWFpbF9fb3B0aW9uc19fYXV0aF9fcGFzcz0ke01BSUxfT1BUSU9OU19BVVRIX1BBU1N9JwogICAgICAtICdtYWlsX19vcHRpb25zX19hdXRoX191c2VyPSR7TUFJTF9PUFRJT05TX0FVVEhfVVNFUn0nCiAgICAgIC0gJ21haWxfX29wdGlvbnNfX3NlY3VyZT0ke01BSUxfT1BUSU9OU19TRUNVUkU6LXRydWV9JwogICAgICAtICdtYWlsX19vcHRpb25zX19wb3J0PSR7TUFJTF9PUFRJT05TX1BPUlQ6LTQ2NX0nCiAgICAgIC0gJ21haWxfX29wdGlvbnNfX3NlcnZpY2U9JHtNQUlMX09QVElPTlNfU0VSVklDRTotTWFpbGd1bn0nCiAgICAgIC0gJ21haWxfX29wdGlvbnNfX2hvc3Q9JHtNQUlMX09QVElPTlNfSE9TVH0nCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdteXNxbDo4LjAnCiAgICB2b2x1bWVzOgogICAgICAtICdnaG9zdC1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFfScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSBsb2NhbGhvc3QKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=",
"tags": [
"cms",
"blog",
@ -441,6 +456,34 @@
"internet"
]
},
"trigger-with-external-database": {
"documentation": "https:\/\/trigger.dev\/docs\/documentation\/guides\/self-hosting",
"slogan": "The open source Background Jobs framework for TypeScript",
"compose": "c2VydmljZXM6CiAgdHJpZ2dlcjoKICAgIGltYWdlOiAnZ2hjci5pby90cmlnZ2VyZG90ZGV2L3RyaWdnZXIuZGV2OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklHR0VSCiAgICAgIC0gTE9HSU5fT1JJR0lOPSRTRVJWSUNFX0ZRRE5fVFJJR0dFUgogICAgICAtIEFQUF9PUklHSU49JFNFUlZJQ0VfRlFETl9UUklHR0VSCiAgICAgIC0gTUFHSUNfTElOS19TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgLSBFTkNSWVBUSU9OX0tFWT0kU0VSVklDRV9QQVNTV09SRF82NF9FTkNSWVBUSU9OCiAgICAgIC0gU0VTU0lPTl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VTU0lPTgogICAgICAtICdEQVRBQkFTRV9VUkw9JHtEQVRBQkFTRV9VUkx9JwogICAgICAtICdESVJFQ1RfVVJMPSR7REFUQUJBU0VfVVJMfScKICAgICAgLSBSVU5USU1FX1BMQVRGT1JNPWRvY2tlci1jb21wb3NlCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtICdBVVRIX0dJVEhVQl9DTElFTlRfSUQ9JHtBVVRIX0dJVEhVQl9DTElFTlRfSUR9JwogICAgICAtICdBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUPSR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIC0gJ1JFU0VORF9BUElfS0VZPSR7UkVTRU5EX0FQSV9LRVl9JwogICAgICAtICdGUk9NX0VNQUlMPSR7RlJPTV9FTUFJTH0nCiAgICAgIC0gJ1JFUExZX1RPX0VNQUlMPSR7UkVQTFlfVE9fRU1BSUx9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBOT05FCg==",
"tags": [
"trigger.dev",
"background jobs",
"typescript",
"trigger",
"jobs",
"cron",
"scheduler"
]
},
"trigger": {
"documentation": "https:\/\/trigger.dev\/docs\/documentation\/guides\/self-hosting",
"slogan": "The open source Background Jobs framework for TypeScript",
"compose": "c2VydmljZXM6CiAgdHJpZ2dlcjoKICAgIGltYWdlOiAnZ2hjci5pby90cmlnZ2VyZG90ZGV2L3RyaWdnZXIuZGV2OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklHR0VSCiAgICAgIC0gTE9HSU5fT1JJR0lOPSRTRVJWSUNFX0ZRRE5fVFJJR0dFUgogICAgICAtIEFQUF9PUklHSU49JFNFUlZJQ0VfRlFETl9UUklHR0VSCiAgICAgIC0gTUFHSUNfTElOS19TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgLSBFTkNSWVBUSU9OX0tFWT0kU0VSVklDRV9QQVNTV09SRF82NF9FTkNSWVBUSU9OCiAgICAgIC0gU0VTU0lPTl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VTU0lPTgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICAtIFBPU1RHUkVTX0hPU1Q9cG9zdGdyZXMKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCJwogICAgICAtICdESVJFQ1RfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCJwogICAgICAtIFJVTlRJTUVfUExBVEZPUk09ZG9ja2VyLWNvbXBvc2UKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gJ0FVVEhfR0lUSFVCX0NMSUVOVF9JRD0ke0FVVEhfR0lUSFVCX0NMSUVOVF9JRH0nCiAgICAgIC0gJ0FVVEhfR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIC0gJ0ZST01fRU1BSUw9JHtGUk9NX0VNQUlMfScKICAgICAgLSAnUkVQTFlfVE9fRU1BSUw9JHtSRVBMWV9UT19FTUFJTH0nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIE5PTkUKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNS1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdHJpZ2dlcn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
"tags": [
"trigger.dev",
"background jobs",
"typescript",
"trigger",
"jobs",
"cron",
"scheduler"
]
},
"umami": {
"documentation": "https:\/\/umami.is\/docs\/getting-started",
"slogan": "Umami is a lightweight, self-hosted web analytics platform designed to provide website owners with insights into visitor behavior without compromising user privacy.",
@ -477,7 +520,7 @@
"weblate": {
"documentation": "https:\/\/docs.weblate.org\/en\/latest\/admin\/install\/docker.html",
"slogan": "Weblate is a libre software web-based continuous localization system.",
"compose": "c2VydmljZXM6CiAgd2VibGF0ZToKICAgIGltYWdlOiAnd2VibGF0ZS93ZWJsYXRlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XRUJMQVRFCiAgICAgIC0gV0VCTEFURV9TSVRFX0RPTUFJTj0kU0VSVklDRV9VUkxfV0VCTEFURQogICAgICAtICdXRUJMQVRFX0FETUlOX05BTUU9JHtXRUJMQVRFX0FETUlOX05BTUU6LUFkbWlufScKICAgICAgLSAnV0VCTEFURV9BRE1JTl9FTUFJTD0ke1dFQkxBVEVfQURNSU5fRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSBXRUJMQVRFX0FETUlOX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1dFQkxBVEUKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7V0VCTEFURV9BRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LXdlYmxhdGV9JwogICAgICAtIFBPU1RHUkVTX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIFBPU1RHUkVTX1BPUlQ9NTQzMgogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3dlYmxhdGUtZGF0YTovYXBwL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODA4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi13ZWJsYXRlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny1hbHBpbmUnCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tYXBwZW5kb25seSB5ZXMnCiAgICB2b2x1bWVzOgogICAgICAtICd3ZWJsYXRlLXJlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
"compose": "c2VydmljZXM6CiAgd2VibGF0ZToKICAgIGltYWdlOiAnd2VibGF0ZS93ZWJsYXRlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XRUJMQVRFCiAgICAgIC0gV0VCTEFURV9TSVRFX0RPTUFJTj0kU0VSVklDRV9VUkxfV0VCTEFURQogICAgICAtICdXRUJMQVRFX0FETUlOX05BTUU9JHtXRUJMQVRFX0FETUlOX05BTUU6LUFkbWlufScKICAgICAgLSAnV0VCTEFURV9BRE1JTl9FTUFJTD0ke1dFQkxBVEVfQURNSU5fRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSBXRUJMQVRFX0FETUlOX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1dFQkxBVEUKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7V0VCTEFURV9BRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LXdlYmxhdGV9JwogICAgICAtIFBPU1RHUkVTX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIFBPU1RHUkVTX1BPUlQ9NTQzMgogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSBSRURJU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgdm9sdW1lczoKICAgICAgLSAnd2VibGF0ZS1kYXRhOi9hcHAvZGF0YScKICAgICAgLSAnd2VibGF0ZS1jYWNoZTovYXBwL2NhY2hlJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMzAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotd2VibGF0ZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjctYWxwaW5lJwogICAgY29tbWFuZDogIi0tYXBwZW5kb25seSB5ZXMgLS1yZXF1aXJlcGFzcyAke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9XG4iCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBSRURJU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgdm9sdW1lczoKICAgICAgLSAnd2VibGF0ZS1yZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
"tags": [
"localization",
"translation",

View File

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