diff --git a/.AI_INSTRUCTIONS_SYNC.md b/.AI_INSTRUCTIONS_SYNC.md
new file mode 100644
index 000000000..bbe0a90e1
--- /dev/null
+++ b/.AI_INSTRUCTIONS_SYNC.md
@@ -0,0 +1,156 @@
+# AI Instructions Synchronization Guide
+
+This document explains how AI instructions are organized and synchronized across different AI tools used with Coolify.
+
+## Overview
+
+Coolify maintains AI instructions in two parallel systems:
+
+1. **CLAUDE.md** - For Claude Code (claude.ai/code)
+2. **.cursor/rules/** - For Cursor IDE and other AI assistants
+
+Both systems share core principles but are optimized for their respective workflows.
+
+## Structure
+
+### CLAUDE.md
+- **Purpose**: Condensed, workflow-focused guide for Claude Code
+- **Format**: Single markdown file
+- **Includes**:
+ - Quick-reference development commands
+ - High-level architecture overview
+ - Core patterns and guidelines
+ - Embedded Laravel Boost guidelines
+ - References to detailed .cursor/rules/ documentation
+
+### .cursor/rules/
+- **Purpose**: Detailed, topic-specific documentation
+- **Format**: Multiple .mdc files organized by topic
+- **Structure**:
+ - `README.mdc` - Main index and overview
+ - `cursor_rules.mdc` - Maintenance guidelines
+ - Topic-specific files (testing-patterns.mdc, security-patterns.mdc, etc.)
+- **Used by**: Cursor IDE, Claude Code (for detailed reference), other AI assistants
+
+## Cross-References
+
+Both systems reference each other:
+
+- **CLAUDE.md** → references `.cursor/rules/` for detailed documentation
+- **.cursor/rules/README.mdc** → references `CLAUDE.md` for Claude Code workflow
+- **.cursor/rules/cursor_rules.mdc** → notes that changes should sync with CLAUDE.md
+
+## Maintaining Consistency
+
+When updating AI instructions, follow these guidelines:
+
+### 1. Core Principles (MUST be consistent)
+- Laravel version (currently Laravel 12)
+- PHP version (8.4)
+- Testing execution rules (Docker for Feature tests, mocking for Unit tests)
+- Security patterns and authorization requirements
+- Code style requirements (Pint, PSR-12)
+
+### 2. Where to Make Changes
+
+**For workflow changes** (how to run commands, development setup):
+- Primary: `CLAUDE.md`
+- Secondary: `.cursor/rules/development-workflow.mdc`
+
+**For architectural patterns** (how code should be structured):
+- Primary: `.cursor/rules/` topic files
+- Secondary: Reference in `CLAUDE.md` "Additional Documentation" section
+
+**For testing patterns**:
+- Both: Must be synchronized
+- `CLAUDE.md` - Contains condensed testing execution rules
+- `.cursor/rules/testing-patterns.mdc` - Contains detailed examples and patterns
+
+### 3. Update Checklist
+
+When making significant changes:
+
+- [ ] Identify if change affects core principles (version numbers, critical patterns)
+- [ ] Update primary location (CLAUDE.md or .cursor/rules/)
+- [ ] Check if update affects cross-referenced content
+- [ ] Update secondary location if needed
+- [ ] Verify cross-references are still accurate
+- [ ] Run: `./vendor/bin/pint CLAUDE.md .cursor/rules/*.mdc` (if applicable)
+
+### 4. Common Inconsistencies to Watch
+
+- **Version numbers**: Laravel, PHP, package versions
+- **Testing instructions**: Docker execution requirements
+- **File paths**: Ensure relative paths work from root
+- **Command syntax**: Docker commands, artisan commands
+- **Architecture decisions**: Laravel 10 structure vs Laravel 12+ structure
+
+## File Organization
+
+```
+/
+├── CLAUDE.md # Claude Code instructions (condensed)
+├── .AI_INSTRUCTIONS_SYNC.md # This file
+└── .cursor/
+ └── rules/
+ ├── README.mdc # Index and overview
+ ├── cursor_rules.mdc # Maintenance guide
+ ├── testing-patterns.mdc # Testing details
+ ├── development-workflow.mdc # Dev setup details
+ ├── security-patterns.mdc # Security details
+ ├── application-architecture.mdc
+ ├── deployment-architecture.mdc
+ ├── database-patterns.mdc
+ ├── frontend-patterns.mdc
+ ├── api-and-routing.mdc
+ ├── form-components.mdc
+ ├── technology-stack.mdc
+ ├── project-overview.mdc
+ └── laravel-boost.mdc # Laravel-specific patterns
+```
+
+## Recent Updates
+
+### 2025-10-07
+- ✅ Added cross-references between CLAUDE.md and .cursor/rules/
+- ✅ Synchronized Laravel version (12) across all files
+- ✅ Added comprehensive testing execution rules (Docker for Feature tests)
+- ✅ Added test design philosophy (prefer mocking over database)
+- ✅ Fixed inconsistencies in testing documentation
+- ✅ Created this synchronization guide
+
+## Maintenance Commands
+
+```bash
+# Check for version inconsistencies
+grep -r "Laravel [0-9]" CLAUDE.md .cursor/rules/*.mdc
+
+# Check for PHP version consistency
+grep -r "PHP [0-9]" CLAUDE.md .cursor/rules/*.mdc
+
+# Format all documentation
+./vendor/bin/pint CLAUDE.md .cursor/rules/*.mdc
+
+# Search for specific patterns across all docs
+grep -r "pattern_to_check" CLAUDE.md .cursor/rules/
+```
+
+## Contributing
+
+When contributing documentation:
+
+1. Check both CLAUDE.md and .cursor/rules/ for existing documentation
+2. Add to appropriate location(s) based on guidelines above
+3. Add cross-references if creating new patterns
+4. Update this file if changing organizational structure
+5. Verify consistency before submitting PR
+
+## Questions?
+
+If unsure about where to document something:
+
+- **Quick reference / workflow** → CLAUDE.md
+- **Detailed patterns / examples** → .cursor/rules/[topic].mdc
+- **Both?** → Start with .cursor/rules/, then reference in CLAUDE.md
+
+When in doubt, prefer detailed documentation in .cursor/rules/ and concise references in CLAUDE.md.
diff --git a/.cursor/rules/README.mdc b/.cursor/rules/README.mdc
index 07f19a816..d0597bb72 100644
--- a/.cursor/rules/README.mdc
+++ b/.cursor/rules/README.mdc
@@ -9,6 +9,10 @@ alwaysApply: false
This comprehensive set of Cursor Rules provides deep insights into **Coolify**, an open-source self-hostable alternative to Heroku/Netlify/Vercel. These rules will help you understand, navigate, and contribute to this complex Laravel-based deployment platform.
+> **Cross-Reference**: This directory is for **detailed, topic-specific rules** used by Cursor IDE and other AI assistants. For Claude Code specifically, also see **[CLAUDE.md](mdc:CLAUDE.md)** which provides a condensed, workflow-focused guide. Both systems share core principles but are optimized for their respective tools.
+>
+> **Maintaining Rules**: When updating these rules, see **[.AI_INSTRUCTIONS_SYNC.md](mdc:.AI_INSTRUCTIONS_SYNC.md)** for synchronization guidelines to keep CLAUDE.md and .cursor/rules/ consistent.
+
## Rule Categories
### 🏗️ Architecture & Foundation
@@ -71,7 +75,7 @@ Coolify uses a **team-based multi-tenancy** model where:
- **Multi-server** support with SSH connections
### 3. Technology Stack
-- **Backend**: Laravel 11 + PHP 8.4
+- **Backend**: Laravel 12 + PHP 8.4
- **Frontend**: Livewire 3.5 + Alpine.js + Tailwind CSS 4.1
- **Database**: PostgreSQL 15 + Redis 7
- **Containerization**: Docker + Docker Compose
diff --git a/.cursor/rules/cursor_rules.mdc b/.cursor/rules/cursor_rules.mdc
index 7dfae3de0..9edccd496 100644
--- a/.cursor/rules/cursor_rules.mdc
+++ b/.cursor/rules/cursor_rules.mdc
@@ -4,6 +4,12 @@ globs: .cursor/rules/*.mdc
alwaysApply: true
---
+# Cursor Rules Maintenance Guide
+
+> **Important**: These rules in `.cursor/rules/` are shared between Cursor IDE and other AI assistants. Changes here should be reflected in **[CLAUDE.md](mdc:CLAUDE.md)** when they affect core workflows or patterns.
+>
+> **Synchronization Guide**: See **[.AI_INSTRUCTIONS_SYNC.md](mdc:.AI_INSTRUCTIONS_SYNC.md)** for detailed guidelines on maintaining consistency between CLAUDE.md and .cursor/rules/.
+
- **Required Rule Structure:**
```markdown
---
diff --git a/.cursor/rules/laravel-boost.mdc b/.cursor/rules/laravel-boost.mdc
index 005ede849..c409a4647 100644
--- a/.cursor/rules/laravel-boost.mdc
+++ b/.cursor/rules/laravel-boost.mdc
@@ -185,7 +185,7 @@ protected function isAccessible(User $user, ?string $path = null): bool
### Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
-- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
+- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
diff --git a/.cursor/rules/testing-patterns.mdc b/.cursor/rules/testing-patterns.mdc
index a0e64dbae..8d250b56a 100644
--- a/.cursor/rules/testing-patterns.mdc
+++ b/.cursor/rules/testing-patterns.mdc
@@ -5,11 +5,56 @@ alwaysApply: false
---
# Coolify Testing Architecture & Patterns
+> **Cross-Reference**: These detailed testing patterns align with the testing guidelines in **[CLAUDE.md](mdc:CLAUDE.md)**. Both documents share the same core principles about Docker execution and mocking preferences.
+
## Testing Philosophy
Coolify employs **comprehensive testing strategies** using modern PHP testing frameworks to ensure reliability of deployment operations, infrastructure management, and user interactions.
-!Important: Always run tests inside `coolify` container.
+### Test Execution Rules
+
+**CRITICAL**: Tests are categorized by database dependency:
+
+#### Unit Tests (`tests/Unit/`)
+- **MUST NOT** use database connections
+- **MUST** use mocking for models and external dependencies
+- **CAN** run outside Docker: `./vendor/bin/pest tests/Unit`
+- Purpose: Test isolated logic, helper functions, and business rules
+
+#### Feature Tests (`tests/Feature/`)
+- **MAY** use database connections (factories, migrations, models)
+- **MUST** run inside Docker container: `docker exec coolify php artisan test`
+- **MUST** use `RefreshDatabase` trait if touching database
+- Purpose: Test API endpoints, workflows, and integration scenarios
+
+**Rule of thumb**: If your test needs `Server::factory()->create()` or any database operation, it's a Feature test and MUST run in Docker.
+
+### Prefer Mocking Over Database
+
+When writing tests, always prefer mocking over real database operations:
+
+```php
+// ❌ BAD: Unit test using database
+it('extracts custom commands', function () {
+ $server = Server::factory()->create(['ip' => '1.2.3.4']);
+ $commands = extract_custom_proxy_commands($server, $yaml);
+ expect($commands)->toBeArray();
+});
+
+// ✅ GOOD: Unit test using mocking
+it('extracts custom commands', function () {
+ $server = Mockery::mock('App\Models\Server');
+ $server->shouldReceive('proxyType')->andReturn('traefik');
+ $commands = extract_custom_proxy_commands($server, $yaml);
+ expect($commands)->toBeArray();
+});
+```
+
+**Design principles for testable code:**
+- Use dependency injection instead of global state
+- Create interfaces for external dependencies (SSH, Docker, etc.)
+- Separate business logic from data persistence
+- Make functions accept interfaces instead of concrete models when possible
## Testing Framework Stack
diff --git a/CLAUDE.md b/CLAUDE.md
index 22e762182..6c594955c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,6 +1,10 @@
# CLAUDE.md
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+This file provides guidance to **Claude Code** (claude.ai/code) when working with code in this repository.
+
+> **Note for AI Assistants**: This file is specifically for Claude Code. If you're using Cursor IDE, refer to the `.cursor/rules/` directory for detailed rule files. Both systems share core principles but are optimized for their respective workflows.
+>
+> **Maintaining Instructions**: When updating AI instructions, see [.AI_INSTRUCTIONS_SYNC.md](.AI_INSTRUCTIONS_SYNC.md) for synchronization guidelines between CLAUDE.md and .cursor/rules/.
## Project Overview
@@ -23,7 +27,14 @@ ### Backend Development
### Code Quality
- `./vendor/bin/pint` - Run Laravel Pint for code formatting
- `./vendor/bin/phpstan` - Run PHPStan for static analysis
-- `./vendor/bin/pest` - Run Pest tests
+- `./vendor/bin/pest` - Run Pest tests (unit tests only, without database)
+
+### Running Tests
+**IMPORTANT**: Tests that require database connections MUST be run inside the Docker container:
+- **Inside Docker**: `docker exec coolify php artisan test` (for feature tests requiring database)
+- **Outside Docker**: `./vendor/bin/pest tests/Unit` (for pure unit tests without database dependencies)
+- Unit tests should use mocking and avoid database connections
+- Feature tests that require database must be run in the `coolify` container
## Architecture Overview
@@ -173,6 +184,21 @@ ### Testing Strategy
- **Mocking**: Use Laravel's built-in mocking for external services
- **Database**: Use RefreshDatabase trait for test isolation
+#### Test Execution Environment
+**CRITICAL**: Database-dependent tests MUST run inside Docker container:
+- **Unit Tests** (`tests/Unit/`): Should NOT use database. Use mocking. Run with `./vendor/bin/pest tests/Unit`
+- **Feature Tests** (`tests/Feature/`): May use database. MUST run inside Docker with `docker exec coolify php artisan test`
+- If a test needs database (factories, migrations, etc.), it belongs in `tests/Feature/`
+- Always mock external services and SSH connections in tests
+
+#### Test Design Philosophy
+**PREFER MOCKING**: When designing features and writing tests:
+- **Design for testability**: Structure code so it can be tested without database (use dependency injection, interfaces)
+- **Mock by default**: Unit tests should mock models and external dependencies using Mockery
+- **Avoid database when possible**: If you can test the logic without database, write it as a Unit test
+- **Only use database when necessary**: Feature tests should test integration points, not isolated logic
+- **Example**: Instead of `Server::factory()->create()`, use `Mockery::mock('App\Models\Server')` in unit tests
+
### Routing Conventions
- Group routes by middleware and prefix
- Use route model binding for cleaner controllers
@@ -228,7 +254,9 @@ ## Important Reminders
## Additional Documentation
-For more detailed guidelines and patterns, refer to the `.cursor/rules/` directory:
+This file contains high-level guidelines for Claude Code. For **more detailed, topic-specific documentation**, refer to the `.cursor/rules/` directory (also accessible by Cursor IDE and other AI assistants):
+
+> **Cross-Reference**: The `.cursor/rules/` directory contains comprehensive, detailed documentation organized by topic. Start with [.cursor/rules/README.mdc](.cursor/rules/README.mdc) for an overview, then explore specific topics below.
### Architecture & Patterns
- [Application Architecture](.cursor/rules/application-architecture.mdc) - Detailed application structure
@@ -434,7 +462,7 @@ ### Laravel 10 Structure
### Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
-- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
+- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
@@ -543,6 +571,10 @@ ### Pest Tests
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
- Tests should test all of the happy paths, failure paths, and weird paths.
- Tests live in the `tests/Feature` and `tests/Unit` directories.
+- **Unit tests** MUST use mocking and avoid database. They can run outside Docker.
+- **Feature tests** can use database but MUST run inside Docker container.
+- **Design for testability**: Structure code to be testable without database when possible. Use dependency injection and interfaces.
+- **Mock by default**: Prefer `Mockery::mock()` over `Model::factory()->create()` in unit tests.
- Pest tests look and behave like this:
Frequency: {$this->frequency}.
S3 Error: {$this->s3_error}";
+
+ if ($this->s3_storage_url) {
+ $message .= "
s3_storage_url}\">Check S3 Configuration";
+ }
+
+ return new PushoverMessage(
+ title: 'Database backup succeeded locally, S3 upload failed',
+ level: 'warning',
+ message: $message,
+ );
+ }
+
+ public function toSlack(): SlackMessage
+ {
+ $title = 'Database backup succeeded locally, S3 upload failed';
+ $description = "Database backup for {$this->name} (db:{$this->database_name}) was created successfully on local storage, but failed to upload to S3.";
+
+ $description .= "\n\n*Frequency:* {$this->frequency}";
+ $description .= "\n\n*S3 Error:* {$this->s3_error}";
+
+ if ($this->s3_storage_url) {
+ $description .= "\n\n*S3 Storage:* <{$this->s3_storage_url}|Check Configuration>";
+ }
+
+ return new SlackMessage(
+ title: $title,
+ description: $description,
+ color: SlackMessage::warningColor()
+ );
+ }
+}
diff --git a/app/Rules/DockerImageFormat.php b/app/Rules/DockerImageFormat.php
new file mode 100644
index 000000000..a6a78a76c
--- /dev/null
+++ b/app/Rules/DockerImageFormat.php
@@ -0,0 +1,41 @@
+ strrpos($imageString, '/'))) {
- $mainPart = substr($imageString, 0, $lastColon);
- $this->tag = substr($imageString, $lastColon + 1);
+ // Check for @sha256: format first (e.g., nginx@sha256:abc123...)
+ if (preg_match('/^(.+)@sha256:([a-f0-9]{64})$/i', $imageString, $matches)) {
+ $mainPart = $matches[1];
+ $this->tag = $matches[2];
+ $this->isImageHash = true;
} else {
- $mainPart = $imageString;
- $this->tag = 'latest';
+ // Split by : to handle the tag, but be careful with registry ports
+ $lastColon = strrpos($imageString, ':');
+ $hasSlash = str_contains($imageString, '/');
+
+ // If the last colon appears after the last slash, it's a tag
+ // Otherwise it might be a port in the registry URL
+ if ($lastColon !== false && (! $hasSlash || $lastColon > strrpos($imageString, '/'))) {
+ $mainPart = substr($imageString, 0, $lastColon);
+ $this->tag = substr($imageString, $lastColon + 1);
+
+ // Check if the tag is a SHA256 hash
+ $this->isImageHash = $this->isSha256Hash($this->tag);
+ } else {
+ $mainPart = $imageString;
+ $this->tag = 'latest';
+ $this->isImageHash = false;
+ }
}
// Split the main part by / to handle registry and image name
@@ -41,6 +54,37 @@ public function parse(string $imageString): self
return $this;
}
+ /**
+ * Check if the given string is a SHA256 hash
+ */
+ private function isSha256Hash(string $hash): bool
+ {
+ // SHA256 hashes are 64 characters long and contain only hexadecimal characters
+ return preg_match('/^[a-f0-9]{64}$/i', $hash) === 1;
+ }
+
+ /**
+ * Check if the current tag is an image hash
+ */
+ public function isImageHash(): bool
+ {
+ return $this->isImageHash;
+ }
+
+ /**
+ * Get the full image name with hash if present
+ */
+ public function getFullImageNameWithHash(): string
+ {
+ $imageName = $this->getFullImageNameWithoutTag();
+
+ if ($this->isImageHash) {
+ return $imageName.'@sha256:'.$this->tag;
+ }
+
+ return $imageName.':'.$this->tag;
+ }
+
public function getFullImageNameWithoutTag(): string
{
if ($this->registryUrl) {
@@ -73,6 +117,10 @@ public function toString(): string
}
$parts[] = $this->imageName;
+ if ($this->isImageHash) {
+ return implode('/', $parts).'@sha256:'.$this->tag;
+ }
+
return implode('/', $parts).':'.$this->tag;
}
}
diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php
index b568e090c..36243e119 100644
--- a/bootstrap/helpers/constants.php
+++ b/bootstrap/helpers/constants.php
@@ -21,13 +21,23 @@
'bitnami/mariadb',
'bitnami/mongodb',
'bitnami/redis',
+ 'bitnamilegacy/mariadb',
+ 'bitnamilegacy/mongodb',
+ 'bitnamilegacy/redis',
+ 'bitnamisecure/mariadb',
+ 'bitnamisecure/mongodb',
+ 'bitnamisecure/redis',
'mysql',
'bitnami/mysql',
+ 'bitnamilegacy/mysql',
+ 'bitnamisecure/mysql',
'mysql/mysql-server',
'mariadb',
'postgis/postgis',
'postgres',
'bitnami/postgresql',
+ 'bitnamilegacy/postgresql',
+ 'bitnamisecure/postgresql',
'supabase/postgres',
'elestio/postgres',
'mongo',
diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php
index 5dbd46b5e..aa7be3236 100644
--- a/bootstrap/helpers/databases.php
+++ b/bootstrap/helpers/databases.php
@@ -237,12 +237,11 @@ function removeOldBackups($backup): void
{
try {
if ($backup->executions) {
- // If local backup is disabled, mark all executions as having local storage deleted
- if ($backup->disable_local_backup && $backup->save_s3) {
- $backup->executions()
- ->where('local_storage_deleted', false)
- ->update(['local_storage_deleted' => true]);
- } else {
+ // Delete old local backups (only if local backup is NOT disabled)
+ // Note: When disable_local_backup is enabled, each execution already marks its own
+ // local_storage_deleted status at the time of backup, so we don't need to retroactively
+ // update old executions
+ if (! $backup->disable_local_backup) {
$localBackupsToDelete = deleteOldBackupsLocally($backup);
if ($localBackupsToDelete->isNotEmpty()) {
$backup->executions()
@@ -261,18 +260,18 @@ function removeOldBackups($backup): void
}
}
- // Delete executions where both local and S3 storage are marked as deleted
- // or where only S3 is enabled and S3 storage is deleted
- if ($backup->disable_local_backup && $backup->save_s3) {
- $backup->executions()
- ->where('s3_storage_deleted', true)
- ->delete();
- } else {
- $backup->executions()
- ->where('local_storage_deleted', true)
- ->where('s3_storage_deleted', true)
- ->delete();
- }
+ // Delete execution records where all backup copies are gone
+ // Case 1: Both local and S3 backups are deleted
+ $backup->executions()
+ ->where('local_storage_deleted', true)
+ ->where('s3_storage_deleted', true)
+ ->delete();
+
+ // Case 2: Local backup is deleted and S3 was never used (s3_uploaded is null)
+ $backup->executions()
+ ->where('local_storage_deleted', true)
+ ->whereNull('s3_uploaded')
+ ->delete();
} catch (\Exception $e) {
throw $e;
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index af26c97bd..b63c3fc3b 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -1122,9 +1122,10 @@ function escapeDollarSign($value)
/**
* Generate Docker build arguments from environment variables collection
+ * Returns only keys (no values) since values are sourced from environment via export
*
* @param \Illuminate\Support\Collection|array $variables Collection of variables with 'key', 'value', and optionally 'is_multiline'
- * @return \Illuminate\Support\Collection Collection of formatted --build-arg strings
+ * @return \Illuminate\Support\Collection Collection of formatted --build-arg strings (keys only)
*/
function generateDockerBuildArgs($variables): \Illuminate\Support\Collection
{
@@ -1132,21 +1133,9 @@ function generateDockerBuildArgs($variables): \Illuminate\Support\Collection
return $variables->map(function ($var) {
$key = is_array($var) ? data_get($var, 'key') : $var->key;
- $value = is_array($var) ? data_get($var, 'value') : $var->value;
- $isMultiline = is_array($var) ? data_get($var, 'is_multiline', false) : ($var->is_multiline ?? false);
- if ($isMultiline) {
- // For multiline variables, strip surrounding quotes and escape for bash
- $raw_value = trim($value, "'");
- $escaped_value = str_replace(['\\', '"', '$', '`'], ['\\\\', '\\"', '\\$', '\\`'], $raw_value);
-
- return "--build-arg {$key}=\"{$escaped_value}\"";
- }
-
- // For regular variables, use escapeshellarg for security
- $value = escapeshellarg($value);
-
- return "--build-arg {$key}={$value}";
+ // Only return the key - Docker will get the value from the environment
+ return "--build-arg {$key}";
});
}
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index 5bc1d005e..924bad307 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -108,7 +108,63 @@ function connectProxyToNetworks(Server $server)
return $commands->flatten();
}
-function generate_default_proxy_configuration(Server $server)
+function extractCustomProxyCommands(Server $server, string $existing_config): array
+{
+ $custom_commands = [];
+ $proxy_type = $server->proxyType();
+
+ if ($proxy_type !== ProxyTypes::TRAEFIK->value || empty($existing_config)) {
+ return $custom_commands;
+ }
+
+ try {
+ $yaml = Yaml::parse($existing_config);
+ $existing_commands = data_get($yaml, 'services.traefik.command', []);
+
+ if (empty($existing_commands)) {
+ return $custom_commands;
+ }
+
+ // Define default commands that Coolify generates
+ $default_command_prefixes = [
+ '--ping=',
+ '--api.',
+ '--entrypoints.http.address=',
+ '--entrypoints.https.address=',
+ '--entrypoints.http.http.encodequerysemicolons=',
+ '--entryPoints.http.http2.maxConcurrentStreams=',
+ '--entrypoints.https.http.encodequerysemicolons=',
+ '--entryPoints.https.http2.maxConcurrentStreams=',
+ '--entrypoints.https.http3',
+ '--providers.file.',
+ '--certificatesresolvers.',
+ '--providers.docker',
+ '--providers.swarm',
+ '--log.level=',
+ '--accesslog.',
+ ];
+
+ // Extract commands that don't match default prefixes (these are custom)
+ foreach ($existing_commands as $command) {
+ $is_default = false;
+ foreach ($default_command_prefixes as $prefix) {
+ if (str_starts_with($command, $prefix)) {
+ $is_default = true;
+ break;
+ }
+ }
+ if (! $is_default) {
+ $custom_commands[] = $command;
+ }
+ }
+ } catch (\Exception $e) {
+ // If we can't parse the config, return empty array
+ // Silently fail to avoid breaking the proxy regeneration
+ }
+
+ return $custom_commands;
+}
+function generateDefaultProxyConfiguration(Server $server, array $custom_commands = [])
{
$proxy_path = $server->proxyPath();
$proxy_type = $server->proxyType();
@@ -228,6 +284,13 @@ function generate_default_proxy_configuration(Server $server)
$config['services']['traefik']['command'][] = '--providers.docker=true';
$config['services']['traefik']['command'][] = '--providers.docker.exposedbydefault=false';
}
+
+ // Append custom commands (e.g., trustedIPs for Cloudflare)
+ if (! empty($custom_commands)) {
+ foreach ($custom_commands as $custom_command) {
+ $config['services']['traefik']['command'][] = $custom_command;
+ }
+ }
} elseif ($proxy_type === 'CADDY') {
$config = [
'networks' => $array_of_networks->toArray(),
diff --git a/config/constants.php b/config/constants.php
index ddda70d19..01eaa7fa1 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,7 +2,7 @@
return [
'coolify' => [
- 'version' => '4.0.0-beta.434',
+ 'version' => '4.0.0-beta.435',
'helper_version' => '1.0.11',
'realtime_version' => '1.0.10',
'self_hosted' => env('SELF_HOSTED', true),
diff --git a/database/migrations/2025_10_03_154100_update_clickhouse_image.php b/database/migrations/2025_10_03_154100_update_clickhouse_image.php
new file mode 100644
index 000000000..e52bbcc16
--- /dev/null
+++ b/database/migrations/2025_10_03_154100_update_clickhouse_image.php
@@ -0,0 +1,32 @@
+string('image')->default('bitnamilegacy/clickhouse')->change();
+ });
+ // Optionally, update any existing rows with the old default to the new one
+ DB::table('standalone_clickhouses')
+ ->where('image', 'bitnami/clickhouse')
+ ->update(['image' => 'bitnamilegacy/clickhouse']);
+ }
+
+ public function down()
+ {
+ Schema::table('standalone_clickhouses', function (Blueprint $table) {
+ $table->string('image')->default('bitnami/clickhouse')->change();
+ });
+ // Optionally, revert any changed values
+ DB::table('standalone_clickhouses')
+ ->where('image', 'bitnamilegacy/clickhouse')
+ ->update(['image' => 'bitnami/clickhouse']);
+ }
+};
\ No newline at end of file
diff --git a/database/migrations/2025_10_07_120723_add_s3_uploaded_to_scheduled_database_backup_executions_table.php b/database/migrations/2025_10_07_120723_add_s3_uploaded_to_scheduled_database_backup_executions_table.php
new file mode 100644
index 000000000..d80f2621b
--- /dev/null
+++ b/database/migrations/2025_10_07_120723_add_s3_uploaded_to_scheduled_database_backup_executions_table.php
@@ -0,0 +1,28 @@
+boolean('s3_uploaded')->nullable()->after('filename');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('scheduled_database_backup_executions', function (Blueprint $table) {
+ $table->dropColumn('s3_uploaded');
+ });
+ }
+};
diff --git a/other/nightly/versions.json b/other/nightly/versions.json
index b5cf3360a..2e5cc5e84 100644
--- a/other/nightly/versions.json
+++ b/other/nightly/versions.json
@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.433"
+ "version": "4.0.0-beta.435"
},
"nightly": {
- "version": "4.0.0-beta.434"
+ "version": "4.0.0-beta.436"
},
"helper": {
"version": "1.0.11"
diff --git a/public/svgs/ente.png b/public/svgs/ente.png
new file mode 100644
index 000000000..f510a7bf7
Binary files /dev/null and b/public/svgs/ente.png differ
diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php
index 1a3c88f80..103f18316 100644
--- a/resources/views/components/modal-confirmation.blade.php
+++ b/resources/views/components/modal-confirmation.blade.php
@@ -114,7 +114,7 @@
}
}
}"
- @keydown.escape.window="modalOpen = false; resetModal()" :class="{ 'z-40': modalOpen }"
+ @keydown.escape.window="if (modalOpen) { modalOpen = false; resetModal(); }" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto">
@if ($customButton)
@if ($buttonFullWidth)
diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php
index defa7bf6c..4b152dd11 100644
--- a/resources/views/components/navbar.blade.php
+++ b/resources/views/components/navbar.blade.php
@@ -59,25 +59,25 @@
if (this.zoom === '90') {
const style = document.createElement('style');
style.textContent = `
- html {
- font-size: 93.75%;
- }
-
- :root {
- --vh: 1vh;
- }
-
- @media (min-width: 1024px) {
- html {
- font-size: 87.5%;
- }
- }
- `;
+ html {
+ font-size: 93.75%;
+ }
+
+ :root {
+ --vh: 1vh;
+ }
+
+ @media (min-width: 1024px) {
+ html {
+ font-size: 87.5%;
+ }
+ }
+ `;
document.head.appendChild(style);
}
}
}">
-