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
- Purpose: Physical/virtual server management
- Key Relationships:
hasMany(Application::class)- Deployed applicationshasMany(StandalonePostgresql::class)- Database instancesbelongsTo(Team::class)- Team ownership
- Key Features:
- SSH connection management
- Resource monitoring
- Proxy configuration (Traefik/Caddy)
- Docker daemon interaction
- Purpose: Application deployment and management
- Key Relationships:
belongsTo(Server::class)- Deployment targetbelongsTo(Environment::class)- Environment contexthasMany(ApplicationDeploymentQueue::class)- Deployment history
- Key Features:
- Git repository integration
- Docker build and deployment
- Environment variable management
- SSL certificate handling
- Purpose: Multi-container service orchestration
- Key Relationships:
hasMany(ServiceApplication::class)- Service componentshasMany(ServiceDatabase::class)- Service databasesbelongsTo(Environment::class)- Environment context
- Key Features:
- Docker Compose generation
- Service dependency management
- Health check configuration
- Purpose: Multi-tenant team management
- Key Relationships:
hasMany(User::class)- Team membershasMany(Project::class)- Team projectshasMany(Server::class)- Team servers
- Key Features:
- Resource limits and quotas
- Team-based access control
- Subscription management
- Purpose: Project organization and grouping
- Key Relationships:
hasMany(Environment::class)- Project environmentsbelongsTo(Team::class)- Team ownership
- Key Features:
- Environment isolation
- Resource organization
- Purpose: Environment-specific configuration
- Key Relationships:
hasMany(Application::class)- Environment applicationshasMany(Service::class)- Environment servicesbelongsTo(Project::class)- Project context
- StandalonePostgresql.php (11KB, 351 lines)
- StandaloneMysql.php (11KB, 351 lines)
- StandaloneMariadb.php (10KB, 337 lines)
- StandaloneMongodb.php (12KB, 370 lines)
- StandaloneRedis.php (12KB, 394 lines)
- StandaloneKeydb.php (11KB, 347 lines)
- StandaloneDragonfly.php (11KB, 347 lines)
- StandaloneClickhouse.php (10KB, 336 lines)
Common Features:
- Database configuration management
- Backup scheduling and execution
- Connection string generation
- Health monitoring
- Purpose: Application environment variable management
- Key Features:
- Encrypted value storage
- Build-time vs runtime variables
- Shared variable inheritance
- Purpose: Global Coolify instance configuration
- Key Features:
- FQDN and port configuration
- Auto-update settings
- Security configurations
Using lorisleiva/laravel-actions for business logic encapsulation:
// 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
Data access abstraction layer:
- Encapsulates database queries
- Provides testable data layer
- Abstracts complex query logic
Business logic services:
- External API integrations
- Complex business operations
- Cross-cutting concerns
- HTTP Request → routes/web.php
- Middleware → Authentication, authorization
- Livewire Component → app/Livewire/
- Action/Service → Business logic execution
- Model/Repository → Data persistence
- Response → Livewire reactive update
- Job Dispatch → Queue system (Redis)
- Job Processing → app/Jobs/
- Action Execution → Business logic
- Event Broadcasting → Real-time updates
- Notification → User feedback
// 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);
});
}
}- Team Membership → User belongs to team
- Resource Ownership → Resource belongs to team
- Policy Authorization → app/Policies/
- Environment Isolation → Project/environment boundaries
- Environment Variables: Encrypted at rest
- SSH Keys: Secure storage and transmission
- API Tokens: Sanctum-based authentication
- Audit Logging: spatie/laravel-activitylog
- InstanceSettings: System-wide settings
- config/: Laravel configuration files
- Team: Team-specific settings
- ServerSetting: Server configurations
- ProjectSetting: Project settings
- Environment: Environment variables
- ApplicationSetting: App-specific settings
- EnvironmentVariable: Runtime configuration
Real-time updates using Laravel Echo and WebSockets:
// Example event structure
class ApplicationDeploymentStarted implements ShouldBroadcast
{
public function broadcastOn(): array
{
return [
new PrivateChannel("team.{$this->application->team->id}"),
];
}
}- Deployment status updates
- Resource monitoring alerts
- Notification dispatching
- Audit log creation
// Environment variables can belong to multiple resource types
class EnvironmentVariable extends Model
{
public function resource(): MorphTo
{
return $this->morphTo();
}
}All major resources include team-based query scoping with request-level caching:
// ✅ CORRECT - Use cached methods (request-level cache via once())
$applications = Application::ownedByCurrentTeamCached();
$servers = Server::ownedByCurrentTeamCached();
// ✅ CORRECT - Filter cached collection in memory
$activeServers = Server::ownedByCurrentTeamCached()->where('is_active', true);
// Only use query builder when you need eager loading or fresh data
$projects = Project::ownedByCurrentTeam()->with('environments')->get();See Database Patterns for full documentation.
Environment variables cascade from:
- Shared Variables → Team-wide defaults
- Project Variables → Project-specific overrides
- Application Variables → Application-specific values
Abstracted git operations supporting:
- GitHub: app/Models/GithubApp.php
- GitLab: app/Models/GitlabApp.php
- Bitbucket: Webhook integration
- Gitea: Self-hosted Git support
- Container Management: Direct Docker API communication
- Image Building: Dockerfile and Buildpack support
- Network Management: Custom Docker networks
- Volume Management: Persistent storage handling
- phpseclib/phpseclib: Secure SSH connections
- Multiplexing: Connection pooling for efficiency
- Key Management: PrivateKey model
tests/
├── Feature/ # Integration tests
├── Unit/ # Unit tests
├── Browser/ # Dusk browser tests
├── Traits/ # Test helper traits
├── Pest.php # Pest configuration
└── TestCase.php # Base test case
- Feature Tests: Full request lifecycle testing
- Unit Tests: Individual class/method testing
- Browser Tests: End-to-end user workflows
- Database Testing: Factories and seeders
- Eager Loading: Prevent N+1 queries
- Query Scoping: Team-based filtering
- Database Indexing: Optimized for common queries
- Redis: Session and cache storage
- Model Caching: Frequently accessed data
- Query Caching: Expensive query results
- Queue Workers: Horizon-managed job processing
- Job Batching: Related job grouping
- Failed Job Handling: Automatic retry logic
Container health status is monitored and updated through multiple independent paths. When modifying status logic, ALL paths must be updated to ensure consistency.
File: app/Actions/Docker/GetContainersStatus.php
Method: aggregateApplicationStatus() (lines 487-540)
Trigger: Scheduled job or manual refresh
Frequency: Every minute (via ServerCheckJob)
Status Aggregation Logic:
// 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)';
}File: 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:
// ⚠️ 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)';
}File: 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 (
:excludedsuffix) - Calculates overall application health from all containers
Status Format with Excluded Containers:
// 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 disabledFile: app/Models/Service.php
Method: complexStatus() (lines 176-288)
Purpose: Aggregates status for multi-container services
Used by: Docker Compose services
Status Calculation:
// 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";
}┌─────────────────────────────────────────────────────────────┐
│ 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) │
└───────────────────────┘
All status aggregation locations MUST follow the same priority:
For Running Containers:
- unhealthy - Container has failing health checks
- unknown - Container health status cannot be determined
- healthy - Container is healthy
For Non-Running States:
- restarting →
degraded (unhealthy) - running + exited →
degraded (unhealthy) - dead/removing →
degraded (unhealthy) - paused →
paused - created/starting →
starting - exited →
exited (unhealthy)
When containers have exclude_from_hc: true flag or restart: no:
Behavior:
- Status is still calculated from container state
:excludedsuffix 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
:excludedsuffix (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
✅ 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))
-
Using the ContainerStatusAggregator Service:
- Import
App\Services\ContainerStatusAggregatorin any class needing status aggregation - Two methods available:
aggregateFromStrings(Collection $statusStrings, int $maxRestartCount = 0)- For pre-formatted status stringsaggregateFromContainers(Collection $containers, int $maxRestartCount = 0)- For raw Docker container objects
- Returns colon format:
running:healthy,degraded:unhealthy, etc. - Automatically handles crash loop detection via
$maxRestartCountparameter
- Import
-
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)
- Restarting →
-
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)
- Run unit tests:
-
Handle excluded containers:
- All containers excluded (
exclude_from_hc: true) - UseCalculatesExcludedStatustrait - Mixed excluded/non-excluded containers - Filter then use
ContainerStatusAggregator - Containers with
restart: no- Treated same asexclude_from_hc: true
- All containers excluded (
-
Use shared trait for excluded containers:
- Import
App\Traits\CalculatesExcludedStatusin status calculation classes - Use
getExcludedContainersFromDockerCompose()to parse exclusions - Use
calculateExcludedStatus()for full Docker inspect objects (ComplexStatusCheck) - Use
calculateExcludedStatusFromStrings()for status strings (PushServerUpdateJob, GetContainersStatus)
- Import
- tests/Unit/ContainerStatusAggregatorTest.php: Core state machine logic (42 comprehensive tests)
- tests/Unit/ContainerHealthStatusTest.php: Health status aggregation integration
- tests/Unit/PushServerUpdateJobStatusAggregationTest.php: Sentinel update logic
- tests/Unit/ExcludeFromHealthCheckTest.php: Excluded container handling
✅ Prevented by ContainerStatusAggregator Service:
- ❌ Old Bug: Forgetting to track
$hasUnknownflag → ✅ 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