coolify/.ai/development/development-workflow.md
Andras Bacsai 3f7c5fbdf9 Consolidate AI documentation into .ai/ directory
- Create .ai/ directory as single source of truth for all AI docs
- Organize by topic: core/, development/, patterns/, meta/
- Update CLAUDE.md to reference .ai/ files instead of embedding content
- Remove 18KB of duplicated Laravel Boost guidelines from CLAUDE.md
- Fix testing command descriptions (pest runs all tests, not just unit)
- Standardize version numbers (Laravel 12.4.1, PHP 8.4.7, Tailwind 4.1.4)
- Replace all .cursor/rules/*.mdc with single coolify-ai-docs.mdc reference
- Delete dev_workflow.mdc (non-Coolify Task Master content)
- Merge cursor_rules.mdc + self_improve.mdc into maintaining-docs.md
- Update .AI_INSTRUCTIONS_SYNC.md to redirect to new location

Benefits:
- Single source of truth - no more duplication
- Consistent versions across all documentation
- Better organization by topic
- Platform-agnostic .ai/ directory works for all AI tools
- Reduced CLAUDE.md from 719 to ~320 lines
- Clear cross-references between files
2025-11-18 14:58:59 +01:00

17 KiB

Coolify Development Workflow

Development Environment Setup

Prerequisites

  • PHP 8.4+ - Latest PHP version for modern features
  • Node.js 18+ - For frontend asset compilation
  • Docker & Docker Compose - Container orchestration
  • PostgreSQL 15 - Primary database
  • Redis 7 - Caching and queues

Local Development Setup

# Clone the repository
git clone https://github.com/coollabsio/coolify.git
cd coolify

# Copy environment configuration
cp .env.example .env

# Start development environment
docker-compose -f docker-compose.dev.yml up -d

# Install PHP dependencies
docker-compose exec app composer install

# Install Node.js dependencies
docker-compose exec app npm install

# Generate application key
docker-compose exec app php artisan key:generate

# Run database migrations
docker-compose exec app php artisan migrate

# Seed development data
docker-compose exec app php artisan db:seed

Native Development

# Install PHP dependencies
composer install

# Install Node.js dependencies
npm install

# Setup environment
cp .env.example .env
php artisan key:generate

# Setup database
createdb coolify_dev
php artisan migrate
php artisan db:seed

# Start development servers
php artisan serve &
npm run dev &
php artisan queue:work &

Development Tools & Configuration

Code Quality Tools

  • Laravel Pint - PHP code style fixer
  • Rector - PHP automated refactoring (989B, 35 lines)
  • PHPStan - Static analysis for type safety
  • ESLint - JavaScript code quality

Development Configuration Files

Git Configuration

Development Workflow Process

1. Feature Development

# Create feature branch
git checkout -b feature/new-deployment-strategy

# Make changes following coding standards
# Run code quality checks
./vendor/bin/pint
./vendor/bin/rector process --dry-run
./vendor/bin/phpstan analyse

# Run tests
./vendor/bin/pest
./vendor/bin/pest --coverage

# Commit changes
git add .
git commit -m "feat: implement blue-green deployment strategy"

2. Code Review Process

# Push feature branch
git push origin feature/new-deployment-strategy

# Create pull request with:
# - Clear description of changes
# - Screenshots for UI changes
# - Test coverage information
# - Breaking change documentation

3. Testing Requirements

  • Unit tests for new models and services
  • Feature tests for API endpoints
  • Browser tests for UI changes
  • Integration tests for deployment workflows

Coding Standards & Conventions

PHP Coding Standards

// Follow PSR-12 coding standards
class ApplicationDeploymentService
{
    public function __construct(
        private readonly DockerService $dockerService,
        private readonly ConfigurationGenerator $configGenerator
    ) {}
    
    public function deploy(Application $application): ApplicationDeploymentQueue
    {
        return DB::transaction(function () use ($application) {
            $deployment = $application->deployments()->create([
                'status' => 'queued',
                'commit_sha' => $application->getLatestCommitSha(),
            ]);
            
            DeployApplicationJob::dispatch($deployment);
            
            return $deployment;
        });
    }
}

Laravel Best Practices

// Use Laravel conventions
class Application extends Model
{
    // Mass assignment protection
    protected $fillable = [
        'name', 'git_repository', 'git_branch', 'fqdn'
    ];
    
    // Type casting
    protected $casts = [
        'environment_variables' => 'array',
        'build_pack' => BuildPack::class,
        'created_at' => 'datetime',
    ];
    
    // Relationships
    public function server(): BelongsTo
    {
        return $this->belongsTo(Server::class);
    }
    
    public function deployments(): HasMany
    {
        return $this->hasMany(ApplicationDeploymentQueue::class);
    }
}

Frontend Standards

// Alpine.js component structure
document.addEventListener('alpine:init', () => {
    Alpine.data('deploymentMonitor', () => ({
        status: 'idle',
        logs: [],
        
        init() {
            this.connectWebSocket();
        },
        
        connectWebSocket() {
            Echo.private(`application.${this.applicationId}`)
                .listen('DeploymentStarted', (e) => {
                    this.status = 'deploying';
                })
                .listen('DeploymentCompleted', (e) => {
                    this.status = 'completed';
                });
        }
    }));
});

CSS/Tailwind Standards

<!-- Use semantic class names and consistent spacing -->
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
    <div class="p-6">
        <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
            Application Status
        </h3>
        <div class="space-y-3">
            <!-- Content with consistent spacing -->
        </div>
    </div>
</div>

Database Development

Migration Best Practices

// Create descriptive migration files
class CreateApplicationDeploymentQueuesTable extends Migration
{
    public function up(): void
    {
        Schema::create('application_deployment_queues', function (Blueprint $table) {
            $table->id();
            $table->foreignId('application_id')->constrained()->cascadeOnDelete();
            $table->string('status')->default('queued');
            $table->string('commit_sha')->nullable();
            $table->text('build_logs')->nullable();
            $table->text('deployment_logs')->nullable();
            $table->timestamp('started_at')->nullable();
            $table->timestamp('finished_at')->nullable();
            $table->timestamps();
            
            $table->index(['application_id', 'status']);
            $table->index('created_at');
        });
    }
    
    public function down(): void
    {
        Schema::dropIfExists('application_deployment_queues');
    }
}

Model Factory Development

// Create comprehensive factories for testing
class ApplicationFactory extends Factory
{
    protected $model = Application::class;
    
    public function definition(): array
    {
        return [
            'name' => $this->faker->words(2, true),
            'fqdn' => $this->faker->domainName,
            'git_repository' => 'https://github.com/' . $this->faker->userName . '/' . $this->faker->word . '.git',
            'git_branch' => 'main',
            'build_pack' => BuildPack::NIXPACKS,
            'server_id' => Server::factory(),
            'environment_id' => Environment::factory(),
        ];
    }
    
    public function withCustomDomain(): static
    {
        return $this->state(fn (array $attributes) => [
            'fqdn' => $this->faker->domainName,
        ]);
    }
}

API Development

Controller Standards

class ApplicationController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:sanctum');
        $this->middleware('team.access');
    }
    
    public function index(Request $request): AnonymousResourceCollection
    {
        $applications = $request->user()
            ->currentTeam
            ->applications()
            ->with(['server', 'environment', 'latestDeployment'])
            ->paginate();
            
        return ApplicationResource::collection($applications);
    }
    
    public function store(StoreApplicationRequest $request): ApplicationResource
    {
        $application = $request->user()
            ->currentTeam
            ->applications()
            ->create($request->validated());
            
        return new ApplicationResource($application);
    }
    
    public function deploy(Application $application): JsonResponse
    {
        $this->authorize('deploy', $application);
        
        $deployment = app(ApplicationDeploymentService::class)
            ->deploy($application);
            
        return response()->json([
            'message' => 'Deployment started successfully',
            'deployment_id' => $deployment->id,
        ]);
    }
}

API Resource Development

class ApplicationResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'fqdn' => $this->fqdn,
            'status' => $this->status,
            'git_repository' => $this->git_repository,
            'git_branch' => $this->git_branch,
            'build_pack' => $this->build_pack,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
            
            // Conditional relationships
            'server' => new ServerResource($this->whenLoaded('server')),
            'environment' => new EnvironmentResource($this->whenLoaded('environment')),
            'latest_deployment' => new DeploymentResource($this->whenLoaded('latestDeployment')),
            
            // Computed attributes
            'deployment_url' => $this->getDeploymentUrl(),
            'can_deploy' => $this->canDeploy(),
        ];
    }
}

Livewire Component Development

Component Structure

class ApplicationShow extends Component
{
    public Application $application;
    public bool $showLogs = false;
    
    protected $listeners = [
        'deployment.started' => 'refreshDeploymentStatus',
        'deployment.completed' => 'refreshDeploymentStatus',
    ];
    
    public function mount(Application $application): void
    {
        $this->authorize('view', $application);
        $this->application = $application;
    }
    
    public function deploy(): void
    {
        $this->authorize('deploy', $this->application);
        
        try {
            app(ApplicationDeploymentService::class)->deploy($this->application);
            
            $this->dispatch('deployment.started', [
                'application_id' => $this->application->id
            ]);
            
            session()->flash('success', 'Deployment started successfully');
        } catch (Exception $e) {
            session()->flash('error', 'Failed to start deployment: ' . $e->getMessage());
        }
    }
    
    public function refreshDeploymentStatus(): void
    {
        $this->application->refresh();
    }
    
    public function render(): View
    {
        return view('livewire.application.show', [
            'deployments' => $this->application
                ->deployments()
                ->latest()
                ->limit(10)
                ->get()
        ]);
    }
}

Queue Job Development

Job Structure

class DeployApplicationJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public int $tries = 3;
    public int $maxExceptions = 1;
    
    public function __construct(
        public ApplicationDeploymentQueue $deployment
    ) {}
    
    public function handle(
        DockerService $dockerService,
        ConfigurationGenerator $configGenerator
    ): void {
        $this->deployment->update(['status' => 'running', 'started_at' => now()]);
        
        try {
            // Generate configuration
            $config = $configGenerator->generateDockerCompose($this->deployment->application);
            
            // Build and deploy
            $imageTag = $dockerService->buildImage($this->deployment->application);
            $dockerService->deployContainer($this->deployment->application, $imageTag);
            
            $this->deployment->update([
                'status' => 'success',
                'finished_at' => now()
            ]);
            
            // Broadcast success
            broadcast(new DeploymentCompleted($this->deployment));
            
        } catch (Exception $e) {
            $this->deployment->update([
                'status' => 'failed',
                'error_message' => $e->getMessage(),
                'finished_at' => now()
            ]);
            
            broadcast(new DeploymentFailed($this->deployment));
            
            throw $e;
        }
    }
    
    public function backoff(): array
    {
        return [1, 5, 10];
    }
    
    public function failed(Throwable $exception): void
    {
        $this->deployment->update([
            'status' => 'failed',
            'error_message' => $exception->getMessage(),
            'finished_at' => now()
        ]);
    }
}

Testing Development

Test Structure

// Feature test example
test('user can deploy application via API', function () {
    $user = User::factory()->create();
    $application = Application::factory()->create([
        'team_id' => $user->currentTeam->id
    ]);
    
    // Mock external services
    $this->mock(DockerService::class, function ($mock) {
        $mock->shouldReceive('buildImage')->andReturn('app:latest');
        $mock->shouldReceive('deployContainer')->andReturn(true);
    });
    
    $response = $this->actingAs($user)
        ->postJson("/api/v1/applications/{$application->id}/deploy");
    
    $response->assertStatus(200)
        ->assertJson([
            'message' => 'Deployment started successfully'
        ]);
    
    expect($application->deployments()->count())->toBe(1);
    expect($application->deployments()->first()->status)->toBe('queued');
});

Documentation Standards

Code Documentation

/**
 * Deploy an application to the specified server.
 * 
 * This method creates a new deployment queue entry and dispatches
 * a background job to handle the actual deployment process.
 * 
 * @param Application $application The application to deploy
 * @param array $options Additional deployment options
 * @return ApplicationDeploymentQueue The created deployment queue entry
 * 
 * @throws DeploymentException When deployment cannot be started
 * @throws ServerConnectionException When server is unreachable
 */
public function deploy(Application $application, array $options = []): ApplicationDeploymentQueue
{
    // Implementation
}

API Documentation

/**
 * @OA\Post(
 *     path="/api/v1/applications/{application}/deploy",
 *     summary="Deploy an application",
 *     description="Triggers a new deployment for the specified application",
 *     operationId="deployApplication",
 *     tags={"Applications"},
 *     security={{"bearerAuth":{}}},
 *     @OA\Parameter(
 *         name="application",
 *         in="path",
 *         required=true,
 *         @OA\Schema(type="integer"),
 *         description="Application ID"
 *     ),
 *     @OA\Response(
 *         response=200,
 *         description="Deployment started successfully",
 *         @OA\JsonContent(
 *             @OA\Property(property="message", type="string"),
 *             @OA\Property(property="deployment_id", type="integer")
 *         )
 *     )
 * )
 */

Performance Optimization

Database Optimization

// Use eager loading to prevent N+1 queries
$applications = Application::with([
    'server:id,name,ip',
    'environment:id,name',
    'latestDeployment:id,application_id,status,created_at'
])->get();

// Use database transactions for consistency
DB::transaction(function () use ($application) {
    $deployment = $application->deployments()->create(['status' => 'queued']);
    $application->update(['last_deployment_at' => now()]);
    DeployApplicationJob::dispatch($deployment);
});

Caching Strategies

// Cache expensive operations
public function getServerMetrics(Server $server): array
{
    return Cache::remember(
        "server.{$server->id}.metrics",
        now()->addMinutes(5),
        fn () => $this->fetchServerMetrics($server)
    );
}

Deployment & Release Process

Version Management

Release Workflow

# Create release branch
git checkout -b release/v4.1.0

# Update version numbers
# Update CHANGELOG.md
# Run full test suite
./vendor/bin/pest
npm run test

# Create release commit
git commit -m "chore: release v4.1.0"

# Create and push tag
git tag v4.1.0
git push origin v4.1.0

# Merge to main
git checkout main
git merge release/v4.1.0

Contributing Guidelines

Pull Request Process

  1. Fork the repository
  2. Create feature branch from main
  3. Implement changes with tests
  4. Run code quality checks
  5. Submit pull request with clear description
  6. Address review feedback
  7. Merge after approval

Code Review Checklist

  • Code follows project standards
  • Tests cover new functionality
  • Documentation is updated
  • No breaking changes without migration
  • Performance impact considered
  • Security implications reviewed

Issue Reporting

  • Use issue templates
  • Provide reproduction steps
  • Include environment details
  • Add relevant logs/screenshots
  • Label appropriately