mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-17 17:21:04 +00:00
Merge branch 'next' into feat/shared-dev-view
This commit is contained in:
@@ -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
148
.ai/README.md
Normal 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)
|
||||
604
.ai/core/application-architecture.md
Normal file
604
.ai/core/application-architecture.md
Normal 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
|
||||
@@ -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
|
||||
@@ -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?
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1,6 +1,3 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
@@ -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.
|
||||
172
.ai/meta/maintaining-docs.md
Normal file
172
.ai/meta/maintaining-docs.md
Normal 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
214
.ai/meta/sync-guide.md
Normal 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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
696
.ai/patterns/frontend-patterns.md
Normal file
696
.ai/patterns/frontend-patterns.md
Normal 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
|
||||
@@ -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
|
||||
@@ -1,2 +0,0 @@
|
||||
reviews:
|
||||
review_status: false
|
||||
@@ -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.
|
||||
@@ -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
|
||||
156
.cursor/rules/coolify-ai-docs.mdc
Normal file
156
.cursor/rules/coolify-ai-docs.mdc
Normal 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.).
|
||||
@@ -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
|
||||
@@ -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.*
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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'
|
||||
|
||||
@@ -4,6 +4,10 @@ on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
manage-stale:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
15
.github/workflows/chore-pr-comments.yml
vendored
15
.github/workflows/chore-pr-comments.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
79
.github/workflows/claude-code-review.yml
vendored
79
.github/workflows/claude-code-review.yml
vendored
@@ -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]')
|
||||
|
||||
65
.github/workflows/claude.yml
vendored
65
.github/workflows/claude.yml
vendored
@@ -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
|
||||
9
.github/workflows/cleanup-ghcr-untagged.yml
vendored
9
.github/workflows/cleanup-ghcr-untagged.yml
vendored
@@ -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']
|
||||
|
||||
89
.github/workflows/coolify-helper-next.yml
vendored
89
.github/workflows/coolify-helper-next.yml
vendored
@@ -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
|
||||
|
||||
|
||||
88
.github/workflows/coolify-helper.yml
vendored
88
.github/workflows/coolify-helper.yml
vendored
@@ -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
|
||||
|
||||
|
||||
81
.github/workflows/coolify-production-build.yml
vendored
81
.github/workflows/coolify-production-build.yml
vendored
@@ -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
|
||||
|
||||
|
||||
89
.github/workflows/coolify-realtime-next.yml
vendored
89
.github/workflows/coolify-realtime-next.yml
vendored
@@ -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
|
||||
|
||||
|
||||
89
.github/workflows/coolify-realtime.yml
vendored
89
.github/workflows/coolify-realtime.yml
vendored
@@ -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
|
||||
|
||||
|
||||
14
.github/workflows/coolify-staging-build.yml
vendored
14
.github/workflows/coolify-staging-build.yml
vendored
@@ -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
|
||||
|
||||
84
.github/workflows/coolify-testing-host.yml
vendored
84
.github/workflows/coolify-testing-host.yml
vendored
@@ -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
14071
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
491
CLAUDE.md
491
CLAUDE.md
@@ -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:
|
||||
|
||||
33
README.md
33
README.md
@@ -1,9 +1,13 @@
|
||||

|
||||
<div align="center">
|
||||
|
||||
[](https://console.algora.io/org/coollabsio/bounties/new)
|
||||
# Coolify
|
||||
An open-source & self-hostable Heroku / Netlify / Vercel alternative.
|
||||
|
||||
# About the Project
|
||||
 [](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
|
||||
|
||||

|
||||
|
||||
# Star History
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#coollabsio/coolify&Date)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.'";
|
||||
|
||||
@@ -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.'";
|
||||
|
||||
@@ -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.'";
|
||||
|
||||
@@ -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.'";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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.'";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.'",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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...'",
|
||||
]);
|
||||
|
||||
57
app/Actions/Server/InstallPrerequisites.php
Normal file
57
app/Actions/Server/InstallPrerequisites.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
40
app/Actions/Server/ValidatePrerequisites.php
Normal file
40
app/Actions/Server/ValidatePrerequisites.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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', []);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
30
app/Console/Commands/CheckTraefikVersionCommand.php
Normal file
30
app/Console/Commands/CheckTraefikVersionCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
219
app/Console/Commands/Cloud/RestoreDatabase.php
Normal file
219
app/Console/Commands/Cloud/RestoreDatabase.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
56
app/Events/S3RestoreJobFinished.php
Normal file
56
app/Events/S3RestoreJobFinished.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
app/Exceptions/DeploymentException.php
Normal file
32
app/Exceptions/DeploymentException.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ class Handler extends ExceptionHandler
|
||||
protected $dontReport = [
|
||||
ProcessException::class,
|
||||
NonReportableException::class,
|
||||
DeploymentException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
}
|
||||
|
||||
173
app/Jobs/CheckTraefikVersionForServerJob.php
Normal file
173
app/Jobs/CheckTraefikVersionForServerJob.php
Normal 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])));
|
||||
}
|
||||
}
|
||||
}
|
||||
45
app/Jobs/CheckTraefikVersionJob.php
Normal file
45
app/Jobs/CheckTraefikVersionJob.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user