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/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml index fec54d54a..2e50abbe7 100644 --- a/.github/workflows/coolify-helper-next.yml +++ b/.github/workflows/coolify-helper-next.yml @@ -44,8 +44,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Get Version id: version @@ -86,8 +86,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Get Version id: version diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index 0c9996ec8..ed6fc3bcb 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -44,8 +44,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Get Version id: version @@ -85,8 +85,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Get Version id: version diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml index 21871b103..477274751 100644 --- a/.github/workflows/coolify-production-build.yml +++ b/.github/workflows/coolify-production-build.yml @@ -51,8 +51,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Get Version id: version @@ -91,8 +91,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Get Version id: version diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml index 7ab4dcc42..8937ea27d 100644 --- a/.github/workflows/coolify-realtime-next.yml +++ b/.github/workflows/coolify-realtime-next.yml @@ -48,8 +48,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Get Version id: version @@ -90,8 +90,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Get Version id: version diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml index 5efe445c5..d8784dd50 100644 --- a/.github/workflows/coolify-realtime.yml +++ b/.github/workflows/coolify-realtime.yml @@ -48,8 +48,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Get Version id: version @@ -90,8 +90,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Get Version id: version diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml index 67b7b03e8..494ef6939 100644 --- a/.github/workflows/coolify-staging-build.yml +++ b/.github/workflows/coolify-staging-build.yml @@ -64,8 +64,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Image (${{ matrix.arch }}) uses: docker/build-push-action@v6 @@ -110,8 +110,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | diff --git a/.github/workflows/coolify-testing-host.yml b/.github/workflows/coolify-testing-host.yml index 24133887a..0c1371573 100644 --- a/.github/workflows/coolify-testing-host.yml +++ b/.github/workflows/coolify-testing-host.yml @@ -44,8 +44,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Image (${{ matrix.arch }}) uses: docker/build-push-action@v6 @@ -81,8 +81,8 @@ jobs: uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index ee3398b04..94651a3c1 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -39,7 +39,7 @@ public function handle(Application $application, bool $previewDeployments = fals foreach ($containersToStop as $containerName) { instant_remote_process(command: [ - "docker stop --time=30 $containerName", + "docker stop -t 30 $containerName", "docker rm -f $containerName", ], server: $server, throwError: false); } diff --git a/app/Actions/Application/StopApplicationOneServer.php b/app/Actions/Application/StopApplicationOneServer.php index 600b1cb9a..bf9fdee72 100644 --- a/app/Actions/Application/StopApplicationOneServer.php +++ b/app/Actions/Application/StopApplicationOneServer.php @@ -26,7 +26,7 @@ public function handle(Application $application, Server $server) if ($containerName) { instant_remote_process( [ - "docker stop --time=30 $containerName", + "docker stop -t 30 $containerName", "docker rm -f $containerName", ], $server diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index 7fdfe9aeb..6da5465c6 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -105,7 +105,7 @@ public function handle(StandaloneClickhouse $database) $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; - $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true"; $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index d1bb119af..cd820523d 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -192,7 +192,7 @@ public function handle(StandaloneDragonfly $database) if ($this->database->enable_ssl) { $this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt"; } - $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true"; $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index 128469e24..863691e1e 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -208,7 +208,7 @@ public function handle(StandaloneKeydb $database) if ($this->database->enable_ssl) { $this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt"; } - $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true"; $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index 29dd7b8fe..498ba0b0b 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -209,7 +209,7 @@ public function handle(StandaloneMariadb $database) $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; - $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true"; $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 5982b68be..9565990c1 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -260,7 +260,7 @@ public function handle(StandaloneMongodb $database) $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; - $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true"; $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; if ($this->database->enable_ssl) { diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index c1df8d6db..337516405 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -210,7 +210,7 @@ public function handle(StandaloneMysql $database) $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; - $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true"; $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 1ae0d56a0..41e39c811 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -223,7 +223,7 @@ public function handle(StandalonePostgresql $database) $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; - $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true"; $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; if ($this->database->enable_ssl) { diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index 4c99a0213..2eaf82fdd 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -205,7 +205,7 @@ public function handle(StandaloneRedis $database) if ($this->database->enable_ssl) { $this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt"; } - $this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true"; $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 5c881e743..c024c14e1 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -49,7 +49,7 @@ private function stopContainer($database, string $containerName, int $timeout = { $server = $database->destination->server; instant_remote_process(command: [ - "docker stop --time=$timeout $containerName", + "docker stop -t $timeout $containerName", "docker rm -f $containerName", ], server: $server, throwError: false); } diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index bfc65d8d2..20c997656 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -75,6 +75,10 @@ public function handle(Server $server, bool $async = true, bool $force = false, ' done', " echo 'Successfully stopped and removed existing coolify-proxy.'", 'fi', + ]); + // Ensure required networks exist BEFORE docker compose up (networks are declared as external) + $commands = $commands->merge(ensureProxyNetworksExist($server)); + $commands = $commands->merge([ "echo 'Starting coolify-proxy.'", 'docker compose up -d --wait --remove-orphans', "echo 'Successfully started coolify-proxy.'", diff --git a/app/Actions/Proxy/StopProxy.php b/app/Actions/Proxy/StopProxy.php index 8f1b8af1c..04d031ec6 100644 --- a/app/Actions/Proxy/StopProxy.php +++ b/app/Actions/Proxy/StopProxy.php @@ -24,7 +24,7 @@ public function handle(Server $server, bool $forceStop = true, int $timeout = 30 } instant_remote_process(command: [ - "docker stop --time=$timeout $containerName 2>/dev/null || true", + "docker stop -t=$timeout $containerName 2>/dev/null || true", "docker rm -f $containerName 2>/dev/null || true", '# Wait for container to be fully removed', 'for i in {1..10}; do', diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index 0bf763d78..a26e7daaa 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -3,6 +3,8 @@ namespace App\Actions\Server; use App\Models\Server; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Sleep; use Lorisleiva\Actions\Concerns\AsAction; @@ -29,7 +31,59 @@ public function handle($manual_update = false) return; } CleanupDocker::dispatch($this->server, false, false); - $this->latestVersion = get_latest_version_of_coolify(); + + // Fetch fresh version from CDN instead of using cache + try { + $response = Http::retry(3, 1000)->timeout(10) + ->get(config('constants.coolify.versions_url')); + + if ($response->successful()) { + $versions = $response->json(); + $this->latestVersion = data_get($versions, 'coolify.v4.version'); + } else { + // Fallback to cache if CDN unavailable + $cacheVersion = get_latest_version_of_coolify(); + + // Validate cache version against current running version + if ($cacheVersion && version_compare($cacheVersion, config('constants.coolify.version'), '<')) { + Log::error('Failed to fetch fresh version from CDN and cache is corrupted/outdated', [ + 'cached_version' => $cacheVersion, + 'current_version' => config('constants.coolify.version'), + ]); + throw new \Exception( + 'Cannot determine latest version: CDN unavailable and cache version '. + "({$cacheVersion}) is older than running version (".config('constants.coolify.version').')' + ); + } + + $this->latestVersion = $cacheVersion; + Log::warning('Failed to fetch fresh version from CDN (unsuccessful response), using validated cache', [ + 'version' => $cacheVersion, + ]); + } + } catch (\Throwable $e) { + $cacheVersion = get_latest_version_of_coolify(); + + // Validate cache version against current running version + if ($cacheVersion && version_compare($cacheVersion, config('constants.coolify.version'), '<')) { + Log::error('Failed to fetch fresh version from CDN and cache is corrupted/outdated', [ + 'error' => $e->getMessage(), + 'cached_version' => $cacheVersion, + 'current_version' => config('constants.coolify.version'), + ]); + throw new \Exception( + 'Cannot determine latest version: CDN unavailable and cache version '. + "({$cacheVersion}) is older than running version (".config('constants.coolify.version').')' + ); + } + + $this->latestVersion = $cacheVersion; + Log::warning('Failed to fetch fresh version from CDN, using validated cache', [ + 'error' => $e->getMessage(), + 'version' => $cacheVersion, + ]); + } + $this->currentVersion = config('constants.coolify.version'); if (! $manual_update) { if (! $settings->is_auto_update_enabled) { @@ -42,6 +96,20 @@ public function handle($manual_update = false) return; } } + + // ALWAYS check for downgrades (even for manual updates) + if (version_compare($this->latestVersion, $this->currentVersion, '<')) { + Log::error('Downgrade prevented', [ + 'target_version' => $this->latestVersion, + 'current_version' => $this->currentVersion, + 'manual_update' => $manual_update, + ]); + throw new \Exception( + "Cannot downgrade from {$this->currentVersion} to {$this->latestVersion}. ". + 'If you need to downgrade, please do so manually via Docker commands.' + ); + } + $this->update(); $settings->new_version_available = false; $settings->save(); @@ -56,8 +124,9 @@ private function update() $image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion; instant_remote_process(["docker pull -q $image"], $this->server, false); + $upgradeScriptUrl = config('constants.coolify.upgrade_script_url'); remote_process([ - 'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh', + "curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh", "bash /data/coolify/source/upgrade.sh $this->latestVersion", ], $this->server); } diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 3f4e96479..23b41e3f2 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -54,7 +54,7 @@ private function stopContainersInParallel(array $containersToStop, Server $serve $timeout = count($containersToStop) > 5 ? 10 : 30; $commands = []; $containerList = implode(' ', $containersToStop); - $commands[] = "docker stop --time=$timeout $containerList"; + $commands[] = "docker stop -t $timeout $containerList"; $commands[] = "docker rm -f $containerList"; instant_remote_process( command: $commands, diff --git a/app/Console/Commands/CleanupNames.php b/app/Console/Commands/CleanupNames.php index 2992e32b9..2451dc3ed 100644 --- a/app/Console/Commands/CleanupNames.php +++ b/app/Console/Commands/CleanupNames.php @@ -63,8 +63,6 @@ class CleanupNames extends Command public function handle(): int { - $this->info('🔍 Scanning for invalid characters in name fields...'); - if ($this->option('backup') && ! $this->option('dry-run')) { $this->createBackup(); } @@ -75,7 +73,7 @@ public function handle(): int : $this->modelsToClean; if ($modelFilter && ! isset($this->modelsToClean[$modelFilter])) { - $this->error("❌ Unknown model: {$modelFilter}"); + $this->error("Unknown model: {$modelFilter}"); $this->info('Available models: '.implode(', ', array_keys($this->modelsToClean))); return self::FAILURE; @@ -88,19 +86,21 @@ public function handle(): int $this->processModel($modelName, $modelClass); } - $this->displaySummary(); - if (! $this->option('dry-run') && $this->totalCleaned > 0) { $this->logChanges(); } + if ($this->option('dry-run')) { + $this->info("Name cleanup: would sanitize {$this->totalCleaned} records"); + } else { + $this->info("Name cleanup: sanitized {$this->totalCleaned} records"); + } + return self::SUCCESS; } protected function processModel(string $modelName, string $modelClass): void { - $this->info("\n📋 Processing {$modelName}..."); - try { $records = $modelClass::all(['id', 'name']); $cleaned = 0; @@ -128,21 +128,17 @@ protected function processModel(string $modelName, string $modelClass): void $cleaned++; $this->totalCleaned++; - $this->warn(" 🧹 {$modelName} #{$record->id}:"); - $this->line(' From: '.$this->truncate($originalName, 80)); - $this->line(' To: '.$this->truncate($sanitizedName, 80)); + // Only log in dry-run mode to preview changes + if ($this->option('dry-run')) { + $this->warn(" 🧹 {$modelName} #{$record->id}:"); + $this->line(' From: '.$this->truncate($originalName, 80)); + $this->line(' To: '.$this->truncate($sanitizedName, 80)); + } } } - if ($cleaned > 0) { - $action = $this->option('dry-run') ? 'would be sanitized' : 'sanitized'; - $this->info(" ✅ {$cleaned}/{$records->count()} records {$action}"); - } else { - $this->info(' ✨ No invalid characters found'); - } - } catch (\Exception $e) { - $this->error(" ❌ Error processing {$modelName}: ".$e->getMessage()); + $this->error("Error processing {$modelName}: ".$e->getMessage()); } } @@ -165,28 +161,6 @@ protected function sanitizeName(string $name): string return $sanitized; } - protected function displaySummary(): void - { - $this->info("\n".str_repeat('=', 60)); - $this->info('📊 CLEANUP SUMMARY'); - $this->info(str_repeat('=', 60)); - - $this->line("Records processed: {$this->totalProcessed}"); - $this->line("Records with invalid characters: {$this->totalCleaned}"); - - if ($this->option('dry-run')) { - $this->warn("\n🔍 DRY RUN - No changes were made to the database"); - $this->info('Run without --dry-run to apply these changes'); - } else { - if ($this->totalCleaned > 0) { - $this->info("\n✅ Database successfully sanitized!"); - $this->info('Changes logged to storage/logs/name-cleanup.log'); - } else { - $this->info("\n✨ No cleanup needed - all names are valid!"); - } - } - } - protected function logChanges(): void { $logFile = storage_path('logs/name-cleanup.log'); @@ -208,8 +182,6 @@ protected function logChanges(): void protected function createBackup(): void { - $this->info('💾 Creating database backup...'); - try { $backupFile = storage_path('backups/name-cleanup-backup-'.now()->format('Y-m-d-H-i-s').'.sql'); @@ -229,15 +201,9 @@ protected function createBackup(): void ); exec($command, $output, $returnCode); - - if ($returnCode === 0) { - $this->info("✅ Backup created: {$backupFile}"); - } else { - $this->warn('⚠️ Backup creation may have failed. Proceeding anyway...'); - } } catch (\Exception $e) { - $this->warn('⚠️ Could not create backup: '.$e->getMessage()); - $this->warn('Proceeding without backup...'); + // Log failure but continue - backup is optional safeguard + Log::warning('Name cleanup backup failed', ['error' => $e->getMessage()]); } } diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php index abf8010c0..199e168fc 100644 --- a/app/Console/Commands/CleanupRedis.php +++ b/app/Console/Commands/CleanupRedis.php @@ -18,10 +18,6 @@ public function handle() $dryRun = $this->option('dry-run'); $skipOverlapping = $this->option('skip-overlapping'); - if ($dryRun) { - $this->info('DRY RUN MODE - No data will be deleted'); - } - $deletedCount = 0; $totalKeys = 0; @@ -29,8 +25,6 @@ public function handle() $keys = $redis->keys('*'); $totalKeys = count($keys); - $this->info("Scanning {$totalKeys} keys for cleanup..."); - foreach ($keys as $key) { $keyWithoutPrefix = str_replace($prefix, '', $key); $type = $redis->command('type', [$keyWithoutPrefix]); @@ -51,14 +45,12 @@ public function handle() // Clean up overlapping queues if not skipped if (! $skipOverlapping) { - $this->info('Cleaning up overlapping queues...'); $overlappingCleaned = $this->cleanupOverlappingQueues($redis, $prefix, $dryRun); $deletedCount += $overlappingCleaned; } // Clean up stale cache locks (WithoutOverlapping middleware) if ($this->option('clear-locks')) { - $this->info('Cleaning up stale cache locks...'); $locksCleaned = $this->cleanupCacheLocks($dryRun); $deletedCount += $locksCleaned; } @@ -66,15 +58,14 @@ public function handle() // Clean up stuck jobs (restart mode = aggressive, runtime mode = conservative) $isRestart = $this->option('restart'); if ($isRestart || $this->option('clear-locks')) { - $this->info($isRestart ? 'Cleaning up stuck jobs (RESTART MODE - aggressive)...' : 'Checking for stuck jobs (runtime mode - conservative)...'); $jobsCleaned = $this->cleanupStuckJobs($redis, $prefix, $dryRun, $isRestart); $deletedCount += $jobsCleaned; } if ($dryRun) { - $this->info("DRY RUN: Would delete {$deletedCount} out of {$totalKeys} keys"); + $this->info("Redis cleanup: would delete {$deletedCount} items"); } else { - $this->info("Deleted {$deletedCount} out of {$totalKeys} keys"); + $this->info("Redis cleanup: deleted {$deletedCount} items"); } } @@ -85,11 +76,8 @@ private function shouldDeleteHashKey($redis, $keyWithoutPrefix, $dryRun) // Delete completed and failed jobs if (in_array($status, ['completed', 'failed'])) { - if ($dryRun) { - $this->line("Would delete job: {$keyWithoutPrefix} (status: {$status})"); - } else { + if (! $dryRun) { $redis->command('del', [$keyWithoutPrefix]); - $this->line("Deleted job: {$keyWithoutPrefix} (status: {$status})"); } return true; @@ -115,11 +103,8 @@ private function shouldDeleteOtherKey($redis, $keyWithoutPrefix, $fullKey, $dryR foreach ($patterns as $pattern => $description) { if (str_contains($keyWithoutPrefix, $pattern)) { - if ($dryRun) { - $this->line("Would delete {$description}: {$keyWithoutPrefix}"); - } else { + if (! $dryRun) { $redis->command('del', [$keyWithoutPrefix]); - $this->line("Deleted {$description}: {$keyWithoutPrefix}"); } return true; @@ -132,11 +117,8 @@ private function shouldDeleteOtherKey($redis, $keyWithoutPrefix, $fullKey, $dryR $weekAgo = now()->subDays(7)->timestamp; if ($timestamp < $weekAgo) { - if ($dryRun) { - $this->line("Would delete old timestamped data: {$keyWithoutPrefix}"); - } else { + if (! $dryRun) { $redis->command('del', [$keyWithoutPrefix]); - $this->line("Deleted old timestamped data: {$keyWithoutPrefix}"); } return true; @@ -160,8 +142,6 @@ private function cleanupOverlappingQueues($redis, $prefix, $dryRun) } } - $this->info('Found '.count($queueKeys).' queue-related keys'); - // Group queues by name pattern to find duplicates $queueGroups = []; foreach ($queueKeys as $queueKey) { @@ -193,7 +173,6 @@ private function cleanupOverlappingQueues($redis, $prefix, $dryRun) private function deduplicateQueueGroup($redis, $baseName, $keys, $dryRun) { $cleanedCount = 0; - $this->line("Processing queue group: {$baseName} (".count($keys).' keys)'); // Sort keys to keep the most recent one usort($keys, function ($a, $b) { @@ -244,11 +223,8 @@ private function deduplicateQueueGroup($redis, $baseName, $keys, $dryRun) } if ($shouldDelete) { - if ($dryRun) { - $this->line(" Would delete empty queue: {$redundantKey}"); - } else { + if (! $dryRun) { $redis->command('del', [$redundantKey]); - $this->line(" Deleted empty queue: {$redundantKey}"); } $cleanedCount++; } @@ -271,15 +247,12 @@ private function deduplicateQueueContents($redis, $queueKey, $dryRun) if (count($uniqueItems) < count($items)) { $duplicates = count($items) - count($uniqueItems); - if ($dryRun) { - $this->line(" Would remove {$duplicates} duplicate jobs from queue: {$queueKey}"); - } else { + if (! $dryRun) { // Rebuild the list with unique items $redis->command('del', [$queueKey]); foreach (array_reverse($uniqueItems) as $item) { $redis->command('lpush', [$queueKey, $item]); } - $this->line(" Removed {$duplicates} duplicate jobs from queue: {$queueKey}"); } $cleanedCount += $duplicates; } @@ -307,13 +280,9 @@ private function cleanupCacheLocks(bool $dryRun): int } } if (empty($lockKeys)) { - $this->info(' No cache locks found.'); - return 0; } - $this->info(' Found '.count($lockKeys).' cache lock(s)'); - foreach ($lockKeys as $lockKey) { // Check TTL to identify stale locks $ttl = $redis->ttl($lockKey); @@ -326,18 +295,11 @@ private function cleanupCacheLocks(bool $dryRun): int $this->warn(" Would delete STALE lock (no expiration): {$lockKey}"); } else { $redis->del($lockKey); - $this->info(" ✓ Deleted STALE lock: {$lockKey}"); } $cleanedCount++; - } elseif ($ttl > 0) { - $this->line(" Skipping active lock (expires in {$ttl}s): {$lockKey}"); } } - if ($cleanedCount === 0) { - $this->info(' No stale locks found (all locks have expiration set)'); - } - return $cleanedCount; } @@ -453,17 +415,11 @@ private function cleanupStuckJobs($redis, string $prefix, bool $dryRun, bool $is $redis->command('hset', [$keyWithoutPrefix, 'status', 'failed']); $redis->command('hset', [$keyWithoutPrefix, 'failed_at', $now]); $redis->command('hset', [$keyWithoutPrefix, 'exception', "Job cleaned up by cleanup:redis - {$reason}"]); - - $this->info(" ✓ Marked as FAILED: {$jobClass} (processing for ".round($processingTime / 60, 1).' min) - '.$reason); } $cleanedCount++; } } - if ($cleanedCount === 0) { - $this->info($isRestart ? ' No jobs to clean up' : ' No stuck jobs found (all jobs running normally)'); - } - return $cleanedCount; } } diff --git a/app/Console/Commands/NotifyDemo.php b/app/Console/Commands/NotifyDemo.php index 990a03869..8e9251ac0 100644 --- a/app/Console/Commands/NotifyDemo.php +++ b/app/Console/Commands/NotifyDemo.php @@ -56,7 +56,7 @@ private function showHelp() php artisan app:demo-notify {channel}

-
Channels:
+
Channels: