diff --git a/.github/workflows/chore-manage-stale-issues-and-prs.yml b/.github/workflows/chore-manage-stale-issues-and-prs.yml index 2afc996cb..58a2b7d7e 100644 --- a/.github/workflows/chore-manage-stale-issues-and-prs.yml +++ b/.github/workflows/chore-manage-stale-issues-and-prs.yml @@ -13,16 +13,16 @@ jobs: id: stale with: stale-issue-message: 'This issue will be automatically closed in a few days if no response is received. Please provide an update with the requested information.' - stale-pr-message: 'This pull request will be automatically closed in a few days if no response is received. Please update your PR or comment if you would like to continue working on it.' + stale-pr-message: 'This pull request requires attention. If no changes or response is received within the next few days, it will be automatically closed. Please update your PR or leave a comment with the requested information.' close-issue-message: 'This issue has been automatically closed due to inactivity.' - close-pr-message: 'This pull request has been automatically closed due to inactivity.' + close-pr-message: 'Thank you for your contribution. Due to inactivity, this PR was automatically closed. If you would like to continue working on this change in the future, feel free to reopen this PR or submit a new one.' days-before-stale: 14 days-before-close: 7 stale-issue-label: '⏱︎ Stale' stale-pr-label: '⏱︎ Stale' - only-labels: '💤 Waiting for feedback' + only-labels: '💤 Waiting for feedback, 💤 Waiting for changes' remove-stale-when-updated: true operations-per-run: 100 - labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback' + labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback, 💤 Waiting for changes' close-issue-reason: 'not_planned' exempt-all-milestones: false diff --git a/.github/workflows/chore-remove-labels-and-assignees-on-close.yml b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml index ea097e328..a3c299b5e 100644 --- a/.github/workflows/chore-remove-labels-and-assignees-on-close.yml +++ b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml @@ -19,8 +19,12 @@ jobs: script: | const { owner, repo } = context.repo; - async function processIssue(issueNumber) { + async function processIssue(issueNumber, isFromPR = false, prBaseBranch = null) { try { + if (isFromPR && prBaseBranch !== 'main') { + return; + } + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ owner, repo, @@ -59,19 +63,19 @@ jobs: } } - if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') { - const issue = context.payload.issue || context.payload.pull_request; - await processIssue(issue.number); + if (context.eventName === 'issues') { + await processIssue(context.payload.issue.number); } if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') { const pr = context.payload.pull_request; - if (pr.body) { + await processIssue(pr.number); + if (pr.merged && pr.base.ref === 'main' && pr.body) { const issueReferences = pr.body.match(/#(\d+)/g); if (issueReferences) { for (const reference of issueReferences) { const issueNumber = parseInt(reference.substring(1)); - await processIssue(issueNumber); + await processIssue(issueNumber, true, pr.base.ref); } } } diff --git a/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml index 4354294b1..a4a2a21f6 100644 --- a/.github/workflows/coolify-helper-next.yml +++ b/.github/workflows/coolify-helper-next.yml @@ -38,7 +38,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Build and Push Image uses: docker/build-push-action@v6 @@ -77,7 +77,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Build and Push Image uses: docker/build-push-action@v6 @@ -119,7 +119,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index 6d852a2b3..78c888a01 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -38,7 +38,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Build and Push Image uses: docker/build-push-action@v6 @@ -77,7 +77,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Build and Push Image uses: docker/build-push-action@v6 @@ -119,7 +119,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml index ef247170f..ad590146b 100644 --- a/.github/workflows/coolify-realtime-next.yml +++ b/.github/workflows/coolify-realtime-next.yml @@ -42,7 +42,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Build and Push Image uses: docker/build-push-action@v6 @@ -82,7 +82,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Build and Push Image uses: docker/build-push-action@v6 @@ -125,7 +125,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml index 9654a21b0..d3af14144 100644 --- a/.github/workflows/coolify-realtime.yml +++ b/.github/workflows/coolify-realtime.yml @@ -42,7 +42,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Build and Push Image uses: docker/build-push-action@v6 @@ -82,7 +82,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Build and Push Image uses: docker/build-push-action@v6 @@ -125,7 +125,7 @@ jobs: - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80ec0614e..dba3676cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,8 @@ You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel. +To understand the tech stack, please refer to the [Tech Stack](TECH_STACK.md) document. + ## Table of Contents 1. [Setup Development Environment](#1-setup-development-environment) diff --git a/TECH_STACK.md b/TECH_STACK.md new file mode 100644 index 000000000..6a779eb29 --- /dev/null +++ b/TECH_STACK.md @@ -0,0 +1,29 @@ +# Coolify Technology Stack + +## Frontend + +- Livewire and Alpine.js +- Blade (PHP templating engine) +- Tailwind CSS +- Monaco Editor (Code editor component) +- XTerm.js (Terminal component) + +## Backend + +- Laravel 11 (PHP Framework) +- PostgreSQL 15 (Database) +- Redis 7 (Caching & Real-time features) +- Soketi (WebSocket Server) + +## DevOps & Infrastructure + +- Docker & Docker Compose +- Nginx (Web Server) +- S6 Overlay (Process Supervisor) +- GitHub Actions (CI/CD) + +## Languages + +- PHP 8.4 +- JavaScript +- Shell/Bash scripts diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 37c73d10f..a6d3bc1a2 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1122,7 +1122,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH'; $nixpacks_php_fallback_path->value = '/index.php'; $nixpacks_php_fallback_path->is_build_time = false; - $nixpacks_php_fallback_path->application_id = $this->application->id; + $nixpacks_php_fallback_path->resourceable_id = $this->application->id; + $nixpacks_php_fallback_path->resourceable_type = 'App\Models\Application'; $nixpacks_php_fallback_path->save(); } if (! $nixpacks_php_root_dir) { @@ -1130,7 +1131,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR'; $nixpacks_php_root_dir->value = '/app/public'; $nixpacks_php_root_dir->is_build_time = false; - $nixpacks_php_root_dir->application_id = $this->application->id; + $nixpacks_php_root_dir->resourceable_id = $this->application->id; + $nixpacks_php_root_dir->resourceable_type = 'App\Models\Application'; $nixpacks_php_root_dir->save(); } @@ -2286,8 +2288,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); private function generate_build_env_variables() { - $variables = collect($this->nixpacks_plan_json->get('variables')); + if ($this->application->build_pack === 'nixpacks') { + $variables = collect($this->nixpacks_plan_json->get('variables')); + } else { + $this->generate_env_variables(); + $variables = collect([])->merge($this->env_args); + } + $this->build_args = $variables->map(function ($value, $key) { + $value = escapeshellarg($value); + return "--build-arg {$key}={$value}"; }); } diff --git a/app/Jobs/PullTemplatesFromCDN.php b/app/Jobs/PullTemplatesFromCDN.php index 45c536e06..9a4c991bc 100644 --- a/app/Jobs/PullTemplatesFromCDN.php +++ b/app/Jobs/PullTemplatesFromCDN.php @@ -25,7 +25,7 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue public function handle(): void { try { - if (isDev() || isCloud()) { + if (isDev()) { return; } $response = Http::retry(3, 1000)->get(config('constants.services.official')); diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 576f87801..f8e28d216 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -442,6 +442,7 @@ class General extends Component { $config = GenerateConfig::run($this->application, true); $fileName = str($this->application->name)->slug()->append('_config.json'); + dd($config); return response()->streamDownload(function () use ($config) { echo $config; diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index 25a96b292..05babeaec 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -88,12 +88,12 @@ class General extends Component if (version_compare($this->redis_version, '6.0', '>=')) { $this->database->runtime_environment_variables()->updateOrCreate( ['key' => 'REDIS_USERNAME'], - ['value' => $this->redis_username, 'standalone_redis_id' => $this->database->id] + ['value' => $this->redis_username, 'resourceable_id' => $this->database->id] ); } $this->database->runtime_environment_variables()->updateOrCreate( ['key' => 'REDIS_PASSWORD'], - ['value' => $this->redis_password, 'standalone_redis_id' => $this->database->id] + ['value' => $this->redis_password, 'resourceable_id' => $this->database->id] ); $this->database->save(); diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 4b66bfdcb..3a7d0faa5 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -77,18 +77,28 @@ class Show extends Component public function syncData(bool $toModel = false) { if ($toModel) { - $this->validate(); + if ($this->isSharedVariable) { + $this->validate([ + 'key' => 'required|string', + 'value' => 'nullable', + 'is_multiline' => 'required|boolean', + 'is_literal' => 'required|boolean', + 'is_shown_once' => 'required|boolean', + 'real_value' => 'nullable', + ]); + } else { + $this->validate(); + $this->env->is_build_time = $this->is_build_time; + $this->env->is_required = $this->is_required; + $this->env->is_shared = $this->is_shared; + } $this->env->key = $this->key; $this->env->value = $this->value; - $this->env->is_build_time = $this->is_build_time; $this->env->is_multiline = $this->is_multiline; $this->env->is_literal = $this->is_literal; $this->env->is_shown_once = $this->is_shown_once; - $this->env->is_required = $this->is_required; - $this->env->is_shared = $this->is_shared; $this->env->save(); } else { - $this->key = $this->env->key; $this->value = $this->env->value; $this->is_build_time = $this->env->is_build_time ?? false; @@ -141,30 +151,15 @@ class Show extends Component public function submit() { try { - if ($this->isSharedVariable) { - $this->validate([ - 'key' => 'required|string', - 'value' => 'nullable', - 'is_shown_once' => 'required|boolean', - ]); - } else { - $this->validate(); - } - if (! $this->isSharedVariable && $this->is_required && str($this->value)->isEmpty()) { $oldValue = $this->env->getOriginal('value'); $this->value = $oldValue; - $this->dispatch('error', 'Required environment variable cannot be empty.'); + $this->dispatch('error', 'Required environment variables cannot be empty.'); return; } $this->serialize(); - - if ($this->isSharedVariable) { - unset($this->is_required); - } - $this->syncData(true); $this->dispatch('success', 'Environment variable updated.'); $this->dispatch('envsUpdated'); diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index d12d8e26a..f993480c7 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -27,6 +27,8 @@ class ExecuteContainerCommand extends Component public Collection $servers; + public bool $hasShell = true; + protected $rules = [ 'server' => 'required', 'container' => 'required', @@ -141,6 +143,16 @@ class ExecuteContainerCommand extends Component } } + private function checkShellAvailability(Server $server, string $container): bool + { + $escapedContainer = escapeshellarg($container); + $result = instant_remote_process([ + "docker exec {$escapedContainer} which bash || docker exec {$escapedContainer} which sh", + ], $server, false); + + return ! empty($result); + } + #[On('connectToServer')] public function connectToServer() { @@ -148,6 +160,7 @@ class ExecuteContainerCommand extends Component if ($this->server->isForceDisabled()) { throw new \RuntimeException('Server is disabled.'); } + $this->hasShell = true; $this->dispatch( 'send-terminal-command', false, @@ -201,6 +214,11 @@ class ExecuteContainerCommand extends Component throw new \RuntimeException('Server ownership verification failed.'); } + $this->hasShell = $this->checkShellAvailability($server, data_get($container, 'container.Names')); + if (! $this->hasShell) { + return; + } + $this->dispatch( 'send-terminal-command', true, diff --git a/app/Models/Application.php b/app/Models/Application.php index 289ef5b0f..3913ce37a 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -3,6 +3,8 @@ namespace App\Models; use App\Enums\ApplicationDeploymentStatus; +use App\Services\ConfigurationGenerator; +use App\Traits\HasConfiguration; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -105,7 +107,7 @@ use Visus\Cuid2\Cuid2; class Application extends BaseModel { - use HasFactory, SoftDeletes; + use HasConfiguration, HasFactory, SoftDeletes; private static $parserVersion = '4'; @@ -1640,35 +1642,28 @@ class Application extends BaseModel } } + public function getLimits(): array + { + return [ + 'limits_memory' => $this->limits_memory, + 'limits_memory_swap' => $this->limits_memory_swap, + 'limits_memory_swappiness' => $this->limits_memory_swappiness, + 'limits_memory_reservation' => $this->limits_memory_reservation, + 'limits_cpus' => $this->limits_cpus, + 'limits_cpuset' => $this->limits_cpuset, + 'limits_cpu_shares' => $this->limits_cpu_shares, + ]; + } + public function generateConfig($is_json = false) { - $config = collect([]); - if ($this->build_pack = 'nixpacks') { - $config = collect([ - 'build_pack' => 'nixpacks', - 'docker_registry_image_name' => $this->docker_registry_image_name, - 'docker_registry_image_tag' => $this->docker_registry_image_tag, - 'install_command' => $this->install_command, - 'build_command' => $this->build_command, - 'start_command' => $this->start_command, - 'base_directory' => $this->base_directory, - 'publish_directory' => $this->publish_directory, - 'custom_docker_run_options' => $this->custom_docker_run_options, - 'ports_exposes' => $this->ports_exposes, - 'ports_mappings' => $this->ports_mapping, - 'settings' => collect([ - 'is_static' => $this->settings->is_static, - ]), - ]); - } - $config = $config->filter(function ($value) { - return str($value)->isNotEmpty(); - }); + $generator = new ConfigurationGenerator($this); + if ($is_json) { - return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + return $generator->toJson(); } - return $config; + return $generator->toArray(); } public function setConfig($config) diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 507ff0d7e..5f686de60 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -4,9 +4,7 @@ namespace App\Models; use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Model; use OpenApi\Attributes as OA; -use Visus\Cuid2\Cuid2; #[OA\Schema( description: 'Environment Variable model', @@ -30,7 +28,7 @@ use Visus\Cuid2\Cuid2; 'updated_at' => ['type' => 'string'], ] )] -class EnvironmentVariable extends Model +class EnvironmentVariable extends BaseModel { protected $guarded = []; @@ -49,12 +47,6 @@ class EnvironmentVariable extends Model protected static function booted() { - static::creating(function (Model $model) { - if (! $model->uuid) { - $model->uuid = (string) new Cuid2; - } - }); - static::created(function (EnvironmentVariable $environment_variable) { if ($environment_variable->resourceable_type === Application::class && ! $environment_variable->is_preview) { $found = ModelsEnvironmentVariable::where('key', $environment_variable->key) diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 6ffe5c4d7..98536d346 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -53,6 +53,7 @@ class EmailChannel if (! $type) { throw new Exception('No email settings found.'); } + config()->set('mail.default', $type); return; } diff --git a/app/Services/ConfigurationGenerator.php b/app/Services/ConfigurationGenerator.php new file mode 100644 index 000000000..a7e4b31be --- /dev/null +++ b/app/Services/ConfigurationGenerator.php @@ -0,0 +1,194 @@ +generateConfig(); + } + + protected function generateConfig(): void + { + if ($this->resource instanceof Application) { + $this->config = [ + 'id' => $this->resource->id, + 'name' => $this->resource->name, + 'uuid' => $this->resource->uuid, + 'description' => $this->resource->description, + 'coolify_details' => [ + 'project_uuid' => $this->resource->project()->uuid, + 'environment_uuid' => $this->resource->environment->uuid, + + 'destination_type' => $this->resource->destination_type, + 'destination_id' => $this->resource->destination_id, + 'source_type' => $this->resource->source_type, + 'source_id' => $this->resource->source_id, + 'private_key_id' => $this->resource->private_key_id, + ], + + 'post_deployment_command' => $this->resource->post_deployment_command, + 'post_deployment_command_container' => $this->resource->post_deployment_command_container, + 'pre_deployment_command' => $this->resource->pre_deployment_command, + 'pre_deployment_command_container' => $this->resource->pre_deployment_command_container, + 'build' => [ + 'type' => $this->resource->build_pack, + 'static_image' => $this->resource->static_image, + 'base_directory' => $this->resource->base_directory, + 'publish_directory' => $this->resource->publish_directory, + 'dockerfile' => $this->resource->dockerfile, + 'dockerfile_location' => $this->resource->dockerfile_location, + 'dockerfile_target_build' => $this->resource->dockerfile_target_build, + 'custom_docker_run_options' => $this->resource->custom_docker_options, + 'compose_parsing_version' => $this->resource->compose_parsing_version, + 'docker_compose' => $this->resource->docker_compose, + 'docker_compose_location' => $this->resource->docker_compose_location, + 'docker_compose_raw' => $this->resource->docker_compose_raw, + 'docker_compose_domains' => $this->resource->docker_compose_domains, + 'docker_compose_custom_start_command' => $this->resource->docker_compose_custom_start_command, + 'docker_compose_custom_build_command' => $this->resource->docker_compose_custom_build_command, + 'install_command' => $this->resource->install_command, + 'build_command' => $this->resource->build_command, + 'start_command' => $this->resource->start_command, + 'watch_paths' => $this->resource->watch_paths, + ], + 'source' => [ + 'git_repository' => $this->resource->git_repository, + 'git_branch' => $this->resource->git_branch, + 'git_commit_sha' => $this->resource->git_commit_sha, + 'repository_project_id' => $this->resource->repository_project_id, + ], + 'docker_registry_image' => $this->getDockerRegistryImage(), + 'domains' => [ + 'fqdn' => $this->resource->fqdn, + 'ports_exposes' => $this->resource->ports_exposes, + 'ports_mappings' => $this->resource->ports_mappings, + 'redirect' => $this->resource->redirect, + 'custom_nginx_configuration' => $this->resource->custom_nginx_configuration, + ], + 'environment_variables' => [ + 'production' => $this->getEnvironmentVariables(), + 'preview' => $this->getPreviewEnvironmentVariables(), + ], + 'settings' => $this->getApplicationSettings(), + 'preview' => $this->getPreview(), + 'limits' => $this->resource->getLimits(), + 'health_check' => [ + 'health_check_path' => $this->resource->health_check_path, + 'health_check_port' => $this->resource->health_check_port, + 'health_check_host' => $this->resource->health_check_host, + 'health_check_method' => $this->resource->health_check_method, + 'health_check_return_code' => $this->resource->health_check_return_code, + 'health_check_scheme' => $this->resource->health_check_scheme, + 'health_check_response_text' => $this->resource->health_check_response_text, + 'health_check_interval' => $this->resource->health_check_interval, + 'health_check_timeout' => $this->resource->health_check_timeout, + 'health_check_retries' => $this->resource->health_check_retries, + 'health_check_start_period' => $this->resource->health_check_start_period, + 'health_check_enabled' => $this->resource->health_check_enabled, + ], + 'webhooks_secrets' => [ + 'manual_webhook_secret_github' => $this->resource->manual_webhook_secret_github, + 'manual_webhook_secret_gitlab' => $this->resource->manual_webhook_secret_gitlab, + 'manual_webhook_secret_bitbucket' => $this->resource->manual_webhook_secret_bitbucket, + 'manual_webhook_secret_gitea' => $this->resource->manual_webhook_secret_gitea, + ], + 'swarm' => [ + 'swarm_replicas' => $this->resource->swarm_replicas, + 'swarm_placement_constraints' => $this->resource->swarm_placement_constraints, + ], + ]; + } + } + + protected function getPreview(): array + { + return [ + 'preview_url_template' => $this->resource->preview_url_template, + ]; + } + + protected function getDockerRegistryImage(): array + { + return [ + 'image' => $this->resource->docker_registry_image_name, + 'tag' => $this->resource->docker_registry_image_tag, + ]; + } + + protected function getEnvironmentVariables(): array + { + $variables = collect([]); + foreach ($this->resource->environment_variables as $env) { + $variables->push([ + 'key' => $env->key, + 'value' => $env->value, + 'is_build_time' => $env->is_build_time, + 'is_preview' => $env->is_preview, + 'is_multiline' => $env->is_multiline, + ]); + } + + return $variables->toArray(); + } + + protected function getPreviewEnvironmentVariables(): array + { + $variables = collect([]); + foreach ($this->resource->environment_variables_preview as $env) { + $variables->push([ + 'key' => $env->key, + 'value' => $env->value, + 'is_build_time' => $env->is_build_time, + 'is_preview' => $env->is_preview, + 'is_multiline' => $env->is_multiline, + ]); + } + + return $variables->toArray(); + } + + protected function getApplicationSettings(): array + { + $removedKeys = ['id', 'application_id', 'created_at', 'updated_at']; + $settings = $this->resource->settings->attributesToArray(); + $settings = collect($settings)->filter(function ($value, $key) use ($removedKeys) { + return ! in_array($key, $removedKeys); + })->sortBy(function ($value, $key) { + return $key; + })->toArray(); + + return $settings; + } + + public function saveJson(string $path): void + { + file_put_contents($path, json_encode($this->config, JSON_PRETTY_PRINT)); + } + + public function saveYaml(string $path): void + { + file_put_contents($path, Yaml::dump($this->config, 6, 2)); + } + + public function toArray(): array + { + return $this->config; + } + + public function toJson(): string + { + return json_encode($this->config, JSON_PRETTY_PRINT); + } + + public function toYaml(): string + { + return Yaml::dump($this->config, 6, 2); + } +} diff --git a/app/Services/DockerImageParser.php b/app/Services/DockerImageParser.php index 4987f953d..1fd6625b3 100644 --- a/app/Services/DockerImageParser.php +++ b/app/Services/DockerImageParser.php @@ -43,7 +43,11 @@ class DockerImageParser public function getFullImageNameWithoutTag(): string { - return $this->registryUrl.'/'.$this->imageName; + if ($this->registryUrl) { + return $this->registryUrl.'/'.$this->imageName; + } + + return $this->imageName; } public function getRegistryUrl(): string diff --git a/app/Traits/HasConfiguration.php b/app/Traits/HasConfiguration.php new file mode 100644 index 000000000..e572c45ea --- /dev/null +++ b/app/Traits/HasConfiguration.php @@ -0,0 +1,42 @@ +uuid}"; + if (! is_dir($configDir)) { + mkdir($configDir, 0755, true); + } + + $generator->saveJson($configDir.'/coolify.json'); + $generator->saveYaml($configDir.'/coolify.yaml'); + + // Generate a README file with basic information + file_put_contents( + $configDir.'/README.md', + generate_readme_file($this->name, now()->toIso8601String()) + ); + } + + public function getConfigurationAsJson(): string + { + return (new ConfigurationGenerator($this))->toJson(); + } + + public function getConfigurationAsYaml(): string + { + return (new ConfigurationGenerator($this))->toYaml(); + } + + public function getConfigurationAsArray(): array + { + return (new ConfigurationGenerator($this))->toArray(); + } +} diff --git a/bootstrap/getHelperVersion.php b/bootstrap/getHelperVersion.php new file mode 100644 index 000000000..766af8db2 --- /dev/null +++ b/bootstrap/getHelperVersion.php @@ -0,0 +1,10 @@ +startsWith('$')) { $foundEnv = EnvironmentVariable::where([ 'key' => $key, - 'application_id' => $resource->id, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, 'is_preview' => false, ])->first(); $value = replaceVariables($value); @@ -2653,7 +2654,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($value->startsWith('SERVICE_')) { $foundEnv = EnvironmentVariable::where([ 'key' => $key, - 'application_id' => $resource->id, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, ])->first(); ['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value); if (! is_null($command)) { @@ -2676,7 +2678,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'key' => $key, 'value' => $fqdn, 'is_build_time' => false, - 'application_id' => $resource->id, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, 'is_preview' => false, ]); } @@ -2687,7 +2690,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'key' => $key, 'value' => $generatedValue, 'is_build_time' => false, - 'application_id' => $resource->id, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, 'is_preview' => false, ]); } @@ -2712,7 +2716,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } $foundEnv = EnvironmentVariable::where([ 'key' => $key, - 'application_id' => $resource->id, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, 'is_preview' => false, ])->first(); if ($foundEnv) { @@ -2722,7 +2727,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($foundEnv) { $foundEnv->update([ 'key' => $key, - 'application_id' => $resource->id, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, 'is_build_time' => $isBuildTime, 'value' => $defaultValue, ]); @@ -2731,7 +2737,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'key' => $key, 'value' => $defaultValue, 'is_build_time' => $isBuildTime, - 'application_id' => $resource->id, + 'resourceable_type' => get_class($resource), + 'resourceable_id' => $resource->id, 'is_preview' => false, ]); } @@ -2872,7 +2879,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal data_forget($service, 'volumes.*.is_directory'); data_forget($service, 'exclude_from_hc'); data_set($service, 'environment', $serviceVariables->toArray()); - updateCompose($service); return $service; }); diff --git a/config/constants.php b/config/constants.php index fc2d92c56..dd8759e14 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,9 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.383', + 'version' => '4.0.0-beta.389', + 'helper_version' => '1.0.5', + 'realtime_version' => '1.0.5', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), diff --git a/database/migrations/2025_01_21_125205_update_finished_at_timestamps_if_not_set.php b/database/migrations/2025_01_21_125205_update_finished_at_timestamps_if_not_set.php new file mode 100644 index 000000000..050e8f1ae --- /dev/null +++ b/database/migrations/2025_01_21_125205_update_finished_at_timestamps_if_not_set.php @@ -0,0 +1,37 @@ +whereNull('finished_at') + ->update(['finished_at' => DB::raw('updated_at')]); + } catch (\Exception $e) { + \Log::error('Failed to update not set finished_at timestamps for application_deployment_queues: '.$e->getMessage()); + } + + try { + DB::table('scheduled_database_backup_executions') + ->whereNull('finished_at') + ->update(['finished_at' => DB::raw('updated_at')]); + } catch (\Exception $e) { + \Log::error('Failed to update not set finished_at timestamps for scheduled_database_backup_executions: '.$e->getMessage()); + } + + try { + DB::table('scheduled_task_executions') + ->whereNull('finished_at') + ->update(['finished_at' => DB::raw('updated_at')]); + } catch (\Exception $e) { + \Log::error('Failed to update not set finished_at timestamps for scheduled_task_executions: '.$e->getMessage()); + } + } +}; diff --git a/database/migrations/2025_01_22_101105_remove_wrongly_created_envs.php b/database/migrations/2025_01_22_101105_remove_wrongly_created_envs.php new file mode 100644 index 000000000..cb1b3cbe6 --- /dev/null +++ b/database/migrations/2025_01_22_101105_remove_wrongly_created_envs.php @@ -0,0 +1,19 @@ +each(function (EnvironmentVariable $environmentVariable) { + $environmentVariable->delete(); + }); + } catch (\Exception $e) { + Log::error('Failed to delete wrongly created environment variables: '.$e->getMessage()); + } + } +}; diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index acc44d02a..bda538bca 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -1,5 +1,6 @@ # Versions + # https://hub.docker.com/_/alpine ARG BASE_IMAGE=alpine:3.21 # https://download.docker.com/linux/static/stable/ @@ -11,7 +12,7 @@ ARG DOCKER_BUILDX_VERSION=0.19.3 # https://github.com/buildpacks/pack/releases ARG PACK_VERSION=0.36.2 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.30.0 +ARG NIXPACKS_VERSION=1.32.0 # https://github.com/minio/mc/releases ARG MINIO_VERSION=RELEASE.2024-11-21T17-21-54Z diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index 10bd80c2b..3032d3ef7 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -119,6 +119,7 @@ COPY --chown=www-data:www-data storage ./storage COPY --chown=www-data:www-data templates ./templates COPY --chown=www-data:www-data resources/views ./resources/views COPY --chown=www-data:www-data artisan artisan +COPY --chown=www-data:www-data openapi.yaml ./openapi.yaml RUN composer dump-autoload diff --git a/package-lock.json b/package-lock.json index 56cfa1586..fed13e28a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "pusher-js": "8.4.0-rc2", "tailwind-scrollbar": "^3.1.0", "tailwindcss": "3.4.17", - "vite": "6.0.7", + "vite": "6.0.11", "vue": "3.5.13" } }, @@ -2784,9 +2784,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", - "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", + "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7220e55a5..4e2cf6700 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "pusher-js": "8.4.0-rc2", "tailwind-scrollbar": "^3.1.0", "tailwindcss": "3.4.17", - "vite": "6.0.7", + "vite": "6.0.11", "vue": "3.5.13" }, "dependencies": { diff --git a/public/svgs/flipt.svg b/public/svgs/flipt.svg new file mode 100644 index 000000000..8c8164f8f --- /dev/null +++ b/public/svgs/flipt.svg @@ -0,0 +1,21 @@ + + \ No newline at end of file diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php index 092e306a5..ecc5785aa 100644 --- a/resources/views/components/server/sidebar.blade.php +++ b/resources/views/components/server/sidebar.blade.php @@ -22,7 +22,7 @@ Log Drains - Metrics @endif @if (!$server->isLocalhost()) diff --git a/resources/views/components/status/running.blade.php b/resources/views/components/status/running.blade.php index 4e5f0c275..27a6d7181 100644 --- a/resources/views/components/status/running.blade.php +++ b/resources/views/components/status/running.blade.php @@ -6,12 +6,8 @@ ])
No shell (bash/sh) is available in this container. Please ensure either bash or sh is installed to use the terminal.
+
+ Warning: Enable these
+ options only if you fully understand their implications and
+ consequences!
Improper use will result in data loss and could cause
+ functional issues.
+
- Warning: Enable these
- options only if you fully understand their implications and
- consequences!
Improper use will result in data loss and could cause
- functional issues.
-