diff --git a/.ai/core/deployment-architecture.md b/.ai/core/deployment-architecture.md index 272f00e4c..927bdc8de 100644 --- a/.ai/core/deployment-architecture.md +++ b/.ai/core/deployment-architecture.md @@ -270,6 +270,84 @@ ### Build Optimization - **Build artifact** reuse - **Parallel build** processing +### Docker Build Cache Preservation + +Coolify provides settings to preserve Docker build cache across deployments, addressing cache invalidation issues. + +#### The Problem + +By default, Coolify injects `ARG` statements into user Dockerfiles for build-time variables. This breaks Docker's cache mechanism because: +1. **ARG declarations invalidate cache** - Any change in ARG values after the `ARG` instruction invalidates all subsequent layers +2. **SOURCE_COMMIT changes every commit** - Causes full rebuilds even when code changes are minimal + +#### Application Settings + +Two toggles in **Advanced Settings** control this behavior: + +| Setting | Default | Description | +|---------|---------|-------------| +| `inject_build_args_to_dockerfile` | `true` | Controls whether Coolify adds `ARG` statements to Dockerfile | +| `include_source_commit_in_build` | `false` | Controls whether `SOURCE_COMMIT` is included in build context | + +**Database columns:** `application_settings.inject_build_args_to_dockerfile`, `application_settings.include_source_commit_in_build` + +#### Buildpack Coverage + +| Build Pack | ARG Injection | Method | +|------------|---------------|--------| +| **Dockerfile** | ✅ Yes | `add_build_env_variables_to_dockerfile()` | +| **Docker Compose** (with `build:`) | ✅ Yes | `modify_dockerfiles_for_compose()` | +| **PR Deployments** (Dockerfile only) | ✅ Yes | `add_build_env_variables_to_dockerfile()` | +| **Nixpacks** | ❌ No | Generates its own Dockerfile internally | +| **Static** | ❌ No | Uses internal Dockerfile | +| **Docker Image** | ❌ No | No build phase | + +#### How It Works + +**When `inject_build_args_to_dockerfile` is enabled (default):** +```dockerfile +# Coolify modifies your Dockerfile to add: +FROM node:20 +ARG MY_VAR=value +ARG COOLIFY_URL=... +ARG SOURCE_COMMIT=abc123 # (if include_source_commit_in_build is true) +# ... rest of your Dockerfile +``` + +**When `inject_build_args_to_dockerfile` is disabled:** +- Coolify does NOT modify the Dockerfile +- `--build-arg` flags are still passed (harmless without matching `ARG` in Dockerfile) +- User must manually add `ARG` statements for any build-time variables they need + +**When `include_source_commit_in_build` is disabled (default):** +- `SOURCE_COMMIT` is NOT included in build-time variables +- `SOURCE_COMMIT` is still available at **runtime** (in container environment) +- Docker cache preserved across different commits + +#### Recommended Configuration + +| Use Case | inject_build_args | include_source_commit | Cache Behavior | +|----------|-------------------|----------------------|----------------| +| Maximum cache preservation | `false` | `false` | Best cache retention | +| Need build-time vars, no commit | `true` | `false` | Cache breaks on var changes | +| Need commit at build-time | `true` | `true` | Cache breaks every commit | +| Manual ARG management | `false` | `true` | Cache preserved (no ARG in Dockerfile) | + +#### Implementation Details + +**Files:** +- `app/Jobs/ApplicationDeploymentJob.php`: + - `set_coolify_variables()` - Conditionally adds SOURCE_COMMIT to Docker build context based on `include_source_commit_in_build` setting + - `generate_coolify_env_variables(bool $forBuildTime)` - Distinguishes build-time vs. runtime variables; excludes cache-busting variables like SOURCE_COMMIT from build context unless explicitly enabled + - `generate_env_variables()` - Populates `$this->env_args` with build-time ARG values, respecting `include_source_commit_in_build` toggle + - `add_build_env_variables_to_dockerfile()` - Injects ARG statements into Dockerfiles after FROM instructions; skips injection if `inject_build_args_to_dockerfile` is disabled + - `modify_dockerfiles_for_compose()` - Applies ARG injection to Docker Compose service Dockerfiles; respects `inject_build_args_to_dockerfile` toggle +- `app/Models/ApplicationSetting.php` - Defines `inject_build_args_to_dockerfile` and `include_source_commit_in_build` boolean properties +- `app/Livewire/Project/Application/Advanced.php` - Livewire component providing UI bindings for cache preservation toggles +- `resources/views/livewire/project/application/advanced.blade.php` - Checkbox UI elements for user-facing toggles + +**Note:** Docker Compose services without a `build:` section (image-only) are automatically skipped. + ### Runtime Optimization - **Container resource** limits - **Auto-scaling** based on metrics @@ -428,7 +506,7 @@ #### `content` - `templates/compose/chaskiq.yaml` - Entrypoint script **Implementation:** -- Parsed: `bootstrap/helpers/parsers.php` (line 717) +- Parsed: `bootstrap/helpers/parsers.php` in `parseCompose()` function (handles `content` field extraction) - Storage: `app/Models/LocalFileVolume.php` - Validation: `tests/Unit/StripCoolifyCustomFieldsTest.php` @@ -481,7 +559,7 @@ #### `is_directory` / `isDirectory` - Pre-creating mount points before container starts **Implementation:** -- Parsed: `bootstrap/helpers/parsers.php` (line 718) +- Parsed: `bootstrap/helpers/parsers.php` in `parseCompose()` function (handles `is_directory`/`isDirectory` field extraction) - Storage: `app/Models/LocalFileVolume.php` (`is_directory` column) - Validation: `tests/Unit/StripCoolifyCustomFieldsTest.php` diff --git a/app/Console/Commands/SyncBunny.php b/app/Console/Commands/SyncBunny.php index e634feadb..7a15dd01e 100644 --- a/app/Console/Commands/SyncBunny.php +++ b/app/Console/Commands/SyncBunny.php @@ -16,7 +16,7 @@ class SyncBunny extends Command * * @var string */ - protected $signature = 'sync:bunny {--templates} {--release} {--github-releases} {--nightly}'; + protected $signature = 'sync:bunny {--templates} {--release} {--github-releases} {--github-versions} {--nightly}'; /** * The console command description. @@ -70,12 +70,25 @@ private function syncReleasesToGitHubRepo(): bool // Write releases.json $this->info('Writing releases.json...'); $releasesPath = "$tmpDir/json/releases.json"; + $releasesDir = dirname($releasesPath); + + // Ensure directory exists + if (! is_dir($releasesDir)) { + $this->info("Creating directory: $releasesDir"); + if (! mkdir($releasesDir, 0755, true)) { + $this->error("Failed to create directory: $releasesDir"); + exec('rm -rf '.escapeshellarg($tmpDir)); + + return false; + } + } + $jsonContent = json_encode($releases, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); $bytesWritten = file_put_contents($releasesPath, $jsonContent); if ($bytesWritten === false) { $this->error("Failed to write releases.json to: $releasesPath"); - $this->error('Possible reasons: directory does not exist, permission denied, or disk full.'); + $this->error('Possible reasons: permission denied or disk full.'); exec('rm -rf '.escapeshellarg($tmpDir)); return false; @@ -158,6 +171,156 @@ private function syncReleasesToGitHubRepo(): bool } } + /** + * Sync versions.json to GitHub repository via PR + */ + private function syncVersionsToGitHubRepo(string $versionsLocation, bool $nightly = false): bool + { + $this->info('Syncing versions.json to GitHub repository...'); + try { + if (! file_exists($versionsLocation)) { + $this->error("versions.json not found at: $versionsLocation"); + + return false; + } + + $file = file_get_contents($versionsLocation); + $json = json_decode($file, true); + $actualVersion = data_get($json, 'coolify.v4.version'); + + $timestamp = time(); + $tmpDir = sys_get_temp_dir().'/coolify-cdn-versions-'.$timestamp; + $branchName = 'update-versions-'.$timestamp; + $targetPath = $nightly ? 'json/versions-nightly.json' : 'json/versions.json'; + + // Clone the repository + $this->info('Cloning coolify-cdn repository...'); + exec('gh repo clone coollabsio/coolify-cdn '.escapeshellarg($tmpDir).' 2>&1', $output, $returnCode); + if ($returnCode !== 0) { + $this->error('Failed to clone repository: '.implode("\n", $output)); + + return false; + } + + // Create feature branch + $this->info('Creating feature branch...'); + $output = []; + exec('cd '.escapeshellarg($tmpDir).' && git checkout -b '.escapeshellarg($branchName).' 2>&1', $output, $returnCode); + if ($returnCode !== 0) { + $this->error('Failed to create branch: '.implode("\n", $output)); + exec('rm -rf '.escapeshellarg($tmpDir)); + + return false; + } + + // Write versions.json + $this->info('Writing versions.json...'); + $versionsPath = "$tmpDir/$targetPath"; + $versionsDir = dirname($versionsPath); + + // Ensure directory exists + if (! is_dir($versionsDir)) { + $this->info("Creating directory: $versionsDir"); + if (! mkdir($versionsDir, 0755, true)) { + $this->error("Failed to create directory: $versionsDir"); + exec('rm -rf '.escapeshellarg($tmpDir)); + + return false; + } + } + + $jsonContent = json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $bytesWritten = file_put_contents($versionsPath, $jsonContent); + + if ($bytesWritten === false) { + $this->error("Failed to write versions.json to: $versionsPath"); + $this->error('Possible reasons: permission denied or disk full.'); + exec('rm -rf '.escapeshellarg($tmpDir)); + + return false; + } + + // Stage and commit + $this->info('Committing changes...'); + $output = []; + exec('cd '.escapeshellarg($tmpDir).' && git add '.escapeshellarg($targetPath).' 2>&1', $output, $returnCode); + if ($returnCode !== 0) { + $this->error('Failed to stage changes: '.implode("\n", $output)); + exec('rm -rf '.escapeshellarg($tmpDir)); + + return false; + } + + $this->info('Checking for changes...'); + $statusOutput = []; + exec('cd '.escapeshellarg($tmpDir).' && git status --porcelain '.escapeshellarg($targetPath).' 2>&1', $statusOutput, $returnCode); + if ($returnCode !== 0) { + $this->error('Failed to check repository status: '.implode("\n", $statusOutput)); + exec('rm -rf '.escapeshellarg($tmpDir)); + + return false; + } + + if (empty(array_filter($statusOutput))) { + $this->info('versions.json is already up to date. No changes to commit.'); + exec('rm -rf '.escapeshellarg($tmpDir)); + + return true; + } + + $envLabel = $nightly ? 'NIGHTLY' : 'PRODUCTION'; + $commitMessage = "Update $envLabel versions.json to $actualVersion - ".date('Y-m-d H:i:s'); + $output = []; + exec('cd '.escapeshellarg($tmpDir).' && git commit -m '.escapeshellarg($commitMessage).' 2>&1', $output, $returnCode); + if ($returnCode !== 0) { + $this->error('Failed to commit changes: '.implode("\n", $output)); + exec('rm -rf '.escapeshellarg($tmpDir)); + + return false; + } + + // Push to remote + $this->info('Pushing branch to remote...'); + $output = []; + exec('cd '.escapeshellarg($tmpDir).' && git push origin '.escapeshellarg($branchName).' 2>&1', $output, $returnCode); + if ($returnCode !== 0) { + $this->error('Failed to push branch: '.implode("\n", $output)); + exec('rm -rf '.escapeshellarg($tmpDir)); + + return false; + } + + // Create pull request + $this->info('Creating pull request...'); + $prTitle = "Update $envLabel versions.json to $actualVersion - ".date('Y-m-d H:i:s'); + $prBody = "Automated update of $envLabel versions.json to version $actualVersion"; + $output = []; + $prCommand = 'gh pr create --repo coollabsio/coolify-cdn --title '.escapeshellarg($prTitle).' --body '.escapeshellarg($prBody).' --base main --head '.escapeshellarg($branchName).' 2>&1'; + exec($prCommand, $output, $returnCode); + + // Clean up + exec('rm -rf '.escapeshellarg($tmpDir)); + + if ($returnCode !== 0) { + $this->error('Failed to create PR: '.implode("\n", $output)); + + return false; + } + + $this->info('Pull request created successfully!'); + if (! empty($output)) { + $this->info('PR URL: '.implode("\n", $output)); + } + $this->info("Version synced: $actualVersion"); + + return true; + } catch (\Throwable $e) { + $this->error('Error syncing versions.json: '.$e->getMessage()); + + return false; + } + } + /** * Execute the console command. */ @@ -167,6 +330,7 @@ public function handle() $only_template = $this->option('templates'); $only_version = $this->option('release'); $only_github_releases = $this->option('github-releases'); + $only_github_versions = $this->option('github-versions'); $nightly = $this->option('nightly'); $bunny_cdn = 'https://cdn.coollabs.io'; $bunny_cdn_path = 'coolify'; @@ -224,7 +388,7 @@ public function handle() $install_script_location = "$parent_dir/other/nightly/$install_script"; $versions_location = "$parent_dir/other/nightly/$versions"; } - if (! $only_template && ! $only_version && ! $only_github_releases) { + if (! $only_template && ! $only_version && ! $only_github_releases && ! $only_github_versions) { if ($nightly) { $this->info('About to sync files NIGHTLY (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.'); } else { @@ -249,6 +413,11 @@ public function handle() return; } elseif ($only_version) { + $this->warn('⚠️ DEPRECATION WARNING: The --release option is deprecated.'); + $this->warn(' Please use --github-versions instead to create a PR to the coolify-cdn repository.'); + $this->warn(' This option will continue to work but may be removed in a future version.'); + $this->newLine(); + if ($nightly) { $this->info('About to sync NIGHLTY versions.json to BunnyCDN.'); } else { @@ -281,6 +450,22 @@ public function handle() // Sync releases to GitHub repository $this->syncReleasesToGitHubRepo(); + return; + } elseif ($only_github_versions) { + $envLabel = $nightly ? 'NIGHTLY' : 'PRODUCTION'; + $file = file_get_contents($versions_location); + $json = json_decode($file, true); + $actual_version = data_get($json, 'coolify.v4.version'); + + $this->info("About to sync $envLabel versions.json ($actual_version) to GitHub repository."); + $confirmed = confirm('Are you sure you want to sync versions.json via GitHub PR?'); + if (! $confirmed) { + return; + } + + // Sync versions.json to GitHub repository + $this->syncVersionsToGitHubRepo($versions_location, $nightly); + return; } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 8c1769181..13938675a 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1957,7 +1957,12 @@ private function deploy_to_additional_destinations() private function set_coolify_variables() { - $this->coolify_variables = "SOURCE_COMMIT={$this->commit} "; + $this->coolify_variables = ''; + + // Only include SOURCE_COMMIT in build context if enabled in settings + if ($this->application->settings->include_source_commit_in_build) { + $this->coolify_variables .= "SOURCE_COMMIT={$this->commit} "; + } if ($this->pull_request_id === 0) { $fqdn = $this->application->fqdn; } else { @@ -2242,12 +2247,15 @@ private function generate_coolify_env_variables(bool $forBuildTime = false): Col $coolify_envs = collect([]); $local_branch = $this->branch; if ($this->pull_request_id !== 0) { - // Add SOURCE_COMMIT if not exists - if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) { - if (! is_null($this->commit)) { - $coolify_envs->put('SOURCE_COMMIT', $this->commit); - } else { - $coolify_envs->put('SOURCE_COMMIT', 'unknown'); + // Only add SOURCE_COMMIT for runtime OR when explicitly enabled for build-time + // SOURCE_COMMIT changes with each commit and breaks Docker cache if included in build + if (! $forBuildTime || $this->application->settings->include_source_commit_in_build) { + if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) { + if (! is_null($this->commit)) { + $coolify_envs->put('SOURCE_COMMIT', $this->commit); + } else { + $coolify_envs->put('SOURCE_COMMIT', 'unknown'); + } } } if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) { @@ -2283,12 +2291,15 @@ private function generate_coolify_env_variables(bool $forBuildTime = false): Col add_coolify_default_environment_variables($this->application, $coolify_envs, $this->application->environment_variables_preview); } else { - // Add SOURCE_COMMIT if not exists - if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) { - if (! is_null($this->commit)) { - $coolify_envs->put('SOURCE_COMMIT', $this->commit); - } else { - $coolify_envs->put('SOURCE_COMMIT', 'unknown'); + // Only add SOURCE_COMMIT for runtime OR when explicitly enabled for build-time + // SOURCE_COMMIT changes with each commit and breaks Docker cache if included in build + if (! $forBuildTime || $this->application->settings->include_source_commit_in_build) { + if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) { + if (! is_null($this->commit)) { + $coolify_envs->put('SOURCE_COMMIT', $this->commit); + } else { + $coolify_envs->put('SOURCE_COMMIT', 'unknown'); + } } } if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) { @@ -2331,7 +2342,11 @@ private function generate_coolify_env_variables(bool $forBuildTime = false): Col private function generate_env_variables() { $this->env_args = collect([]); - $this->env_args->put('SOURCE_COMMIT', $this->commit); + + // Only include SOURCE_COMMIT in build args if enabled in settings + if ($this->application->settings->include_source_commit_in_build) { + $this->env_args->put('SOURCE_COMMIT', $this->commit); + } $coolify_envs = $this->generate_coolify_env_variables(forBuildTime: true); $coolify_envs->each(function ($value, $key) { @@ -3344,100 +3359,121 @@ private function add_build_env_variables_to_dockerfile() { if ($this->dockerBuildkitSupported) { // We dont need to add build secrets to dockerfile for buildkit, as we already added them with --secret flag in function generate_docker_env_flags_for_secrets + return; + } + + // Skip ARG injection if disabled by user - preserves Docker build cache + if ($this->application->settings->inject_build_args_to_dockerfile === false) { + $this->application_deployment_queue->addLogEntry('Skipping Dockerfile ARG injection (disabled in settings).', hidden: true); + + return; + } + + $this->execute_remote_command([ + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), + 'hidden' => true, + 'save' => 'dockerfile', + 'ignore_errors' => true, + ]); + $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); + + // Find all FROM instruction positions + $fromLines = $this->findFromInstructionLines($dockerfile); + + // If no FROM instructions found, skip ARG insertion + if (empty($fromLines)) { + return; + } + + // Collect all ARG statements to insert + $argsToInsert = collect(); + + if ($this->pull_request_id === 0) { + // Only add environment variables that are available during build + $envs = $this->application->environment_variables() + ->where('key', 'not like', 'NIXPACKS_%') + ->where('is_buildtime', true) + ->get(); + foreach ($envs as $env) { + if (data_get($env, 'is_multiline') === true) { + $argsToInsert->push("ARG {$env->key}"); + } else { + $argsToInsert->push("ARG {$env->key}={$env->real_value}"); + } + } + // Add Coolify variables as ARGs + if ($this->coolify_variables) { + $coolify_vars = collect(explode(' ', trim($this->coolify_variables))) + ->filter() + ->map(function ($var) { + return "ARG {$var}"; + }); + $argsToInsert = $argsToInsert->merge($coolify_vars); + } } else { - $this->execute_remote_command([ + // Only add preview environment variables that are available during build + $envs = $this->application->environment_variables_preview() + ->where('key', 'not like', 'NIXPACKS_%') + ->where('is_buildtime', true) + ->get(); + foreach ($envs as $env) { + if (data_get($env, 'is_multiline') === true) { + $argsToInsert->push("ARG {$env->key}"); + } else { + $argsToInsert->push("ARG {$env->key}={$env->real_value}"); + } + } + // Add Coolify variables as ARGs + if ($this->coolify_variables) { + $coolify_vars = collect(explode(' ', trim($this->coolify_variables))) + ->filter() + ->map(function ($var) { + return "ARG {$var}"; + }); + $argsToInsert = $argsToInsert->merge($coolify_vars); + } + } + + // Development logging to show what ARGs are being injected + if (isDev()) { + $this->application_deployment_queue->addLogEntry('[DEBUG] ========================================'); + $this->application_deployment_queue->addLogEntry('[DEBUG] Dockerfile ARG Injection'); + $this->application_deployment_queue->addLogEntry('[DEBUG] ========================================'); + $this->application_deployment_queue->addLogEntry('[DEBUG] ARGs to inject: '.$argsToInsert->count()); + foreach ($argsToInsert as $arg) { + // Only show ARG key, not the value (for security) + $argKey = str($arg)->after('ARG ')->before('=')->toString(); + $this->application_deployment_queue->addLogEntry("[DEBUG] - {$argKey}"); + } + } + + // Insert ARGs after each FROM instruction (in reverse order to maintain correct line numbers) + if ($argsToInsert->isNotEmpty()) { + foreach (array_reverse($fromLines) as $fromLineIndex) { + // Insert all ARGs after this FROM instruction + foreach ($argsToInsert->reverse() as $arg) { + $dockerfile->splice($fromLineIndex + 1, 0, [$arg]); + } + } + $envs_mapped = $envs->mapWithKeys(function ($env) { + return [$env->key => $env->real_value]; + }); + $secrets_hash = $this->generate_secrets_hash($envs_mapped); + $argsToInsert->push("ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"); + } + + $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); + $this->application_deployment_queue->addLogEntry('Final Dockerfile:', type: 'info', hidden: true); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), + 'hidden' => true, + ], + [ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, - 'save' => 'dockerfile', 'ignore_errors' => true, ]); - $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); - - // Find all FROM instruction positions - $fromLines = $this->findFromInstructionLines($dockerfile); - - // If no FROM instructions found, skip ARG insertion - if (empty($fromLines)) { - return; - } - - // Collect all ARG statements to insert - $argsToInsert = collect(); - - if ($this->pull_request_id === 0) { - // Only add environment variables that are available during build - $envs = $this->application->environment_variables() - ->where('key', 'not like', 'NIXPACKS_%') - ->where('is_buildtime', true) - ->get(); - foreach ($envs as $env) { - if (data_get($env, 'is_multiline') === true) { - $argsToInsert->push("ARG {$env->key}"); - } else { - $argsToInsert->push("ARG {$env->key}={$env->real_value}"); - } - } - // Add Coolify variables as ARGs - if ($this->coolify_variables) { - $coolify_vars = collect(explode(' ', trim($this->coolify_variables))) - ->filter() - ->map(function ($var) { - return "ARG {$var}"; - }); - $argsToInsert = $argsToInsert->merge($coolify_vars); - } - } else { - // Only add preview environment variables that are available during build - $envs = $this->application->environment_variables_preview() - ->where('key', 'not like', 'NIXPACKS_%') - ->where('is_buildtime', true) - ->get(); - foreach ($envs as $env) { - if (data_get($env, 'is_multiline') === true) { - $argsToInsert->push("ARG {$env->key}"); - } else { - $argsToInsert->push("ARG {$env->key}={$env->real_value}"); - } - } - // Add Coolify variables as ARGs - if ($this->coolify_variables) { - $coolify_vars = collect(explode(' ', trim($this->coolify_variables))) - ->filter() - ->map(function ($var) { - return "ARG {$var}"; - }); - $argsToInsert = $argsToInsert->merge($coolify_vars); - } - } - - // Insert ARGs after each FROM instruction (in reverse order to maintain correct line numbers) - if ($argsToInsert->isNotEmpty()) { - foreach (array_reverse($fromLines) as $fromLineIndex) { - // Insert all ARGs after this FROM instruction - foreach ($argsToInsert->reverse() as $arg) { - $dockerfile->splice($fromLineIndex + 1, 0, [$arg]); - } - } - $envs_mapped = $envs->mapWithKeys(function ($env) { - return [$env->key => $env->real_value]; - }); - $secrets_hash = $this->generate_secrets_hash($envs_mapped); - $argsToInsert->push("ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"); - } - - $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); - $this->application_deployment_queue->addLogEntry('Final Dockerfile:', type: 'info', hidden: true); - $this->execute_remote_command( - [ - executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), - 'hidden' => true, - ], - [ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), - 'hidden' => true, - 'ignore_errors' => true, - ]); - } } private function modify_dockerfile_for_secrets($dockerfile_path) @@ -3510,6 +3546,13 @@ private function modify_dockerfiles_for_compose($composeFile) return; } + // Skip ARG injection if disabled by user - preserves Docker build cache + if ($this->application->settings->inject_build_args_to_dockerfile === false) { + $this->application_deployment_queue->addLogEntry('Skipping Docker Compose Dockerfile ARG injection (disabled in settings).', hidden: true); + + return; + } + // Generate env variables if not already done // This populates $this->env_args with both user-defined and COOLIFY_* variables if (! $this->env_args || $this->env_args->isEmpty()) { @@ -3600,6 +3643,18 @@ private function modify_dockerfiles_for_compose($composeFile) continue; } + // Development logging to show what ARGs are being injected for Docker Compose + if (isDev()) { + $this->application_deployment_queue->addLogEntry('[DEBUG] ========================================'); + $this->application_deployment_queue->addLogEntry("[DEBUG] Docker Compose ARG Injection - Service: {$serviceName}"); + $this->application_deployment_queue->addLogEntry('[DEBUG] ========================================'); + $this->application_deployment_queue->addLogEntry('[DEBUG] ARGs to inject: '.$argsToAdd->count()); + foreach ($argsToAdd as $arg) { + $argKey = str($arg)->after('ARG ')->toString(); + $this->application_deployment_queue->addLogEntry("[DEBUG] - {$argKey}"); + } + } + $totalAdded = 0; $offset = 0; diff --git a/app/Livewire/ActivityMonitor.php b/app/Livewire/ActivityMonitor.php index d01b55afb..bc310e715 100644 --- a/app/Livewire/ActivityMonitor.php +++ b/app/Livewire/ActivityMonitor.php @@ -10,7 +10,7 @@ class ActivityMonitor extends Component { public ?string $header = null; - public $activityId; + public $activityId = null; public $eventToDispatch = 'activityFinished'; @@ -49,9 +49,24 @@ public function newMonitorActivity($activityId, $eventToDispatch = 'activityFini public function hydrateActivity() { + if ($this->activityId === null) { + $this->activity = null; + + return; + } + $this->activity = Activity::find($this->activityId); } + public function updatedActivityId($value) + { + if ($value) { + $this->hydrateActivity(); + $this->isPollingActive = true; + self::$eventDispatched = false; + } + } + public function polling() { $this->hydrateActivity(); diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index ed15ab258..cf7ef3e0b 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -37,6 +37,12 @@ class Advanced extends Component #[Validate(['boolean'])] public bool $disableBuildCache = false; + #[Validate(['boolean'])] + public bool $injectBuildArgsToDockerfile = true; + + #[Validate(['boolean'])] + public bool $includeSourceCommitInBuild = false; + #[Validate(['boolean'])] public bool $isLogDrainEnabled = false; @@ -110,6 +116,8 @@ public function syncData(bool $toModel = false) $this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled; $this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled; $this->application->settings->disable_build_cache = $this->disableBuildCache; + $this->application->settings->inject_build_args_to_dockerfile = $this->injectBuildArgsToDockerfile; + $this->application->settings->include_source_commit_in_build = $this->includeSourceCommitInBuild; $this->application->settings->save(); } else { $this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled(); @@ -134,6 +142,8 @@ public function syncData(bool $toModel = false) $this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled; $this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network; $this->disableBuildCache = $this->application->settings->disable_build_cache; + $this->injectBuildArgsToDockerfile = $this->application->settings->inject_build_args_to_dockerfile ?? true; + $this->includeSourceCommitInBuild = $this->application->settings->include_source_commit_in_build ?? false; } } diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index fd191e587..26feb1a5e 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -133,6 +133,8 @@ private function validateServerPath(string $path): bool public string $customLocation = ''; + public ?int $activityId = null; + public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB'; public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'; @@ -156,9 +158,15 @@ public function getListeners() return [ "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', + 'slideOverClosed' => 'resetActivityId', ]; } + public function resetActivityId() + { + $this->activityId = null; + } + public function mount() { $this->parameters = get_route_parameters(); @@ -327,6 +335,9 @@ public function runImport() 'serverId' => $this->server->id, ]); + // Track the activity ID + $this->activityId = $activity->id; + // Dispatch activity to the monitor and open slide-over $this->dispatch('activityMonitor', $activity->id); $this->dispatch('databaserestore'); @@ -548,6 +559,9 @@ public function restoreFromS3() 'serverId' => $this->server->id, ]); + // Track the activity ID + $this->activityId = $activity->id; + // Dispatch activity to the monitor and open slide-over $this->dispatch('activityMonitor', $activity->id); $this->dispatch('databaserestore'); diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index 26cb937b3..de545e9bb 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -15,6 +15,8 @@ class ApplicationSetting extends Model 'is_container_label_escape_enabled' => 'boolean', 'is_container_label_readonly_enabled' => 'boolean', 'use_build_secrets' => 'boolean', + 'inject_build_args_to_dockerfile' => 'boolean', + 'include_source_commit_in_build' => 'boolean', 'is_auto_deploy_enabled' => 'boolean', 'is_force_https_enabled' => 'boolean', 'is_debug_enabled' => 'boolean', diff --git a/bootstrap/helpers/sudo.php b/bootstrap/helpers/sudo.php index f7336beeb..7a7fc3680 100644 --- a/bootstrap/helpers/sudo.php +++ b/bootstrap/helpers/sudo.php @@ -31,6 +31,20 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array 'true', 'if', 'fi', + 'for', + 'do', + 'done', + 'while', + 'until', + 'case', + 'esac', + 'select', + 'then', + 'else', + 'elif', + 'break', + 'continue', + '#', ]) ) { return "sudo $line"; diff --git a/config/constants.php b/config/constants.php index 5135b1fe0..a59345708 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.449', + 'version' => '4.0.0-beta.450', 'helper_version' => '1.0.12', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/database/migrations/2025_11_26_124200_add_build_cache_settings_to_application_settings.php b/database/migrations/2025_11_26_124200_add_build_cache_settings_to_application_settings.php new file mode 100644 index 000000000..5f41816f6 --- /dev/null +++ b/database/migrations/2025_11_26_124200_add_build_cache_settings_to_application_settings.php @@ -0,0 +1,30 @@ +boolean('inject_build_args_to_dockerfile')->default(true)->after('use_build_secrets'); + $table->boolean('include_source_commit_in_build')->default(false)->after('inject_build_args_to_dockerfile'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_settings', function (Blueprint $table) { + $table->dropColumn('inject_build_args_to_dockerfile'); + $table->dropColumn('include_source_commit_in_build'); + }); + } +}; diff --git a/other/nightly/install.sh b/other/nightly/install.sh index bcd37e71f..ac4e3caff 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -288,9 +288,9 @@ if [ "$OS_TYPE" = 'amzn' ]; then dnf install -y findutils >/dev/null fi -LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',') -LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',') -LATEST_REALTIME_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',') +LATEST_VERSION=$(curl -L --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',') +LATEST_HELPER_VERSION=$(curl -L --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',') +LATEST_REALTIME_VERSION=$(curl -L --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',') if [ -z "$LATEST_HELPER_VERSION" ]; then LATEST_HELPER_VERSION=latest @@ -705,10 +705,10 @@ else fi echo -e "5. Download required files from CDN. " -curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml -curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml -curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production -curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh +curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml +curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml +curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production +curl -fsSL -L $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh echo -e "6. Setting up environment variable file" diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 8911c7d7b..562febf01 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.449" + "version": "4.0.0-beta.450" }, "nightly": { - "version": "4.0.0-beta.450" + "version": "4.0.0-beta.451" }, "helper": { "version": "1.0.12" diff --git a/resources/views/components/slide-over.blade.php b/resources/views/components/slide-over.blade.php index 3cb8ec3ab..13769c7b6 100644 --- a/resources/views/components/slide-over.blade.php +++ b/resources/views/components/slide-over.blade.php @@ -1,7 +1,13 @@ @props(['closeWithX' => false, 'fullScreen' => false])