Merge branch 'next' into feat/shared-dev-view

This commit is contained in:
Andras Bacsai
2025-11-26 09:37:45 +01:00
328 changed files with 31085 additions and 10678 deletions

View File

@@ -1,156 +1,41 @@
# AI Instructions Synchronization Guide
This document explains how AI instructions are organized and synchronized across different AI tools used with Coolify.
**This file has moved!**
## Overview
All AI documentation and synchronization guidelines are now in the `.ai/` directory.
Coolify maintains AI instructions in two parallel systems:
## New Locations
1. **CLAUDE.md** - For Claude Code (claude.ai/code)
2. **.cursor/rules/** - For Cursor IDE and other AI assistants
- **Sync Guide**: [.ai/meta/sync-guide.md](.ai/meta/sync-guide.md)
- **Maintaining Docs**: [.ai/meta/maintaining-docs.md](.ai/meta/maintaining-docs.md)
- **Documentation Hub**: [.ai/README.md](.ai/README.md)
Both systems share core principles but are optimized for their respective workflows.
## Quick Overview
## 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
All AI instructions are now organized in `.ai/` directory:
```
/
├── 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
.ai/
├── README.md # Navigation hub
├── core/ # Project information
── development/ # Dev workflows
├── patterns/ # Code patterns
└── meta/ # Documentation guides
```
## Recent Updates
### For AI Assistants
### 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
- **Claude Code**: Use `CLAUDE.md` (references `.ai/` files)
- **Cursor IDE**: Use `.cursor/rules/coolify-ai-docs.mdc` (references `.ai/` files)
- **All Tools**: Browse `.ai/` directory for detailed documentation
## Maintenance Commands
### Key Principles
```bash
# Check for version inconsistencies
grep -r "Laravel [0-9]" CLAUDE.md .cursor/rules/*.mdc
1. **Single Source of Truth**: Each piece of information exists in ONE file only
2. **Cross-Reference**: Other files reference the source, don't duplicate
3. **Organized by Topic**: Core, Development, Patterns, Meta
4. **Version Consistency**: All versions in `.ai/core/technology-stack.md`
# Check for PHP version consistency
grep -r "PHP [0-9]" CLAUDE.md .cursor/rules/*.mdc
## For More Information
# 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.
See [.ai/meta/sync-guide.md](.ai/meta/sync-guide.md) for complete synchronization guidelines and [.ai/meta/maintaining-docs.md](.ai/meta/maintaining-docs.md) for documentation maintenance instructions.

148
.ai/README.md Normal file
View File

@@ -0,0 +1,148 @@
# Coolify AI Documentation
Welcome to the Coolify AI documentation hub. This directory contains all AI assistant instructions organized by topic for easy navigation and maintenance.
## Quick Start
- **For Claude Code**: Start with [CLAUDE.md in root directory](../CLAUDE.md)
- **For Cursor IDE**: Check `.cursor/rules/coolify-ai-docs.mdc` which references this directory
- **For Other AI Tools**: Continue reading below
## Documentation Structure
### 📚 Core Documentation
Essential project information and architecture:
- **[Technology Stack](core/technology-stack.md)** - All versions, packages, and dependencies (Laravel 12.4.1, PHP 8.4.7, etc.)
- **[Project Overview](core/project-overview.md)** - What Coolify is and how it works
- **[Application Architecture](core/application-architecture.md)** - System design and component relationships
- **[Deployment Architecture](core/deployment-architecture.md)** - How deployments work end-to-end, including Coolify Docker Compose extensions (custom fields)
### 💻 Development
Day-to-day development practices:
- **[Workflow](development/development-workflow.md)** - Development setup, commands, and daily workflows
- **[Testing Patterns](development/testing-patterns.md)** - How to write and run tests (Unit vs Feature, Docker requirements)
- **[Laravel Boost](development/laravel-boost.md)** - Laravel-specific guidelines and best practices
### 🎨 Patterns
Code patterns and best practices by domain:
- **[Database Patterns](patterns/database-patterns.md)** - Eloquent, migrations, relationships
- **[Frontend Patterns](patterns/frontend-patterns.md)** - Livewire, Alpine.js, Tailwind CSS
- **[Security Patterns](patterns/security-patterns.md)** - Authentication, authorization, security best practices
- **[Form Components](patterns/form-components.md)** - Enhanced form components with authorization
- **[API & Routing](patterns/api-and-routing.md)** - API design, routing conventions, REST patterns
### 📖 Meta
Documentation about documentation:
- **[Maintaining Docs](meta/maintaining-docs.md)** - How to update and improve this documentation
- **[Sync Guide](meta/sync-guide.md)** - Keeping documentation synchronized across tools
## Quick Decision Tree
**What do you need help with?**
### Running Commands
→ [development/development-workflow.md](development/development-workflow.md)
- Frontend: `npm run dev`, `npm run build`
- Backend: `php artisan serve`, `php artisan migrate`
- Tests: Docker for Feature tests, mocking for Unit tests
- Code quality: `./vendor/bin/pint`, `./vendor/bin/phpstan`
### Writing Tests
→ [development/testing-patterns.md](development/testing-patterns.md)
- **Unit tests**: No database, use mocking, run outside Docker
- **Feature tests**: Can use database, must run inside Docker
- Command: `docker exec coolify php artisan test`
### Building UI
→ [patterns/frontend-patterns.md](patterns/frontend-patterns.md) or [patterns/form-components.md](patterns/form-components.md)
- Livewire components with server-side state
- Alpine.js for client-side interactivity
- Tailwind CSS 4.1.4 for styling
- Form components with built-in authorization
### Database Work
→ [patterns/database-patterns.md](patterns/database-patterns.md)
- Eloquent ORM patterns
- Migration best practices
- Relationship definitions
- Query optimization
### Security & Auth
→ [patterns/security-patterns.md](patterns/security-patterns.md)
- Team-based access control
- Policy and gate patterns
- Form authorization (canGate, canResource)
- API security
### Laravel-Specific Questions
→ [development/laravel-boost.md](development/laravel-boost.md)
- Laravel 12 patterns
- Livewire 3 best practices
- Pest testing patterns
- Laravel conventions
### Docker Compose Extensions
→ [core/deployment-architecture.md](core/deployment-architecture.md#coolify-docker-compose-extensions)
- Custom fields: `exclude_from_hc`, `content`, `isDirectory`
- How to use inline file content
- Health check exclusion patterns
- Volume creation control
### Version Numbers
→ [core/technology-stack.md](core/technology-stack.md)
- **Single source of truth** for all version numbers
- Don't duplicate versions elsewhere, reference this file
## Navigation Tips
1. **Start broad**: Begin with project-overview or ../CLAUDE.md
2. **Get specific**: Navigate to topic-specific files for details
3. **Cross-reference**: Files link to related topics
4. **Single source**: Version numbers and critical data exist in ONE place only
## For AI Assistants
### Important Patterns to Follow
**Testing Commands:**
- Unit tests: `./vendor/bin/pest tests/Unit` (no database, outside Docker)
- Feature tests: `docker exec coolify php artisan test` (requires database, inside Docker)
- NEVER run Feature tests outside Docker - they will fail with database connection errors
**Version Numbers:**
- Always use exact versions from [technology-stack.md](core/technology-stack.md)
- Laravel 12.4.1, PHP 8.4.7, Tailwind 4.1.4
- Don't use "v12" or "8.4" - be precise
**Form Authorization:**
- ALWAYS include `canGate` and `:canResource` on form components
- See [form-components.md](patterns/form-components.md) for examples
**Livewire Components:**
- MUST have exactly ONE root element
- See [frontend-patterns.md](patterns/frontend-patterns.md) for details
**Code Style:**
- Run `./vendor/bin/pint` before finalizing changes
- Follow PSR-12 standards
- Use PHP 8.4 features (constructor promotion, typed properties, etc.)
## Contributing
When updating documentation:
1. Read [meta/maintaining-docs.md](meta/maintaining-docs.md)
2. Follow the single source of truth principle
3. Update cross-references when moving content
4. Test all links work
5. Run Pint on markdown files if applicable
## Questions?
- **Claude Code users**: Check [../CLAUDE.md](../CLAUDE.md) first
- **Cursor IDE users**: Check `.cursor/rules/coolify-ai-docs.mdc`
- **Documentation issues**: See [meta/maintaining-docs.md](meta/maintaining-docs.md)
- **Sync issues**: See [meta/sync-guide.md](meta/sync-guide.md)

View File

@@ -0,0 +1,604 @@
# Coolify Application Architecture
## Laravel Project Structure
### **Core Application Directory** ([app/](mdc:app))
```
app/
├── Actions/ # Business logic actions (Action pattern)
├── Console/ # Artisan commands
├── Contracts/ # Interface definitions
├── Data/ # Data Transfer Objects (Spatie Laravel Data)
├── Enums/ # Enumeration classes
├── Events/ # Event classes
├── Exceptions/ # Custom exception classes
├── Helpers/ # Utility helper classes
├── Http/ # HTTP layer (Controllers, Middleware, Requests)
├── Jobs/ # Background job classes
├── Listeners/ # Event listeners
├── Livewire/ # Livewire components (Frontend)
├── Models/ # Eloquent models (Domain entities)
├── Notifications/ # Notification classes
├── Policies/ # Authorization policies
├── Providers/ # Service providers
├── Repositories/ # Repository pattern implementations
├── Services/ # Service layer classes
├── Traits/ # Reusable trait classes
└── View/ # View composers and creators
```
## Core Domain Models
### **Infrastructure Management**
#### **[Server.php](mdc:app/Models/Server.php)** (46KB, 1343 lines)
- **Purpose**: Physical/virtual server management
- **Key Relationships**:
- `hasMany(Application::class)` - Deployed applications
- `hasMany(StandalonePostgresql::class)` - Database instances
- `belongsTo(Team::class)` - Team ownership
- **Key Features**:
- SSH connection management
- Resource monitoring
- Proxy configuration (Traefik/Caddy)
- Docker daemon interaction
#### **[Application.php](mdc:app/Models/Application.php)** (74KB, 1734 lines)
- **Purpose**: Application deployment and management
- **Key Relationships**:
- `belongsTo(Server::class)` - Deployment target
- `belongsTo(Environment::class)` - Environment context
- `hasMany(ApplicationDeploymentQueue::class)` - Deployment history
- **Key Features**:
- Git repository integration
- Docker build and deployment
- Environment variable management
- SSL certificate handling
#### **[Service.php](mdc:app/Models/Service.php)** (58KB, 1325 lines)
- **Purpose**: Multi-container service orchestration
- **Key Relationships**:
- `hasMany(ServiceApplication::class)` - Service components
- `hasMany(ServiceDatabase::class)` - Service databases
- `belongsTo(Environment::class)` - Environment context
- **Key Features**:
- Docker Compose generation
- Service dependency management
- Health check configuration
### **Team & Project Organization**
#### **[Team.php](mdc:app/Models/Team.php)** (8.9KB, 308 lines)
- **Purpose**: Multi-tenant team management
- **Key Relationships**:
- `hasMany(User::class)` - Team members
- `hasMany(Project::class)` - Team projects
- `hasMany(Server::class)` - Team servers
- **Key Features**:
- Resource limits and quotas
- Team-based access control
- Subscription management
#### **[Project.php](mdc:app/Models/Project.php)** (4.3KB, 156 lines)
- **Purpose**: Project organization and grouping
- **Key Relationships**:
- `hasMany(Environment::class)` - Project environments
- `belongsTo(Team::class)` - Team ownership
- **Key Features**:
- Environment isolation
- Resource organization
#### **[Environment.php](mdc:app/Models/Environment.php)**
- **Purpose**: Environment-specific configuration
- **Key Relationships**:
- `hasMany(Application::class)` - Environment applications
- `hasMany(Service::class)` - Environment services
- `belongsTo(Project::class)` - Project context
### **Database Management Models**
#### **Standalone Database Models**
- **[StandalonePostgresql.php](mdc:app/Models/StandalonePostgresql.php)** (11KB, 351 lines)
- **[StandaloneMysql.php](mdc:app/Models/StandaloneMysql.php)** (11KB, 351 lines)
- **[StandaloneMariadb.php](mdc:app/Models/StandaloneMariadb.php)** (10KB, 337 lines)
- **[StandaloneMongodb.php](mdc:app/Models/StandaloneMongodb.php)** (12KB, 370 lines)
- **[StandaloneRedis.php](mdc:app/Models/StandaloneRedis.php)** (12KB, 394 lines)
- **[StandaloneKeydb.php](mdc:app/Models/StandaloneKeydb.php)** (11KB, 347 lines)
- **[StandaloneDragonfly.php](mdc:app/Models/StandaloneDragonfly.php)** (11KB, 347 lines)
- **[StandaloneClickhouse.php](mdc:app/Models/StandaloneClickhouse.php)** (10KB, 336 lines)
**Common Features**:
- Database configuration management
- Backup scheduling and execution
- Connection string generation
- Health monitoring
### **Configuration & Settings**
#### **[EnvironmentVariable.php](mdc:app/Models/EnvironmentVariable.php)** (7.6KB, 219 lines)
- **Purpose**: Application environment variable management
- **Key Features**:
- Encrypted value storage
- Build-time vs runtime variables
- Shared variable inheritance
#### **[InstanceSettings.php](mdc:app/Models/InstanceSettings.php)** (3.2KB, 124 lines)
- **Purpose**: Global Coolify instance configuration
- **Key Features**:
- FQDN and port configuration
- Auto-update settings
- Security configurations
## Architectural Patterns
### **Action Pattern** ([app/Actions/](mdc:app/Actions))
Using [lorisleiva/laravel-actions](mdc:composer.json) for business logic encapsulation:
```php
// Example Action structure
class DeployApplication extends Action
{
public function handle(Application $application): void
{
// Business logic for deployment
}
public function asJob(Application $application): void
{
// Queue job implementation
}
}
```
**Key Action Categories**:
- **Application/**: Deployment and management actions
- **Database/**: Database operations
- **Server/**: Server management actions
- **Service/**: Service orchestration actions
### **Repository Pattern** ([app/Repositories/](mdc:app/Repositories))
Data access abstraction layer:
- Encapsulates database queries
- Provides testable data layer
- Abstracts complex query logic
### **Service Layer** ([app/Services/](mdc:app/Services))
Business logic services:
- External API integrations
- Complex business operations
- Cross-cutting concerns
## Data Flow Architecture
### **Request Lifecycle**
1. **HTTP Request** → [routes/web.php](mdc:routes/web.php)
2. **Middleware** → Authentication, authorization
3. **Livewire Component** → [app/Livewire/](mdc:app/Livewire)
4. **Action/Service** → Business logic execution
5. **Model/Repository** → Data persistence
6. **Response** → Livewire reactive update
### **Background Processing**
1. **Job Dispatch** → Queue system (Redis)
2. **Job Processing** → [app/Jobs/](mdc:app/Jobs)
3. **Action Execution** → Business logic
4. **Event Broadcasting** → Real-time updates
5. **Notification** → User feedback
## Security Architecture
### **Multi-Tenant Isolation**
```php
// Team-based query scoping
class Application extends Model
{
public function scopeOwnedByCurrentTeam($query)
{
return $query->whereHas('environment.project.team', function ($q) {
$q->where('id', currentTeam()->id);
});
}
}
```
### **Authorization Layers**
1. **Team Membership** → User belongs to team
2. **Resource Ownership** → Resource belongs to team
3. **Policy Authorization** → [app/Policies/](mdc:app/Policies)
4. **Environment Isolation** → Project/environment boundaries
### **Data Protection**
- **Environment Variables**: Encrypted at rest
- **SSH Keys**: Secure storage and transmission
- **API Tokens**: Sanctum-based authentication
- **Audit Logging**: [spatie/laravel-activitylog](mdc:composer.json)
## Configuration Hierarchy
### **Global Configuration**
- **[InstanceSettings](mdc:app/Models/InstanceSettings.php)**: System-wide settings
- **[config/](mdc:config)**: Laravel configuration files
### **Team Configuration**
- **[Team](mdc:app/Models/Team.php)**: Team-specific settings
- **[ServerSetting](mdc:app/Models/ServerSetting.php)**: Server configurations
### **Project Configuration**
- **[ProjectSetting](mdc:app/Models/ProjectSetting.php)**: Project settings
- **[Environment](mdc:app/Models/Environment.php)**: Environment variables
### **Application Configuration**
- **[ApplicationSetting](mdc:app/Models/ApplicationSetting.php)**: App-specific settings
- **[EnvironmentVariable](mdc:app/Models/EnvironmentVariable.php)**: Runtime configuration
## Event-Driven Architecture
### **Event Broadcasting** ([app/Events/](mdc:app/Events))
Real-time updates using Laravel Echo and WebSockets:
```php
// Example event structure
class ApplicationDeploymentStarted implements ShouldBroadcast
{
public function broadcastOn(): array
{
return [
new PrivateChannel("team.{$this->application->team->id}"),
];
}
}
```
### **Event Listeners** ([app/Listeners/](mdc:app/Listeners))
- Deployment status updates
- Resource monitoring alerts
- Notification dispatching
- Audit log creation
## Database Design Patterns
### **Polymorphic Relationships**
```php
// Environment variables can belong to multiple resource types
class EnvironmentVariable extends Model
{
public function resource(): MorphTo
{
return $this->morphTo();
}
}
```
### **Team-Based Soft Scoping**
All major resources include team-based query scoping:
```php
// Automatic team filtering
$applications = Application::ownedByCurrentTeam()->get();
$servers = Server::ownedByCurrentTeam()->get();
```
### **Configuration Inheritance**
Environment variables cascade from:
1. **Shared Variables** → Team-wide defaults
2. **Project Variables** → Project-specific overrides
3. **Application Variables** → Application-specific values
## Integration Patterns
### **Git Provider Integration**
Abstracted git operations supporting:
- **GitHub**: [app/Models/GithubApp.php](mdc:app/Models/GithubApp.php)
- **GitLab**: [app/Models/GitlabApp.php](mdc:app/Models/GitlabApp.php)
- **Bitbucket**: Webhook integration
- **Gitea**: Self-hosted Git support
### **Docker Integration**
- **Container Management**: Direct Docker API communication
- **Image Building**: Dockerfile and Buildpack support
- **Network Management**: Custom Docker networks
- **Volume Management**: Persistent storage handling
### **SSH Communication**
- **[phpseclib/phpseclib](mdc:composer.json)**: Secure SSH connections
- **Multiplexing**: Connection pooling for efficiency
- **Key Management**: [PrivateKey](mdc:app/Models/PrivateKey.php) model
## Testing Architecture
### **Test Structure** ([tests/](mdc:tests))
```
tests/
├── Feature/ # Integration tests
├── Unit/ # Unit tests
├── Browser/ # Dusk browser tests
├── Traits/ # Test helper traits
├── Pest.php # Pest configuration
└── TestCase.php # Base test case
```
### **Testing Patterns**
- **Feature Tests**: Full request lifecycle testing
- **Unit Tests**: Individual class/method testing
- **Browser Tests**: End-to-end user workflows
- **Database Testing**: Factories and seeders
## Performance Considerations
### **Query Optimization**
- **Eager Loading**: Prevent N+1 queries
- **Query Scoping**: Team-based filtering
- **Database Indexing**: Optimized for common queries
### **Caching Strategy**
- **Redis**: Session and cache storage
- **Model Caching**: Frequently accessed data
- **Query Caching**: Expensive query results
### **Background Processing**
- **Queue Workers**: Horizon-managed job processing
- **Job Batching**: Related job grouping
- **Failed Job Handling**: Automatic retry logic
## Container Status Monitoring System
### **Overview**
Container health status is monitored and updated through **multiple independent paths**. When modifying status logic, **ALL paths must be updated** to ensure consistency.
### **Critical Implementation Locations**
#### **1. SSH-Based Status Updates (Scheduled)**
**File**: [app/Actions/Docker/GetContainersStatus.php](mdc:app/Actions/Docker/GetContainersStatus.php)
**Method**: `aggregateApplicationStatus()` (lines 487-540)
**Trigger**: Scheduled job or manual refresh
**Frequency**: Every minute (via `ServerCheckJob`)
**Status Aggregation Logic**:
```php
// Tracks multiple status flags
$hasRunning = false;
$hasRestarting = false;
$hasUnhealthy = false;
$hasUnknown = false; // ⚠️ CRITICAL: Must track unknown
$hasExited = false;
// ... more states
// Priority: restarting > degraded > running (unhealthy > unknown > healthy)
if ($hasRunning) {
if ($hasUnhealthy) return 'running (unhealthy)';
elseif ($hasUnknown) return 'running (unknown)';
else return 'running (healthy)';
}
```
#### **2. Sentinel-Based Status Updates (Real-time)**
**File**: [app/Jobs/PushServerUpdateJob.php](mdc:app/Jobs/PushServerUpdateJob.php)
**Method**: `aggregateMultiContainerStatuses()` (lines 269-298)
**Trigger**: Sentinel push updates from remote servers
**Frequency**: Every ~30 seconds (real-time)
**Status Aggregation Logic**:
```php
// ⚠️ MUST match GetContainersStatus logic
$hasRunning = false;
$hasUnhealthy = false;
$hasUnknown = false; // ⚠️ CRITICAL: Added to fix bug
foreach ($relevantStatuses as $status) {
if (str($status)->contains('running')) {
$hasRunning = true;
if (str($status)->contains('unhealthy')) $hasUnhealthy = true;
if (str($status)->contains('unknown')) $hasUnknown = true; // ⚠️ CRITICAL
}
}
// Priority: unhealthy > unknown > healthy
if ($hasRunning) {
if ($hasUnhealthy) $aggregatedStatus = 'running (unhealthy)';
elseif ($hasUnknown) $aggregatedStatus = 'running (unknown)';
else $aggregatedStatus = 'running (healthy)';
}
```
#### **3. Multi-Server Status Aggregation**
**File**: [app/Actions/Shared/ComplexStatusCheck.php](mdc:app/Actions/Shared/ComplexStatusCheck.php)
**Method**: `resource()` (lines 48-210)
**Purpose**: Aggregates status across multiple servers for applications
**Used by**: Applications with multiple destinations
**Key Features**:
- Aggregates statuses from main + additional servers
- Handles excluded containers (`:excluded` suffix)
- Calculates overall application health from all containers
**Status Format with Excluded Containers**:
```php
// When all containers excluded from health checks:
return 'running:unhealthy:excluded'; // Container running but unhealthy, monitoring disabled
return 'running:unknown:excluded'; // Container running, health unknown, monitoring disabled
return 'running:healthy:excluded'; // Container running and healthy, monitoring disabled
return 'degraded:excluded'; // Some containers down, monitoring disabled
return 'exited:excluded'; // All containers stopped, monitoring disabled
```
#### **4. Service-Level Status Aggregation**
**File**: [app/Models/Service.php](mdc:app/Models/Service.php)
**Method**: `complexStatus()` (lines 176-288)
**Purpose**: Aggregates status for multi-container services
**Used by**: Docker Compose services
**Status Calculation**:
```php
// Aggregates status from all service applications and databases
// Handles excluded containers separately
// Returns status with :excluded suffix when all containers excluded
if (!$hasNonExcluded && $complexStatus === null && $complexHealth === null) {
// All services excluded - calculate from excluded containers
return "{$excludedStatus}:excluded";
}
```
### **Status Flow Diagram**
```
┌─────────────────────────────────────────────────────────────┐
│ Container Status Sources │
└─────────────────────────────────────────────────────────────┘
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌──────────────┐
│ SSH-Based │ │ Sentinel-Based │ │ Multi-Server │
│ (Scheduled) │ │ (Real-time) │ │ Aggregation │
├───────────────┤ ├─────────────────┤ ├──────────────┤
│ ServerCheck │ │ PushServerUp- │ │ ComplexStatus│
│ Job │ │ dateJob │ │ Check │
│ │ │ │ │ │
│ Every ~1min │ │ Every ~30sec │ │ On demand │
└───────┬───────┘ └────────┬────────┘ └──────┬───────┘
│ │ │
└────────────────────┼────────────────────┘
┌───────────────────────┐
│ Application/Service │
│ Status Property │
└───────────────────────┘
┌───────────────────────┐
│ UI Display (Livewire) │
└───────────────────────┘
```
### **Status Priority System**
All status aggregation locations **MUST** follow the same priority:
**For Running Containers**:
1. **unhealthy** - Container has failing health checks
2. **unknown** - Container health status cannot be determined
3. **healthy** - Container is healthy
**For Non-Running States**:
1. **restarting**`degraded (unhealthy)`
2. **running + exited**`degraded (unhealthy)`
3. **dead/removing**`degraded (unhealthy)`
4. **paused**`paused`
5. **created/starting**`starting`
6. **exited**`exited (unhealthy)`
### **Excluded Containers**
When containers have `exclude_from_hc: true` flag or `restart: no`:
**Behavior**:
- Status is still calculated from container state
- `:excluded` suffix is appended to indicate monitoring disabled
- UI shows "(Monitoring Disabled)" badge
- Action buttons respect the actual container state
**Format**: `{actual-status}:excluded`
**Examples**: `running:unknown:excluded`, `degraded:excluded`, `exited:excluded`
**All-Excluded Scenario**:
When ALL containers are excluded from health checks:
- All three status update paths (PushServerUpdateJob, GetContainersStatus, ComplexStatusCheck) **MUST** calculate status from excluded containers
- Status is returned with `:excluded` suffix (e.g., `running:healthy:excluded`)
- **NEVER** skip status updates - always calculate from excluded containers
- This ensures consistent status regardless of which update mechanism runs
- Shared logic is in `app/Traits/CalculatesExcludedStatus.php`
### **Important Notes for Developers**
**Container Status Aggregation Service**:
The container status aggregation logic is centralized in `App\Services\ContainerStatusAggregator`.
**Status Format Standard**:
- **Backend/Storage**: Colon format (`running:healthy`, `degraded:unhealthy`)
- **UI/Display**: Transform to human format (`Running (Healthy)`, `Degraded (Unhealthy)`)
1. **Using the ContainerStatusAggregator Service**:
- Import `App\Services\ContainerStatusAggregator` in any class needing status aggregation
- Two methods available:
- `aggregateFromStrings(Collection $statusStrings, int $maxRestartCount = 0)` - For pre-formatted status strings
- `aggregateFromContainers(Collection $containers, int $maxRestartCount = 0)` - For raw Docker container objects
- Returns colon format: `running:healthy`, `degraded:unhealthy`, etc.
- Automatically handles crash loop detection via `$maxRestartCount` parameter
2. **State Machine Priority** (handled by service):
- Restarting → `degraded:unhealthy` (highest priority)
- Crash loop (exited with restarts) → `degraded:unhealthy`
- Mixed state (running + exited) → `degraded:unhealthy`
- Running → `running:unhealthy` / `running:unknown` / `running:healthy`
- Dead/Removing → `degraded:unhealthy`
- Paused → `paused:unknown`
- Starting/Created → `starting:unknown`
- Exited → `exited:unhealthy` (lowest priority)
3. **Test both update paths**:
- Run unit tests: `./vendor/bin/pest tests/Unit/ContainerStatusAggregatorTest.php`
- Run integration tests: `./vendor/bin/pest tests/Unit/`
- Test SSH updates (manual refresh)
- Test Sentinel updates (wait 30 seconds)
4. **Handle excluded containers**:
- All containers excluded (`exclude_from_hc: true`) - Use `CalculatesExcludedStatus` trait
- Mixed excluded/non-excluded containers - Filter then use `ContainerStatusAggregator`
- Containers with `restart: no` - Treated same as `exclude_from_hc: true`
5. **Use shared trait for excluded containers**:
- Import `App\Traits\CalculatesExcludedStatus` in status calculation classes
- Use `getExcludedContainersFromDockerCompose()` to parse exclusions
- Use `calculateExcludedStatus()` for full Docker inspect objects (ComplexStatusCheck)
- Use `calculateExcludedStatusFromStrings()` for status strings (PushServerUpdateJob, GetContainersStatus)
### **Related Tests**
- **[tests/Unit/ContainerStatusAggregatorTest.php](mdc:tests/Unit/ContainerStatusAggregatorTest.php)**: Core state machine logic (42 comprehensive tests)
- **[tests/Unit/ContainerHealthStatusTest.php](mdc:tests/Unit/ContainerHealthStatusTest.php)**: Health status aggregation integration
- **[tests/Unit/PushServerUpdateJobStatusAggregationTest.php](mdc:tests/Unit/PushServerUpdateJobStatusAggregationTest.php)**: Sentinel update logic
- **[tests/Unit/ExcludeFromHealthCheckTest.php](mdc:tests/Unit/ExcludeFromHealthCheckTest.php)**: Excluded container handling
### **Common Bugs to Avoid**
**Prevented by ContainerStatusAggregator Service**:
- ❌ **Old Bug**: Forgetting to track `$hasUnknown` flag → ✅ Now centralized in service
- ❌ **Old Bug**: Inconsistent priority across paths → ✅ Single source of truth
- ❌ **Old Bug**: Forgetting to update all 4 locations → ✅ Only one location to update
**Still Relevant**:
**Bug**: Forgetting to filter excluded containers before aggregation
**Fix**: Always use `CalculatesExcludedStatus` trait to filter before calling `ContainerStatusAggregator`
**Bug**: Not passing `$maxRestartCount` for crash loop detection
**Fix**: Calculate max restart count from containers and pass to `aggregateFromStrings()`/`aggregateFromContainers()`
**Bug**: Not handling excluded containers with `:excluded` suffix
**Fix**: Check for `:excluded` suffix in UI logic and button visibility

View File

@@ -1,8 +1,3 @@
---
description: Docker orchestration, deployment workflows, and containerization patterns
globs: app/Jobs/*.php, app/Actions/Application/*.php, app/Actions/Server/*.php, docker/*.*, *.yml, *.yaml
alwaysApply: false
---
# Coolify Deployment Architecture
## Deployment Philosophy
@@ -308,3 +303,286 @@ Routes: [routes/api.php](mdc:routes/api.php)
- **External database** connections
- **Third-party monitoring** tools
- **Custom notification** channels
---
## Coolify Docker Compose Extensions
Coolify extends standard Docker Compose with custom fields (often called "magic fields") that provide Coolify-specific functionality. These extensions are processed during deployment and stripped before sending the final compose file to Docker, maintaining full compatibility with Docker's compose specification.
### Overview
**Why Custom Fields?**
- Enable Coolify-specific features without breaking Docker Compose compatibility
- Simplify configuration by embedding content directly in compose files
- Allow fine-grained control over health check monitoring
- Reduce external file dependencies
**Processing Flow:**
1. User defines compose file with custom fields
2. Coolify parses and processes custom fields (creates files, stores settings)
3. Custom fields are stripped from final compose sent to Docker
4. Docker receives standard, valid compose file
### Service-Level Extensions
#### `exclude_from_hc`
**Type:** Boolean
**Default:** `false`
**Purpose:** Exclude specific services from health check monitoring while still showing their status
**Example Usage:**
```yaml
services:
watchtower:
image: containrrr/watchtower
exclude_from_hc: true # Don't monitor this service's health
backup:
image: postgres:16
exclude_from_hc: true # Backup containers don't need monitoring
restart: always
```
**Behavior:**
- Container status is still calculated from Docker state (running, exited, etc.)
- Status displays with `:excluded` suffix (e.g., `running:healthy:excluded`)
- UI shows "Monitoring Disabled" indicator
- Functionally equivalent to `restart: no` for health check purposes
- See [Container Status with All Excluded](application-architecture.md#container-status-when-all-containers-excluded) for detailed status handling
**Use Cases:**
- Sidecar containers (watchtower, log collectors)
- Backup/maintenance containers
- One-time initialization containers
- Containers that intentionally restart frequently
**Implementation:**
- Parsed: `bootstrap/helpers/parsers.php`
- Status logic: `app/Traits/CalculatesExcludedStatus.php`
- Validation: `tests/Unit/ExcludeFromHealthCheckTest.php`
### Volume-Level Extensions
Volume extensions only work with **long syntax** (array/object format), not short syntax (string format).
#### `content`
**Type:** String (supports multiline with `|` or `>`)
**Purpose:** Embed file content directly in compose file for automatic creation during deployment
**Example Usage:**
```yaml
services:
app:
image: node:20
volumes:
# Inline entrypoint script
- type: bind
source: ./entrypoint.sh
target: /app/entrypoint.sh
content: |
#!/bin/sh
set -e
echo "Starting application..."
npm run migrate
exec "$@"
# Configuration file with environment variables
- type: bind
source: ./config.xml
target: /etc/app/config.xml
content: |
<?xml version='1.0' encoding='UTF-8'?>
<config>
<database>
<host>${DB_HOST}</host>
<port>${DB_PORT}</port>
</database>
</config>
```
**Behavior:**
- Content is written to the host at `source` path before container starts
- File is created with mode `644` (readable by all, writable by owner)
- Environment variables in content are interpolated at deployment time
- Content is stored in `LocalFileVolume` model (encrypted at rest)
- Original `docker_compose_raw` retains content for editing
**Use Cases:**
- Entrypoint scripts
- Configuration files
- Environment-specific settings
- Small initialization scripts
- Templates that require dynamic content
**Limitations:**
- Not suitable for large files (use git repo or external storage instead)
- Binary files not supported
- Changes require redeployment
**Real-World Examples:**
- `templates/compose/traccar.yaml` - XML configuration file
- `templates/compose/supabase.yaml` - Multiple config files
- `templates/compose/chaskiq.yaml` - Entrypoint script
**Implementation:**
- Parsed: `bootstrap/helpers/parsers.php` (line 717)
- Storage: `app/Models/LocalFileVolume.php`
- Validation: `tests/Unit/StripCoolifyCustomFieldsTest.php`
#### `is_directory` / `isDirectory`
**Type:** Boolean
**Default:** `true` (if neither `content` nor explicit flag provided)
**Purpose:** Indicate whether bind mount source should be created as directory or file
**Example Usage:**
```yaml
services:
app:
volumes:
# Explicit file
- type: bind
source: ./config.json
target: /app/config.json
is_directory: false # Create as file
# Explicit directory
- type: bind
source: ./logs
target: /var/log/app
is_directory: true # Create as directory
# Auto-detected as file (has content)
- type: bind
source: ./script.sh
target: /entrypoint.sh
content: |
#!/bin/sh
echo "Hello"
# is_directory: false implied by content presence
```
**Behavior:**
- If `is_directory: true` → Creates directory with `mkdir -p`
- If `is_directory: false` → Creates empty file with `touch`
- If `content` provided → Implies `is_directory: false`
- If neither specified → Defaults to `true` (directory)
**Naming Conventions:**
- `is_directory` (snake_case) - **Preferred**, consistent with PHP/Laravel conventions
- `isDirectory` (camelCase) - **Legacy support**, both work identically
**Use Cases:**
- Disambiguating files vs directories when no content provided
- Ensuring correct bind mount type for Docker
- Pre-creating mount points before container starts
**Implementation:**
- Parsed: `bootstrap/helpers/parsers.php` (line 718)
- Storage: `app/Models/LocalFileVolume.php` (`is_directory` column)
- Validation: `tests/Unit/StripCoolifyCustomFieldsTest.php`
### Custom Field Stripping
**Function:** `stripCoolifyCustomFields()` in `bootstrap/helpers/docker.php`
All custom fields are removed before the compose file is sent to Docker. This happens in two contexts:
**1. Validation (User-Triggered)**
```php
// In validateComposeFile() - Edit Docker Compose modal
$yaml_compose = Yaml::parse($compose);
$yaml_compose = stripCoolifyCustomFields($yaml_compose); // Strip custom fields
// Send to docker compose config for validation
```
**2. Deployment (Automatic)**
```php
// In Service::parse() - During deployment
$docker_compose = parseCompose($docker_compose_raw);
// Custom fields are processed and then stripped
// Final compose sent to Docker has no custom fields
```
**What Gets Stripped:**
- Service-level: `exclude_from_hc`
- Volume-level: `content`, `isDirectory`, `is_directory`
**What's Preserved:**
- All standard Docker Compose fields
- Environment variables
- Standard volume definitions (after custom fields removed)
### Important Notes
#### Long vs Short Volume Syntax
**✅ Long Syntax (Works with Custom Fields):**
```yaml
volumes:
- type: bind
source: ./data
target: /app/data
content: "Hello" # ✅ Custom fields work here
```
**❌ Short Syntax (Custom Fields Ignored):**
```yaml
volumes:
- "./data:/app/data" # ❌ Cannot add custom fields to strings
```
#### Docker Compose Compatibility
Custom fields are **Coolify-specific** and won't work with standalone `docker compose` CLI:
```bash
# ❌ Won't work - Docker doesn't recognize custom fields
docker compose -f compose.yaml up
# ✅ Works - Use Coolify's deployment (strips custom fields first)
# Deploy through Coolify UI or API
```
#### Editing Custom Fields
When editing in "Edit Docker Compose" modal:
- Custom fields are preserved in the editor
- "Validate" button strips them temporarily for Docker validation
- "Save" button preserves them in `docker_compose_raw`
- They're processed again on next deployment
### Template Examples
See these templates for real-world usage:
**Service Exclusions:**
- `templates/compose/budibase.yaml` - Excludes watchtower from monitoring
- `templates/compose/pgbackweb.yaml` - Excludes backup service
- `templates/compose/elasticsearch-with-kibana.yaml` - Excludes elasticsearch
**Inline Content:**
- `templates/compose/traccar.yaml` - XML configuration (multiline)
- `templates/compose/supabase.yaml` - Multiple config files
- `templates/compose/searxng.yaml` - Settings file
- `templates/compose/invoice-ninja.yaml` - Nginx config
**Directory Flags:**
- `templates/compose/paperless.yaml` - Explicit directory creation
### Testing
**Unit Tests:**
- `tests/Unit/StripCoolifyCustomFieldsTest.php` - Custom field stripping logic
- `tests/Unit/ExcludeFromHealthCheckTest.php` - Health check exclusion behavior
- `tests/Unit/ContainerStatusAggregatorTest.php` - Status aggregation with exclusions
**Test Coverage:**
- ✅ All custom fields (exclude_from_hc, content, isDirectory, is_directory)
- ✅ Multiline content (YAML `|` syntax)
- ✅ Short vs long volume syntax
- ✅ Field stripping without data loss
- ✅ Standard Docker Compose field preservation

View File

@@ -1,8 +1,3 @@
---
description: High-level project mission, core concepts, and architectural overview
globs: README.md, CONTRIBUTING.md, CHANGELOG.md, *.md
alwaysApply: false
---
# Coolify Project Overview
## What is Coolify?

View File

@@ -1,23 +1,19 @@
---
description: Complete technology stack, dependencies, and infrastructure components
globs: composer.json, package.json, docker-compose*.yml, config/*.php
alwaysApply: false
---
# Coolify Technology Stack
Complete technology stack, dependencies, and infrastructure components.
## Backend Framework
### **Laravel 12.4.1** (PHP Framework)
- **Location**: [composer.json](mdc:composer.json)
- **Purpose**: Core application framework
- **Key Features**:
- **Key Features**:
- Eloquent ORM for database interactions
- Artisan CLI for development tasks
- Queue system for background jobs
- Event-driven architecture
### **PHP 8.4**
- **Requirement**: `^8.4` in [composer.json](mdc:composer.json)
### **PHP 8.4.7**
- **Requirement**: `^8.4` in composer.json
- **Features Used**:
- Typed properties and return types
- Attributes for validation and configuration
@@ -28,11 +24,11 @@ alwaysApply: false
### **Livewire 3.5.20** (Primary Frontend Framework)
- **Purpose**: Server-side rendering with reactive components
- **Location**: [app/Livewire/](mdc:app/Livewire/)
- **Location**: `app/Livewire/`
- **Key Components**:
- [Dashboard.php](mdc:app/Livewire/Dashboard.php) - Main interface
- [ActivityMonitor.php](mdc:app/Livewire/ActivityMonitor.php) - Real-time monitoring
- [MonacoEditor.php](mdc:app/Livewire/MonacoEditor.php) - Code editor
- Dashboard - Main interface
- ActivityMonitor - Real-time monitoring
- MonacoEditor - Code editor
### **Alpine.js** (Client-Side Interactivity)
- **Purpose**: Lightweight JavaScript for DOM manipulation
@@ -40,8 +36,7 @@ alwaysApply: false
- **Usage**: Declarative directives in Blade templates
### **Tailwind CSS 4.1.4** (Styling Framework)
- **Location**: [package.json](mdc:package.json)
- **Configuration**: [postcss.config.cjs](mdc:postcss.config.cjs)
- **Configuration**: `postcss.config.cjs`
- **Extensions**:
- `@tailwindcss/forms` - Form styling
- `@tailwindcss/typography` - Content typography
@@ -57,24 +52,24 @@ alwaysApply: false
### **PostgreSQL 15** (Primary Database)
- **Purpose**: Main application data storage
- **Features**: JSONB support, advanced indexing
- **Models**: [app/Models/](mdc:app/Models/)
- **Models**: `app/Models/`
### **Redis 7** (Caching & Real-time)
- **Purpose**:
- **Purpose**:
- Session storage
- Queue backend
- Real-time data caching
- WebSocket session management
### **Supported Databases** (For User Applications)
- **PostgreSQL**: [StandalonePostgresql.php](mdc:app/Models/StandalonePostgresql.php)
- **MySQL**: [StandaloneMysql.php](mdc:app/Models/StandaloneMysql.php)
- **MariaDB**: [StandaloneMariadb.php](mdc:app/Models/StandaloneMariadb.php)
- **MongoDB**: [StandaloneMongodb.php](mdc:app/Models/StandaloneMongodb.php)
- **Redis**: [StandaloneRedis.php](mdc:app/Models/StandaloneRedis.php)
- **KeyDB**: [StandaloneKeydb.php](mdc:app/Models/StandaloneKeydb.php)
- **Dragonfly**: [StandaloneDragonfly.php](mdc:app/Models/StandaloneDragonfly.php)
- **ClickHouse**: [StandaloneClickhouse.php](mdc:app/Models/StandaloneClickhouse.php)
- **PostgreSQL**: StandalonePostgresql
- **MySQL**: StandaloneMysql
- **MariaDB**: StandaloneMariadb
- **MongoDB**: StandaloneMongodb
- **Redis**: StandaloneRedis
- **KeyDB**: StandaloneKeydb
- **Dragonfly**: StandaloneDragonfly
- **ClickHouse**: StandaloneClickhouse
## Authentication & Security
@@ -101,7 +96,7 @@ alwaysApply: false
### **Queue System**
- **Backend**: Redis-based queues
- **Jobs**: [app/Jobs/](mdc:app/Jobs/)
- **Jobs**: `app/Jobs/`
- **Processing**: Background deployment and monitoring tasks
## Development Tools
@@ -130,21 +125,21 @@ alwaysApply: false
- **Gitea**: Self-hosted Git service
### **Cloud Storage**
- **AWS S3**: [league/flysystem-aws-s3-v3](mdc:composer.json)
- **SFTP**: [league/flysystem-sftp-v3](mdc:composer.json)
- **AWS S3**: league/flysystem-aws-s3-v3
- **SFTP**: league/flysystem-sftp-v3
- **Local Storage**: File system integration
### **Notification Services**
- **Email**: [resend/resend-laravel](mdc:composer.json)
- **Email**: resend/resend-laravel
- **Discord**: Custom webhook integration
- **Slack**: Webhook notifications
- **Telegram**: Bot API integration
- **Pushover**: Push notifications
### **Monitoring & Logging**
- **Sentry**: [sentry/sentry-laravel](mdc:composer.json) - Error tracking
- **Laravel Ray**: [spatie/laravel-ray](mdc:composer.json) - Debug tool
- **Activity Log**: [spatie/laravel-activitylog](mdc:composer.json)
- **Sentry**: sentry/sentry-laravel - Error tracking
- **Laravel Ray**: spatie/laravel-ray - Debug tool
- **Activity Log**: spatie/laravel-activitylog
## DevOps & Infrastructure
@@ -181,9 +176,9 @@ alwaysApply: false
## API & Documentation
### **OpenAPI/Swagger**
- **Documentation**: [openapi.json](mdc:openapi.json) (373KB)
- **Generator**: [zircote/swagger-php](mdc:composer.json)
- **API Routes**: [routes/api.php](mdc:routes/api.php)
- **Documentation**: openapi.json (373KB)
- **Generator**: zircote/swagger-php
- **API Routes**: `routes/api.php`
### **WebSocket Communication**
- **Laravel Echo**: Real-time event broadcasting
@@ -192,7 +187,7 @@ alwaysApply: false
## Package Management
### **PHP Dependencies** ([composer.json](mdc:composer.json))
### **PHP Dependencies** (composer.json)
```json
{
"require": {
@@ -205,7 +200,7 @@ alwaysApply: false
}
```
### **JavaScript Dependencies** ([package.json](mdc:package.json))
### **JavaScript Dependencies** (package.json)
```json
{
"devDependencies": {
@@ -223,15 +218,15 @@ alwaysApply: false
## Configuration Files
### **Build Configuration**
- **[vite.config.js](mdc:vite.config.js)**: Frontend build setup
- **[postcss.config.cjs](mdc:postcss.config.cjs)**: CSS processing
- **[rector.php](mdc:rector.php)**: PHP refactoring rules
- **[pint.json](mdc:pint.json)**: Code style configuration
- **vite.config.js**: Frontend build setup
- **postcss.config.cjs**: CSS processing
- **rector.php**: PHP refactoring rules
- **pint.json**: Code style configuration
### **Testing Configuration**
- **[phpunit.xml](mdc:phpunit.xml)**: Unit test configuration
- **[phpunit.dusk.xml](mdc:phpunit.dusk.xml)**: Browser test configuration
- **[tests/Pest.php](mdc:tests/Pest.php)**: Pest testing setup
- **phpunit.xml**: Unit test configuration
- **phpunit.dusk.xml**: Browser test configuration
- **tests/Pest.php**: Pest testing setup
## Version Requirements

View File

@@ -1,8 +1,3 @@
---
description: Development setup, coding standards, contribution guidelines, and best practices
globs: **/*.php, composer.json, package.json, *.md, .env.example
alwaysApply: false
---
# Coolify Development Workflow
## Development Environment Setup

View File

@@ -1,6 +1,3 @@
---
alwaysApply: true
---
<laravel-boost-guidelines>
=== foundation rules ===

View File

@@ -1,8 +1,3 @@
---
description: Testing strategies with Pest PHP, Laravel Dusk, and quality assurance patterns
globs: tests/**/*.php, database/factories/*.php
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.

View File

@@ -0,0 +1,172 @@
# Maintaining AI Documentation
Guidelines for creating and maintaining AI documentation to ensure consistency and effectiveness across all AI tools (Claude Code, Cursor IDE, etc.).
## Documentation Structure
All AI documentation lives in the `.ai/` directory with the following structure:
```
.ai/
├── README.md # Navigation hub
├── core/ # Core project information
├── development/ # Development practices
├── patterns/ # Code patterns and best practices
└── meta/ # Documentation maintenance guides
```
> **Note**: `CLAUDE.md` is in the repository root, not in the `.ai/` directory.
## Required File Structure
When creating new documentation files:
```markdown
# Title
Brief description of what this document covers.
## Section 1
- **Main Points in Bold**
- Sub-points with details
- Examples and explanations
## Section 2
### Subsection
Content with code examples:
```language
// ✅ DO: Show good examples
const goodExample = true;
// ❌ DON'T: Show anti-patterns
const badExample = false;
```
```
## File References
- Use relative paths: `See [technology-stack.md](../core/technology-stack.md)`
- For code references: `` `app/Models/Application.php` ``
- Keep links working across different tools
## Content Guidelines
### DO:
- Start with high-level overview
- Include specific, actionable requirements
- Show examples of correct implementation
- Reference existing code when possible
- Keep documentation DRY by cross-referencing
- Use bullet points for clarity
- Include both DO and DON'T examples
### DON'T:
- Create theoretical examples when real code exists
- Duplicate content across multiple files
- Use tool-specific formatting that won't work elsewhere
- Make assumptions about versions - specify exact versions
## Rule Improvement Triggers
Update documentation when you notice:
- New code patterns not covered by existing docs
- Repeated similar implementations across files
- Common error patterns that could be prevented
- New libraries or tools being used consistently
- Emerging best practices in the codebase
## Analysis Process
When updating documentation:
1. Compare new code with existing rules
2. Identify patterns that should be standardized
3. Look for references to external documentation
4. Check for consistent error handling patterns
5. Monitor test patterns and coverage
## Rule Updates
### Add New Documentation When:
- A new technology/pattern is used in 3+ files
- Common bugs could be prevented by documentation
- Code reviews repeatedly mention the same feedback
- New security or performance patterns emerge
### Modify Existing Documentation When:
- Better examples exist in the codebase
- Additional edge cases are discovered
- Related documentation has been updated
- Implementation details have changed
## Quality Checks
Before committing documentation changes:
- [ ] Documentation is actionable and specific
- [ ] Examples come from actual code
- [ ] References are up to date
- [ ] Patterns are consistently enforced
- [ ] Cross-references work correctly
- [ ] Version numbers are exact and current
## Continuous Improvement
- Monitor code review comments
- Track common development questions
- Update docs after major refactors
- Add links to relevant documentation
- Cross-reference related docs
## Deprecation
When patterns become outdated:
1. Mark outdated patterns as deprecated
2. Remove docs that no longer apply
3. Update references to deprecated patterns
4. Document migration paths for old patterns
## Synchronization
### Single Source of Truth
- Each piece of information should exist in exactly ONE location
- Other files should reference the source, not duplicate it
- Example: Version numbers live in `core/technology-stack.md`, other files reference it
### Cross-Tool Compatibility
- **CLAUDE.md**: Main instructions for Claude Code users (references `.ai/` files)
- **.cursor/rules/**: Single master file pointing to `.ai/` documentation
- **Both tools**: Should get same information from `.ai/` directory
### When to Update What
**Version Changes** (Laravel, PHP, packages):
1. Update `core/technology-stack.md` (single source)
2. Verify CLAUDE.md references it correctly
3. No other files should duplicate version numbers
**Workflow Changes** (commands, setup):
1. Update `development/workflow.md`
2. Ensure CLAUDE.md quick reference is updated
3. Verify all cross-references work
**Pattern Changes** (how to write code):
1. Update appropriate file in `patterns/`
2. Add/update examples from real codebase
3. Cross-reference from related docs
## Documentation Files
Keep documentation files only when explicitly needed. Don't create docs that merely describe obvious functionality - the code itself should be clear.
## Breaking Changes
When making breaking changes to documentation structure:
1. Update this maintaining-docs.md file
2. Update `.ai/README.md` navigation
3. Update CLAUDE.md references
4. Update `.cursor/rules/coolify-ai-docs.mdc`
5. Test all cross-references still work
6. Document the changes in sync-guide.md

214
.ai/meta/sync-guide.md Normal file
View File

@@ -0,0 +1,214 @@
# 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 with a **single source of truth** approach:
1. **CLAUDE.md** - Main entry point for Claude Code (references `.ai/` directory)
2. **.cursor/rules/coolify-ai-docs.mdc** - Master reference file for Cursor IDE (references `.ai/` directory)
3. **.ai/** - Single source of truth containing all detailed documentation
All AI tools (Claude Code, Cursor IDE, etc.) reference the same `.ai/` directory to ensure consistency.
## Structure
### CLAUDE.md (Root Directory)
- **Purpose**: Entry point for Claude Code with quick-reference guide
- **Format**: Single markdown file
- **Includes**:
- Quick-reference development commands
- High-level architecture overview
- Essential patterns and guidelines
- References to detailed `.ai/` documentation
### .cursor/rules/coolify-ai-docs.mdc
- **Purpose**: Master reference file for Cursor IDE
- **Format**: Single .mdc file with frontmatter
- **Content**: Quick decision tree and references to `.ai/` directory
- **Note**: Replaces all previous topic-specific .mdc files
### .ai/ Directory (Single Source of Truth)
- **Purpose**: All detailed, topic-specific documentation
- **Format**: Organized markdown files by category
- **Structure**:
```
.ai/
├── README.md # Navigation hub
├── core/ # Project information
│ ├── technology-stack.md # Version numbers (SINGLE SOURCE OF TRUTH)
│ ├── project-overview.md
│ ├── application-architecture.md
│ └── deployment-architecture.md
├── development/ # Development practices
│ ├── development-workflow.md
│ ├── testing-patterns.md
│ └── laravel-boost.md
├── patterns/ # Code patterns
│ ├── database-patterns.md
│ ├── frontend-patterns.md
│ ├── security-patterns.md
│ ├── form-components.md
│ └── api-and-routing.md
└── meta/ # Documentation guides
├── maintaining-docs.md
└── sync-guide.md (this file)
```
- **Used by**: All AI tools through CLAUDE.md or coolify-ai-docs.mdc
## Cross-References
All systems reference the `.ai/` directory as the source of truth:
- **CLAUDE.md** → references `.ai/` files for detailed documentation
- **.cursor/rules/coolify-ai-docs.mdc** → references `.ai/` files for detailed documentation
- **.ai/README.md** → provides navigation to all documentation
## Maintaining Consistency
### 1. Core Principles (MUST be consistent)
These are defined ONCE in `.ai/core/technology-stack.md`:
- Laravel version (currently Laravel 12.4.1)
- PHP version (8.4.7)
- All package versions (Livewire 3.5.20, Tailwind 4.1.4, etc.)
**Exception**: CLAUDE.md is permitted to show essential version numbers as a quick reference for convenience. These must stay synchronized with `technology-stack.md`. When updating versions, update both locations.
Other critical patterns defined in `.ai/`:
- 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 version numbers** (Laravel, PHP, packages):
1. Update `.ai/core/technology-stack.md` (single source of truth)
2. Update CLAUDE.md quick reference section (essential versions only)
3. Verify both files stay synchronized
4. Never duplicate version numbers in other locations
**For workflow changes** (how to run commands, development setup):
1. Update `.ai/development/development-workflow.md`
2. Update quick reference in CLAUDE.md if needed
3. Verify `.cursor/rules/coolify-ai-docs.mdc` references are correct
**For architectural patterns** (how code should be structured):
1. Update appropriate file in `.ai/core/`
2. Add cross-references from related docs
3. Update CLAUDE.md if it needs to highlight this pattern
**For code patterns** (how to write code):
1. Update appropriate file in `.ai/patterns/`
2. Add examples from real codebase
3. Cross-reference from related docs
**For testing patterns**:
1. Update `.ai/development/testing-patterns.md`
2. Ensure CLAUDE.md testing section references it
### 3. Update Checklist
When making significant changes:
- [ ] Identify if change affects core principles (version numbers, critical patterns)
- [ ] Update primary location in `.ai/` directory
- [ ] Check if CLAUDE.md needs quick-reference update
- [ ] Verify `.cursor/rules/coolify-ai-docs.mdc` references are still accurate
- [ ] Update cross-references in related `.ai/` files
- [ ] Verify all relative paths work correctly
- [ ] Test links in markdown files
- [ ] Run: `./vendor/bin/pint` on modified files (if applicable)
### 4. Common Inconsistencies to Watch
- **Version numbers**: Should ONLY exist in `.ai/core/technology-stack.md`
- **Testing instructions**: Docker execution requirements must be consistent
- **File paths**: Ensure relative paths work from their location
- **Command syntax**: Docker commands, artisan commands must be accurate
- **Cross-references**: Links must point to current file locations
## File Organization
```
/
├── CLAUDE.md # Claude Code entry point
├── .AI_INSTRUCTIONS_SYNC.md # Redirect to this file
├── .cursor/
│ └── rules/
│ └── coolify-ai-docs.mdc # Cursor IDE master reference
└── .ai/ # SINGLE SOURCE OF TRUTH
├── README.md # Navigation hub
├── core/ # Project information
├── development/ # Development practices
├── patterns/ # Code patterns
└── meta/ # Documentation guides
```
## Recent Updates
### 2025-11-18 - Documentation Consolidation
- ✅ Consolidated all documentation into `.ai/` directory
- ✅ Created single source of truth for version numbers
- ✅ Reduced CLAUDE.md from 719 to 319 lines
- ✅ Replaced 11 .cursor/rules/*.mdc files with single coolify-ai-docs.mdc
- ✅ Organized by topic: core/, development/, patterns/, meta/
- ✅ Standardized version numbers (Laravel 12.4.1, PHP 8.4.7, Tailwind 4.1.4)
- ✅ Created comprehensive navigation with .ai/README.md
### 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
## Maintenance Commands
```bash
# Check for version inconsistencies (should only be in technology-stack.md)
# Note: CLAUDE.md is allowed to show quick reference versions
grep -r "Laravel 12" .ai/ CLAUDE.md .cursor/rules/coolify-ai-docs.mdc
grep -r "PHP 8.4" .ai/ CLAUDE.md .cursor/rules/coolify-ai-docs.mdc
# Check for broken cross-references to old .mdc files
grep -r "\.cursor/rules/.*\.mdc" .ai/ CLAUDE.md
# Format all documentation
./vendor/bin/pint CLAUDE.md .ai/**/*.md
# Search for specific patterns across all docs
grep -r "pattern_to_check" CLAUDE.md .ai/ .cursor/rules/
# Verify all markdown links work (from repository root)
find .ai -name "*.md" -exec grep -H "\[.*\](.*)" {} \;
```
## Contributing
When contributing documentation:
1. **Check `.ai/` directory** for existing documentation
2. **Update `.ai/` files** - this is the single source of truth
3. **Use cross-references** - never duplicate content
4. **Update CLAUDE.md** if adding critical quick-reference information
5. **Verify `.cursor/rules/coolify-ai-docs.mdc`** still references correctly
6. **Test all links** work from their respective locations
7. **Update this sync-guide.md** if changing organizational structure
8. **Verify consistency** before submitting PR
## Questions?
If unsure about where to document something:
- **Version numbers**`.ai/core/technology-stack.md` (ONLY location)
- **Quick reference / commands** → CLAUDE.md + `.ai/development/development-workflow.md`
- **Detailed patterns / examples**`.ai/patterns/[topic].md`
- **Architecture / concepts**`.ai/core/[topic].md`
- **Development practices**`.ai/development/[topic].md`
- **Documentation guides**`.ai/meta/[topic].md`
**Golden Rule**: Each piece of information exists in ONE location in `.ai/`, other files reference it.
When in doubt, prefer detailed documentation in `.ai/` and lightweight references in CLAUDE.md and coolify-ai-docs.mdc.

View File

@@ -1,8 +1,3 @@
---
description: RESTful API design, routing patterns, webhooks, and HTTP communication
globs: routes/*.php, app/Http/Controllers/**/*.php, app/Http/Resources/*.php, app/Http/Requests/*.php
alwaysApply: false
---
# Coolify API & Routing Architecture
## Routing Structure

View File

@@ -1,8 +1,3 @@
---
description: Database architecture, models, migrations, relationships, and data management patterns
globs: app/Models/*.php, database/migrations/*.php, database/seeders/*.php, app/Actions/Database/*.php
alwaysApply: false
---
# Coolify Database Architecture & Patterns
## Database Strategy

View File

@@ -1,8 +1,3 @@
---
description: Enhanced form components with built-in authorization system
globs: resources/views/**/*.blade.php, app/View/Components/Forms/*.php
alwaysApply: true
---
# Enhanced Form Components with Authorization

View File

@@ -0,0 +1,696 @@
# Coolify Frontend Architecture & Patterns
## Frontend Philosophy
Coolify uses a **server-side first** approach with minimal JavaScript, leveraging Livewire for reactivity and Alpine.js for lightweight client-side interactions.
## Core Frontend Stack
### Livewire 3.5+ (Primary Framework)
- **Server-side rendering** with reactive components
- **Real-time updates** without page refreshes
- **State management** handled on the server
- **WebSocket integration** for live updates
### Alpine.js (Client-Side Interactivity)
- **Lightweight JavaScript** for DOM manipulation
- **Declarative directives** in HTML
- **Component-like behavior** without build steps
- **Perfect companion** to Livewire
### Tailwind CSS 4.1+ (Styling)
- **Utility-first** CSS framework
- **Custom design system** for deployment platform
- **Responsive design** built-in
- **Dark mode support**
## Livewire Component Structure
### Location: [app/Livewire/](mdc:app/Livewire)
#### Core Application Components
- **[Dashboard.php](mdc:app/Livewire/Dashboard.php)** - Main dashboard interface
- **[ActivityMonitor.php](mdc:app/Livewire/ActivityMonitor.php)** - Real-time activity tracking
- **[MonacoEditor.php](mdc:app/Livewire/MonacoEditor.php)** - Code editor component
#### Server Management
- **Server/** directory - Server configuration and monitoring
- Real-time server status updates
- SSH connection management
- Resource monitoring
#### Project & Application Management
- **Project/** directory - Project organization
- Application deployment interfaces
- Environment variable management
- Service configuration
#### Settings & Configuration
- **Settings/** directory - System configuration
- **[SettingsEmail.php](mdc:app/Livewire/SettingsEmail.php)** - Email notification setup
- **[SettingsOauth.php](mdc:app/Livewire/SettingsOauth.php)** - OAuth provider configuration
- **[SettingsBackup.php](mdc:app/Livewire/SettingsBackup.php)** - Backup configuration
#### User & Team Management
- **Team/** directory - Team collaboration features
- **Profile/** directory - User profile management
- **Security/** directory - Security settings
## Blade Template Organization
### Location: [resources/views/](mdc:resources/views)
#### Layout Structure
- **layouts/** - Base layout templates
- **components/** - Reusable UI components
- **livewire/** - Livewire component views
#### Feature-Specific Views
- **server/** - Server management interfaces
- **auth/** - Authentication pages
- **emails/** - Email templates
- **errors/** - Error pages
## Interactive Components
### Monaco Editor Integration
- **Code editing** for configuration files
- **Syntax highlighting** for multiple languages
- **Live validation** and error detection
- **Integration** with deployment process
### Terminal Emulation (XTerm.js)
- **Real-time terminal** access to servers
- **WebSocket-based** communication
- **Multi-session** support
- **Secure connection** through SSH
### Real-Time Updates
- **WebSocket connections** via Laravel Echo
- **Live deployment logs** streaming
- **Server monitoring** with live metrics
- **Activity notifications** in real-time
## Alpine.js Patterns
### Common Directives Used
```html
<!-- State management -->
<div x-data="{ open: false }">
<!-- Event handling -->
<button x-on:click="open = !open">
<!-- Conditional rendering -->
<div x-show="open">
<!-- Data binding -->
<input x-model="searchTerm">
<!-- Component initialization -->
<div x-init="initializeComponent()">
```
### Integration with Livewire
```html
<!-- Livewire actions with Alpine state -->
<button
x-data="{ loading: false }"
x-on:click="loading = true"
wire:click="deploy"
wire:loading.attr="disabled"
wire:target="deploy"
>
<span x-show="!loading">Deploy</span>
<span x-show="loading">Deploying...</span>
</button>
```
## Tailwind CSS Patterns
### Design System
- **Consistent spacing** using Tailwind scale
- **Color palette** optimized for deployment platform
- **Typography** hierarchy for technical content
- **Component classes** for reusable elements
### Responsive Design
```html
<!-- Mobile-first responsive design -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
<!-- Content adapts to screen size -->
</div>
```
### Dark Mode Support
```html
<!-- Dark mode variants -->
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<!-- Automatic dark mode switching -->
</div>
```
## Build Process
### Vite Configuration ([vite.config.js](mdc:vite.config.js))
- **Fast development** with hot module replacement
- **Optimized production** builds
- **Asset versioning** for cache busting
- **CSS processing** with PostCSS
### Asset Compilation
```bash
# Development
npm run dev
# Production build
npm run build
```
## State Management Patterns
### Server-Side State (Livewire)
- **Component properties** for persistent state
- **Session storage** for user preferences
- **Database models** for application state
- **Cache layer** for performance
### Client-Side State (Alpine.js)
- **Local component state** for UI interactions
- **Form validation** and user feedback
- **Modal and dropdown** state management
- **Temporary UI states** (loading, hover, etc.)
## Real-Time Features
### WebSocket Integration
```php
// Livewire component with real-time updates
class ActivityMonitor extends Component
{
public function getListeners()
{
return [
'deployment.started' => 'refresh',
'deployment.finished' => 'refresh',
'server.status.changed' => 'updateServerStatus',
];
}
}
```
### Event Broadcasting
- **Laravel Echo** for client-side WebSocket handling
- **Pusher protocol** for real-time communication
- **Private channels** for user-specific events
- **Presence channels** for collaborative features
## Performance Patterns
### Lazy Loading
```php
// Livewire lazy loading
class ServerList extends Component
{
public function placeholder()
{
return view('components.loading-skeleton');
}
}
```
### Caching Strategies
- **Fragment caching** for expensive operations
- **Image optimization** with lazy loading
- **Asset bundling** and compression
- **CDN integration** for static assets
## Enhanced Form Components
### Built-in Authorization System
Coolify features **enhanced form components** with automatic authorization handling:
```html
<!-- ✅ New Pattern: Single line with built-in authorization -->
<x-forms.input canGate="update" :canResource="$application" id="application.name" label="Name" />
<x-forms.checkbox instantSave canGate="update" :canResource="$application" id="application.settings.is_static" label="Static Site" />
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
<!-- ❌ Old Pattern: Verbose @can/@else blocks (deprecated) -->
@can('update', $application)
<x-forms.input id="application.name" label="Name" />
@else
<x-forms.input disabled id="application.name" label="Name" />
@endcan
```
### Authorization Parameters
```php
// Available on all form components (Input, Select, Textarea, Checkbox, Button)
public ?string $canGate = null; // Gate name: 'update', 'view', 'deploy', 'delete'
public mixed $canResource = null; // Resource model instance to check against
public bool $autoDisable = true; // Automatically disable if no permission (default: true)
```
### Benefits
- **90% code reduction** for authorization-protected forms
- **Consistent security** across all form components
- **Automatic disabling** for unauthorized users
- **Smart behavior** (disables instantSave on checkboxes for unauthorized users)
For complete documentation, see **[form-components.md](.ai/patterns/form-components.md)**
## Form Handling Patterns
### Livewire Component Data Synchronization Pattern
**IMPORTANT**: All Livewire components must use the **manual `syncData()` pattern** for synchronizing component properties with Eloquent models.
#### Property Naming Convention
- **Component properties**: Use camelCase (e.g., `$gitRepository`, `$isStatic`)
- **Database columns**: Use snake_case (e.g., `git_repository`, `is_static`)
- **View bindings**: Use camelCase matching component properties (e.g., `id="gitRepository"`)
#### The syncData() Method Pattern
```php
use Livewire\Attributes\Validate;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class MyComponent extends Component
{
use AuthorizesRequests;
public Application $application;
// Properties with validation attributes
#[Validate(['required'])]
public string $name;
#[Validate(['string', 'nullable'])]
public ?string $description = null;
#[Validate(['boolean', 'required'])]
public bool $isStatic = false;
public function mount()
{
$this->authorize('view', $this->application);
$this->syncData(); // Load from model
}
public function syncData(bool $toModel = false): void
{
if ($toModel) {
$this->validate();
// Sync TO model (camelCase → snake_case)
$this->application->name = $this->name;
$this->application->description = $this->description;
$this->application->is_static = $this->isStatic;
$this->application->save();
} else {
// Sync FROM model (snake_case → camelCase)
$this->name = $this->application->name;
$this->description = $this->application->description;
$this->isStatic = $this->application->is_static;
}
}
public function submit()
{
$this->authorize('update', $this->application);
$this->syncData(toModel: true); // Save to model
$this->dispatch('success', 'Saved successfully.');
}
}
```
#### Validation with #[Validate] Attributes
All component properties should have `#[Validate]` attributes:
```php
// Boolean properties
#[Validate(['boolean'])]
public bool $isEnabled = false;
// Required strings
#[Validate(['string', 'required'])]
public string $name;
// Nullable strings
#[Validate(['string', 'nullable'])]
public ?string $description = null;
// With constraints
#[Validate(['integer', 'min:1'])]
public int $timeout;
```
#### Benefits of syncData() Pattern
- **Explicit Control**: Clear visibility of what's being synchronized
- **Type Safety**: #[Validate] attributes provide compile-time validation info
- **Easy Debugging**: Single method to check for data flow issues
- **Maintainability**: All sync logic in one place
- **Flexibility**: Can add custom logic (encoding, transformations, etc.)
#### Creating New Form Components with syncData()
#### Step-by-Step Component Creation Guide
**Step 1: Define properties in camelCase with #[Validate] attributes**
```php
use Livewire\Attributes\Validate;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class MyFormComponent extends Component
{
use AuthorizesRequests;
// The model we're syncing with
public Application $application;
// Component properties in camelCase with validation
#[Validate(['string', 'required'])]
public string $name;
#[Validate(['string', 'nullable'])]
public ?string $gitRepository = null;
#[Validate(['string', 'nullable'])]
public ?string $installCommand = null;
#[Validate(['boolean'])]
public bool $isStatic = false;
}
```
**Step 2: Implement syncData() method**
```php
public function syncData(bool $toModel = false): void
{
if ($toModel) {
$this->validate();
// Sync TO model (component camelCase → database snake_case)
$this->application->name = $this->name;
$this->application->git_repository = $this->gitRepository;
$this->application->install_command = $this->installCommand;
$this->application->is_static = $this->isStatic;
$this->application->save();
} else {
// Sync FROM model (database snake_case → component camelCase)
$this->name = $this->application->name;
$this->gitRepository = $this->application->git_repository;
$this->installCommand = $this->application->install_command;
$this->isStatic = $this->application->is_static;
}
}
```
**Step 3: Implement mount() to load initial data**
```php
public function mount()
{
$this->authorize('view', $this->application);
$this->syncData(); // Load data from model to component properties
}
```
**Step 4: Implement action methods with authorization**
```php
public function instantSave()
{
try {
$this->authorize('update', $this->application);
$this->syncData(toModel: true); // Save component properties to model
$this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
$this->authorize('update', $this->application);
$this->syncData(toModel: true); // Save component properties to model
$this->dispatch('success', 'Changes saved successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
```
**Step 5: Create Blade view with camelCase bindings**
```blade
<div>
<form wire:submit="submit">
<x-forms.input
canGate="update"
:canResource="$application"
id="name"
label="Name"
required />
<x-forms.input
canGate="update"
:canResource="$application"
id="gitRepository"
label="Git Repository" />
<x-forms.input
canGate="update"
:canResource="$application"
id="installCommand"
label="Install Command" />
<x-forms.checkbox
instantSave
canGate="update"
:canResource="$application"
id="isStatic"
label="Static Site" />
<x-forms.button
canGate="update"
:canResource="$application"
type="submit">
Save Changes
</x-forms.button>
</form>
</div>
```
**Key Points**:
- Use `wire:model="camelCase"` and `id="camelCase"` in Blade views
- Component properties are camelCase, database columns are snake_case
- Always include authorization checks (`authorize()`, `canGate`, `canResource`)
- Use `instantSave` for checkboxes that save immediately without form submission
#### Special Patterns
**Pattern 1: Related Models (e.g., Application → Settings)**
```php
public function syncData(bool $toModel = false): void
{
if ($toModel) {
$this->validate();
// Sync main model
$this->application->name = $this->name;
$this->application->save();
// Sync related model
$this->application->settings->is_static = $this->isStatic;
$this->application->settings->save();
} else {
// From main model
$this->name = $this->application->name;
// From related model
$this->isStatic = $this->application->settings->is_static;
}
}
```
**Pattern 2: Custom Encoding/Decoding**
```php
public function syncData(bool $toModel = false): void
{
if ($toModel) {
$this->validate();
// Encode before saving
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
} else {
// Decode when loading
$this->customLabels = $this->application->parseContainerLabels();
}
}
```
**Pattern 3: Error Rollback**
```php
public function submit()
{
$this->authorize('update', $this->resource);
$original = $this->model->getOriginal();
try {
$this->syncData(toModel: true);
$this->dispatch('success', 'Saved successfully.');
} catch (\Throwable $e) {
// Rollback on error
$this->model->setRawAttributes($original);
$this->model->save();
$this->syncData(); // Reload from model
return handleError($e, $this);
}
}
```
#### Property Type Patterns
**Required Strings**
```php
#[Validate(['string', 'required'])]
public string $name; // No ?, no default, always has value
```
**Nullable Strings**
```php
#[Validate(['string', 'nullable'])]
public ?string $description = null; // ?, = null, can be empty
```
**Booleans**
```php
#[Validate(['boolean'])]
public bool $isEnabled = false; // Always has default value
```
**Integers with Constraints**
```php
#[Validate(['integer', 'min:1'])]
public int $timeout; // Required
#[Validate(['integer', 'min:1', 'nullable'])]
public ?int $port = null; // Nullable
```
#### Testing Checklist
After creating a new component with syncData(), verify:
- [ ] All checkboxes save correctly (especially `instantSave` ones)
- [ ] All form inputs persist to database
- [ ] Custom encoded fields (like labels) display correctly if applicable
- [ ] Form validation works for all fields
- [ ] No console errors in browser
- [ ] Authorization checks work (`@can` directives and `authorize()` calls)
- [ ] Error rollback works if exceptions occur
- [ ] Related models save correctly if applicable (e.g., Application + ApplicationSetting)
#### Common Pitfalls to Avoid
1. **snake_case in component properties**: Always use camelCase for component properties (e.g., `$gitRepository` not `$git_repository`)
2. **Missing #[Validate] attributes**: Every property should have validation attributes for type safety
3. **Forgetting to call syncData()**: Must call `syncData()` in `mount()` to load initial data
4. **Missing authorization**: Always use `authorize()` in methods and `canGate`/`canResource` in views
5. **View binding mismatch**: Use camelCase in Blade (e.g., `id="gitRepository"` not `id="git_repository"`)
6. **wire:model vs wire:model.live**: Use `.live` for `instantSave` checkboxes to avoid timing issues
7. **Validation sync**: If using `rules()` method, keep it in sync with `#[Validate]` attributes
8. **Related models**: Don't forget to save both main and related models in syncData() method
### Livewire Forms
```php
class ServerCreateForm extends Component
{
public $name;
public $ip;
protected $rules = [
'name' => 'required|min:3',
'ip' => 'required|ip',
];
public function save()
{
$this->validate();
// Save logic
}
}
```
### Real-Time Validation
- **Live validation** as user types
- **Server-side validation** rules
- **Error message** display
- **Success feedback** patterns
## Component Communication
### Parent-Child Communication
```php
// Parent component
$this->emit('serverCreated', $server->id);
// Child component
protected $listeners = ['serverCreated' => 'refresh'];
```
### Cross-Component Events
- **Global events** for application-wide updates
- **Scoped events** for feature-specific communication
- **Browser events** for JavaScript integration
## Error Handling & UX
### Loading States
- **Skeleton screens** during data loading
- **Progress indicators** for long operations
- **Optimistic updates** with rollback capability
### Error Display
- **Toast notifications** for user feedback
- **Inline validation** errors
- **Global error** handling
- **Retry mechanisms** for failed operations
## Accessibility Patterns
### ARIA Labels and Roles
```html
<button
aria-label="Deploy application"
aria-describedby="deploy-help"
wire:click="deploy"
>
Deploy
</button>
```
### Keyboard Navigation
- **Tab order** management
- **Keyboard shortcuts** for power users
- **Focus management** in modals and forms
- **Screen reader** compatibility
## Mobile Optimization
### Touch-Friendly Interface
- **Larger tap targets** for mobile devices
- **Swipe gestures** where appropriate
- **Mobile-optimized** forms and navigation
### Progressive Enhancement
- **Core functionality** works without JavaScript
- **Enhanced experience** with JavaScript enabled
- **Offline capabilities** where possible

View File

@@ -1,8 +1,3 @@
---
description: Security architecture, authentication, authorization patterns, and enhanced form component security
globs: app/Policies/*.php, app/View/Components/Forms/*.php, app/Http/Middleware/*.php, resources/views/**/*.blade.php
alwaysApply: true
---
# Coolify Security Architecture & Patterns
## Security Philosophy

View File

@@ -1,2 +0,0 @@
reviews:
review_status: false

View File

@@ -1,297 +0,0 @@
---
description: Complete guide to Coolify Cursor rules and development patterns
globs: .cursor/rules/*.mdc
alwaysApply: false
---
# Coolify Cursor Rules - Complete Guide
## Overview
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
- **[project-overview.mdc](mdc:.cursor/rules/project-overview.mdc)** - What Coolify is and its core mission
- **[technology-stack.mdc](mdc:.cursor/rules/technology-stack.mdc)** - Complete technology stack and dependencies
- **[application-architecture.mdc](mdc:.cursor/rules/application-architecture.mdc)** - Laravel application structure and patterns
### 🎨 Frontend Development
- **[frontend-patterns.mdc](mdc:.cursor/rules/frontend-patterns.mdc)** - Livewire + Alpine.js + Tailwind architecture
- **[form-components.mdc](mdc:.cursor/rules/form-components.mdc)** - Enhanced form components with built-in authorization
### 🗄️ Data & Backend
- **[database-patterns.mdc](mdc:.cursor/rules/database-patterns.mdc)** - Database architecture, models, and data management
- **[deployment-architecture.mdc](mdc:.cursor/rules/deployment-architecture.mdc)** - Docker orchestration and deployment workflows
### 🌐 API & Communication
- **[api-and-routing.mdc](mdc:.cursor/rules/api-and-routing.mdc)** - RESTful APIs, webhooks, and routing patterns
### 🧪 Quality Assurance
- **[testing-patterns.mdc](mdc:.cursor/rules/testing-patterns.mdc)** - Testing strategies with Pest PHP and Laravel Dusk
### 🔧 Development Process
- **[development-workflow.mdc](mdc:.cursor/rules/development-workflow.mdc)** - Development setup, coding standards, and contribution guidelines
### 🔒 Security
- **[security-patterns.mdc](mdc:.cursor/rules/security-patterns.mdc)** - Security architecture, authentication, and best practices
## Quick Navigation
### Core Application Files
- **[app/Models/Application.php](mdc:app/Models/Application.php)** - Main application entity (74KB, highly complex)
- **[app/Models/Server.php](mdc:app/Models/Server.php)** - Server management (46KB, complex)
- **[app/Models/Service.php](mdc:app/Models/Service.php)** - Service definitions (58KB, complex)
- **[app/Models/Team.php](mdc:app/Models/Team.php)** - Multi-tenant structure (8.9KB)
### Configuration Files
- **[composer.json](mdc:composer.json)** - PHP dependencies and Laravel setup
- **[package.json](mdc:package.json)** - Frontend dependencies and build scripts
- **[vite.config.js](mdc:vite.config.js)** - Frontend build configuration
- **[docker-compose.dev.yml](mdc:docker-compose.dev.yml)** - Development environment
### API Documentation
- **[openapi.json](mdc:openapi.json)** - Complete API documentation (373KB)
- **[routes/api.php](mdc:routes/api.php)** - API endpoint definitions (13KB)
- **[routes/web.php](mdc:routes/web.php)** - Web application routes (21KB)
## Key Concepts to Understand
### 1. Multi-Tenant Architecture
Coolify uses a **team-based multi-tenancy** model where:
- Users belong to multiple teams
- Resources are scoped to teams
- Access control is team-based
- Data isolation is enforced at the database level
### 2. Deployment Philosophy
- **Docker-first** approach for all deployments
- **Zero-downtime** deployments with health checks
- **Git-based** workflows with webhook integration
- **Multi-server** support with SSH connections
### 3. Technology Stack
- **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
- **Testing**: Pest PHP 3.8 + Laravel Dusk
### 4. Security Model
- **Defense-in-depth** security architecture
- **OAuth integration** with multiple providers
- **API token** authentication with Sanctum
- **Encrypted storage** for sensitive data
- **SSH key** management for server access
## Development Quick Start
### Local Setup
```bash
# Clone and setup
git clone https://github.com/coollabsio/coolify.git
cd coolify
cp .env.example .env
# Docker development (recommended)
docker-compose -f docker-compose.dev.yml up -d
docker-compose exec app composer install
docker-compose exec app npm install
docker-compose exec app php artisan migrate
```
### Code Quality
```bash
# PHP code style
./vendor/bin/pint
# Static analysis
./vendor/bin/phpstan analyse
# Run tests
./vendor/bin/pest
```
## Common Patterns
### Livewire Components
```php
class ApplicationShow extends Component
{
public Application $application;
protected $listeners = [
'deployment.started' => 'refresh',
'deployment.completed' => 'refresh',
];
public function deploy(): void
{
$this->authorize('deploy', $this->application);
app(ApplicationDeploymentService::class)->deploy($this->application);
}
}
```
### API Controllers
```php
class ApplicationController extends Controller
{
public function __construct()
{
$this->middleware('auth:sanctum');
$this->middleware('team.access');
}
public function deploy(Application $application): JsonResponse
{
$this->authorize('deploy', $application);
$deployment = app(ApplicationDeploymentService::class)->deploy($application);
return response()->json(['deployment_id' => $deployment->id]);
}
}
```
### Queue Jobs
```php
class DeployApplicationJob implements ShouldQueue
{
public function handle(DockerService $dockerService): void
{
$this->deployment->update(['status' => 'running']);
try {
$dockerService->deployContainer($this->deployment->application);
$this->deployment->update(['status' => 'success']);
} catch (Exception $e) {
$this->deployment->update(['status' => 'failed']);
throw $e;
}
}
}
```
## Testing Patterns
### Feature Tests
```php
test('user can deploy application via API', function () {
$user = User::factory()->create();
$application = Application::factory()->create(['team_id' => $user->currentTeam->id]);
$response = $this->actingAs($user)
->postJson("/api/v1/applications/{$application->id}/deploy");
$response->assertStatus(200);
expect($application->deployments()->count())->toBe(1);
});
```
### Browser Tests
```php
test('user can create application through UI', function () {
$user = User::factory()->create();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit('/applications/create')
->type('name', 'Test App')
->press('Create Application')
->assertSee('Application created successfully');
});
});
```
## Security Considerations
### Authentication
- Multi-provider OAuth support
- API token authentication
- Team-based access control
- Session management
### Data Protection
- Encrypted environment variables
- Secure SSH key storage
- Input validation and sanitization
- SQL injection prevention
### Container Security
- Non-root container users
- Minimal capabilities
- Read-only filesystems
- Network isolation
## Performance Optimization
### Database
- Eager loading relationships
- Query optimization
- Connection pooling
- Caching strategies
### Frontend
- Lazy loading components
- Asset optimization
- CDN integration
- Real-time updates via WebSockets
## Contributing Guidelines
### Code Standards
- PSR-12 PHP coding standards
- Laravel best practices
- Comprehensive test coverage
- Security-first approach
### Pull Request Process
1. Fork repository
2. Create feature branch
3. Implement with tests
4. Run quality checks
5. Submit PR with clear description
## Useful Commands
### Development
```bash
# Start development environment
docker-compose -f docker-compose.dev.yml up -d
# Run tests
./vendor/bin/pest
# Code formatting
./vendor/bin/pint
# Frontend development
npm run dev
```
### Production
```bash
# Install Coolify
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
# Update Coolify
./scripts/upgrade.sh
```
## Resources
### Documentation
- **[README.md](mdc:README.md)** - Project overview and installation
- **[CONTRIBUTING.md](mdc:CONTRIBUTING.md)** - Contribution guidelines
- **[CHANGELOG.md](mdc:CHANGELOG.md)** - Release history
- **[TECH_STACK.md](mdc:TECH_STACK.md)** - Technology overview
### Configuration
- **[config/](mdc:config)** - Laravel configuration files
- **[database/migrations/](mdc:database/migrations)** - Database schema
- **[tests/](mdc:tests)** - Test suite
This comprehensive rule set provides everything needed to understand, develop, and contribute to the Coolify project effectively. Each rule focuses on specific aspects while maintaining connections to the broader architecture.

View File

@@ -1,368 +0,0 @@
---
description: Laravel application structure, patterns, and architectural decisions
globs: app/**/*.php, config/*.php, bootstrap/**/*.php
alwaysApply: false
---
# Coolify Application Architecture
## Laravel Project Structure
### **Core Application Directory** ([app/](mdc:app))
```
app/
├── Actions/ # Business logic actions (Action pattern)
├── Console/ # Artisan commands
├── Contracts/ # Interface definitions
├── Data/ # Data Transfer Objects (Spatie Laravel Data)
├── Enums/ # Enumeration classes
├── Events/ # Event classes
├── Exceptions/ # Custom exception classes
├── Helpers/ # Utility helper classes
├── Http/ # HTTP layer (Controllers, Middleware, Requests)
├── Jobs/ # Background job classes
├── Listeners/ # Event listeners
├── Livewire/ # Livewire components (Frontend)
├── Models/ # Eloquent models (Domain entities)
├── Notifications/ # Notification classes
├── Policies/ # Authorization policies
├── Providers/ # Service providers
├── Repositories/ # Repository pattern implementations
├── Services/ # Service layer classes
├── Traits/ # Reusable trait classes
└── View/ # View composers and creators
```
## Core Domain Models
### **Infrastructure Management**
#### **[Server.php](mdc:app/Models/Server.php)** (46KB, 1343 lines)
- **Purpose**: Physical/virtual server management
- **Key Relationships**:
- `hasMany(Application::class)` - Deployed applications
- `hasMany(StandalonePostgresql::class)` - Database instances
- `belongsTo(Team::class)` - Team ownership
- **Key Features**:
- SSH connection management
- Resource monitoring
- Proxy configuration (Traefik/Caddy)
- Docker daemon interaction
#### **[Application.php](mdc:app/Models/Application.php)** (74KB, 1734 lines)
- **Purpose**: Application deployment and management
- **Key Relationships**:
- `belongsTo(Server::class)` - Deployment target
- `belongsTo(Environment::class)` - Environment context
- `hasMany(ApplicationDeploymentQueue::class)` - Deployment history
- **Key Features**:
- Git repository integration
- Docker build and deployment
- Environment variable management
- SSL certificate handling
#### **[Service.php](mdc:app/Models/Service.php)** (58KB, 1325 lines)
- **Purpose**: Multi-container service orchestration
- **Key Relationships**:
- `hasMany(ServiceApplication::class)` - Service components
- `hasMany(ServiceDatabase::class)` - Service databases
- `belongsTo(Environment::class)` - Environment context
- **Key Features**:
- Docker Compose generation
- Service dependency management
- Health check configuration
### **Team & Project Organization**
#### **[Team.php](mdc:app/Models/Team.php)** (8.9KB, 308 lines)
- **Purpose**: Multi-tenant team management
- **Key Relationships**:
- `hasMany(User::class)` - Team members
- `hasMany(Project::class)` - Team projects
- `hasMany(Server::class)` - Team servers
- **Key Features**:
- Resource limits and quotas
- Team-based access control
- Subscription management
#### **[Project.php](mdc:app/Models/Project.php)** (4.3KB, 156 lines)
- **Purpose**: Project organization and grouping
- **Key Relationships**:
- `hasMany(Environment::class)` - Project environments
- `belongsTo(Team::class)` - Team ownership
- **Key Features**:
- Environment isolation
- Resource organization
#### **[Environment.php](mdc:app/Models/Environment.php)**
- **Purpose**: Environment-specific configuration
- **Key Relationships**:
- `hasMany(Application::class)` - Environment applications
- `hasMany(Service::class)` - Environment services
- `belongsTo(Project::class)` - Project context
### **Database Management Models**
#### **Standalone Database Models**
- **[StandalonePostgresql.php](mdc:app/Models/StandalonePostgresql.php)** (11KB, 351 lines)
- **[StandaloneMysql.php](mdc:app/Models/StandaloneMysql.php)** (11KB, 351 lines)
- **[StandaloneMariadb.php](mdc:app/Models/StandaloneMariadb.php)** (10KB, 337 lines)
- **[StandaloneMongodb.php](mdc:app/Models/StandaloneMongodb.php)** (12KB, 370 lines)
- **[StandaloneRedis.php](mdc:app/Models/StandaloneRedis.php)** (12KB, 394 lines)
- **[StandaloneKeydb.php](mdc:app/Models/StandaloneKeydb.php)** (11KB, 347 lines)
- **[StandaloneDragonfly.php](mdc:app/Models/StandaloneDragonfly.php)** (11KB, 347 lines)
- **[StandaloneClickhouse.php](mdc:app/Models/StandaloneClickhouse.php)** (10KB, 336 lines)
**Common Features**:
- Database configuration management
- Backup scheduling and execution
- Connection string generation
- Health monitoring
### **Configuration & Settings**
#### **[EnvironmentVariable.php](mdc:app/Models/EnvironmentVariable.php)** (7.6KB, 219 lines)
- **Purpose**: Application environment variable management
- **Key Features**:
- Encrypted value storage
- Build-time vs runtime variables
- Shared variable inheritance
#### **[InstanceSettings.php](mdc:app/Models/InstanceSettings.php)** (3.2KB, 124 lines)
- **Purpose**: Global Coolify instance configuration
- **Key Features**:
- FQDN and port configuration
- Auto-update settings
- Security configurations
## Architectural Patterns
### **Action Pattern** ([app/Actions/](mdc:app/Actions))
Using [lorisleiva/laravel-actions](mdc:composer.json) for business logic encapsulation:
```php
// Example Action structure
class DeployApplication extends Action
{
public function handle(Application $application): void
{
// Business logic for deployment
}
public function asJob(Application $application): void
{
// Queue job implementation
}
}
```
**Key Action Categories**:
- **Application/**: Deployment and management actions
- **Database/**: Database operations
- **Server/**: Server management actions
- **Service/**: Service orchestration actions
### **Repository Pattern** ([app/Repositories/](mdc:app/Repositories))
Data access abstraction layer:
- Encapsulates database queries
- Provides testable data layer
- Abstracts complex query logic
### **Service Layer** ([app/Services/](mdc:app/Services))
Business logic services:
- External API integrations
- Complex business operations
- Cross-cutting concerns
## Data Flow Architecture
### **Request Lifecycle**
1. **HTTP Request** → [routes/web.php](mdc:routes/web.php)
2. **Middleware** → Authentication, authorization
3. **Livewire Component** → [app/Livewire/](mdc:app/Livewire)
4. **Action/Service** → Business logic execution
5. **Model/Repository** → Data persistence
6. **Response** → Livewire reactive update
### **Background Processing**
1. **Job Dispatch** → Queue system (Redis)
2. **Job Processing** → [app/Jobs/](mdc:app/Jobs)
3. **Action Execution** → Business logic
4. **Event Broadcasting** → Real-time updates
5. **Notification** → User feedback
## Security Architecture
### **Multi-Tenant Isolation**
```php
// Team-based query scoping
class Application extends Model
{
public function scopeOwnedByCurrentTeam($query)
{
return $query->whereHas('environment.project.team', function ($q) {
$q->where('id', currentTeam()->id);
});
}
}
```
### **Authorization Layers**
1. **Team Membership** → User belongs to team
2. **Resource Ownership** → Resource belongs to team
3. **Policy Authorization** → [app/Policies/](mdc:app/Policies)
4. **Environment Isolation** → Project/environment boundaries
### **Data Protection**
- **Environment Variables**: Encrypted at rest
- **SSH Keys**: Secure storage and transmission
- **API Tokens**: Sanctum-based authentication
- **Audit Logging**: [spatie/laravel-activitylog](mdc:composer.json)
## Configuration Hierarchy
### **Global Configuration**
- **[InstanceSettings](mdc:app/Models/InstanceSettings.php)**: System-wide settings
- **[config/](mdc:config)**: Laravel configuration files
### **Team Configuration**
- **[Team](mdc:app/Models/Team.php)**: Team-specific settings
- **[ServerSetting](mdc:app/Models/ServerSetting.php)**: Server configurations
### **Project Configuration**
- **[ProjectSetting](mdc:app/Models/ProjectSetting.php)**: Project settings
- **[Environment](mdc:app/Models/Environment.php)**: Environment variables
### **Application Configuration**
- **[ApplicationSetting](mdc:app/Models/ApplicationSetting.php)**: App-specific settings
- **[EnvironmentVariable](mdc:app/Models/EnvironmentVariable.php)**: Runtime configuration
## Event-Driven Architecture
### **Event Broadcasting** ([app/Events/](mdc:app/Events))
Real-time updates using Laravel Echo and WebSockets:
```php
// Example event structure
class ApplicationDeploymentStarted implements ShouldBroadcast
{
public function broadcastOn(): array
{
return [
new PrivateChannel("team.{$this->application->team->id}"),
];
}
}
```
### **Event Listeners** ([app/Listeners/](mdc:app/Listeners))
- Deployment status updates
- Resource monitoring alerts
- Notification dispatching
- Audit log creation
## Database Design Patterns
### **Polymorphic Relationships**
```php
// Environment variables can belong to multiple resource types
class EnvironmentVariable extends Model
{
public function resource(): MorphTo
{
return $this->morphTo();
}
}
```
### **Team-Based Soft Scoping**
All major resources include team-based query scoping:
```php
// Automatic team filtering
$applications = Application::ownedByCurrentTeam()->get();
$servers = Server::ownedByCurrentTeam()->get();
```
### **Configuration Inheritance**
Environment variables cascade from:
1. **Shared Variables** → Team-wide defaults
2. **Project Variables** → Project-specific overrides
3. **Application Variables** → Application-specific values
## Integration Patterns
### **Git Provider Integration**
Abstracted git operations supporting:
- **GitHub**: [app/Models/GithubApp.php](mdc:app/Models/GithubApp.php)
- **GitLab**: [app/Models/GitlabApp.php](mdc:app/Models/GitlabApp.php)
- **Bitbucket**: Webhook integration
- **Gitea**: Self-hosted Git support
### **Docker Integration**
- **Container Management**: Direct Docker API communication
- **Image Building**: Dockerfile and Buildpack support
- **Network Management**: Custom Docker networks
- **Volume Management**: Persistent storage handling
### **SSH Communication**
- **[phpseclib/phpseclib](mdc:composer.json)**: Secure SSH connections
- **Multiplexing**: Connection pooling for efficiency
- **Key Management**: [PrivateKey](mdc:app/Models/PrivateKey.php) model
## Testing Architecture
### **Test Structure** ([tests/](mdc:tests))
```
tests/
├── Feature/ # Integration tests
├── Unit/ # Unit tests
├── Browser/ # Dusk browser tests
├── Traits/ # Test helper traits
├── Pest.php # Pest configuration
└── TestCase.php # Base test case
```
### **Testing Patterns**
- **Feature Tests**: Full request lifecycle testing
- **Unit Tests**: Individual class/method testing
- **Browser Tests**: End-to-end user workflows
- **Database Testing**: Factories and seeders
## Performance Considerations
### **Query Optimization**
- **Eager Loading**: Prevent N+1 queries
- **Query Scoping**: Team-based filtering
- **Database Indexing**: Optimized for common queries
### **Caching Strategy**
- **Redis**: Session and cache storage
- **Model Caching**: Frequently accessed data
- **Query Caching**: Expensive query results
### **Background Processing**
- **Queue Workers**: Horizon-managed job processing
- **Job Batching**: Related job grouping
- **Failed Job Handling**: Automatic retry logic

View File

@@ -0,0 +1,156 @@
---
title: Coolify AI Documentation
description: Master reference to all Coolify AI documentation in .ai/ directory
globs: **/*
alwaysApply: true
---
# Coolify AI Documentation
All Coolify AI documentation has been consolidated in the **`.ai/`** directory for better organization and single source of truth.
## Quick Start
- **For Claude Code**: Start with `CLAUDE.md` in the root directory
- **For Cursor IDE**: Start with `.ai/README.md` for navigation
- **For All AI Tools**: Browse `.ai/` directory by topic
## Documentation Structure
All detailed documentation lives in `.ai/` with the following organization:
### 📚 Core Documentation
- **[Technology Stack](.ai/core/technology-stack.md)** - All versions, packages, dependencies (SINGLE SOURCE OF TRUTH for versions)
- **[Project Overview](.ai/core/project-overview.md)** - What Coolify is, high-level architecture
- **[Application Architecture](.ai/core/application-architecture.md)** - System design, components, relationships
- **[Deployment Architecture](.ai/core/deployment-architecture.md)** - Deployment flows, Docker, proxies
### 💻 Development
- **[Development Workflow](.ai/development/development-workflow.md)** - Dev setup, commands, daily workflows
- **[Testing Patterns](.ai/development/testing-patterns.md)** - How to write/run tests, Docker requirements
- **[Laravel Boost](.ai/development/laravel-boost.md)** - Laravel-specific guidelines (SINGLE SOURCE for Laravel Boost)
### 🎨 Code Patterns
- **[Database Patterns](.ai/patterns/database-patterns.md)** - Eloquent, migrations, relationships
- **[Frontend Patterns](.ai/patterns/frontend-patterns.md)** - Livewire, Alpine.js, Tailwind CSS
- **[Security Patterns](.ai/patterns/security-patterns.md)** - Auth, authorization, security
- **[Form Components](.ai/patterns/form-components.md)** - Enhanced forms with authorization
- **[API & Routing](.ai/patterns/api-and-routing.md)** - API design, routing conventions
### 📖 Meta
- **[Maintaining Docs](.ai/meta/maintaining-docs.md)** - How to update/improve documentation
- **[Sync Guide](.ai/meta/sync-guide.md)** - Keeping docs synchronized
## Quick Decision Tree
**What are you working on?**
### Running Commands
→ `.ai/development/development-workflow.md`
- `npm run dev` / `npm run build` - Frontend
- `php artisan serve` / `php artisan migrate` - Backend
- `docker exec coolify php artisan test` - Feature tests (requires Docker)
- `./vendor/bin/pest tests/Unit` - Unit tests (no Docker needed)
- `./vendor/bin/pint` - Code formatting
### Writing Tests
→ `.ai/development/testing-patterns.md`
- **Unit tests**: No database, use mocking, run outside Docker
- **Feature tests**: Can use database, MUST run inside Docker
- Critical: Docker execution requirements prevent database connection errors
### Building UI
→ `.ai/patterns/frontend-patterns.md` + `.ai/patterns/form-components.md`
- Livewire 3.5.20 with server-side state
- Alpine.js for client interactions
- Tailwind CSS 4.1.4 styling
- Form components with `canGate` authorization
### Database Work
→ `.ai/patterns/database-patterns.md`
- Eloquent ORM patterns
- Migration best practices
- Relationship definitions
- Query optimization
### Security & Authorization
→ `.ai/patterns/security-patterns.md` + `.ai/patterns/form-components.md`
- Team-based access control
- Policy and gate patterns
- Form authorization (`canGate`, `canResource`)
- API security with Sanctum
### Laravel-Specific
→ `.ai/development/laravel-boost.md`
- Laravel 12.4.1 patterns
- Livewire 3 best practices
- Pest testing patterns
- Laravel conventions
### Version Numbers
→ `.ai/core/technology-stack.md`
- **SINGLE SOURCE OF TRUTH** for all version numbers
- Laravel 12.4.1, PHP 8.4.7, Tailwind 4.1.4, etc.
- Never duplicate versions - always reference this file
## Critical Patterns (Always Follow)
### Testing Commands
```bash
# Unit tests (no database, outside Docker)
./vendor/bin/pest tests/Unit
# Feature tests (requires database, inside Docker)
docker exec coolify php artisan test
```
**NEVER** run Feature tests outside Docker - they will fail with database connection errors.
### Form Authorization
ALWAYS include authorization on form components:
```blade
<x-forms.input canGate="update" :canResource="$resource" id="name" label="Name" />
```
### Livewire Components
MUST have exactly ONE root element. No exceptions.
### Version Numbers
Use exact versions from `technology-stack.md`:
- ✅ Laravel 12.4.1
- ❌ Laravel 12 or "v12"
### Code Style
```bash
# Always run before committing
./vendor/bin/pint
```
## For AI Assistants
### Important Notes
1. **Single Source of Truth**: Each piece of information exists in ONE location only
2. **Cross-Reference, Don't Duplicate**: Link to other files instead of copying content
3. **Version Precision**: Always use exact versions from `technology-stack.md`
4. **Docker for Feature Tests**: This is non-negotiable for database-dependent tests
5. **Form Authorization**: Security requirement, not optional
### When to Use Which File
- **Quick commands**: `CLAUDE.md` or `development-workflow.md`
- **Detailed patterns**: Topic-specific files in `.ai/patterns/`
- **Testing**: `.ai/development/testing-patterns.md`
- **Laravel specifics**: `.ai/development/laravel-boost.md`
- **Versions**: `.ai/core/technology-stack.md`
## Maintaining Documentation
When updating documentation:
1. Read `.ai/meta/maintaining-docs.md` first
2. Follow single source of truth principle
3. Update cross-references when moving content
4. Test all links work
5. See `.ai/meta/sync-guide.md` for sync guidelines
## Migration Note
This file replaces all previous `.cursor/rules/*.mdc` files. All content has been migrated to `.ai/` directory for better organization and to serve as single source of truth for all AI tools (Claude Code, Cursor IDE, etc.).

View File

@@ -1,59 +0,0 @@
---
description: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness.
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
---
description: Clear, one-line description of what the rule enforces
globs: path/to/files/*.ext, other/path/**/*
alwaysApply: boolean
---
- **Main Points in Bold**
- Sub-points with details
- Examples and explanations
```
- **File References:**
- Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files
- Example: [prisma.mdc](mdc:.cursor/rules/prisma.mdc) for rule references
- Example: [schema.prisma](mdc:prisma/schema.prisma) for code references
- **Code Examples:**
- Use language-specific code blocks
```typescript
// ✅ DO: Show good examples
const goodExample = true;
// ❌ DON'T: Show anti-patterns
const badExample = false;
```
- **Rule Content Guidelines:**
- Start with high-level overview
- Include specific, actionable requirements
- Show examples of correct implementation
- Reference existing code when possible
- Keep rules DRY by referencing other rules
- **Rule Maintenance:**
- Update rules when new patterns emerge
- Add examples from actual codebase
- Remove outdated patterns
- Cross-reference related rules
- **Best Practices:**
- Use bullet points for clarity
- Keep descriptions concise
- Include both DO and DON'T examples
- Reference actual code over theoretical examples
- Use consistent formatting across rules

View File

@@ -1,219 +0,0 @@
---
description: Guide for using Task Master to manage task-driven development workflows
globs: **/*
alwaysApply: true
---
# Task Master Development Workflow
This guide outlines the typical process for using Task Master to manage software development projects.
## Primary Interaction: MCP Server vs. CLI
Task Master offers two primary ways to interact:
1. **MCP Server (Recommended for Integrated Tools)**:
- For AI agents and integrated development environments (like Cursor), interacting via the **MCP server is the preferred method**.
- The MCP server exposes Task Master functionality through a set of tools (e.g., `get_tasks`, `add_subtask`).
- This method offers better performance, structured data exchange, and richer error handling compared to CLI parsing.
- Refer to [`mcp.mdc`](mdc:.cursor/rules/mcp.mdc) for details on the MCP architecture and available tools.
- A comprehensive list and description of MCP tools and their corresponding CLI commands can be found in [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc).
- **Restart the MCP server** if core logic in `scripts/modules` or MCP tool/direct function definitions change.
2. **`task-master` CLI (For Users & Fallback)**:
- The global `task-master` command provides a user-friendly interface for direct terminal interaction.
- It can also serve as a fallback if the MCP server is inaccessible or a specific function isn't exposed via MCP.
- Install globally with `npm install -g task-master-ai` or use locally via `npx task-master-ai ...`.
- The CLI commands often mirror the MCP tools (e.g., `task-master list` corresponds to `get_tasks`).
- Refer to [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc) for a detailed command reference.
## Standard Development Workflow Process
- Start new projects by running `initialize_project` tool / `task-master init` or `parse_prd` / `task-master parse-prd --input='<prd-file.txt>'` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to generate initial tasks.json
- Begin coding sessions with `get_tasks` / `task-master list` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to see current tasks, status, and IDs
- Determine the next task to work on using `next_task` / `task-master next` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)).
- Analyze task complexity with `analyze_project_complexity` / `task-master analyze-complexity --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) before breaking down tasks
- Review complexity report using `complexity_report` / `task-master complexity-report` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)).
- Select tasks based on dependencies (all marked 'done'), priority level, and ID order
- Clarify tasks by checking task files in tasks/ directory or asking for user input
- View specific task details using `get_task` / `task-master show <id>` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to understand implementation requirements
- Break down complex tasks using `expand_task` / `task-master expand --id=<id> --force --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) with appropriate flags like `--force` (to replace existing subtasks) and `--research`.
- Clear existing subtasks if needed using `clear_subtasks` / `task-master clear-subtasks --id=<id>` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) before regenerating
- Implement code following task details, dependencies, and project standards
- Verify tasks according to test strategies before marking as complete (See [`tests.mdc`](mdc:.cursor/rules/tests.mdc))
- Mark completed tasks with `set_task_status` / `task-master set-status --id=<id> --status=done` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc))
- Update dependent tasks when implementation differs from original plan using `update` / `task-master update --from=<id> --prompt="..."` or `update_task` / `task-master update-task --id=<id> --prompt="..."` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc))
- Add new tasks discovered during implementation using `add_task` / `task-master add-task --prompt="..." --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)).
- Add new subtasks as needed using `add_subtask` / `task-master add-subtask --parent=<id> --title="..."` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)).
- Append notes or details to subtasks using `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='Add implementation notes here...\nMore details...'` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)).
- Generate task files with `generate` / `task-master generate` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) after updating tasks.json
- Maintain valid dependency structure with `add_dependency`/`remove_dependency` tools or `task-master add-dependency`/`remove-dependency` commands, `validate_dependencies` / `task-master validate-dependencies`, and `fix_dependencies` / `task-master fix-dependencies` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) when needed
- Respect dependency chains and task priorities when selecting work
- Report progress regularly using `get_tasks` / `task-master list`
## Task Complexity Analysis
- Run `analyze_project_complexity` / `task-master analyze-complexity --research` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) for comprehensive analysis
- Review complexity report via `complexity_report` / `task-master complexity-report` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) for a formatted, readable version.
- Focus on tasks with highest complexity scores (8-10) for detailed breakdown
- Use analysis results to determine appropriate subtask allocation
- Note that reports are automatically used by the `expand_task` tool/command
## Task Breakdown Process
- Use `expand_task` / `task-master expand --id=<id>`. It automatically uses the complexity report if found, otherwise generates default number of subtasks.
- Use `--num=<number>` to specify an explicit number of subtasks, overriding defaults or complexity report recommendations.
- Add `--research` flag to leverage Perplexity AI for research-backed expansion.
- Add `--force` flag to clear existing subtasks before generating new ones (default is to append).
- Use `--prompt="<context>"` to provide additional context when needed.
- Review and adjust generated subtasks as necessary.
- Use `expand_all` tool or `task-master expand --all` to expand multiple pending tasks at once, respecting flags like `--force` and `--research`.
- If subtasks need complete replacement (regardless of the `--force` flag on `expand`), clear them first with `clear_subtasks` / `task-master clear-subtasks --id=<id>`.
## Implementation Drift Handling
- When implementation differs significantly from planned approach
- When future tasks need modification due to current implementation choices
- When new dependencies or requirements emerge
- Use `update` / `task-master update --from=<futureTaskId> --prompt='<explanation>\nUpdate context...' --research` to update multiple future tasks.
- Use `update_task` / `task-master update-task --id=<taskId> --prompt='<explanation>\nUpdate context...' --research` to update a single specific task.
## Task Status Management
- Use 'pending' for tasks ready to be worked on
- Use 'done' for completed and verified tasks
- Use 'deferred' for postponed tasks
- Add custom status values as needed for project-specific workflows
## Task Structure Fields
- **id**: Unique identifier for the task (Example: `1`, `1.1`)
- **title**: Brief, descriptive title (Example: `"Initialize Repo"`)
- **description**: Concise summary of what the task involves (Example: `"Create a new repository, set up initial structure."`)
- **status**: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`)
- **dependencies**: IDs of prerequisite tasks (Example: `[1, 2.1]`)
- Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending)
- This helps quickly identify which prerequisite tasks are blocking work
- **priority**: Importance level (Example: `"high"`, `"medium"`, `"low"`)
- **details**: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`)
- **testStrategy**: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`)
- **subtasks**: List of smaller, more specific tasks (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`)
- Refer to task structure details (previously linked to `tasks.mdc`).
## Configuration Management (Updated)
Taskmaster configuration is managed through two main mechanisms:
1. **`.taskmasterconfig` File (Primary):**
* Located in the project root directory.
* Stores most configuration settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default subtasks/priority, project name, etc.
* **Managed via `task-master models --setup` command.** Do not edit manually unless you know what you are doing.
* **View/Set specific models via `task-master models` command or `models` MCP tool.**
* Created automatically when you run `task-master models --setup` for the first time.
2. **Environment Variables (`.env` / `mcp.json`):**
* Used **only** for sensitive API keys and specific endpoint URLs.
* Place API keys (one per provider) in a `.env` file in the project root for CLI usage.
* For MCP/Cursor integration, configure these keys in the `env` section of `.cursor/mcp.json`.
* Available keys/variables: See `assets/env.example` or the Configuration section in the command reference (previously linked to `taskmaster.mdc`).
**Important:** Non-API key settings (like model selections, `MAX_TOKENS`, `TASKMASTER_LOG_LEVEL`) are **no longer configured via environment variables**. Use the `task-master models` command (or `--setup` for interactive configuration) or the `models` MCP tool.
**If AI commands FAIL in MCP** verify that the API key for the selected provider is present in the `env` section of `.cursor/mcp.json`.
**If AI commands FAIL in CLI** verify that the API key for the selected provider is present in the `.env` file in the root of the project.
## Determining the Next Task
- Run `next_task` / `task-master next` to show the next task to work on.
- The command identifies tasks with all dependencies satisfied
- Tasks are prioritized by priority level, dependency count, and ID
- The command shows comprehensive task information including:
- Basic task details and description
- Implementation details
- Subtasks (if they exist)
- Contextual suggested actions
- Recommended before starting any new development work
- Respects your project's dependency structure
- Ensures tasks are completed in the appropriate sequence
- Provides ready-to-use commands for common task actions
## Viewing Specific Task Details
- Run `get_task` / `task-master show <id>` to view a specific task.
- Use dot notation for subtasks: `task-master show 1.2` (shows subtask 2 of task 1)
- Displays comprehensive information similar to the next command, but for a specific task
- For parent tasks, shows all subtasks and their current status
- For subtasks, shows parent task information and relationship
- Provides contextual suggested actions appropriate for the specific task
- Useful for examining task details before implementation or checking status
## Managing Task Dependencies
- Use `add_dependency` / `task-master add-dependency --id=<id> --depends-on=<id>` to add a dependency.
- Use `remove_dependency` / `task-master remove-dependency --id=<id> --depends-on=<id>` to remove a dependency.
- The system prevents circular dependencies and duplicate dependency entries
- Dependencies are checked for existence before being added or removed
- Task files are automatically regenerated after dependency changes
- Dependencies are visualized with status indicators in task listings and files
## Iterative Subtask Implementation
Once a task has been broken down into subtasks using `expand_task` or similar methods, follow this iterative process for implementation:
1. **Understand the Goal (Preparation):**
* Use `get_task` / `task-master show <subtaskId>` (see [`taskmaster.mdc`](mdc:.cursor/rules/taskmaster.mdc)) to thoroughly understand the specific goals and requirements of the subtask.
2. **Initial Exploration & Planning (Iteration 1):**
* This is the first attempt at creating a concrete implementation plan.
* Explore the codebase to identify the precise files, functions, and even specific lines of code that will need modification.
* Determine the intended code changes (diffs) and their locations.
* Gather *all* relevant details from this exploration phase.
3. **Log the Plan:**
* Run `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<detailed plan>'`.
* Provide the *complete and detailed* findings from the exploration phase in the prompt. Include file paths, line numbers, proposed diffs, reasoning, and any potential challenges identified. Do not omit details. The goal is to create a rich, timestamped log within the subtask's `details`.
4. **Verify the Plan:**
* Run `get_task` / `task-master show <subtaskId>` again to confirm that the detailed implementation plan has been successfully appended to the subtask's details.
5. **Begin Implementation:**
* Set the subtask status using `set_task_status` / `task-master set-status --id=<subtaskId> --status=in-progress`.
* Start coding based on the logged plan.
6. **Refine and Log Progress (Iteration 2+):**
* As implementation progresses, you will encounter challenges, discover nuances, or confirm successful approaches.
* **Before appending new information**: Briefly review the *existing* details logged in the subtask (using `get_task` or recalling from context) to ensure the update adds fresh insights and avoids redundancy.
* **Regularly** use `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<update details>\n- What worked...\n- What didn't work...'` to append new findings.
* **Crucially, log:**
* What worked ("fundamental truths" discovered).
* What didn't work and why (to avoid repeating mistakes).
* Specific code snippets or configurations that were successful.
* Decisions made, especially if confirmed with user input.
* Any deviations from the initial plan and the reasoning.
* The objective is to continuously enrich the subtask's details, creating a log of the implementation journey that helps the AI (and human developers) learn, adapt, and avoid repeating errors.
7. **Review & Update Rules (Post-Implementation):**
* Once the implementation for the subtask is functionally complete, review all code changes and the relevant chat history.
* Identify any new or modified code patterns, conventions, or best practices established during the implementation.
* Create new or update existing rules following internal guidelines (previously linked to `cursor_rules.mdc` and `self_improve.mdc`).
8. **Mark Task Complete:**
* After verifying the implementation and updating any necessary rules, mark the subtask as completed: `set_task_status` / `task-master set-status --id=<subtaskId> --status=done`.
9. **Commit Changes (If using Git):**
* Stage the relevant code changes and any updated/new rule files (`git add .`).
* Craft a comprehensive Git commit message summarizing the work done for the subtask, including both code implementation and any rule adjustments.
* Execute the commit command directly in the terminal (e.g., `git commit -m 'feat(module): Implement feature X for subtask <subtaskId>\n\n- Details about changes...\n- Updated rule Y for pattern Z'`).
* Consider if a Changeset is needed according to internal versioning guidelines (previously linked to `changeset.mdc`). If so, run `npm run changeset`, stage the generated file, and amend the commit or create a new one.
10. **Proceed to Next Subtask:**
* Identify the next subtask (e.g., using `next_task` / `task-master next`).
## Code Analysis & Refactoring Techniques
- **Top-Level Function Search**:
- Useful for understanding module structure or planning refactors.
- Use grep/ripgrep to find exported functions/constants:
`rg "export (async function|function|const) \w+"` or similar patterns.
- Can help compare functions between files during migrations or identify potential naming conflicts.
---
*This workflow provides a general guideline. Adapt it based on your specific project needs and team practices.*

View File

@@ -1,354 +0,0 @@
---
description: Livewire components, Alpine.js patterns, Tailwind CSS, and enhanced form components
globs: app/Livewire/**/*.php, resources/views/**/*.blade.php, resources/js/**/*.js, resources/css/**/*.css
alwaysApply: false
---
# Coolify Frontend Architecture & Patterns
## Frontend Philosophy
Coolify uses a **server-side first** approach with minimal JavaScript, leveraging Livewire for reactivity and Alpine.js for lightweight client-side interactions.
## Core Frontend Stack
### Livewire 3.5+ (Primary Framework)
- **Server-side rendering** with reactive components
- **Real-time updates** without page refreshes
- **State management** handled on the server
- **WebSocket integration** for live updates
### Alpine.js (Client-Side Interactivity)
- **Lightweight JavaScript** for DOM manipulation
- **Declarative directives** in HTML
- **Component-like behavior** without build steps
- **Perfect companion** to Livewire
### Tailwind CSS 4.1+ (Styling)
- **Utility-first** CSS framework
- **Custom design system** for deployment platform
- **Responsive design** built-in
- **Dark mode support**
## Livewire Component Structure
### Location: [app/Livewire/](mdc:app/Livewire)
#### Core Application Components
- **[Dashboard.php](mdc:app/Livewire/Dashboard.php)** - Main dashboard interface
- **[ActivityMonitor.php](mdc:app/Livewire/ActivityMonitor.php)** - Real-time activity tracking
- **[MonacoEditor.php](mdc:app/Livewire/MonacoEditor.php)** - Code editor component
#### Server Management
- **Server/** directory - Server configuration and monitoring
- Real-time server status updates
- SSH connection management
- Resource monitoring
#### Project & Application Management
- **Project/** directory - Project organization
- Application deployment interfaces
- Environment variable management
- Service configuration
#### Settings & Configuration
- **Settings/** directory - System configuration
- **[SettingsEmail.php](mdc:app/Livewire/SettingsEmail.php)** - Email notification setup
- **[SettingsOauth.php](mdc:app/Livewire/SettingsOauth.php)** - OAuth provider configuration
- **[SettingsBackup.php](mdc:app/Livewire/SettingsBackup.php)** - Backup configuration
#### User & Team Management
- **Team/** directory - Team collaboration features
- **Profile/** directory - User profile management
- **Security/** directory - Security settings
## Blade Template Organization
### Location: [resources/views/](mdc:resources/views)
#### Layout Structure
- **layouts/** - Base layout templates
- **components/** - Reusable UI components
- **livewire/** - Livewire component views
#### Feature-Specific Views
- **server/** - Server management interfaces
- **auth/** - Authentication pages
- **emails/** - Email templates
- **errors/** - Error pages
## Interactive Components
### Monaco Editor Integration
- **Code editing** for configuration files
- **Syntax highlighting** for multiple languages
- **Live validation** and error detection
- **Integration** with deployment process
### Terminal Emulation (XTerm.js)
- **Real-time terminal** access to servers
- **WebSocket-based** communication
- **Multi-session** support
- **Secure connection** through SSH
### Real-Time Updates
- **WebSocket connections** via Laravel Echo
- **Live deployment logs** streaming
- **Server monitoring** with live metrics
- **Activity notifications** in real-time
## Alpine.js Patterns
### Common Directives Used
```html
<!-- State management -->
<div x-data="{ open: false }">
<!-- Event handling -->
<button x-on:click="open = !open">
<!-- Conditional rendering -->
<div x-show="open">
<!-- Data binding -->
<input x-model="searchTerm">
<!-- Component initialization -->
<div x-init="initializeComponent()">
```
### Integration with Livewire
```html
<!-- Livewire actions with Alpine state -->
<button
x-data="{ loading: false }"
x-on:click="loading = true"
wire:click="deploy"
wire:loading.attr="disabled"
wire:target="deploy"
>
<span x-show="!loading">Deploy</span>
<span x-show="loading">Deploying...</span>
</button>
```
## Tailwind CSS Patterns
### Design System
- **Consistent spacing** using Tailwind scale
- **Color palette** optimized for deployment platform
- **Typography** hierarchy for technical content
- **Component classes** for reusable elements
### Responsive Design
```html
<!-- Mobile-first responsive design -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
<!-- Content adapts to screen size -->
</div>
```
### Dark Mode Support
```html
<!-- Dark mode variants -->
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<!-- Automatic dark mode switching -->
</div>
```
## Build Process
### Vite Configuration ([vite.config.js](mdc:vite.config.js))
- **Fast development** with hot module replacement
- **Optimized production** builds
- **Asset versioning** for cache busting
- **CSS processing** with PostCSS
### Asset Compilation
```bash
# Development
npm run dev
# Production build
npm run build
```
## State Management Patterns
### Server-Side State (Livewire)
- **Component properties** for persistent state
- **Session storage** for user preferences
- **Database models** for application state
- **Cache layer** for performance
### Client-Side State (Alpine.js)
- **Local component state** for UI interactions
- **Form validation** and user feedback
- **Modal and dropdown** state management
- **Temporary UI states** (loading, hover, etc.)
## Real-Time Features
### WebSocket Integration
```php
// Livewire component with real-time updates
class ActivityMonitor extends Component
{
public function getListeners()
{
return [
'deployment.started' => 'refresh',
'deployment.finished' => 'refresh',
'server.status.changed' => 'updateServerStatus',
];
}
}
```
### Event Broadcasting
- **Laravel Echo** for client-side WebSocket handling
- **Pusher protocol** for real-time communication
- **Private channels** for user-specific events
- **Presence channels** for collaborative features
## Performance Patterns
### Lazy Loading
```php
// Livewire lazy loading
class ServerList extends Component
{
public function placeholder()
{
return view('components.loading-skeleton');
}
}
```
### Caching Strategies
- **Fragment caching** for expensive operations
- **Image optimization** with lazy loading
- **Asset bundling** and compression
- **CDN integration** for static assets
## Enhanced Form Components
### Built-in Authorization System
Coolify features **enhanced form components** with automatic authorization handling:
```html
<!-- ✅ New Pattern: Single line with built-in authorization -->
<x-forms.input canGate="update" :canResource="$application" id="application.name" label="Name" />
<x-forms.checkbox instantSave canGate="update" :canResource="$application" id="application.settings.is_static" label="Static Site" />
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
<!-- ❌ Old Pattern: Verbose @can/@else blocks (deprecated) -->
@can('update', $application)
<x-forms.input id="application.name" label="Name" />
@else
<x-forms.input disabled id="application.name" label="Name" />
@endcan
```
### Authorization Parameters
```php
// Available on all form components (Input, Select, Textarea, Checkbox, Button)
public ?string $canGate = null; // Gate name: 'update', 'view', 'deploy', 'delete'
public mixed $canResource = null; // Resource model instance to check against
public bool $autoDisable = true; // Automatically disable if no permission (default: true)
```
### Benefits
- **90% code reduction** for authorization-protected forms
- **Consistent security** across all form components
- **Automatic disabling** for unauthorized users
- **Smart behavior** (disables instantSave on checkboxes for unauthorized users)
For complete documentation, see **[form-components.mdc](mdc:.cursor/rules/form-components.mdc)**
## Form Handling Patterns
### Livewire Forms
```php
class ServerCreateForm extends Component
{
public $name;
public $ip;
protected $rules = [
'name' => 'required|min:3',
'ip' => 'required|ip',
];
public function save()
{
$this->validate();
// Save logic
}
}
```
### Real-Time Validation
- **Live validation** as user types
- **Server-side validation** rules
- **Error message** display
- **Success feedback** patterns
## Component Communication
### Parent-Child Communication
```php
// Parent component
$this->emit('serverCreated', $server->id);
// Child component
protected $listeners = ['serverCreated' => 'refresh'];
```
### Cross-Component Events
- **Global events** for application-wide updates
- **Scoped events** for feature-specific communication
- **Browser events** for JavaScript integration
## Error Handling & UX
### Loading States
- **Skeleton screens** during data loading
- **Progress indicators** for long operations
- **Optimistic updates** with rollback capability
### Error Display
- **Toast notifications** for user feedback
- **Inline validation** errors
- **Global error** handling
- **Retry mechanisms** for failed operations
## Accessibility Patterns
### ARIA Labels and Roles
```html
<button
aria-label="Deploy application"
aria-describedby="deploy-help"
wire:click="deploy"
>
Deploy
</button>
```
### Keyboard Navigation
- **Tab order** management
- **Keyboard shortcuts** for power users
- **Focus management** in modals and forms
- **Screen reader** compatibility
## Mobile Optimization
### Touch-Friendly Interface
- **Larger tap targets** for mobile devices
- **Swipe gestures** where appropriate
- **Mobile-optimized** forms and navigation
### Progressive Enhancement
- **Core functionality** works without JavaScript
- **Enhanced experience** with JavaScript enabled
- **Offline capabilities** where possible

View File

@@ -1,59 +0,0 @@
---
description: Guidelines for continuously improving Cursor rules based on emerging code patterns and best practices.
globs: **/*
alwaysApply: true
---
- **Rule Improvement Triggers:**
- New code patterns not covered by existing rules
- Repeated similar implementations across files
- Common error patterns that could be prevented
- New libraries or tools being used consistently
- Emerging best practices in the codebase
- **Analysis Process:**
- Compare new code with existing rules
- Identify patterns that should be standardized
- Look for references to external documentation
- Check for consistent error handling patterns
- Monitor test patterns and coverage
- **Rule Updates:**
- **Add New Rules When:**
- A new technology/pattern is used in 3+ files
- Common bugs could be prevented by a rule
- Code reviews repeatedly mention the same feedback
- New security or performance patterns emerge
- **Modify Existing Rules When:**
- Better examples exist in the codebase
- Additional edge cases are discovered
- Related rules have been updated
- Implementation details have changed
- **Rule Quality Checks:**
- Rules should be actionable and specific
- Examples should come from actual code
- References should be up to date
- Patterns should be consistently enforced
- **Continuous Improvement:**
- Monitor code review comments
- Track common development questions
- Update rules after major refactors
- Add links to relevant documentation
- Cross-reference related rules
- **Rule Deprecation:**
- Mark outdated patterns as deprecated
- Remove rules that no longer apply
- Update references to deprecated rules
- Document migration paths for old patterns
- **Documentation Updates:**
- Keep examples synchronized with code
- Update references to external docs
- Maintain links between related rules
- Document breaking changes
Follow [cursor_rules.mdc](mdc:.cursor/rules/cursor_rules.mdc) for proper rule formatting and structure.

View File

@@ -4,6 +4,11 @@ on:
schedule:
- cron: '0 1 * * *'
permissions:
issues: write
discussions: write
pull-requests: write
jobs:
lock-threads:
runs-on: ubuntu-latest
@@ -13,5 +18,5 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '30'
pr-inactive-days: '30'
discussion-inactive-days: '30'
pr-inactive-days: '30'

View File

@@ -4,6 +4,10 @@ on:
schedule:
- cron: '0 2 * * *'
permissions:
issues: write
pull-requests: write
jobs:
manage-stale:
runs-on: ubuntu-latest

View File

@@ -3,20 +3,13 @@ on:
pull_request_target:
types:
- labeled
permissions:
pull-requests: write
jobs:
add-comment:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
actions: none
checks: none
deployments: none
issues: none
packages: none
repository-projects: none
security-events: none
statuses: none
strategy:
matrix:
include:

View File

@@ -8,6 +8,10 @@ on:
pull_request_target:
types: [closed]
permissions:
issues: write
pull-requests: write
jobs:
remove-labels-and-assignees:
runs-on: ubuntu-latest

View File

@@ -1,79 +0,0 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
if: false
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
# model: "claude-opus-4-1-20250805"
# Direct prompt for automated review (no @claude mention needed)
direct_prompt: |
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns
- Test coverage
Be constructive and helpful in your feedback.
# Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
# use_sticky_comment: true
# Optional: Customize review based on file types
# direct_prompt: |
# Review this PR focusing on:
# - For TypeScript files: Type safety and proper interface usage
# - For API endpoints: Security, input validation, and error handling
# - For React components: Performance, accessibility, and best practices
# - For tests: Coverage, edge cases, and test quality
# Optional: Different prompts for different authors
# direct_prompt: |
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
# 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
# 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
# Optional: Add specific tools for running tests or linting
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
# Optional: Skip review for certain conditions
# if: |
# !contains(github.event.pull_request.title, '[skip-review]') &&
# !contains(github.event.pull_request.title, '[WIP]')

View File

@@ -1,65 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'Claude') ||
(github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'Claude') ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
# model: "claude-opus-4-1-20250805"
# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"
# Optional: Allow Claude to run specific commands
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
# Optional: Add custom instructions for Claude to customize its behavior for your project
# custom_instructions: |
# Follow our coding standards
# Ensure all new code has tests
# Use TypeScript for new files
# Optional: Custom environment variables for Claude
# claude_env: |
# NODE_ENV: test

View File

@@ -1,17 +1,14 @@
name: Cleanup Untagged GHCR Images
on:
workflow_dispatch: # Manual trigger only
workflow_dispatch:
env:
GITHUB_REGISTRY: ghcr.io
permissions:
packages: write
jobs:
cleanup-all-packages:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
package: ['coolify', 'coolify-helper', 'coolify-realtime', 'coolify-testing-host']

View File

@@ -7,19 +7,31 @@ on:
- .github/workflows/coolify-helper-next.yml
- docker/coolify-helper/Dockerfile
permissions:
contents: read
packages: write
env:
GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-helper"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
build-push:
strategy:
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: aarch64
platform: linux/aarch64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
@@ -40,66 +52,27 @@ jobs:
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
- name: Build and Push Image (${{ matrix.arch }})
uses: docker/build-push-action@v6
with:
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
platforms: ${{ matrix.platform }}
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
labels: |
coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
runs-on: ubuntu-24.04
needs: build-push
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: docker/setup-buildx-action@v3
- name: Login to ${{ env.GITHUB_REGISTRY }}
@@ -124,14 +97,16 @@ jobs:
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next

View File

@@ -7,19 +7,31 @@ on:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
permissions:
contents: read
packages: write
env:
GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-helper"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
build-push:
strategy:
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: aarch64
platform: linux/aarch64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
@@ -40,65 +52,25 @@ jobs:
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
- name: Build and Push Image (${{ matrix.arch }})
uses: docker/build-push-action@v6
with:
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
platforms: ${{ matrix.platform }}
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
labels: |
coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
runs-on: ubuntu-24.04
needs: build-push
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: docker/setup-buildx-action@v3
@@ -124,14 +96,16 @@ jobs:
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest

View File

@@ -14,16 +14,31 @@ on:
- templates/**
- CHANGELOG.md
permissions:
contents: read
packages: write
env:
GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify"
jobs:
amd64:
runs-on: ubuntu-latest
build-push:
strategy:
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: aarch64
platform: linux/aarch64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
@@ -44,60 +59,24 @@ jobs:
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
- name: Build and Push Image (${{ matrix.arch }})
uses: docker/build-push-action@v6
with:
context: .
file: docker/production/Dockerfile
platforms: linux/amd64
platforms: ${{ matrix.platform }}
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
aarch64:
runs-on: [self-hosted, arm64]
steps:
- uses: actions/checkout@v4
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
file: docker/production/Dockerfile
platforms: linux/aarch64
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [amd64, aarch64]
runs-on: ubuntu-24.04
needs: build-push
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: docker/setup-buildx-action@v3
@@ -123,14 +102,16 @@ jobs:
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest

View File

@@ -11,19 +11,31 @@ on:
- docker/coolify-realtime/package-lock.json
- docker/coolify-realtime/soketi-entrypoint.sh
permissions:
contents: read
packages: write
env:
GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-realtime"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
build-push:
strategy:
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: aarch64
platform: linux/aarch64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
@@ -44,67 +56,26 @@ jobs:
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
- name: Build and Push Image (${{ matrix.arch }})
uses: docker/build-push-action@v6
with:
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/amd64
platforms: ${{ matrix.platform }}
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
labels: |
coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/aarch64
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
runs-on: ubuntu-24.04
needs: build-push
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: docker/setup-buildx-action@v3
@@ -130,14 +101,16 @@ jobs:
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next

View File

@@ -11,19 +11,31 @@ on:
- docker/coolify-realtime/package-lock.json
- docker/coolify-realtime/soketi-entrypoint.sh
permissions:
contents: read
packages: write
env:
GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-realtime"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
build-push:
strategy:
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: aarch64
platform: linux/aarch64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
@@ -44,67 +56,26 @@ jobs:
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
- name: Build and Push Image (${{ matrix.arch }})
uses: docker/build-push-action@v6
with:
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/amd64
platforms: ${{ matrix.platform }}
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
labels: |
coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/aarch64
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
runs-on: ubuntu-24.04
needs: build-push
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: docker/setup-buildx-action@v3
@@ -130,14 +101,16 @@ jobs:
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest

View File

@@ -17,6 +17,10 @@ on:
- templates/**
- CHANGELOG.md
permissions:
contents: read
packages: write
env:
GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
@@ -34,11 +38,10 @@ jobs:
platform: linux/aarch64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Sanitize branch name for Docker tag
id: sanitize
@@ -82,11 +85,10 @@ jobs:
merge-manifest:
runs-on: ubuntu-24.04
needs: build-push
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Sanitize branch name for Docker tag
id: sanitize

View File

@@ -7,19 +7,31 @@ on:
- .github/workflows/coolify-testing-host.yml
- docker/testing-host/Dockerfile
permissions:
contents: read
packages: write
env:
GITHUB_REGISTRY: ghcr.io
DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-testing-host"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
build-push:
strategy:
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: aarch64
platform: linux/aarch64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
@@ -35,62 +47,26 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and Push Image
- name: Build and Push Image (${{ matrix.arch }})
uses: docker/build-push-action@v6
with:
context: .
file: docker/testing-host/Dockerfile
platforms: linux/amd64
platforms: ${{ matrix.platform }}
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
labels: |
coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
file: docker/testing-host/Dockerfile
platforms: linux/aarch64
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }}
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
runs-on: ubuntu-24.04
needs: build-push
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: docker/setup-buildx-action@v3
@@ -111,13 +87,15 @@ jobs:
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64 \
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
run: |
docker buildx imagetools create \
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64 \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1

14071
CHANGELOG.md

File diff suppressed because it is too large Load Diff

491
CLAUDE.md
View File

@@ -2,9 +2,9 @@
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.
> **Note for AI Assistants**: This file is specifically for Claude Code. All detailed documentation is in the `.ai/` directory. Both Claude Code and Cursor IDE use the same source files in `.ai/` for consistency.
>
> **Maintaining Instructions**: When updating AI instructions, see [.AI_INSTRUCTIONS_SYNC.md](.AI_INSTRUCTIONS_SYNC.md) for synchronization guidelines between CLAUDE.md and .cursor/rules/.
> **Maintaining Instructions**: When updating AI instructions, see [.ai/meta/sync-guide.md](.ai/meta/sync-guide.md) and [.ai/meta/maintaining-docs.md](.ai/meta/maintaining-docs.md) for guidelines.
## Project Overview
@@ -27,7 +27,8 @@ Only run artisan commands inside "coolify" container when in 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 (unit tests only, without database)
- `./vendor/bin/pest tests/Unit` - Run unit tests only (no database, can run outside Docker)
- `./vendor/bin/pest` - Run ALL tests (includes Feature tests, may require database)
### Running Tests
**IMPORTANT**: Tests that require database connections MUST be run inside the Docker container:
@@ -39,12 +40,14 @@ Only run artisan commands inside "coolify" container when in development.
## Architecture Overview
### Technology Stack
- **Backend**: Laravel 12 (PHP 8.4)
- **Frontend**: Livewire 3.5+ with Alpine.js and Tailwind CSS 4.1+
- **Backend**: Laravel 12.4.1 (PHP 8.4.7)
- **Frontend**: Livewire 3.5.20 with Alpine.js and Tailwind CSS 4.1.4
- **Database**: PostgreSQL 15 (primary), Redis 7 (cache/queues)
- **Real-time**: Soketi (WebSocket server)
- **Containerization**: Docker & Docker Compose
- **Queue Management**: Laravel Horizon
- **Queue Management**: Laravel Horizon 5.30.3
> **Note**: For complete version information and all dependencies, see [.ai/core/technology-stack.md](.ai/core/technology-stack.md)
### Key Components
@@ -256,453 +259,61 @@ When developing features:
## Additional Documentation
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):
This file contains high-level guidelines for Claude Code. For **more detailed, topic-specific documentation**, refer to the `.ai/` directory:
> **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.
> **Documentation Hub**: The `.ai/` directory contains comprehensive, detailed documentation organized by topic. Start with [.ai/README.md](.ai/README.md) for navigation, then explore specific topics below.
### Architecture & Patterns
- [Application Architecture](.cursor/rules/application-architecture.mdc) - Detailed application structure
- [Deployment Architecture](.cursor/rules/deployment-architecture.mdc) - Deployment patterns and flows
- [Database Patterns](.cursor/rules/database-patterns.mdc) - Database design and query patterns
- [Frontend Patterns](.cursor/rules/frontend-patterns.mdc) - Livewire and Alpine.js patterns
- [API & Routing](.cursor/rules/api-and-routing.mdc) - API design and routing conventions
### Core Documentation
- [Technology Stack](.ai/core/technology-stack.md) - All versions, packages, and dependencies (single source of truth)
- [Project Overview](.ai/core/project-overview.md) - What Coolify is and how it works
- [Application Architecture](.ai/core/application-architecture.md) - System design and component relationships
- [Deployment Architecture](.ai/core/deployment-architecture.md) - How deployments work end-to-end
### Development & Security
- [Development Workflow](.cursor/rules/development-workflow.mdc) - Development best practices
- [Security Patterns](.cursor/rules/security-patterns.mdc) - Security implementation details
- [Form Components](.cursor/rules/form-components.mdc) - Enhanced form components with authorization
- [Testing Patterns](.cursor/rules/testing-patterns.mdc) - Testing strategies and examples
### Development Practices
- [Development Workflow](.ai/development/development-workflow.md) - Development setup, commands, and workflows
- [Testing Patterns](.ai/development/testing-patterns.md) - Testing strategies and examples (Docker requirements!)
- [Laravel Boost](.ai/development/laravel-boost.md) - Laravel-specific guidelines and best practices
### Project Information
- [Project Overview](.cursor/rules/project-overview.mdc) - High-level project structure
- [Technology Stack](.cursor/rules/technology-stack.mdc) - Detailed tech stack information
- [Cursor Rules Guide](.cursor/rules/cursor_rules.mdc) - How to maintain cursor rules
### Code Patterns
- [Database Patterns](.ai/patterns/database-patterns.md) - Eloquent, migrations, relationships
- [Frontend Patterns](.ai/patterns/frontend-patterns.md) - Livewire, Alpine.js, Tailwind CSS
- [Security Patterns](.ai/patterns/security-patterns.md) - Authentication, authorization, security
- [Form Components](.ai/patterns/form-components.md) - Enhanced form components with authorization
- [API & Routing](.ai/patterns/api-and-routing.md) - API design and routing conventions
===
### Meta Documentation
- [Maintaining Docs](.ai/meta/maintaining-docs.md) - How to update and improve AI documentation
- [Sync Guide](.ai/meta/sync-guide.md) - Keeping documentation synchronized
<laravel-boost-guidelines>
=== foundation rules ===
## Laravel Boost Guidelines
# Laravel Boost Guidelines
> **Full Guidelines**: See [.ai/development/laravel-boost.md](.ai/development/laravel-boost.md) for complete Laravel Boost guidelines.
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
### Essential Laravel Patterns
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- Use PHP 8.4 constructor property promotion and typed properties
- Follow PSR-12 (run `./vendor/bin/pint` before committing)
- Use Eloquent ORM, avoid raw queries
- Use Form Request classes for validation
- Queue heavy operations with Laravel Horizon
- Never use `env()` outside config files
- Use named routes with `route()` function
- Laravel 12 with Laravel 10 structure (no bootstrap/app.php)
- php - 8.4.7
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v12
- laravel/horizon (HORIZON) - v5
- laravel/prompts (PROMPTS) - v0
- laravel/sanctum (SANCTUM) - v4
- laravel/socialite (SOCIALITE) - v5
- livewire/livewire (LIVEWIRE) - v3
- laravel/dusk (DUSK) - v8
- laravel/pint (PINT) - v1
- laravel/telescope (TELESCOPE) - v5
- pestphp/pest (PEST) - v3
- phpunit/phpunit (PHPUNIT) - v11
- rector/rector (RECTOR) - v2
- laravel-echo (ECHO) - v2
- tailwindcss (TAILWINDCSS) - v4
- vue (VUE) - v3
### Testing Requirements
- **Unit tests**: No database, use mocking, run with `./vendor/bin/pest tests/Unit`
- **Feature tests**: Can use database, run with `docker exec coolify php artisan test`
- Every change must have tests
- Use Pest for all tests
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
### Livewire & Frontend
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure - don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
=== boost rules ===
## Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters.
## URLs
- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port.
## Tinker / Debugging
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
- Use the `database-query` tool when you only need to read from the database.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
- You can and should pass multiple queries at once. The most relevant results will be returned first.
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit"
3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit"
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms
=== php rules ===
## PHP
- Always use curly braces for control structures, even if it has one line.
### Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
- Do not allow empty `__construct()` methods with zero parameters.
### Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
<code-snippet name="Explicit Return Types and Method Params" lang="php">
protected function isAccessible(User $user, ?string $path = null): bool
{
...
}
</code-snippet>
## Comments
- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
## PHPDoc Blocks
- Add useful array shape type definitions for arrays when appropriate.
## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
=== laravel/core rules ===
## Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
- If you're creating a generic PHP class, use `artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
### Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
### Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
### Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
### Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
### URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
### Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
### Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] <name>` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
### Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== laravel/v12 rules ===
## Laravel 12
- Use the `search-docs` tool to get version specific documentation.
- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure.
- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not to need migrate to the new Laravel structure unless the user explicitly requests that.
### Laravel 10 Structure
- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`.
- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure:
- Middleware registration happens in `app/Http/Kernel.php`
- Exception handling is in `app/Exceptions/Handler.php`
- Console commands and schedule register in `app/Console/Kernel.php`
- Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php`
### 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 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.
=== livewire/core rules ===
## Livewire Core
- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests.
- Use the `php artisan make:livewire [Posts\\CreatePost]` artisan command to create new components
- State should live on the server, with the UI reflecting it.
- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions.
## Livewire Best Practices
- Livewire components require a single root element.
- Use `wire:loading` and `wire:dirty` for delightful loading states.
- Add `wire:key` in loops:
```blade
@foreach ($items as $item)
<div wire:key="item-{{ $item->id }}">
{{ $item->name }}
</div>
@endforeach
```
- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects:
<code-snippet name="Lifecycle hook examples" lang="php">
public function mount(User $user) { $this->user = $user; }
public function updatedSearch() { $this->resetPage(); }
</code-snippet>
## Testing Livewire
<code-snippet name="Example Livewire component test" lang="php">
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1)
->assertSee(1)
->assertStatus(200);
</code-snippet>
<code-snippet name="Testing a Livewire component exists within a page" lang="php">
$this->get('/posts/create')
->assertSeeLivewire(CreatePost::class);
</code-snippet>
=== livewire/v3 rules ===
## Livewire 3
### Key Changes From Livewire 2
- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions.
- Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default.
- Components now use the `App\Livewire` namespace (not `App\Http\Livewire`).
- Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`).
- Use the `components.layouts.app` view as the typical layout path (not `layouts.app`).
### New Directives
- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples.
### Alpine
- Alpine is now included with Livewire, don't manually include Alpine.js.
- Plugins included with Alpine: persist, intersect, collapse, and focus.
### Lifecycle Hooks
- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring:
<code-snippet name="livewire:load example" lang="js">
document.addEventListener('livewire:init', function () {
Livewire.hook('request', ({ fail }) => {
if (fail && fail.status === 419) {
alert('Your session expired');
}
});
Livewire.hook('message.failed', (message, component) => {
console.error(message);
});
});
</code-snippet>
=== pint/core rules ===
## Laravel Pint Code Formatter
- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues.
=== pest/core rules ===
## Pest
### Testing
- If you need to verify a feature is working, write or update a Unit / Feature test.
### Pest Tests
- All tests must be written using Pest. Use `php artisan make:test --pest <name>`.
- 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:
<code-snippet name="Basic Pest Test Example" lang="php">
it('is true', function () {
expect(true)->toBeTrue();
});
</code-snippet>
### Running Tests
**IMPORTANT**: Always run tests in the correct environment based on database dependencies:
**Unit Tests (no database):**
- Run outside Docker: `./vendor/bin/pest tests/Unit`
- Run specific file: `./vendor/bin/pest tests/Unit/ProxyCustomCommandsTest.php`
- These tests use mocking and don't require PostgreSQL
**Feature Tests (with database):**
- Run inside Docker: `docker exec coolify php artisan test`
- Run specific file: `docker exec coolify php artisan test tests/Feature/ExampleTest.php`
- Filter by name: `docker exec coolify php artisan test --filter=testName`
- These tests require PostgreSQL and use factories/migrations
**General Guidelines:**
- Run the minimal number of tests using an appropriate filter before finalizing code edits
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite
- If you get database connection errors, you're running a Feature test outside Docker - move it inside
### Pest Assertions
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
<code-snippet name="Pest Example Asserting postJson Response" lang="php">
it('returns all', function () {
$response = $this->postJson('/api/docs', []);
$response->assertSuccessful();
});
</code-snippet>
### Mocking
- Mocking can be very helpful when appropriate.
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
- You can also create partial mocks using the same import or self method.
### Datasets
- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules.
<code-snippet name="Pest Dataset Example" lang="php">
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
</code-snippet>
=== tailwindcss/core rules ===
## Tailwind Core
- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own.
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..)
- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically
- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
### Spacing
- When listing items, use gap utilities for spacing, don't use margins.
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
<div class="flex gap-8">
<div>Superior</div>
<div>Michigan</div>
<div>Erie</div>
</div>
</code-snippet>
### Dark Mode
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
=== tailwindcss/v4 rules ===
## Tailwind 4
- Always use Tailwind CSS v4 - do not use the deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.
- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3:
<code-snippet name="Tailwind v4 Import Tailwind Diff" lang="diff"
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
</code-snippet>
### Replaced Utilities
- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement.
- Opacity values are still numeric.
| Deprecated | Replacement |
|------------+--------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |
=== tests rules ===
## Test Enforcement
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed.
- **For Unit tests**: Use `./vendor/bin/pest tests/Unit/YourTest.php` (runs outside Docker)
- **For Feature tests**: Use `docker exec coolify php artisan test --filter=YourTest` (runs inside Docker)
- Choose the correct test type based on database dependency:
- No database needed? → Unit test with mocking
- Database needed? → Feature test in Docker
</laravel-boost-guidelines>
- Livewire components require single root element
- Use `wire:model.live` for real-time updates
- Alpine.js included with Livewire
- Tailwind CSS 4.1.4 (use new utilities, not deprecated ones)
- Use `gap` utilities for spacing, not margins
Random other things you should remember:

View File

@@ -1,9 +1,13 @@
![Latest Release Version](https://img.shields.io/badge/dynamic/json?labelColor=grey&color=6366f1&label=Latest_released_version&url=https%3A%2F%2Fcdn.coollabs.io%2Fcoolify%2Fversions.json&query=coolify.v4.version&style=for-the-badge
)
<div align="center">
[![Bounty Issues](https://img.shields.io/static/v1?labelColor=grey&color=6366f1&label=Algora&message=%F0%9F%92%8E+Bounty+issues&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties/new)
# Coolify
An open-source & self-hostable Heroku / Netlify / Vercel alternative.
# About the Project
![Latest Release Version](https://img.shields.io/badge/dynamic/json?labelColor=grey&color=6366f1&label=Latest%20released%20version&url=https%3A%2F%2Fcdn.coollabs.io%2Fcoolify%2Fversions.json&query=coolify.v4.version&style=for-the-badge
) [![Bounty Issues](https://img.shields.io/static/v1?labelColor=grey&color=6366f1&label=Algora&message=%F0%9F%92%8E+Bounty+issues&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties/new)
</div>
## About the Project
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
@@ -15,7 +19,7 @@ No vendor lock-in, which means that all the configurations for your applications
For more information, take a look at our landing page at [coolify.io](https://coolify.io).
# Installation
## Installation
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
@@ -25,11 +29,11 @@ You can find the installation script source [here](./scripts/install.sh).
> [!NOTE]
> Please refer to the [docs](https://coolify.io/docs/installation) for more information about the installation.
# Support
## Support
Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
# Cloud
## Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io)
@@ -44,14 +48,14 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
- Better support
- Less maintenance for you
# Donations
## Donations
To stay completely free and open-source, with no feature behind the paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the project's future development.
[coolify.io/sponsorships](https://coolify.io/sponsorships)
Thank you so much!
## Big Sponsors
### Big Sponsors
* [23M](https://23m.com?ref=coolify.io) - Your experts for high-availability hosting solutions!
* [Algora](https://algora.io?ref=coolify.io) - Open source contribution platform
@@ -69,7 +73,6 @@ Thank you so much!
* [Dade2](https://dade2.net/?ref=coolify.io) - IT Consulting, Cloud Solutions & System Integration
* [Formbricks](https://formbricks.com?ref=coolify.io) - The open source feedback platform
* [GoldenVM](https://billing.goldenvm.com?ref=coolify.io) - Premium virtual machine hosting solutions
* [Gozunga](https://gozunga.com?ref=coolify.io) - Seriously Simple Cloud Infrastructure
* [Hetzner](http://htznr.li/CoolifyXHetzner) - Server, cloud, hosting, and data center solutions
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - Web hosting and VPS solutions
* [JobsCollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - 30,000+ remote jobs for developers
@@ -89,7 +92,7 @@ Thank you so much!
* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform
## Small Sponsors
### Small Sponsors
<a href="https://open-elements.com/?utm_source=coolify.io"><img width="60px" alt="OpenElements" src="https://github.com/OpenElements.png"/></a>
<a href="https://xaman.app/?utm_source=coolify.io"><img width="60px" alt="XamanApp" src="https://github.com/XamanApp.png"/></a>
@@ -142,7 +145,7 @@ Thank you so much!
...and many more at [GitHub Sponsors](https://github.com/sponsors/coollabsio)
# Recognitions
## Recognitions
<p>
<a href="https://news.ycombinator.com/item?id=26624341">
@@ -158,17 +161,17 @@ Thank you so much!
<a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
# Core Maintainers
## Core Maintainers
| Andras Bacsai | 🏔️ Peak |
|------------|------------|
| <img src="https://github.com/andrasbacsai.png" width="200px" alt="Andras Bacsai" /> | <img src="https://github.com/peaklabs-dev.png" width="200px" alt="peaklabs-dev" /> |
| <a href="https://github.com/andrasbacsai"><img src="https://api.iconify.design/devicon:github.svg" width="25px"></a> <a href="https://x.com/heyandras"><img src="https://api.iconify.design/devicon:twitter.svg" width="25px"></a> <a href="https://bsky.app/profile/heyandras.dev"><img src="https://api.iconify.design/simple-icons:bluesky.svg" width="25px"></a> | <a href="https://github.com/peaklabs-dev"><img src="https://api.iconify.design/devicon:github.svg" width="25px"></a> <a href="https://x.com/peaklabs_dev"><img src="https://api.iconify.design/devicon:twitter.svg" width="25px"></a> <a href="https://bsky.app/profile/peaklabs.dev"><img src="https://api.iconify.design/simple-icons:bluesky.svg" width="25px"></a> |
# Repo Activity
## Repo Activity
![Alt](https://repobeats.axiom.co/api/embed/eab1c8066f9c59d0ad37b76c23ebb5ccac4278ae.svg "Repobeats analytics image")
# Star History
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date)

View File

@@ -22,7 +22,7 @@ class RestartDatabase
if (! $server->isFunctional()) {
return 'Server is not functional';
}
StopDatabase::run($database);
StopDatabase::run($database, dockerCleanup: false);
return StartDatabase::run($database);
}

View File

@@ -105,7 +105,7 @@ class StartClickhouse
$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 --timeout=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker stop --time=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.'";

View File

@@ -192,7 +192,7 @@ class StartDragonfly
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 --timeout=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker stop --time=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.'";

View File

@@ -208,7 +208,7 @@ class StartKeydb
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 --timeout=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker stop --time=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.'";

View File

@@ -209,7 +209,7 @@ class StartMariadb
$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 --timeout=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker stop --time=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.'";

View File

@@ -260,7 +260,7 @@ class StartMongodb
$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 --timeout=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker stop --time=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) {

View File

@@ -210,7 +210,7 @@ class StartMysql
$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 --timeout=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker stop --time=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";

View File

@@ -223,7 +223,7 @@ class StartPostgresql
$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 --timeout=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker stop --time=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) {
@@ -231,8 +231,6 @@ class StartPostgresql
}
$this->commands[] = "echo 'Database started.'";
ray($this->commands);
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
}

View File

@@ -205,7 +205,7 @@ class StartRedis
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 --timeout=10 $container_name 2>/dev/null || true";
$this->commands[] = "docker stop --time=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.'";

View File

@@ -49,7 +49,7 @@ class StopDatabase
{
$server = $database->destination->server;
instant_remote_process(command: [
"docker stop --timeout=$timeout $containerName",
"docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
], server: $server, throwError: false);
}

View File

@@ -8,13 +8,17 @@ use App\Events\ServiceChecked;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Services\ContainerStatusAggregator;
use App\Traits\CalculatesExcludedStatus;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Lorisleiva\Actions\Concerns\AsAction;
class GetContainersStatus
{
use AsAction;
use CalculatesExcludedStatus;
public string $jobQueue = 'high';
@@ -28,6 +32,10 @@ class GetContainersStatus
protected ?Collection $applicationContainerStatuses;
protected ?Collection $applicationContainerRestartCounts;
protected ?Collection $serviceContainerStatuses;
public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null)
{
$this->containers = $containers;
@@ -95,11 +103,15 @@ class GetContainersStatus
$labels = data_get($container, 'Config.Labels');
}
$containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
$containerHealth = data_get($container, 'State.Health.Status');
if ($containerStatus === 'restarting') {
$containerStatus = "restarting ($containerHealth)";
$healthSuffix = $containerHealth ?? 'unknown';
$containerStatus = "restarting:$healthSuffix";
} elseif ($containerStatus === 'exited') {
// Keep as-is, no health suffix for exited containers
} else {
$containerStatus = "$containerStatus ($containerHealth)";
$healthSuffix = $containerHealth ?? 'unknown';
$containerStatus = "$containerStatus:$healthSuffix";
}
$labels = Arr::undot(format_docker_labels_to_json($labels));
$applicationId = data_get($labels, 'coolify.applicationId');
@@ -136,6 +148,18 @@ class GetContainersStatus
if ($containerName) {
$this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus);
}
// Track restart counts for applications
$restartCount = data_get($container, 'RestartCount', 0);
if (! isset($this->applicationContainerRestartCounts)) {
$this->applicationContainerRestartCounts = collect();
}
if (! $this->applicationContainerRestartCounts->has($applicationId)) {
$this->applicationContainerRestartCounts->put($applicationId, collect());
}
if ($containerName) {
$this->applicationContainerRestartCounts->get($applicationId)->put($containerName, $restartCount);
}
} else {
// Notify user that this container should not be there.
}
@@ -207,23 +231,34 @@ class GetContainersStatus
if ($serviceLabelId) {
$subType = data_get($labels, 'coolify.service.subType');
$subId = data_get($labels, 'coolify.service.subId');
$service = $services->where('id', $serviceLabelId)->first();
if (! $service) {
$parentService = $services->where('id', $serviceLabelId)->first();
if (! $parentService) {
continue;
}
// Store container status for aggregation
if (! isset($this->serviceContainerStatuses)) {
$this->serviceContainerStatuses = collect();
}
$key = $serviceLabelId.':'.$subType.':'.$subId;
if (! $this->serviceContainerStatuses->has($key)) {
$this->serviceContainerStatuses->put($key, collect());
}
$containerName = data_get($labels, 'com.docker.compose.service');
if ($containerName) {
$this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
}
// Mark service as found
if ($subType === 'application') {
$service = $service->applications()->where('id', $subId)->first();
$service = $parentService->applications()->where('id', $subId)->first();
} else {
$service = $service->databases()->where('id', $subId)->first();
$service = $parentService->databases()->where('id', $subId)->first();
}
if ($service) {
$foundServices[] = "$service->id-$service->name";
$statusFromDb = $service->status;
if ($statusFromDb !== $containerStatus) {
$service->update(['status' => $containerStatus]);
} else {
$service->update(['last_online_at' => now()]);
}
}
}
}
@@ -291,7 +326,24 @@ class GetContainersStatus
continue;
}
$application->update(['status' => 'exited']);
// If container was recently restarting (crash loop), keep it as degraded for a grace period
// This prevents false "exited" status during the brief moment between container removal and recreation
$recentlyRestarted = $application->restart_count > 0 &&
$application->last_restart_at &&
$application->last_restart_at->greaterThan(now()->subSeconds(30));
if ($recentlyRestarted) {
// Keep it as degraded if it was recently in a crash loop
$application->update(['status' => 'degraded:unhealthy']);
} else {
// Reset restart count when application exits completely
$application->update([
'status' => 'exited',
'restart_count' => 0,
'last_restart_at' => null,
'last_restart_type' => null,
]);
}
}
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) {
@@ -340,88 +392,144 @@ class GetContainersStatus
continue;
}
$aggregatedStatus = $this->aggregateApplicationStatus($application, $containerStatuses);
if ($aggregatedStatus) {
$statusFromDb = $application->status;
if ($statusFromDb !== $aggregatedStatus) {
$application->update(['status' => $aggregatedStatus]);
} else {
$application->update(['last_online_at' => now()]);
}
// Track restart counts first
$maxRestartCount = 0;
if (isset($this->applicationContainerRestartCounts) && $this->applicationContainerRestartCounts->has($applicationId)) {
$containerRestartCounts = $this->applicationContainerRestartCounts->get($applicationId);
$maxRestartCount = $containerRestartCounts->max() ?? 0;
}
// Wrap all database updates in a transaction to ensure consistency
DB::transaction(function () use ($application, $maxRestartCount, $containerStatuses) {
$previousRestartCount = $application->restart_count ?? 0;
if ($maxRestartCount > $previousRestartCount) {
// Restart count increased - this is a crash restart
$application->update([
'restart_count' => $maxRestartCount,
'last_restart_at' => now(),
'last_restart_type' => 'crash',
]);
// Send notification
$containerName = $application->name;
$projectUuid = data_get($application, 'environment.project.uuid');
$environmentName = data_get($application, 'environment.name');
$applicationUuid = data_get($application, 'uuid');
if ($projectUuid && $applicationUuid && $environmentName) {
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid;
} else {
$url = null;
}
}
// Aggregate status after tracking restart counts
$aggregatedStatus = $this->aggregateApplicationStatus($application, $containerStatuses, $maxRestartCount);
if ($aggregatedStatus) {
$statusFromDb = $application->status;
if ($statusFromDb !== $aggregatedStatus) {
$application->update(['status' => $aggregatedStatus]);
} else {
$application->update(['last_online_at' => now()]);
}
}
});
}
}
// Aggregate multi-container service statuses
$this->aggregateServiceContainerStatuses($services);
ServiceChecked::dispatch($this->server->team->id);
}
private function aggregateApplicationStatus($application, Collection $containerStatuses): ?string
private function aggregateApplicationStatus($application, Collection $containerStatuses, int $maxRestartCount = 0): ?string
{
// Parse docker compose to check for excluded containers
$dockerComposeRaw = data_get($application, 'docker_compose_raw');
$excludedContainers = collect();
if ($dockerComposeRaw) {
try {
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
$services = data_get($dockerCompose, 'services', []);
foreach ($services as $serviceName => $serviceConfig) {
// Check if container should be excluded
$excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
$restartPolicy = data_get($serviceConfig, 'restart', 'always');
if ($excludeFromHc || $restartPolicy === 'no') {
$excludedContainers->push($serviceName);
}
}
} catch (\Exception $e) {
// If we can't parse, treat all containers as included
}
}
$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
// Filter out excluded containers
$relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) {
return ! $excludedContainers->contains($containerName);
});
// If all containers are excluded, don't update status
// If all containers are excluded, calculate status from excluded containers
if ($relevantStatuses->isEmpty()) {
return null;
return $this->calculateExcludedStatusFromStrings($containerStatuses);
}
$hasRunning = false;
$hasRestarting = false;
$hasUnhealthy = false;
$hasExited = false;
// Use ContainerStatusAggregator service for state machine logic
$aggregator = new ContainerStatusAggregator;
foreach ($relevantStatuses as $status) {
if (str($status)->contains('restarting')) {
$hasRestarting = true;
} elseif (str($status)->contains('running')) {
$hasRunning = true;
if (str($status)->contains('unhealthy')) {
$hasUnhealthy = true;
return $aggregator->aggregateFromStrings($relevantStatuses, $maxRestartCount);
}
private function aggregateServiceContainerStatuses($services)
{
if (! isset($this->serviceContainerStatuses) || $this->serviceContainerStatuses->isEmpty()) {
return;
}
foreach ($this->serviceContainerStatuses as $key => $containerStatuses) {
// Parse key: serviceId:subType:subId
[$serviceId, $subType, $subId] = explode(':', $key);
$service = $services->where('id', $serviceId)->first();
if (! $service) {
continue;
}
// Get the service sub-resource (ServiceApplication or ServiceDatabase)
$subResource = null;
if ($subType === 'application') {
$subResource = $service->applications()->where('id', $subId)->first();
} elseif ($subType === 'database') {
$subResource = $service->databases()->where('id', $subId)->first();
}
if (! $subResource) {
continue;
}
// Parse docker compose from service to check for excluded containers
$dockerComposeRaw = data_get($service, 'docker_compose_raw');
$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
// Filter out excluded containers
$relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) {
return ! $excludedContainers->contains($containerName);
});
// If all containers are excluded, calculate status from excluded containers
if ($relevantStatuses->isEmpty()) {
$aggregatedStatus = $this->calculateExcludedStatusFromStrings($containerStatuses);
if ($aggregatedStatus) {
$statusFromDb = $subResource->status;
if ($statusFromDb !== $aggregatedStatus) {
$subResource->update(['status' => $aggregatedStatus]);
} else {
$subResource->update(['last_online_at' => now()]);
}
}
continue;
}
// Use ContainerStatusAggregator service for state machine logic
$aggregator = new ContainerStatusAggregator;
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses);
// Update service sub-resource status with aggregated result
if ($aggregatedStatus) {
$statusFromDb = $subResource->status;
if ($statusFromDb !== $aggregatedStatus) {
$subResource->update(['status' => $aggregatedStatus]);
} else {
$subResource->update(['last_online_at' => now()]);
}
} elseif (str($status)->contains('exited')) {
$hasExited = true;
$hasUnhealthy = true;
}
}
if ($hasRestarting) {
return 'degraded (unhealthy)';
}
if ($hasRunning && $hasExited) {
return 'degraded (unhealthy)';
}
if ($hasRunning) {
return $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)';
}
// All containers are exited
return 'exited (unhealthy)';
}
}

View File

@@ -13,7 +13,7 @@ class StartProxy
{
use AsAction;
public function handle(Server $server, bool $async = true, bool $force = false): string|Activity
public function handle(Server $server, bool $async = true, bool $force = false, bool $restarting = false): string|Activity
{
$proxyType = $server->proxyType();
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) {
@@ -22,7 +22,10 @@ class StartProxy
$server->proxy->set('status', 'starting');
$server->save();
$server->refresh();
ProxyStatusChangedUI::dispatch($server->team_id);
if (! $restarting) {
ProxyStatusChangedUI::dispatch($server->team_id);
}
$commands = collect([]);
$proxy_path = $server->proxyPath();
@@ -60,7 +63,16 @@ class StartProxy
'docker compose pull',
'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then',
" echo 'Stopping and removing existing coolify-proxy.'",
' docker rm -f coolify-proxy || true',
' docker stop coolify-proxy 2>/dev/null || true',
' docker rm -f coolify-proxy 2>/dev/null || true',
' # Wait for container to be fully removed',
' for i in {1..10}; do',
' if ! docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then',
' break',
' fi',
' echo "Waiting for coolify-proxy to be removed... ($i/10)"',
' sleep 1',
' done',
" echo 'Successfully stopped and removed existing coolify-proxy.'",
'fi',
"echo 'Starting coolify-proxy.'",

View File

@@ -12,17 +12,27 @@ class StopProxy
{
use AsAction;
public function handle(Server $server, bool $forceStop = true, int $timeout = 30)
public function handle(Server $server, bool $forceStop = true, int $timeout = 30, bool $restarting = false)
{
try {
$containerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
$server->proxy->status = 'stopping';
$server->save();
ProxyStatusChangedUI::dispatch($server->team_id);
if (! $restarting) {
ProxyStatusChangedUI::dispatch($server->team_id);
}
instant_remote_process(command: [
"docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
"docker stop --time=$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',
" if ! docker ps -a --format \"{{.Names}}\" | grep -q \"^$containerName$\"; then",
' break',
' fi',
' sleep 1',
'done',
], server: $server, throwError: false);
$server->proxy->force_stop = $forceStop;
@@ -32,7 +42,10 @@ class StopProxy
return handleError($e);
} finally {
ProxyDashboardCacheService::clearCache($server);
ProxyStatusChanged::dispatch($server->id);
if (! $restarting) {
ProxyStatusChanged::dispatch($server->id);
}
}
}
}

View File

@@ -20,7 +20,7 @@ class CleanupDocker
$realtimeImageWithoutPrefix = 'coollabsio/coolify-realtime';
$realtimeImageWithoutPrefixVersion = "coollabsio/coolify-realtime:$realtimeImageVersion";
$helperImageVersion = data_get($settings, 'helper_version');
$helperImageVersion = getHelperVersion();
$helperImage = config('constants.coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion";
$helperImageWithoutPrefix = 'coollabsio/coolify-helper';

View File

@@ -59,8 +59,6 @@ class InstallDocker
$command = collect([]);
if (isDev() && $server->id === 0) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'sleep 1',
"echo 'Installing Docker Engine...'",
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
'sleep 4',
@@ -70,35 +68,6 @@ class InstallDocker
return remote_process($command, $server);
} else {
if ($supported_os_type->contains('debian')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'apt-get update -y',
'command -v curl >/dev/null || apt install -y curl',
'command -v wget >/dev/null || apt install -y wget',
'command -v git >/dev/null || apt install -y git',
'command -v jq >/dev/null || apt install -y jq',
]);
} elseif ($supported_os_type->contains('rhel')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'command -v curl >/dev/null || dnf install -y curl',
'command -v wget >/dev/null || dnf install -y wget',
'command -v git >/dev/null || dnf install -y git',
'command -v jq >/dev/null || dnf install -y jq',
]);
} elseif ($supported_os_type->contains('sles')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'zypper update -y',
'command -v curl >/dev/null || zypper install -y curl',
'command -v wget >/dev/null || zypper install -y wget',
'command -v git >/dev/null || zypper install -y git',
'command -v jq >/dev/null || zypper install -y jq',
]);
} else {
throw new \Exception('Unsupported OS');
}
$command = $command->merge([
"echo 'Installing Docker Engine...'",
]);

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class InstallPrerequisites
{
use AsAction;
public string $jobQueue = 'high';
public function handle(Server $server)
{
$supported_os_type = $server->validateOS();
if (! $supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install prerequisites manually.');
}
$command = collect([]);
if ($supported_os_type->contains('debian')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'apt-get update -y',
'command -v curl >/dev/null || apt install -y curl',
'command -v wget >/dev/null || apt install -y wget',
'command -v git >/dev/null || apt install -y git',
'command -v jq >/dev/null || apt install -y jq',
]);
} elseif ($supported_os_type->contains('rhel')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'command -v curl >/dev/null || dnf install -y curl',
'command -v wget >/dev/null || dnf install -y wget',
'command -v git >/dev/null || dnf install -y git',
'command -v jq >/dev/null || dnf install -y jq',
]);
} elseif ($supported_os_type->contains('sles')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
'zypper update -y',
'command -v curl >/dev/null || zypper install -y curl',
'command -v wget >/dev/null || zypper install -y wget',
'command -v git >/dev/null || zypper install -y git',
'command -v jq >/dev/null || zypper install -y jq',
]);
} else {
throw new \Exception('Unsupported OS type for prerequisites installation');
}
$command->push("echo 'Prerequisites installed successfully.'");
return remote_process($command, $server);
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Actions\Server;
use App\Jobs\PullHelperImageJob;
use App\Models\Server;
use Illuminate\Support\Sleep;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -50,7 +49,9 @@ class UpdateCoolify
private function update()
{
PullHelperImageJob::dispatch($this->server);
$helperImage = config('constants.coolify.helper_image');
$latest_version = getHelperVersion();
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
instant_remote_process(["docker pull -q $image"], $this->server, false);

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class ValidatePrerequisites
{
use AsAction;
public string $jobQueue = 'high';
/**
* Validate that required commands are available on the server.
*
* @return array{success: bool, missing: array<string>, found: array<string>}
*/
public function handle(Server $server): array
{
$requiredCommands = ['git', 'curl', 'jq'];
$missing = [];
$found = [];
foreach ($requiredCommands as $cmd) {
$result = instant_remote_process(["command -v {$cmd}"], $server, false);
if (! $result) {
$missing[] = $cmd;
} else {
$found[] = $cmd;
}
}
return [
'success' => empty($missing),
'missing' => $missing,
'found' => $found,
];
}
}

View File

@@ -45,6 +45,16 @@ class ValidateServer
throw new \Exception($this->error);
}
$validationResult = $server->validatePrerequisites();
if (! $validationResult['success']) {
$missingCommands = implode(', ', $validationResult['missing']);
$this->error = "Prerequisites ({$missingCommands}) are not installed. Please install them before continuing or use the validation with installation endpoint.";
$server->update([
'validation_logs' => $this->error,
]);
throw new \Exception($this->error);
}
$this->docker_installed = $server->validateDockerEngine();
$this->docker_compose_installed = $server->validateDockerCompose();
if (! $this->docker_installed || ! $this->docker_compose_installed) {

View File

@@ -20,18 +20,23 @@ class StartService
}
$service->saveComposeConfigs();
$service->isConfigurationChanged(save: true);
$commands[] = 'cd '.$service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
$workdir = $service->workdir();
// $commands[] = "cd {$workdir}";
$commands[] = "echo 'Saved configuration files to {$workdir}.'";
// Ensure .env exists in the correct directory before docker compose tries to load it
// This is defensive programming - saveComposeConfigs() already creates it,
// but we guarantee it here in case of any edge cases or manual deployments
$commands[] = "touch {$workdir}/.env";
if ($pullLatestImages) {
$commands[] = "echo 'Pulling images.'";
$commands[] = 'docker compose pull';
$commands[] = "docker compose --project-directory {$workdir} pull";
}
if ($service->networks()->count() > 0) {
$commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
}
$commands[] = 'echo Starting service.';
$commands[] = 'docker compose up -d --remove-orphans --force-recreate --build';
$commands[] = "docker compose --project-directory {$workdir} -f {$workdir}/docker-compose.yml --project-name {$service->uuid} up -d --remove-orphans --force-recreate --build";
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
if (data_get($service, 'connect_to_docker_network')) {
$compose = data_get($service, 'docker_compose', []);

View File

@@ -3,11 +3,14 @@
namespace App\Actions\Shared;
use App\Models\Application;
use App\Services\ContainerStatusAggregator;
use App\Traits\CalculatesExcludedStatus;
use Lorisleiva\Actions\Concerns\AsAction;
class ComplexStatusCheck
{
use AsAction;
use CalculatesExcludedStatus;
public function handle(Application $application)
{
@@ -17,11 +20,11 @@ class ComplexStatusCheck
$is_main_server = $application->destination->server->id === $server->id;
if (! $server->isFunctional()) {
if ($is_main_server) {
$application->update(['status' => 'exited:unhealthy']);
$application->update(['status' => 'exited']);
continue;
} else {
$application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']);
$application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited']);
continue;
}
@@ -46,11 +49,11 @@ class ComplexStatusCheck
}
} else {
if ($is_main_server) {
$application->update(['status' => 'exited:unhealthy']);
$application->update(['status' => 'exited']);
continue;
} else {
$application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']);
$application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited']);
continue;
}
@@ -61,74 +64,25 @@ class ComplexStatusCheck
private function aggregateContainerStatuses($application, $containers)
{
$dockerComposeRaw = data_get($application, 'docker_compose_raw');
$excludedContainers = collect();
$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
if ($dockerComposeRaw) {
try {
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
$services = data_get($dockerCompose, 'services', []);
foreach ($services as $serviceName => $serviceConfig) {
$excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
$restartPolicy = data_get($serviceConfig, 'restart', 'always');
if ($excludeFromHc || $restartPolicy === 'no') {
$excludedContainers->push($serviceName);
}
}
} catch (\Exception $e) {
// If we can't parse, treat all containers as included
}
}
$hasRunning = false;
$hasRestarting = false;
$hasUnhealthy = false;
$hasExited = false;
$relevantContainerCount = 0;
foreach ($containers as $container) {
// Filter non-excluded containers
$relevantContainers = collect($containers)->filter(function ($container) use ($excludedContainers) {
$labels = data_get($container, 'Config.Labels', []);
$serviceName = data_get($labels, 'com.docker.compose.service');
if ($serviceName && $excludedContainers->contains($serviceName)) {
continue;
}
return ! ($serviceName && $excludedContainers->contains($serviceName));
});
$relevantContainerCount++;
$containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
if ($containerStatus === 'restarting') {
$hasRestarting = true;
$hasUnhealthy = true;
} elseif ($containerStatus === 'running') {
$hasRunning = true;
if ($containerHealth === 'unhealthy') {
$hasUnhealthy = true;
}
} elseif ($containerStatus === 'exited') {
$hasExited = true;
$hasUnhealthy = true;
}
// If all containers are excluded, calculate status from excluded containers
// but mark it with :excluded to indicate monitoring is disabled
if ($relevantContainers->isEmpty()) {
return $this->calculateExcludedStatus($containers, $excludedContainers);
}
if ($relevantContainerCount === 0) {
return 'running:healthy';
}
// Use ContainerStatusAggregator service for state machine logic
$aggregator = new ContainerStatusAggregator;
if ($hasRestarting) {
return 'degraded:unhealthy';
}
if ($hasRunning && $hasExited) {
return 'degraded:unhealthy';
}
if ($hasRunning) {
return $hasUnhealthy ? 'running:unhealthy' : 'running:healthy';
}
return 'exited:unhealthy';
return $aggregator->aggregateFromContainers($relevantContainers);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Console\Commands;
use App\Jobs\CheckTraefikVersionJob;
use Illuminate\Console\Command;
class CheckTraefikVersionCommand extends Command
{
protected $signature = 'traefik:check-version';
protected $description = 'Check Traefik proxy versions on all servers and send notifications for outdated versions';
public function handle(): int
{
$this->info('Checking Traefik versions on all servers...');
try {
CheckTraefikVersionJob::dispatch();
$this->info('Traefik version check job dispatched successfully.');
$this->info('Notifications will be sent to teams with outdated Traefik versions.');
return Command::SUCCESS;
} catch (\Exception $e) {
$this->error('Failed to dispatch Traefik version check job: '.$e->getMessage());
return Command::FAILURE;
}
}
}

View File

@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Redis;
class CleanupRedis extends Command
{
protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup} {--clear-locks : Clear stale WithoutOverlapping locks}';
protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup} {--clear-locks : Clear stale WithoutOverlapping locks} {--restart : Aggressive cleanup mode for system restart (marks all processing jobs as failed)}';
protected $description = 'Cleanup Redis (Horizon jobs, metrics, overlapping queues, cache locks, and related data)';
@@ -63,6 +63,14 @@ class CleanupRedis extends Command
$deletedCount += $locksCleaned;
}
// 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");
} else {
@@ -332,4 +340,130 @@ class CleanupRedis extends Command
return $cleanedCount;
}
/**
* Clean up stuck jobs based on mode (restart vs runtime).
*
* @param mixed $redis Redis connection
* @param string $prefix Horizon prefix
* @param bool $dryRun Dry run mode
* @param bool $isRestart Restart mode (aggressive) vs runtime mode (conservative)
* @return int Number of jobs cleaned
*/
private function cleanupStuckJobs($redis, string $prefix, bool $dryRun, bool $isRestart): int
{
$cleanedCount = 0;
$now = time();
// Get all keys with the horizon prefix
$cursor = 0;
$keys = [];
do {
$result = $redis->scan($cursor, ['match' => '*', 'count' => 100]);
// Guard against scan() returning false
if ($result === false) {
$this->error('Redis scan failed, stopping key retrieval');
break;
}
$cursor = $result[0];
$keys = array_merge($keys, $result[1]);
} while ($cursor !== 0);
foreach ($keys as $key) {
$keyWithoutPrefix = str_replace($prefix, '', $key);
$type = $redis->command('type', [$keyWithoutPrefix]);
// Only process hash-type keys (individual jobs)
if ($type !== 5) {
continue;
}
$data = $redis->command('hgetall', [$keyWithoutPrefix]);
$status = data_get($data, 'status');
$payload = data_get($data, 'payload');
// Only process jobs in "processing" or "reserved" state
if (! in_array($status, ['processing', 'reserved'])) {
continue;
}
// Parse job payload to get job class and started time
$payloadData = json_decode($payload, true);
// Check for JSON decode errors
if ($payloadData === null || json_last_error() !== JSON_ERROR_NONE) {
$errorMsg = json_last_error_msg();
$truncatedPayload = is_string($payload) ? substr($payload, 0, 200) : 'non-string payload';
$this->error("Failed to decode job payload for {$keyWithoutPrefix}: {$errorMsg}. Payload: {$truncatedPayload}");
continue;
}
$jobClass = data_get($payloadData, 'displayName', 'Unknown');
// Prefer reserved_at (when job started processing), fallback to created_at
$reservedAt = (int) data_get($data, 'reserved_at', 0);
$createdAt = (int) data_get($data, 'created_at', 0);
$startTime = $reservedAt ?: $createdAt;
// If we can't determine when the job started, skip it
if (! $startTime) {
continue;
}
// Calculate how long the job has been processing
$processingTime = $now - $startTime;
$shouldFail = false;
$reason = '';
if ($isRestart) {
// RESTART MODE: Mark ALL processing/reserved jobs as failed
// Safe because all workers are dead on restart
$shouldFail = true;
$reason = 'System restart - all workers terminated';
} else {
// RUNTIME MODE: Only mark truly stuck jobs as failed
// Be conservative to avoid killing legitimate long-running jobs
// Skip ApplicationDeploymentJob entirely (has dynamic_timeout, can run 2+ hours)
if (str_contains($jobClass, 'ApplicationDeploymentJob')) {
continue;
}
// Skip DatabaseBackupJob (large backups can take hours)
if (str_contains($jobClass, 'DatabaseBackupJob')) {
continue;
}
// For other jobs, only fail if processing > 12 hours
if ($processingTime > 43200) { // 12 hours
$shouldFail = true;
$reason = 'Processing for more than 12 hours';
}
}
if ($shouldFail) {
if ($dryRun) {
$this->warn(" Would mark as FAILED: {$jobClass} (processing for ".round($processingTime / 60, 1)." min) - {$reason}");
} else {
// Mark job as failed
$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;
}
}

View File

@@ -222,9 +222,14 @@ class CleanupStuckedResources extends Command
try {
$scheduled_backups = ScheduledDatabaseBackup::all();
foreach ($scheduled_backups as $scheduled_backup) {
if (! $scheduled_backup->server()) {
echo "Deleting stuck scheduledbackup: {$scheduled_backup->name}\n";
$scheduled_backup->delete();
try {
$server = $scheduled_backup->server();
if (! $server) {
echo "Deleting stuck scheduledbackup: {$scheduled_backup->name}\n";
$scheduled_backup->delete();
}
} catch (\Throwable $e) {
echo "Error checking server for scheduledbackup {$scheduled_backup->id}: {$e->getMessage()}\n";
}
}
} catch (\Throwable $e) {
@@ -416,7 +421,7 @@ class CleanupStuckedResources extends Command
foreach ($serviceApplications as $service) {
if (! data_get($service, 'service')) {
echo 'ServiceApplication without service: '.$service->name.'\n';
DeleteResourceJob::dispatch($service);
$service->forceDelete();
continue;
}
@@ -429,7 +434,7 @@ class CleanupStuckedResources extends Command
foreach ($serviceDatabases as $service) {
if (! data_get($service, 'service')) {
echo 'ServiceDatabase without service: '.$service->name.'\n';
DeleteResourceJob::dispatch($service);
$service->forceDelete();
continue;
}

View File

@@ -0,0 +1,219 @@
<?php
namespace App\Console\Commands\Cloud;
use Illuminate\Console\Command;
class RestoreDatabase extends Command
{
protected $signature = 'cloud:restore-database {file : Path to the database dump file} {--debug : Show detailed debug output}';
protected $description = 'Restore a PostgreSQL database from a dump file (development mode only)';
private bool $debug = false;
public function handle(): int
{
$this->debug = $this->option('debug');
if (! $this->isDevelopment()) {
$this->error('This command can only be run in development mode.');
return 1;
}
$filePath = $this->argument('file');
if (! file_exists($filePath)) {
$this->error("File not found: {$filePath}");
return 1;
}
if (! is_readable($filePath)) {
$this->error("File is not readable: {$filePath}");
return 1;
}
try {
$this->info('Starting database restoration...');
$database = config('database.connections.pgsql.database');
$host = config('database.connections.pgsql.host');
$port = config('database.connections.pgsql.port');
$username = config('database.connections.pgsql.username');
$password = config('database.connections.pgsql.password');
if (! $database || ! $username) {
$this->error('Database configuration is incomplete.');
return 1;
}
$this->info("Restoring to database: {$database}");
// Drop all tables
if (! $this->dropAllTables($database, $host, $port, $username, $password)) {
return 1;
}
// Restore the database dump
if (! $this->restoreDatabaseDump($filePath, $database, $host, $port, $username, $password)) {
return 1;
}
$this->info('Database restoration completed successfully!');
return 0;
} catch (\Exception $e) {
$this->error("An error occurred: {$e->getMessage()}");
return 1;
}
}
private function dropAllTables(string $database, string $host, string $port, string $username, string $password): bool
{
$this->info('Dropping all tables...');
// SQL to drop all tables
$dropTablesSQL = <<<'SQL'
DO $$ DECLARE
r RECORD;
BEGIN
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
END $$;
SQL;
// Build the psql command to drop all tables
$command = sprintf(
'PGPASSWORD=%s psql -h %s -p %s -U %s -d %s -c %s',
escapeshellarg($password),
escapeshellarg($host),
escapeshellarg($port),
escapeshellarg($username),
escapeshellarg($database),
escapeshellarg($dropTablesSQL)
);
if ($this->debug) {
$this->line('<comment>Executing drop command:</comment>');
$this->line($command);
}
$output = shell_exec($command.' 2>&1');
if ($this->debug) {
$this->line("<comment>Output:</comment> {$output}");
}
$this->info('All tables dropped successfully.');
return true;
}
private function restoreDatabaseDump(string $filePath, string $database, string $host, string $port, string $username, string $password): bool
{
$this->info('Restoring database from dump file...');
// Handle gzipped files by decompressing first
$actualFile = $filePath;
if (str_ends_with($filePath, '.gz')) {
$actualFile = rtrim($filePath, '.gz');
$this->info('Decompressing gzipped dump file...');
$decompressCommand = sprintf(
'gunzip -c %s > %s',
escapeshellarg($filePath),
escapeshellarg($actualFile)
);
if ($this->debug) {
$this->line('<comment>Executing decompress command:</comment>');
$this->line($decompressCommand);
}
$decompressOutput = shell_exec($decompressCommand.' 2>&1');
if ($this->debug && $decompressOutput) {
$this->line("<comment>Decompress output:</comment> {$decompressOutput}");
}
}
// Use pg_restore for custom format dumps
$command = sprintf(
'PGPASSWORD=%s pg_restore -h %s -p %s -U %s -d %s -v %s',
escapeshellarg($password),
escapeshellarg($host),
escapeshellarg($port),
escapeshellarg($username),
escapeshellarg($database),
escapeshellarg($actualFile)
);
if ($this->debug) {
$this->line('<comment>Executing restore command:</comment>');
$this->line($command);
}
// Execute the restore command
$process = proc_open(
$command,
[
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
],
$pipes
);
if (! is_resource($process)) {
$this->error('Failed to start restoration process.');
return false;
}
$output = stream_get_contents($pipes[1]);
$error = stream_get_contents($pipes[2]);
$exitCode = proc_close($process);
// Clean up decompressed file if we created one
if ($actualFile !== $filePath && file_exists($actualFile)) {
unlink($actualFile);
}
if ($this->debug) {
if ($output) {
$this->line('<comment>Output:</comment>');
$this->line($output);
}
if ($error) {
$this->line('<comment>Error output:</comment>');
$this->line($error);
}
$this->line("<comment>Exit code:</comment> {$exitCode}");
}
if ($exitCode !== 0) {
$this->error("Restoration failed with exit code: {$exitCode}");
if ($error) {
$this->error('Error details:');
$this->error($error);
}
return false;
}
if ($output && ! $this->debug) {
$this->line($output);
}
return true;
}
private function isDevelopment(): bool
{
return app()->environment(['local', 'development', 'dev']);
}
}

View File

@@ -4,6 +4,9 @@ namespace App\Console\Commands;
use App\Jobs\CheckHelperImageJob;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\ScheduledTaskExecution;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
@@ -45,6 +48,44 @@ class Dev extends Command
} else {
echo "Instance already initialized.\n";
}
// Clean up stuck jobs and stale locks on development startup
try {
echo "Cleaning up Redis (stuck jobs and stale locks)...\n";
Artisan::call('cleanup:redis', ['--restart' => true, '--clear-locks' => true]);
echo "Redis cleanup completed.\n";
} catch (\Throwable $e) {
echo "Error in cleanup:redis: {$e->getMessage()}\n";
}
try {
$updatedTaskCount = ScheduledTaskExecution::where('status', 'running')->update([
'status' => 'failed',
'message' => 'Marked as failed during Coolify startup - job was interrupted',
'finished_at' => Carbon::now(),
]);
if ($updatedTaskCount > 0) {
echo "Marked {$updatedTaskCount} stuck scheduled task executions as failed\n";
}
} catch (\Throwable $e) {
echo "Could not cleanup stuck scheduled task executions: {$e->getMessage()}\n";
}
try {
$updatedBackupCount = ScheduledDatabaseBackupExecution::where('status', 'running')->update([
'status' => 'failed',
'message' => 'Marked as failed during Coolify startup - job was interrupted',
'finished_at' => Carbon::now(),
]);
if ($updatedBackupCount > 0) {
echo "Marked {$updatedBackupCount} stuck database backup executions as failed\n";
}
} catch (\Throwable $e) {
echo "Could not cleanup stuck database backup executions: {$e->getMessage()}\n";
}
CheckHelperImageJob::dispatch();
}
}

View File

@@ -167,7 +167,7 @@ class Emails extends Command
]);
}
$output = 'Because of an error, the backup of the database '.$db->name.' failed.';
$this->mail = (new BackupFailed($backup, $db, $output))->toMail();
$this->mail = (new BackupFailed($backup, $db, $output, $backup->database_name ?? 'unknown'))->toMail();
$this->sendEmail();
break;
case 'backup-success':

View File

@@ -10,9 +10,12 @@ use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\ScheduledTaskExecution;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
@@ -73,7 +76,7 @@ class Init extends Command
$this->cleanupUnusedNetworkFromCoolifyProxy();
try {
$this->call('cleanup:redis', ['--clear-locks' => true]);
$this->call('cleanup:redis', ['--restart' => true, '--clear-locks' => true]);
} catch (\Throwable $e) {
echo "Error in cleanup:redis command: {$e->getMessage()}\n";
}
@@ -86,6 +89,7 @@ class Init extends Command
$this->call('cleanup:stucked-resources');
} catch (\Throwable $e) {
echo "Error in cleanup:stucked-resources command: {$e->getMessage()}\n";
echo "Continuing with initialization - cleanup errors will not prevent Coolify from starting\n";
}
try {
$updatedCount = ApplicationDeploymentQueue::whereIn('status', [
@@ -102,6 +106,34 @@ class Init extends Command
echo "Could not cleanup inprogress deployments: {$e->getMessage()}\n";
}
try {
$updatedTaskCount = ScheduledTaskExecution::where('status', 'running')->update([
'status' => 'failed',
'message' => 'Marked as failed during Coolify startup - job was interrupted',
'finished_at' => Carbon::now(),
]);
if ($updatedTaskCount > 0) {
echo "Marked {$updatedTaskCount} stuck scheduled task executions as failed\n";
}
} catch (\Throwable $e) {
echo "Could not cleanup stuck scheduled task executions: {$e->getMessage()}\n";
}
try {
$updatedBackupCount = ScheduledDatabaseBackupExecution::where('status', 'running')->update([
'status' => 'failed',
'message' => 'Marked as failed during Coolify startup - job was interrupted',
'finished_at' => Carbon::now(),
]);
if ($updatedBackupCount > 0) {
echo "Marked {$updatedBackupCount} stuck database backup executions as failed\n";
}
} catch (\Throwable $e) {
echo "Could not cleanup stuck database backup executions: {$e->getMessage()}\n";
}
try {
$localhost = $this->servers->where('id', 0)->first();
if ($localhost) {

View File

@@ -26,9 +26,9 @@ class SyncBunny extends Command
protected $description = 'Sync files to BunnyCDN';
/**
* Fetch GitHub releases and sync to CDN
* Fetch GitHub releases and sync to GitHub repository
*/
private function syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn)
private function syncReleasesToGitHubRepo(): bool
{
$this->info('Fetching releases from GitHub...');
try {
@@ -37,33 +37,122 @@ class SyncBunny extends Command
'per_page' => 30, // Fetch more releases for better changelog
]);
if ($response->successful()) {
$releases = $response->json();
// Save releases to a temporary file
$releases_file = "$parent_dir/releases.json";
file_put_contents($releases_file, json_encode($releases, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
// Upload to CDN
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: $releases_file)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/releases.json"),
$pool->purge("$bunny_cdn/coolify/releases.json"),
]);
// Clean up temporary file
unlink($releases_file);
$this->info('releases.json uploaded & purged...');
$this->info('Total releases synced: '.count($releases));
return true;
} else {
if (! $response->successful()) {
$this->error('Failed to fetch releases from GitHub: '.$response->status());
return false;
}
$releases = $response->json();
$timestamp = time();
$tmpDir = sys_get_temp_dir().'/coolify-cdn-'.$timestamp;
$branchName = 'update-releases-'.$timestamp;
// 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...');
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 releases.json
$this->info('Writing releases.json...');
$releasesPath = "$tmpDir/json/releases.json";
$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.');
exec('rm -rf '.escapeshellarg($tmpDir));
return false;
}
// Stage and commit
$this->info('Committing changes...');
exec('cd '.escapeshellarg($tmpDir).' && git add json/releases.json 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 json/releases.json 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('Releases are already up to date. No changes to commit.');
exec('rm -rf '.escapeshellarg($tmpDir));
return true;
}
$commitMessage = 'Update releases.json with latest releases - '.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...');
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 releases.json - '.date('Y-m-d H:i:s');
$prBody = 'Automated update of releases.json with latest '.count($releases).' releases from GitHub API';
$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 Output: '.implode("\n", $output));
}
$this->info('Total releases synced: '.count($releases));
return true;
} catch (\Throwable $e) {
$this->error('Error fetching releases: '.$e->getMessage());
$this->error('Error syncing releases: '.$e->getMessage());
return false;
}
@@ -174,11 +263,7 @@ class SyncBunny extends Command
return;
}
// First sync GitHub releases
$this->info('Syncing GitHub releases first...');
$this->syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn);
// Then sync versions.json
// Sync versions.json to BunnyCDN
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: $versions_location)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
@@ -187,14 +272,14 @@ class SyncBunny extends Command
return;
} elseif ($only_github_releases) {
$this->info('About to sync GitHub releases to BunnyCDN.');
$this->info('About to sync GitHub releases to GitHub repository.');
$confirmed = confirm('Are you sure you want to sync GitHub releases?');
if (! $confirmed) {
return;
}
// Use the reusable function
$this->syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn);
// Sync releases to GitHub repository
$this->syncReleasesToGitHubRepo();
return;
}

View File

@@ -5,6 +5,7 @@ namespace App\Console;
use App\Jobs\CheckAndStartSentinelJob;
use App\Jobs\CheckForUpdatesJob;
use App\Jobs\CheckHelperImageJob;
use App\Jobs\CheckTraefikVersionJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\PullChangelog;
use App\Jobs\PullTemplatesFromCDN;
@@ -83,6 +84,8 @@ class Kernel extends ConsoleKernel
$this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily();
$this->scheduleInstance->job(new CheckTraefikVersionJob)->weekly()->sundays()->at('00:00')->timezone($this->instanceTimezone)->onOneServer();
$this->scheduleInstance->command('cleanup:database --yes')->daily();
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
}

View File

@@ -10,6 +10,8 @@ class ServerMetadata extends Data
{
public function __construct(
public ?ProxyTypes $type,
public ?ProxyStatus $status
public ?ProxyStatus $status,
public ?string $last_saved_settings = null,
public ?string $last_applied_settings = null
) {}
}

View File

@@ -17,17 +17,23 @@ class RestoreJobFinished
$tmpPath = data_get($data, 'tmpPath');
$container = data_get($data, 'container');
$serverId = data_get($data, 'serverId');
if (filled($scriptPath) && filled($tmpPath) && filled($container) && filled($serverId)) {
if (str($tmpPath)->startsWith('/tmp/')
&& str($scriptPath)->startsWith('/tmp/')
&& ! str($tmpPath)->contains('..')
&& ! str($scriptPath)->contains('..')
&& strlen($tmpPath) > 5 // longer than just "/tmp/"
&& strlen($scriptPath) > 5
) {
$commands[] = "docker exec {$container} sh -c 'rm {$scriptPath}'";
$commands[] = "docker exec {$container} sh -c 'rm {$tmpPath}'";
instant_remote_process($commands, Server::find($serverId), throwError: true);
if (filled($container) && filled($serverId)) {
$commands = [];
if (isSafeTmpPath($scriptPath)) {
$commands[] = 'docker exec '.escapeshellarg($container)." sh -c 'rm ".escapeshellarg($scriptPath)." 2>/dev/null || true'";
}
if (isSafeTmpPath($tmpPath)) {
$commands[] = 'docker exec '.escapeshellarg($container)." sh -c 'rm ".escapeshellarg($tmpPath)." 2>/dev/null || true'";
}
if (! empty($commands)) {
$server = Server::find($serverId);
if ($server) {
instant_remote_process($commands, $server, throwError: false);
}
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Events;
use App\Models\Server;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class S3RestoreJobFinished
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct($data)
{
$containerName = data_get($data, 'containerName');
$serverTmpPath = data_get($data, 'serverTmpPath');
$scriptPath = data_get($data, 'scriptPath');
$containerTmpPath = data_get($data, 'containerTmpPath');
$container = data_get($data, 'container');
$serverId = data_get($data, 'serverId');
// Most cleanup now happens inline during restore process
// This acts as a safety net for edge cases (errors, interruptions)
if (filled($serverId)) {
$commands = [];
// Ensure helper container is removed (may already be gone from inline cleanup)
if (filled($containerName)) {
$commands[] = 'docker rm -f '.escapeshellarg($containerName).' 2>/dev/null || true';
}
// Clean up server temp file if still exists (should already be cleaned)
if (isSafeTmpPath($serverTmpPath)) {
$commands[] = 'rm -f '.escapeshellarg($serverTmpPath).' 2>/dev/null || true';
}
// Clean up any remaining files in database container (may already be cleaned)
if (filled($container)) {
if (isSafeTmpPath($containerTmpPath)) {
$commands[] = 'docker exec '.escapeshellarg($container).' rm -f '.escapeshellarg($containerTmpPath).' 2>/dev/null || true';
}
if (isSafeTmpPath($scriptPath)) {
$commands[] = 'docker exec '.escapeshellarg($container).' rm -f '.escapeshellarg($scriptPath).' 2>/dev/null || true';
}
}
if (! empty($commands)) {
$server = Server::find($serverId);
if ($server) {
instant_remote_process($commands, $server, throwError: false);
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Exceptions;
use Exception;
/**
* Exception for expected deployment failures caused by user/application errors.
* These are not Coolify bugs and should not be logged to laravel.log.
* Examples: Nixpacks detection failures, missing Dockerfiles, invalid configs, etc.
*/
class DeploymentException extends Exception
{
/**
* Create a new deployment exception instance.
*
* @param string $message
* @param int $code
*/
public function __construct($message = '', $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
/**
* Create from another exception, preserving its message and stack trace.
*/
public static function fromException(\Throwable $exception): static
{
return new static($exception->getMessage(), $exception->getCode(), $exception);
}
}

View File

@@ -30,6 +30,7 @@ class Handler extends ExceptionHandler
protected $dontReport = [
ProcessException::class,
NonReportableException::class,
DeploymentException::class,
];
/**

View File

@@ -246,6 +246,40 @@ class Github extends Controller
if ($action === 'closed') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
// Cancel any active deployments for this PR immediately
$activeDeployment = \App\Models\ApplicationDeploymentQueue::where('application_id', $application->id)
->where('pull_request_id', $pull_request_id)
->whereIn('status', [
\App\Enums\ApplicationDeploymentStatus::QUEUED->value,
\App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value,
])
->first();
if ($activeDeployment) {
try {
// Mark deployment as cancelled
$activeDeployment->update([
'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
// Add cancellation log entry
$activeDeployment->addLogEntry('Deployment cancelled: Pull request closed.', 'stderr');
// Check if helper container exists and kill it
$deployment_uuid = $activeDeployment->deployment_uuid;
$server = $application->destination->server;
$checkCommand = "docker ps -a --filter name={$deployment_uuid} --format '{{.Names}}'";
$containerExists = instant_remote_process([$checkCommand], $server);
if ($containerExists && str($containerExists)->trim()->isNotEmpty()) {
instant_remote_process(["docker rm -f {$deployment_uuid}"], $server);
$activeDeployment->addLogEntry('Deployment container stopped.');
}
} catch (\Throwable $e) {
// Silently handle errors during deployment cancellation
}
}
DeleteResourceJob::dispatch($found);
$return_payloads->push([
'application' => $application->name,
@@ -481,6 +515,42 @@ class Github extends Controller
if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
// Cancel any active deployments for this PR immediately
$activeDeployment = \App\Models\ApplicationDeploymentQueue::where('application_id', $application->id)
->where('pull_request_id', $pull_request_id)
->whereIn('status', [
\App\Enums\ApplicationDeploymentStatus::QUEUED->value,
\App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value,
])
->first();
if ($activeDeployment) {
try {
// Mark deployment as cancelled
$activeDeployment->update([
'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
// Add cancellation log entry
$activeDeployment->addLogEntry('Deployment cancelled: Pull request closed.', 'stderr');
// Check if helper container exists and kill it
$deployment_uuid = $activeDeployment->deployment_uuid;
$server = $application->destination->server;
$checkCommand = "docker ps -a --filter name={$deployment_uuid} --format '{{.Names}}'";
$containerExists = instant_remote_process([$checkCommand], $server);
if ($containerExists && str($containerExists)->trim()->isNotEmpty()) {
instant_remote_process(["docker rm -f {$deployment_uuid}"], $server);
$activeDeployment->addLogEntry('Deployment container stopped.');
}
} catch (\Throwable $e) {
// Silently handle errors during deployment cancellation
}
}
// Clean up any deployed containers
$containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pull_request_id);
if ($containers->isNotEmpty()) {
$containers->each(function ($container) use ($application) {

File diff suppressed because it is too large Load Diff

View File

@@ -33,6 +33,9 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
// New version available
$settings->update(['new_version_available' => true]);
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
// Invalidate cache to ensure fresh data is loaded
invalidate_versions_cache();
} else {
$settings->update(['new_version_available' => false]);
}

View File

@@ -0,0 +1,173 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use App\Notifications\Server\TraefikVersionOutdated;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CheckTraefikVersionForServerJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $timeout = 60;
/**
* Create a new job instance.
*/
public function __construct(
public Server $server,
public array $traefikVersions
) {}
/**
* Execute the job.
*/
public function handle(): void
{
// Detect current version (makes SSH call)
$currentVersion = getTraefikVersionFromDockerCompose($this->server);
// Update detected version in database
$this->server->update(['detected_traefik_version' => $currentVersion]);
if (! $currentVersion) {
return;
}
// Check if image tag is 'latest' by inspecting the image (makes SSH call)
$imageTag = instant_remote_process([
"docker inspect coolify-proxy --format '{{.Config.Image}}' 2>/dev/null",
], $this->server, false);
// Handle empty/null response from SSH command
if (empty(trim($imageTag))) {
return;
}
if (str_contains(strtolower(trim($imageTag)), ':latest')) {
return;
}
// Parse current version to extract major.minor.patch
$current = ltrim($currentVersion, 'v');
if (! preg_match('/^(\d+\.\d+)\.(\d+)$/', $current, $matches)) {
return;
}
$currentBranch = $matches[1]; // e.g., "3.6"
// Find the latest version for this branch
$latestForBranch = $this->traefikVersions["v{$currentBranch}"] ?? null;
if (! $latestForBranch) {
// User is on a branch we don't track - check if newer branches exist
$newerBranchInfo = $this->getNewerBranchInfo($currentBranch);
if ($newerBranchInfo) {
$this->storeOutdatedInfo($current, $newerBranchInfo['latest'], 'minor_upgrade', $newerBranchInfo['target']);
} else {
// No newer branch found, clear outdated info
$this->server->update(['traefik_outdated_info' => null]);
}
return;
}
// Compare patch version within the same branch
$latest = ltrim($latestForBranch, 'v');
// Always check for newer branches first
$newerBranchInfo = $this->getNewerBranchInfo($currentBranch);
if (version_compare($current, $latest, '<')) {
// Patch update available
$this->storeOutdatedInfo($current, $latest, 'patch_update', null, $newerBranchInfo);
} elseif ($newerBranchInfo) {
// Only newer branch available (no patch update)
$this->storeOutdatedInfo($current, $newerBranchInfo['latest'], 'minor_upgrade', $newerBranchInfo['target']);
} else {
// Fully up to date
$this->server->update(['traefik_outdated_info' => null]);
}
}
/**
* Get information about newer branches if available.
*/
private function getNewerBranchInfo(string $currentBranch): ?array
{
$newestBranch = null;
$newestVersion = null;
foreach ($this->traefikVersions as $branch => $version) {
$branchNum = ltrim($branch, 'v');
if (version_compare($branchNum, $currentBranch, '>')) {
if (! $newestVersion || version_compare($version, $newestVersion, '>')) {
$newestBranch = $branchNum;
$newestVersion = $version;
}
}
}
if ($newestVersion) {
return [
'target' => "v{$newestBranch}",
'latest' => ltrim($newestVersion, 'v'),
];
}
return null;
}
/**
* Store outdated information in database and send immediate notification.
*/
private function storeOutdatedInfo(string $current, string $latest, string $type, ?string $upgradeTarget = null, ?array $newerBranchInfo = null): void
{
$outdatedInfo = [
'current' => $current,
'latest' => $latest,
'type' => $type,
'checked_at' => now()->toIso8601String(),
];
// For minor upgrades, add the upgrade_target field (e.g., "v3.6")
if ($type === 'minor_upgrade' && $upgradeTarget) {
$outdatedInfo['upgrade_target'] = $upgradeTarget;
}
// If there's a newer branch available (even for patch updates), include that info
if ($newerBranchInfo) {
$outdatedInfo['newer_branch_target'] = $newerBranchInfo['target'];
$outdatedInfo['newer_branch_latest'] = $newerBranchInfo['latest'];
}
$this->server->update(['traefik_outdated_info' => $outdatedInfo]);
// Send immediate notification to the team
$this->sendNotification($outdatedInfo);
}
/**
* Send notification to team about outdated Traefik.
*/
private function sendNotification(array $outdatedInfo): void
{
// Attach the outdated info as a dynamic property for the notification
$this->server->outdatedInfo = $outdatedInfo;
// Get the team and send notification
$team = $this->server->team()->first();
if ($team) {
$team->notify(new TraefikVersionOutdated(collect([$this->server])));
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Jobs;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CheckTraefikVersionJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public function handle(): void
{
// Load versions from cached data
$traefikVersions = get_traefik_versions();
if (empty($traefikVersions)) {
return;
}
// Query all servers with Traefik proxy that are reachable
$servers = Server::whereNotNull('proxy')
->whereProxyType(ProxyTypes::TRAEFIK->value)
->whereRelation('settings', 'is_reachable', true)
->whereRelation('settings', 'is_usable', true)
->get();
if ($servers->isEmpty()) {
return;
}
// Dispatch individual server check jobs in parallel
// Each job will send immediate notifications when outdated Traefik is detected
foreach ($servers as $server) {
CheckTraefikVersionForServerJob::dispatch($server, $traefikVersions);
}
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Jobs;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -20,10 +22,51 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
public function handle(): void
{
try {
// Get all active deployments on this server
$activeDeployments = ApplicationDeploymentQueue::where('server_id', $this->server->id)
->whereIn('status', [
ApplicationDeploymentStatus::IN_PROGRESS->value,
ApplicationDeploymentStatus::QUEUED->value,
])
->pluck('deployment_uuid')
->toArray();
\Log::info('CleanupHelperContainersJob - Active deployments', [
'server' => $this->server->name,
'active_deployment_uuids' => $activeDeployments,
]);
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("'.config('constants.coolify.registry_url').'/coollabsio/coolify-helper")))\''], $this->server, false);
$containerIds = collect(json_decode($containers))->pluck('ID');
if ($containerIds->count() > 0) {
foreach ($containerIds as $containerId) {
$helperContainers = collect(json_decode($containers));
if ($helperContainers->count() > 0) {
foreach ($helperContainers as $container) {
$containerId = data_get($container, 'ID');
$containerName = data_get($container, 'Names');
// Check if this container belongs to an active deployment
$isActiveDeployment = false;
foreach ($activeDeployments as $deploymentUuid) {
if (str_contains($containerName, $deploymentUuid)) {
$isActiveDeployment = true;
break;
}
}
if ($isActiveDeployment) {
\Log::info('CleanupHelperContainersJob - Skipping active deployment container', [
'container' => $containerName,
'id' => $containerId,
]);
continue;
}
\Log::info('CleanupHelperContainersJob - Removing orphaned helper container', [
'container' => $containerName,
'id' => $containerId,
]);
instant_remote_process_with_timeout(['docker container rm -f '.$containerId], $this->server, false);
}
}

View File

@@ -3,18 +3,35 @@
namespace App\Jobs;
use App\Actions\CoolifyTask\RunRemoteProcess;
use App\Enums\ProcessStatus;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Spatie\Activitylog\Models\Activity;
class CoolifyTask implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*/
public $tries = 3;
/**
* The maximum number of unhandled exceptions to allow before failing.
*/
public $maxExceptions = 1;
/**
* The number of seconds the job can run before timing out.
*/
public $timeout = 600;
/**
* Create a new job instance.
*/
@@ -42,4 +59,53 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue
$remote_process();
}
/**
* Calculate the number of seconds to wait before retrying the job.
*/
public function backoff(): array
{
return [30, 90, 180]; // 30s, 90s, 180s between retries
}
/**
* Handle a job failure.
*/
public function failed(?\Throwable $exception): void
{
Log::channel('scheduled-errors')->error('CoolifyTask permanently failed', [
'job' => 'CoolifyTask',
'activity_id' => $this->activity->id,
'server_uuid' => $this->activity->getExtraProperty('server_uuid'),
'command_preview' => substr($this->activity->getExtraProperty('command') ?? '', 0, 200),
'error' => $exception?->getMessage(),
'total_attempts' => $this->attempts(),
'trace' => $exception?->getTraceAsString(),
]);
// Update activity status to reflect permanent failure
$this->activity->properties = $this->activity->properties->merge([
'status' => ProcessStatus::ERROR->value,
'error' => $exception?->getMessage() ?? 'Job permanently failed',
'failed_at' => now()->toIso8601String(),
]);
$this->activity->save();
// Dispatch cleanup event on failure (same as on success)
if ($this->call_event_on_finish) {
try {
$eventClass = "App\\Events\\$this->call_event_on_finish";
if (! is_null($this->call_event_data)) {
event(new $eventClass($this->call_event_data));
} else {
event(new $eventClass($this->activity->causer_id));
}
Log::info('Cleanup event dispatched after job failure', [
'event' => $this->call_event_on_finish,
]);
} catch (\Throwable $e) {
Log::error('Error dispatching cleanup event on failure: '.$e->getMessage());
}
}
}
}

View File

@@ -23,6 +23,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;
use Visus\Cuid2\Cuid2;
@@ -31,6 +32,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $maxExceptions = 1;
public ?Team $team = null;
public Server $server;
@@ -74,7 +77,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public ScheduledDatabaseBackup $backup)
{
$this->onQueue('high');
$this->timeout = $backup->timeout;
$this->timeout = $backup->timeout ?? 3600;
}
public function handle(): void
@@ -636,7 +639,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else {
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
}
$commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc alias set temporary {$endpoint} {$key} \"{$secret}\"";
// Escape S3 credentials to prevent command injection
$escapedEndpoint = escapeshellarg($endpoint);
$escapedKey = escapeshellarg($key);
$escapedSecret = escapeshellarg($secret);
$commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc alias set temporary {$escapedEndpoint} {$escapedKey} {$escapedSecret}";
$commands[] = "docker exec backup-of-{$this->backup_log_uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server);
@@ -653,24 +662,42 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
private function getFullImageName(): string
{
$settings = instanceSettings();
$helperImage = config('constants.coolify.helper_image');
$latestVersion = $settings->helper_version;
$latestVersion = getHelperVersion();
return "{$helperImage}:{$latestVersion}";
}
public function failed(?Throwable $exception): void
{
Log::channel('scheduled-errors')->error('DatabaseBackup permanently failed', [
'job' => 'DatabaseBackupJob',
'backup_id' => $this->backup->uuid,
'database' => $this->database?->name ?? 'unknown',
'database_type' => get_class($this->database ?? new \stdClass),
'server' => $this->server?->name ?? 'unknown',
'total_attempts' => $this->attempts(),
'error' => $exception?->getMessage(),
'trace' => $exception?->getTraceAsString(),
]);
$log = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->first();
if ($log) {
$log->update([
'status' => 'failed',
'message' => 'Job failed: '.($exception?->getMessage() ?? 'Unknown error'),
'message' => 'Job permanently failed after '.$this->attempts().' attempts: '.($exception?->getMessage() ?? 'Unknown error'),
'size' => 0,
'filename' => null,
'finished_at' => Carbon::now(),
]);
}
// Notify team about permanent failure
if ($this->team) {
$databaseName = $log?->database_name ?? 'unknown';
$output = $this->backup_output ?? $exception?->getMessage() ?? 'Unknown error';
$this->team->notify(new BackupFailed($this->backup, $this->database, $output, $databaseName));
}
}
}

View File

@@ -124,16 +124,54 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
$this->resource->delete();
}
// Cancel any active deployments for this PR (same logic as API cancel_deployment)
$activeDeployments = \App\Models\ApplicationDeploymentQueue::where('application_id', $application->id)
->where('pull_request_id', $pull_request_id)
->whereIn('status', [
\App\Enums\ApplicationDeploymentStatus::QUEUED->value,
\App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value,
])
->get();
foreach ($activeDeployments as $activeDeployment) {
try {
// Mark deployment as cancelled
$activeDeployment->update([
'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
// Add cancellation log entry
$activeDeployment->addLogEntry('Deployment cancelled: Pull request closed.', 'stderr');
// Check if helper container exists and kill it
$deployment_uuid = $activeDeployment->deployment_uuid;
$escapedDeploymentUuid = escapeshellarg($deployment_uuid);
$checkCommand = "docker ps -a --filter name={$escapedDeploymentUuid} --format '{{.Names}}'";
$containerExists = instant_remote_process([$checkCommand], $server);
if ($containerExists && str($containerExists)->trim()->isNotEmpty()) {
instant_remote_process(["docker rm -f {$escapedDeploymentUuid}"], $server);
$activeDeployment->addLogEntry('Deployment container stopped.');
} else {
$activeDeployment->addLogEntry('Helper container not yet started. Deployment will be cancelled when job checks status.');
}
} catch (\Throwable $e) {
// Silently handle errors during deployment cancellation
}
}
try {
if ($server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}-{$pull_request_id}"], $server);
$escapedStackName = escapeshellarg("{$application->uuid}-{$pull_request_id}");
instant_remote_process(["docker stack rm {$escapedStackName}"], $server);
} else {
$containers = getCurrentApplicationContainerStatus($server, $application->id, $pull_request_id)->toArray();
$this->stopPreviewContainers($containers, $server);
}
} catch (\Throwable $e) {
// Log the error but don't fail the job
ray('Error stopping preview containers: '.$e->getMessage());
\Log::warning('Error stopping preview containers for application '.$application->uuid.', PR #'.$pull_request_id.': '.$e->getMessage());
}
// Finally, force delete to trigger resource cleanup
@@ -156,7 +194,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
"docker stop --time=$timeout $containerList",
"docker rm -f $containerList",
];
instant_remote_process(
command: $commands,
server: $server,

View File

@@ -1,30 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000;
public function __construct(public Server $server)
{
$this->onQueue('high');
}
public function handle(): void
{
$helperImage = config('constants.coolify.helper_image');
$latest_version = instanceSettings()->helper_version;
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
}
}

View File

@@ -13,6 +13,8 @@ use App\Models\Server;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use App\Services\ContainerStatusAggregator;
use App\Traits\CalculatesExcludedStatus;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -25,6 +27,7 @@ use Laravel\Horizon\Contracts\Silenced;
class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
{
use CalculatesExcludedStatus;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
@@ -67,6 +70,8 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
public Collection $applicationContainerStatuses;
public Collection $serviceContainerStatuses;
public bool $foundProxy = false;
public bool $foundLogDrainContainer = false;
@@ -90,6 +95,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
$this->foundApplicationPreviewsIds = collect();
$this->foundServiceDatabaseIds = collect();
$this->applicationContainerStatuses = collect();
$this->serviceContainerStatuses = collect();
$this->allApplicationIds = collect();
$this->allDatabaseUuids = collect();
$this->allTcpProxyUuids = collect();
@@ -99,6 +105,20 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
public function handle()
{
// Defensive initialization for Collection properties to handle queue deserialization edge cases
$this->serviceContainerStatuses ??= collect();
$this->applicationContainerStatuses ??= collect();
$this->foundApplicationIds ??= collect();
$this->foundDatabaseUuids ??= collect();
$this->foundServiceApplicationIds ??= collect();
$this->foundApplicationPreviewsIds ??= collect();
$this->foundServiceDatabaseIds ??= collect();
$this->allApplicationIds ??= collect();
$this->allDatabaseUuids ??= collect();
$this->allTcpProxyUuids ??= collect();
$this->allServiceApplicationIds ??= collect();
$this->allServiceDatabaseIds ??= collect();
// TODO: Swarm is not supported yet
if (! $this->data) {
throw new \Exception('No data provided');
@@ -108,7 +128,6 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
$this->server->sentinelHeartbeat();
$this->containers = collect(data_get($data, 'containers'));
$filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage');
ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot);
@@ -141,65 +160,88 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
foreach ($this->containers as $container) {
$containerStatus = data_get($container, 'state', 'exited');
$containerHealth = data_get($container, 'health_status', 'unhealthy');
$containerStatus = "$containerStatus ($containerHealth)";
$rawHealthStatus = data_get($container, 'health_status');
$containerHealth = $rawHealthStatus ?? 'unknown';
// Only append health status if container is not exited
if ($containerStatus !== 'exited') {
$containerStatus = "$containerStatus:$containerHealth";
}
$labels = collect(data_get($container, 'labels'));
$coolify_managed = $labels->has('coolify.managed');
if ($coolify_managed) {
$name = data_get($container, 'name');
if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) {
$this->foundLogDrainContainer = true;
}
if ($labels->has('coolify.applicationId')) {
$applicationId = $labels->get('coolify.applicationId');
$pullRequestId = $labels->get('coolify.pullRequestId', '0');
try {
if ($pullRequestId === '0') {
if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) {
$this->foundApplicationIds->push($applicationId);
}
// Store container status for aggregation
if (! $this->applicationContainerStatuses->has($applicationId)) {
$this->applicationContainerStatuses->put($applicationId, collect());
}
$containerName = $labels->get('com.docker.compose.service');
if ($containerName) {
$this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus);
}
} else {
$previewKey = $applicationId.':'.$pullRequestId;
if ($this->allApplicationPreviewsIds->contains($previewKey) && $this->isRunning($containerStatus)) {
$this->foundApplicationPreviewsIds->push($previewKey);
}
$this->updateApplicationPreviewStatus($applicationId, $pullRequestId, $containerStatus);
if (! $coolify_managed) {
continue;
}
$name = data_get($container, 'name');
if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) {
$this->foundLogDrainContainer = true;
}
if ($labels->has('coolify.applicationId')) {
$applicationId = $labels->get('coolify.applicationId');
$pullRequestId = $labels->get('coolify.pullRequestId', '0');
try {
if ($pullRequestId === '0') {
if ($this->allApplicationIds->contains($applicationId)) {
$this->foundApplicationIds->push($applicationId);
}
// Store container status for aggregation
if (! $this->applicationContainerStatuses->has($applicationId)) {
$this->applicationContainerStatuses->put($applicationId, collect());
}
$containerName = $labels->get('com.docker.compose.service');
if ($containerName) {
$this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus);
}
} catch (\Exception $e) {
}
} elseif ($labels->has('coolify.serviceId')) {
$serviceId = $labels->get('coolify.serviceId');
$subType = $labels->get('coolify.service.subType');
$subId = $labels->get('coolify.service.subId');
if ($subType === 'application' && $this->isRunning($containerStatus)) {
$this->foundServiceApplicationIds->push($subId);
$this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
} elseif ($subType === 'database' && $this->isRunning($containerStatus)) {
$this->foundServiceDatabaseIds->push($subId);
$this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
}
} else {
$uuid = $labels->get('com.docker.compose.service');
$type = $labels->get('coolify.type');
if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) {
$this->foundProxy = true;
} elseif ($type === 'service' && $this->isRunning($containerStatus)) {
} else {
if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) {
$this->foundDatabaseUuids->push($uuid);
if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
} else {
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false);
}
$previewKey = $applicationId.':'.$pullRequestId;
if ($this->allApplicationPreviewsIds->contains($previewKey)) {
$this->foundApplicationPreviewsIds->push($previewKey);
}
$this->updateApplicationPreviewStatus($applicationId, $pullRequestId, $containerStatus);
}
} catch (\Exception $e) {
}
} elseif ($labels->has('coolify.serviceId')) {
$serviceId = $labels->get('coolify.serviceId');
$subType = $labels->get('coolify.service.subType');
$subId = $labels->get('coolify.service.subId');
if ($subType === 'application') {
$this->foundServiceApplicationIds->push($subId);
// Store container status for aggregation
$key = $serviceId.':'.$subType.':'.$subId;
if (! $this->serviceContainerStatuses->has($key)) {
$this->serviceContainerStatuses->put($key, collect());
}
$containerName = $labels->get('com.docker.compose.service');
if ($containerName) {
$this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
}
} elseif ($subType === 'database') {
$this->foundServiceDatabaseIds->push($subId);
// Store container status for aggregation
$key = $serviceId.':'.$subType.':'.$subId;
if (! $this->serviceContainerStatuses->has($key)) {
$this->serviceContainerStatuses->put($key, collect());
}
$containerName = $labels->get('com.docker.compose.service');
if ($containerName) {
$this->serviceContainerStatuses->get($key)->put($containerName, $containerStatus);
}
}
} else {
$uuid = $labels->get('com.docker.compose.service');
$type = $labels->get('coolify.type');
if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) {
$this->foundProxy = true;
} elseif ($type === 'service' && $this->isRunning($containerStatus)) {
} else {
if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) {
$this->foundDatabaseUuids->push($uuid);
if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
} else {
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false);
}
}
}
@@ -218,6 +260,9 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
// Aggregate multi-container application statuses
$this->aggregateMultiContainerStatuses();
// Aggregate multi-container service statuses
$this->aggregateServiceContainerStatuses();
$this->checkLogDrainContainer();
}
@@ -235,57 +280,28 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
// Parse docker compose to check for excluded containers
$dockerComposeRaw = data_get($application, 'docker_compose_raw');
$excludedContainers = collect();
if ($dockerComposeRaw) {
try {
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
$services = data_get($dockerCompose, 'services', []);
foreach ($services as $serviceName => $serviceConfig) {
// Check if container should be excluded
$excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
$restartPolicy = data_get($serviceConfig, 'restart', 'always');
if ($excludeFromHc || $restartPolicy === 'no') {
$excludedContainers->push($serviceName);
}
}
} catch (\Exception $e) {
// If we can't parse, treat all containers as included
}
}
$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
// Filter out excluded containers
$relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) {
return ! $excludedContainers->contains($containerName);
});
// If all containers are excluded, don't update status
// If all containers are excluded, calculate status from excluded containers
if ($relevantStatuses->isEmpty()) {
$aggregatedStatus = $this->calculateExcludedStatusFromStrings($containerStatuses);
if ($aggregatedStatus && $application->status !== $aggregatedStatus) {
$application->status = $aggregatedStatus;
$application->save();
}
continue;
}
// Aggregate status: if any container is running, app is running
$hasRunning = false;
$hasUnhealthy = false;
foreach ($relevantStatuses as $status) {
if (str($status)->contains('running')) {
$hasRunning = true;
if (str($status)->contains('unhealthy')) {
$hasUnhealthy = true;
}
}
}
$aggregatedStatus = null;
if ($hasRunning) {
$aggregatedStatus = $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)';
} else {
// All containers are exited
$aggregatedStatus = 'exited (unhealthy)';
}
// Use ContainerStatusAggregator service for state machine logic
$aggregator = new ContainerStatusAggregator;
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0);
// Update application status with aggregated result
if ($aggregatedStatus && $application->status !== $aggregatedStatus) {
@@ -295,6 +311,66 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
}
}
private function aggregateServiceContainerStatuses()
{
if ($this->serviceContainerStatuses->isEmpty()) {
return;
}
foreach ($this->serviceContainerStatuses as $key => $containerStatuses) {
// Parse key: serviceId:subType:subId
[$serviceId, $subType, $subId] = explode(':', $key);
$service = $this->services->where('id', $serviceId)->first();
if (! $service) {
continue;
}
// Get the service sub-resource (ServiceApplication or ServiceDatabase)
$subResource = null;
if ($subType === 'application') {
$subResource = $service->applications()->where('id', $subId)->first();
} elseif ($subType === 'database') {
$subResource = $service->databases()->where('id', $subId)->first();
}
if (! $subResource) {
continue;
}
// Parse docker compose from service to check for excluded containers
$dockerComposeRaw = data_get($service, 'docker_compose_raw');
$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);
// Filter out excluded containers
$relevantStatuses = $containerStatuses->filter(function ($status, $containerName) use ($excludedContainers) {
return ! $excludedContainers->contains($containerName);
});
// If all containers are excluded, calculate status from excluded containers
if ($relevantStatuses->isEmpty()) {
$aggregatedStatus = $this->calculateExcludedStatusFromStrings($containerStatuses);
if ($aggregatedStatus && $subResource->status !== $aggregatedStatus) {
$subResource->status = $aggregatedStatus;
$subResource->save();
}
continue;
}
// Use ContainerStatusAggregator service for state machine logic
// NOTE: Sentinel does NOT provide restart count data, so maxRestartCount is always 0
$aggregator = new ContainerStatusAggregator;
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0);
// Update service sub-resource status with aggregated result
if ($aggregatedStatus && $subResource->status !== $aggregatedStatus) {
$subResource->status = $aggregatedStatus;
$subResource->save();
}
}
}
private function updateApplicationStatus(string $applicationId, string $containerStatus)
{
$application = $this->applications->where('id', $applicationId)->first();

View File

@@ -31,12 +31,12 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
public function handle()
{
try {
StopProxy::run($this->server);
StopProxy::run($this->server, restarting: true);
$this->server->proxy->force_stop = false;
$this->server->save();
StartProxy::run($this->server, force: true);
StartProxy::run($this->server, force: true, restarting: true);
} catch (\Throwable $e) {
return handleError($e);

View File

@@ -52,7 +52,7 @@ class ScheduledJobManager implements ShouldQueue
{
return [
(new WithoutOverlapping('scheduled-job-manager'))
->expireAfter(60) // Lock expires after 1 minute to prevent stale locks
->expireAfter(90) // Lock expires after 90s to handle high-load environments with many tasks
->dontRelease(), // Don't re-queue on lock conflict
];
}

View File

@@ -18,14 +18,30 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ScheduledTaskJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*/
public $tries = 3;
/**
* The maximum number of unhandled exceptions to allow before failing.
*/
public $maxExceptions = 1;
/**
* The number of seconds the job can run before timing out.
*/
public $timeout = 300;
public Team $team;
public Server $server;
public ?Server $server = null;
public ScheduledTask $task;
@@ -33,6 +49,11 @@ class ScheduledTaskJob implements ShouldQueue
public ?ScheduledTaskExecution $task_log = null;
/**
* Store execution ID to survive job serialization for timeout handling.
*/
protected ?int $executionId = null;
public string $task_status = 'failed';
public ?string $task_output = null;
@@ -55,6 +76,9 @@ class ScheduledTaskJob implements ShouldQueue
}
$this->team = Team::findOrFail($task->team_id);
$this->server_timezone = $this->getServerTimezone();
// Set timeout from task configuration
$this->timeout = $this->task->timeout ?? 300;
}
private function getServerTimezone(): string
@@ -70,11 +94,18 @@ class ScheduledTaskJob implements ShouldQueue
public function handle(): void
{
$startTime = Carbon::now();
try {
$this->task_log = ScheduledTaskExecution::create([
'scheduled_task_id' => $this->task->id,
'started_at' => $startTime,
'retry_count' => $this->attempts() - 1,
]);
// Store execution ID for timeout handling
$this->executionId = $this->task_log->id;
$this->server = $this->resource->destination->server;
if ($this->resource->type() === 'application') {
@@ -129,15 +160,101 @@ class ScheduledTaskJob implements ShouldQueue
'message' => $this->task_output ?? $e->getMessage(),
]);
}
$this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
// Log the error to the scheduled-errors channel
Log::channel('scheduled-errors')->error('ScheduledTask execution failed', [
'job' => 'ScheduledTaskJob',
'task_id' => $this->task->uuid,
'task_name' => $this->task->name,
'server' => $this->server?->name ?? 'unknown',
'attempt' => $this->attempts(),
'error' => $e->getMessage(),
]);
// Only notify and throw on final failure
// Re-throw to trigger Laravel's retry mechanism with backoff
throw $e;
} finally {
ScheduledTaskDone::dispatch($this->team->id);
if ($this->task_log) {
$finishedAt = Carbon::now();
$duration = round($startTime->floatDiffInSeconds($finishedAt), 2);
$this->task_log->update([
'finished_at' => Carbon::now()->toImmutable(),
'finished_at' => $finishedAt->toImmutable(),
'duration' => $duration,
]);
}
}
}
/**
* Calculate the number of seconds to wait before retrying the job.
*/
public function backoff(): array
{
return [30, 60, 120]; // 30s, 60s, 120s between retries
}
/**
* Handle a job failure.
*/
public function failed(?\Throwable $exception): void
{
Log::channel('scheduled-errors')->error('ScheduledTask permanently failed', [
'job' => 'ScheduledTaskJob',
'task_id' => $this->task->uuid,
'task_name' => $this->task->name,
'server' => $this->server?->name ?? 'unknown',
'total_attempts' => $this->attempts(),
'error' => $exception?->getMessage(),
'trace' => $exception?->getTraceAsString(),
]);
// Reload execution log from database
// When a job times out, failed() is called in a fresh process with the original
// queue payload, so $executionId will be null. We need to query for the latest execution.
$execution = null;
// Try to find execution using stored ID first (works for non-timeout failures)
if ($this->executionId) {
$execution = ScheduledTaskExecution::find($this->executionId);
}
// If no stored ID or not found, query for the most recent execution log for this task
if (! $execution) {
$execution = ScheduledTaskExecution::query()
->where('scheduled_task_id', $this->task->id)
->orderBy('created_at', 'desc')
->first();
}
// Last resort: check task_log property
if (! $execution && $this->task_log) {
$execution = $this->task_log;
}
if ($execution) {
$errorMessage = 'Job permanently failed after '.$this->attempts().' attempts';
if ($exception) {
$errorMessage .= ': '.$exception->getMessage();
}
$execution->update([
'status' => 'failed',
'message' => $errorMessage,
'error_details' => $exception?->getTraceAsString(),
'finished_at' => Carbon::now()->toImmutable(),
]);
} else {
Log::channel('scheduled-errors')->warning('Could not find execution log to update', [
'execution_id' => $this->executionId,
'task_id' => $this->task->uuid,
]);
}
// Notify team about permanent failure
$this->team?->notify(new TaskFailed($this->task, $exception?->getMessage() ?? 'Unknown error'));
}
}

View File

@@ -87,7 +87,7 @@ class ServerManagerJob implements ShouldQueue
Log::channel('scheduled-errors')->error('Failed to dispatch ServerConnectionCheck', [
'server_id' => $server->id,
'server_name' => $server->name,
'error' => $e->getMessage(),
'error' => get_class($e).': '.$e->getMessage(),
]);
}
});
@@ -103,7 +103,7 @@ class ServerManagerJob implements ShouldQueue
Log::channel('scheduled-errors')->error('Error processing server tasks', [
'server_id' => $server->id,
'server_name' => $server->name,
'error' => $e->getMessage(),
'error' => get_class($e).': '.$e->getMessage(),
]);
}
}

View File

@@ -72,6 +72,42 @@ class ValidateAndInstallServerJob implements ShouldQueue
return;
}
// Check and install prerequisites
$validationResult = $this->server->validatePrerequisites();
if (! $validationResult['success']) {
if ($this->numberOfTries >= $this->maxTries) {
$missingCommands = implode(', ', $validationResult['missing']);
$errorMessage = "Prerequisites ({$missingCommands}) could not be installed after {$this->maxTries} attempts. Please install them manually before continuing.";
$this->server->update([
'validation_logs' => $errorMessage,
'is_validating' => false,
]);
Log::error('ValidateAndInstallServer: Prerequisites installation failed after max tries', [
'server_id' => $this->server->id,
'attempts' => $this->numberOfTries,
'missing_commands' => $validationResult['missing'],
'found_commands' => $validationResult['found'],
]);
return;
}
Log::info('ValidateAndInstallServer: Installing prerequisites', [
'server_id' => $this->server->id,
'attempt' => $this->numberOfTries + 1,
'missing_commands' => $validationResult['missing'],
'found_commands' => $validationResult['found'],
]);
// Install prerequisites
$this->server->installPrerequisites();
// Retry validation after installation
self::dispatch($this->server, $this->numberOfTries + 1)->delay(now()->addSeconds(30));
return;
}
// Check if Docker is installed
$dockerInstalled = $this->server->validateDockerEngine();
$dockerComposeInstalled = $this->server->validateDockerCompose();

View File

@@ -28,12 +28,20 @@ class ActivityMonitor extends Component
protected $listeners = ['activityMonitor' => 'newMonitorActivity'];
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished', $eventData = null)
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished', $eventData = null, $header = null)
{
// Reset event dispatched flag for new activity
self::$eventDispatched = false;
$this->activityId = $activityId;
$this->eventToDispatch = $eventToDispatch;
$this->eventData = $eventData;
// Update header if provided
if ($header !== null) {
$this->header = $header;
}
$this->hydrateActivity();
$this->isPollingActive = true;

View File

@@ -14,7 +14,10 @@ use Visus\Cuid2\Cuid2;
class Index extends Component
{
protected $listeners = ['refreshBoardingIndex' => 'validateServer'];
protected $listeners = [
'refreshBoardingIndex' => 'validateServer',
'prerequisitesInstalled' => 'handlePrerequisitesInstalled',
];
#[\Livewire\Attributes\Url(as: 'step', history: true)]
public string $currentState = 'welcome';
@@ -76,6 +79,10 @@ class Index extends Component
public ?string $minDockerVersion = null;
public int $prerequisiteInstallAttempts = 0;
public int $maxPrerequisiteInstallAttempts = 3;
public function mount()
{
if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
@@ -320,6 +327,62 @@ class Index extends Component
return handleError(error: $e, livewire: $this);
}
try {
// Check prerequisites
$validationResult = $this->createdServer->validatePrerequisites();
if (! $validationResult['success']) {
// Check if we've exceeded max attempts
if ($this->prerequisiteInstallAttempts >= $this->maxPrerequisiteInstallAttempts) {
$missingCommands = implode(', ', $validationResult['missing']);
throw new \Exception("Prerequisites ({$missingCommands}) could not be installed after {$this->maxPrerequisiteInstallAttempts} attempts. Please install them manually.");
}
// Start async installation and wait for completion via ActivityMonitor
$activity = $this->createdServer->installPrerequisites();
$this->prerequisiteInstallAttempts++;
$this->dispatch('activityMonitor', $activity->id, 'prerequisitesInstalled');
// Return early - handlePrerequisitesInstalled() will be called when installation completes
return;
}
// Prerequisites are already installed, continue with validation
$this->continueValidation();
} catch (\Throwable $e) {
return handleError(error: $e, livewire: $this);
}
}
public function handlePrerequisitesInstalled()
{
try {
// Revalidate prerequisites after installation completes
$validationResult = $this->createdServer->validatePrerequisites();
if (! $validationResult['success']) {
// Installation completed but prerequisites still missing - retry
$missingCommands = implode(', ', $validationResult['missing']);
if ($this->prerequisiteInstallAttempts >= $this->maxPrerequisiteInstallAttempts) {
throw new \Exception("Prerequisites ({$missingCommands}) could not be installed after {$this->maxPrerequisiteInstallAttempts} attempts. Please install them manually.");
}
// Try again
$activity = $this->createdServer->installPrerequisites();
$this->prerequisiteInstallAttempts++;
$this->dispatch('activityMonitor', $activity->id, 'prerequisitesInstalled');
return;
}
// Prerequisites validated successfully - continue with Docker validation
$this->continueValidation();
} catch (\Throwable $e) {
return handleError(error: $e, livewire: $this);
}
}
private function continueValidation()
{
try {
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true);
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
@@ -347,6 +410,8 @@ class Index extends Component
}
$this->createdServer->proxy->type = $proxyType;
$this->createdServer->proxy->status = 'exited';
$this->createdServer->proxy->last_saved_settings = null;
$this->createdServer->proxy->last_applied_settings = null;
$this->createdServer->save();
$this->getProjects();
}

View File

@@ -1,35 +0,0 @@
<?php
namespace App\Livewire\Concerns;
trait SynchronizesModelData
{
/**
* Define the mapping between component properties and model keys.
*
* @return array<string, string> Array mapping property names to model keys (e.g., ['content' => 'fileStorage.content'])
*/
abstract protected function getModelBindings(): array;
/**
* Synchronize component properties TO the model.
* Copies values from component properties to the model.
*/
protected function syncToModel(): void
{
foreach ($this->getModelBindings() as $property => $modelKey) {
data_set($this, $modelKey, $this->{$property});
}
}
/**
* Synchronize component properties FROM the model.
* Copies values from the model to component properties.
*/
protected function syncFromModel(): void
{
foreach ($this->getModelBindings() as $property => $modelKey) {
$this->{$property} = data_get($this, $modelKey);
}
}
}

View File

@@ -62,6 +62,9 @@ class Discord extends Component
#[Validate(['boolean'])]
public bool $serverPatchDiscordNotifications = false;
#[Validate(['boolean'])]
public bool $traefikOutdatedDiscordNotifications = true;
#[Validate(['boolean'])]
public bool $discordPingEnabled = true;
@@ -98,6 +101,7 @@ class Discord extends Component
$this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications;
$this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications;
$this->settings->server_patch_discord_notifications = $this->serverPatchDiscordNotifications;
$this->settings->traefik_outdated_discord_notifications = $this->traefikOutdatedDiscordNotifications;
$this->settings->discord_ping_enabled = $this->discordPingEnabled;
@@ -120,6 +124,7 @@ class Discord extends Component
$this->serverReachableDiscordNotifications = $this->settings->server_reachable_discord_notifications;
$this->serverUnreachableDiscordNotifications = $this->settings->server_unreachable_discord_notifications;
$this->serverPatchDiscordNotifications = $this->settings->server_patch_discord_notifications;
$this->traefikOutdatedDiscordNotifications = $this->settings->traefik_outdated_discord_notifications;
$this->discordPingEnabled = $this->settings->discord_ping_enabled;
}

View File

@@ -104,6 +104,9 @@ class Email extends Component
#[Validate(['boolean'])]
public bool $serverPatchEmailNotifications = false;
#[Validate(['boolean'])]
public bool $traefikOutdatedEmailNotifications = true;
#[Validate(['nullable', 'email'])]
public ?string $testEmailAddress = null;
@@ -155,6 +158,7 @@ class Email extends Component
$this->settings->server_reachable_email_notifications = $this->serverReachableEmailNotifications;
$this->settings->server_unreachable_email_notifications = $this->serverUnreachableEmailNotifications;
$this->settings->server_patch_email_notifications = $this->serverPatchEmailNotifications;
$this->settings->traefik_outdated_email_notifications = $this->traefikOutdatedEmailNotifications;
$this->settings->save();
} else {
@@ -187,6 +191,7 @@ class Email extends Component
$this->serverReachableEmailNotifications = $this->settings->server_reachable_email_notifications;
$this->serverUnreachableEmailNotifications = $this->settings->server_unreachable_email_notifications;
$this->serverPatchEmailNotifications = $this->settings->server_patch_email_notifications;
$this->traefikOutdatedEmailNotifications = $this->settings->traefik_outdated_email_notifications;
}
}

View File

@@ -70,6 +70,9 @@ class Pushover extends Component
#[Validate(['boolean'])]
public bool $serverPatchPushoverNotifications = false;
#[Validate(['boolean'])]
public bool $traefikOutdatedPushoverNotifications = true;
public function mount()
{
try {
@@ -104,6 +107,7 @@ class Pushover extends Component
$this->settings->server_reachable_pushover_notifications = $this->serverReachablePushoverNotifications;
$this->settings->server_unreachable_pushover_notifications = $this->serverUnreachablePushoverNotifications;
$this->settings->server_patch_pushover_notifications = $this->serverPatchPushoverNotifications;
$this->settings->traefik_outdated_pushover_notifications = $this->traefikOutdatedPushoverNotifications;
$this->settings->save();
refreshSession();
@@ -125,6 +129,7 @@ class Pushover extends Component
$this->serverReachablePushoverNotifications = $this->settings->server_reachable_pushover_notifications;
$this->serverUnreachablePushoverNotifications = $this->settings->server_unreachable_pushover_notifications;
$this->serverPatchPushoverNotifications = $this->settings->server_patch_pushover_notifications;
$this->traefikOutdatedPushoverNotifications = $this->settings->traefik_outdated_pushover_notifications;
}
}

View File

@@ -67,6 +67,9 @@ class Slack extends Component
#[Validate(['boolean'])]
public bool $serverPatchSlackNotifications = false;
#[Validate(['boolean'])]
public bool $traefikOutdatedSlackNotifications = true;
public function mount()
{
try {
@@ -100,6 +103,7 @@ class Slack extends Component
$this->settings->server_reachable_slack_notifications = $this->serverReachableSlackNotifications;
$this->settings->server_unreachable_slack_notifications = $this->serverUnreachableSlackNotifications;
$this->settings->server_patch_slack_notifications = $this->serverPatchSlackNotifications;
$this->settings->traefik_outdated_slack_notifications = $this->traefikOutdatedSlackNotifications;
$this->settings->save();
refreshSession();
@@ -120,6 +124,7 @@ class Slack extends Component
$this->serverReachableSlackNotifications = $this->settings->server_reachable_slack_notifications;
$this->serverUnreachableSlackNotifications = $this->settings->server_unreachable_slack_notifications;
$this->serverPatchSlackNotifications = $this->settings->server_patch_slack_notifications;
$this->traefikOutdatedSlackNotifications = $this->settings->traefik_outdated_slack_notifications;
}
}

View File

@@ -70,6 +70,9 @@ class Telegram extends Component
#[Validate(['boolean'])]
public bool $serverPatchTelegramNotifications = false;
#[Validate(['boolean'])]
public bool $traefikOutdatedTelegramNotifications = true;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsDeploymentSuccessThreadId = null;
@@ -109,6 +112,9 @@ class Telegram extends Component
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsServerPatchThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsTraefikOutdatedThreadId = null;
public function mount()
{
try {
@@ -143,6 +149,7 @@ class Telegram extends Component
$this->settings->server_reachable_telegram_notifications = $this->serverReachableTelegramNotifications;
$this->settings->server_unreachable_telegram_notifications = $this->serverUnreachableTelegramNotifications;
$this->settings->server_patch_telegram_notifications = $this->serverPatchTelegramNotifications;
$this->settings->traefik_outdated_telegram_notifications = $this->traefikOutdatedTelegramNotifications;
$this->settings->telegram_notifications_deployment_success_thread_id = $this->telegramNotificationsDeploymentSuccessThreadId;
$this->settings->telegram_notifications_deployment_failure_thread_id = $this->telegramNotificationsDeploymentFailureThreadId;
@@ -157,6 +164,7 @@ class Telegram extends Component
$this->settings->telegram_notifications_server_reachable_thread_id = $this->telegramNotificationsServerReachableThreadId;
$this->settings->telegram_notifications_server_unreachable_thread_id = $this->telegramNotificationsServerUnreachableThreadId;
$this->settings->telegram_notifications_server_patch_thread_id = $this->telegramNotificationsServerPatchThreadId;
$this->settings->telegram_notifications_traefik_outdated_thread_id = $this->telegramNotificationsTraefikOutdatedThreadId;
$this->settings->save();
} else {
@@ -177,6 +185,7 @@ class Telegram extends Component
$this->serverReachableTelegramNotifications = $this->settings->server_reachable_telegram_notifications;
$this->serverUnreachableTelegramNotifications = $this->settings->server_unreachable_telegram_notifications;
$this->serverPatchTelegramNotifications = $this->settings->server_patch_telegram_notifications;
$this->traefikOutdatedTelegramNotifications = $this->settings->traefik_outdated_telegram_notifications;
$this->telegramNotificationsDeploymentSuccessThreadId = $this->settings->telegram_notifications_deployment_success_thread_id;
$this->telegramNotificationsDeploymentFailureThreadId = $this->settings->telegram_notifications_deployment_failure_thread_id;
@@ -191,6 +200,7 @@ class Telegram extends Component
$this->telegramNotificationsServerReachableThreadId = $this->settings->telegram_notifications_server_reachable_thread_id;
$this->telegramNotificationsServerUnreachableThreadId = $this->settings->telegram_notifications_server_unreachable_thread_id;
$this->telegramNotificationsServerPatchThreadId = $this->settings->telegram_notifications_server_patch_thread_id;
$this->telegramNotificationsTraefikOutdatedThreadId = $this->settings->telegram_notifications_traefik_outdated_thread_id;
}
}

Some files were not shown because too many files have changed in this diff Show More