v4.0.0-beta.453 (#7467)
This commit is contained in:
commit
b7282ad565
162 changed files with 5749 additions and 1146 deletions
|
|
@ -283,14 +283,22 @@ ### **Polymorphic Relationships**
|
|||
|
||||
### **Team-Based Soft Scoping**
|
||||
|
||||
All major resources include team-based query scoping:
|
||||
All major resources include team-based query scoping with request-level caching:
|
||||
|
||||
```php
|
||||
// Automatic team filtering
|
||||
$applications = Application::ownedByCurrentTeam()->get();
|
||||
$servers = Server::ownedByCurrentTeam()->get();
|
||||
// ✅ 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](.ai/patterns/database-patterns.md#request-level-caching-with-ownedbycurrentteamcached) for full documentation.
|
||||
|
||||
### **Configuration Inheritance**
|
||||
|
||||
Environment variables cascade from:
|
||||
|
|
|
|||
|
|
@ -243,6 +243,59 @@ ### Database Indexes
|
|||
- **Composite indexes** for common queries
|
||||
- **Unique constraints** for business rules
|
||||
|
||||
### Request-Level Caching with ownedByCurrentTeamCached()
|
||||
|
||||
Many models have both `ownedByCurrentTeam()` (returns query builder) and `ownedByCurrentTeamCached()` (returns cached collection). **Always prefer the cached version** to avoid duplicate database queries within the same request.
|
||||
|
||||
**Models with cached methods available:**
|
||||
- `Server`, `PrivateKey`, `Project`
|
||||
- `Application`
|
||||
- `StandalonePostgresql`, `StandaloneMysql`, `StandaloneRedis`, `StandaloneMariadb`, `StandaloneMongodb`, `StandaloneKeydb`, `StandaloneDragonfly`, `StandaloneClickhouse`
|
||||
- `Service`, `ServiceApplication`, `ServiceDatabase`
|
||||
|
||||
**Usage patterns:**
|
||||
```php
|
||||
// ✅ CORRECT - Uses request-level cache (via Laravel's once() helper)
|
||||
$servers = Server::ownedByCurrentTeamCached();
|
||||
|
||||
// ❌ AVOID - Makes a new database query each time
|
||||
$servers = Server::ownedByCurrentTeam()->get();
|
||||
|
||||
// ✅ CORRECT - Filter cached collection in memory
|
||||
$activeServers = Server::ownedByCurrentTeamCached()->where('is_active', true);
|
||||
$server = Server::ownedByCurrentTeamCached()->firstWhere('id', $serverId);
|
||||
$serverIds = Server::ownedByCurrentTeamCached()->pluck('id');
|
||||
|
||||
// ❌ AVOID - Making filtered database queries when data is already cached
|
||||
$activeServers = Server::ownedByCurrentTeam()->where('is_active', true)->get();
|
||||
```
|
||||
|
||||
**When to use which:**
|
||||
- `ownedByCurrentTeamCached()` - **Default choice** for reading team data
|
||||
- `ownedByCurrentTeam()` - Only when you need to chain query builder methods that can't be done on collections (like `with()` for eager loading), or when you explicitly need a fresh database query
|
||||
|
||||
**Implementation pattern for new models:**
|
||||
```php
|
||||
/**
|
||||
* Get query builder for resources owned by current team.
|
||||
* If you need all resources without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return self::whereTeamId(currentTeam()->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all resources owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return self::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Data Consistency Patterns
|
||||
|
||||
### Database Transactions
|
||||
|
|
|
|||
8
.github/workflows/coolify-helper-next.yml
vendored
8
.github/workflows/coolify-helper-next.yml
vendored
|
|
@ -44,8 +44,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
@ -86,8 +86,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
|
|||
8
.github/workflows/coolify-helper.yml
vendored
8
.github/workflows/coolify-helper.yml
vendored
|
|
@ -44,8 +44,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
@ -85,8 +85,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
@ -91,8 +91,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
|
|||
8
.github/workflows/coolify-realtime-next.yml
vendored
8
.github/workflows/coolify-realtime-next.yml
vendored
|
|
@ -48,8 +48,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
@ -90,8 +90,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
|
|||
8
.github/workflows/coolify-realtime.yml
vendored
8
.github/workflows/coolify-realtime.yml
vendored
|
|
@ -48,8 +48,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
@ -90,8 +90,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
|
|
|||
8
.github/workflows/coolify-staging-build.yml
vendored
8
.github/workflows/coolify-staging-build.yml
vendored
|
|
@ -64,8 +64,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
|
|
@ -110,8 +110,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
|
|
|
|||
8
.github/workflows/coolify-testing-host.yml
vendored
8
.github/workflows/coolify-testing-host.yml
vendored
|
|
@ -44,8 +44,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
|
|
@ -81,8 +81,8 @@ jobs:
|
|||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -5389,7 +5389,6 @@ ### 🚀 Features
|
|||
- Add static ipv4 ipv6 support
|
||||
- Server disabled by overflow
|
||||
- Preview deployment logs
|
||||
- Collect webhooks during maintenance
|
||||
- Logs and execute commands with several servers
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@ ### Performance Considerations
|
|||
- Queue heavy operations
|
||||
- Optimize database queries with proper indexes
|
||||
- Use chunking for large data operations
|
||||
- **CRITICAL**: Use `ownedByCurrentTeamCached()` instead of `ownedByCurrentTeam()->get()`
|
||||
|
||||
### Code Style
|
||||
- Follow PSR-12 coding standards
|
||||
|
|
@ -317,4 +318,5 @@ ### Livewire & Frontend
|
|||
|
||||
|
||||
Random other things you should remember:
|
||||
- App\Models\Application::team must return a relationship instance., always use team()
|
||||
- App\Models\Application::team must return a relationship instance., always use team()
|
||||
- Always use `Model::ownedByCurrentTeamCached()` instead of `Model::ownedByCurrentTeam()->get()` for team-scoped queries to avoid duplicate database queries
|
||||
176
app/Actions/Application/CleanupPreviewDeployment.php
Normal file
176
app/Actions/Application/CleanupPreviewDeployment.php
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions\Application;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class CleanupPreviewDeployment
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
/**
|
||||
* Clean up a PR preview deployment completely.
|
||||
*
|
||||
* This handles:
|
||||
* 1. Cancelling active deployments for the PR (QUEUED/IN_PROGRESS → CANCELLED_BY_USER)
|
||||
* 2. Killing helper containers by deployment_uuid
|
||||
* 3. Stopping/removing all running PR containers
|
||||
* 4. Dispatching DeleteResourceJob for thorough cleanup (volumes, networks, database records)
|
||||
*
|
||||
* This unifies the cleanup logic from GitHub webhook handler to be used across all providers.
|
||||
*/
|
||||
public function handle(
|
||||
Application $application,
|
||||
int $pull_request_id,
|
||||
?ApplicationPreview $preview = null
|
||||
): array {
|
||||
$result = [
|
||||
'cancelled_deployments' => 0,
|
||||
'killed_containers' => 0,
|
||||
'status' => 'success',
|
||||
];
|
||||
|
||||
$server = $application->destination->server;
|
||||
|
||||
if (! $server->isFunctional()) {
|
||||
return [
|
||||
...$result,
|
||||
'status' => 'failed',
|
||||
'message' => 'Server is not functional',
|
||||
];
|
||||
}
|
||||
|
||||
// Step 1: Cancel all active deployments for this PR and kill helper containers
|
||||
$result['cancelled_deployments'] = $this->cancelActiveDeployments(
|
||||
$application,
|
||||
$pull_request_id,
|
||||
$server
|
||||
);
|
||||
|
||||
// Step 2: Stop and remove all running PR containers
|
||||
$result['killed_containers'] = $this->stopRunningContainers(
|
||||
$application,
|
||||
$pull_request_id,
|
||||
$server
|
||||
);
|
||||
|
||||
// Step 3: Find or use provided preview, then dispatch cleanup job for thorough cleanup
|
||||
if (! $preview) {
|
||||
$preview = ApplicationPreview::where('application_id', $application->id)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($preview) {
|
||||
DeleteResourceJob::dispatch($preview);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all active (QUEUED/IN_PROGRESS) deployments for this PR.
|
||||
*/
|
||||
private function cancelActiveDeployments(
|
||||
Application $application,
|
||||
int $pull_request_id,
|
||||
$server
|
||||
): int {
|
||||
$activeDeployments = ApplicationDeploymentQueue::where('application_id', $application->id)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->whereIn('status', [
|
||||
ApplicationDeploymentStatus::QUEUED->value,
|
||||
ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
])
|
||||
->get();
|
||||
|
||||
$cancelled = 0;
|
||||
foreach ($activeDeployments as $deployment) {
|
||||
try {
|
||||
// Mark deployment as cancelled
|
||||
$deployment->update([
|
||||
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
|
||||
]);
|
||||
|
||||
// Add cancellation log entry
|
||||
$deployment->addLogEntry('Deployment cancelled: Pull request closed.', 'stderr');
|
||||
|
||||
// Try to kill helper container if it exists
|
||||
$this->killHelperContainer($deployment->deployment_uuid, $server);
|
||||
$cancelled++;
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning("Failed to cancel deployment {$deployment->id}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
return $cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill the helper container used during deployment.
|
||||
*/
|
||||
private function killHelperContainer(string $deployment_uuid, $server): void
|
||||
{
|
||||
try {
|
||||
$escapedUuid = escapeshellarg($deployment_uuid);
|
||||
$checkCommand = "docker ps -a --filter name={$escapedUuid} --format '{{.Names}}'";
|
||||
$containerExists = instant_remote_process([$checkCommand], $server);
|
||||
|
||||
if ($containerExists && str($containerExists)->trim()->isNotEmpty()) {
|
||||
instant_remote_process(["docker rm -f {$escapedUuid}"], $server);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silently handle - container may already be gone
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop and remove all running containers for this PR.
|
||||
*/
|
||||
private function stopRunningContainers(
|
||||
Application $application,
|
||||
int $pull_request_id,
|
||||
$server
|
||||
): int {
|
||||
$killed = 0;
|
||||
|
||||
try {
|
||||
if ($server->isSwarm()) {
|
||||
$escapedStackName = escapeshellarg("{$application->uuid}-{$pull_request_id}");
|
||||
instant_remote_process(["docker stack rm {$escapedStackName}"], $server);
|
||||
$killed++;
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus(
|
||||
$server,
|
||||
$application->id,
|
||||
$pull_request_id
|
||||
);
|
||||
|
||||
if ($containers->isNotEmpty()) {
|
||||
foreach ($containers as $container) {
|
||||
$containerName = data_get($container, 'Names');
|
||||
if ($containerName) {
|
||||
$escapedContainerName = escapeshellarg($containerName);
|
||||
instant_remote_process(
|
||||
["docker rm -f {$escapedContainerName}"],
|
||||
$server
|
||||
);
|
||||
$killed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning("Error stopping containers for PR #{$pull_request_id}: {$e->getMessage()}");
|
||||
}
|
||||
|
||||
return $killed;
|
||||
}
|
||||
}
|
||||
|
|
@ -461,9 +461,10 @@ private function aggregateApplicationStatus($application, Collection $containerS
|
|||
}
|
||||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// Use preserveRestarting: true so applications show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
|
||||
return $aggregator->aggregateFromStrings($relevantStatuses, $maxRestartCount);
|
||||
return $aggregator->aggregateFromStrings($relevantStatuses, $maxRestartCount, preserveRestarting: true);
|
||||
}
|
||||
|
||||
private function aggregateServiceContainerStatuses($services)
|
||||
|
|
@ -518,8 +519,9 @@ private function aggregateServiceContainerStatuses($services)
|
|||
}
|
||||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// Use preserveRestarting: true so individual sub-resources show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses);
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, preserveRestarting: true);
|
||||
|
||||
// Update service sub-resource status with aggregated result
|
||||
if ($aggregatedStatus) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ class CheckUpdates
|
|||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
$osId = 'unknown';
|
||||
$packageManager = null;
|
||||
|
||||
try {
|
||||
if ($server->serverStatus() === false) {
|
||||
return [
|
||||
|
|
@ -93,6 +96,16 @@ public function handle(Server $server)
|
|||
$out['osId'] = $osId;
|
||||
$out['package_manager'] = $packageManager;
|
||||
|
||||
return $out;
|
||||
case 'pacman':
|
||||
// Sync database first, then check for updates
|
||||
// Using -Sy to refresh package database before querying available updates
|
||||
instant_remote_process(['pacman -Sy'], $server);
|
||||
$output = instant_remote_process(['pacman -Qu 2>/dev/null'], $server);
|
||||
$out = $this->parsePacmanOutput($output);
|
||||
$out['osId'] = $osId;
|
||||
$out['package_manager'] = $packageManager;
|
||||
|
||||
return $out;
|
||||
default:
|
||||
return [
|
||||
|
|
@ -219,4 +232,45 @@ private function parseAptOutput(string $output): array
|
|||
'updates' => $updates,
|
||||
];
|
||||
}
|
||||
|
||||
private function parsePacmanOutput(string $output): array
|
||||
{
|
||||
$updates = [];
|
||||
$unparsedLines = [];
|
||||
$lines = explode("\n", $output);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
// Format: package current_version -> new_version
|
||||
if (preg_match('/^(\S+)\s+(\S+)\s+->\s+(\S+)$/', $line, $matches)) {
|
||||
$updates[] = [
|
||||
'package' => $matches[1],
|
||||
'current_version' => $matches[2],
|
||||
'new_version' => $matches[3],
|
||||
'architecture' => 'unknown',
|
||||
'repository' => 'unknown',
|
||||
];
|
||||
} else {
|
||||
// Log unmatched lines for debugging purposes
|
||||
$unparsedLines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
$result = [
|
||||
'total_updates' => count($updates),
|
||||
'updates' => $updates,
|
||||
];
|
||||
|
||||
// Include unparsed lines in the result for debugging if any exist
|
||||
if (! empty($unparsedLines)) {
|
||||
$result['unparsed_lines'] = $unparsedLines;
|
||||
\Illuminate\Support\Facades\Log::debug('Pacman output contained unparsed lines', [
|
||||
'unparsed_lines' => $unparsedLines,
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ class CleanupDocker
|
|||
|
||||
public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $deleteUnusedNetworks = false)
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
$realtimeImage = config('constants.coolify.realtime_image');
|
||||
$realtimeImageVersion = config('constants.coolify.realtime_version');
|
||||
$realtimeImageWithVersion = "$realtimeImage:$realtimeImageVersion";
|
||||
|
|
@ -26,9 +25,25 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
|
|||
$helperImageWithoutPrefix = 'coollabsio/coolify-helper';
|
||||
$helperImageWithoutPrefixVersion = "coollabsio/coolify-helper:$helperImageVersion";
|
||||
|
||||
$cleanupLog = [];
|
||||
|
||||
// Get all application image repositories to exclude from prune
|
||||
$applications = $server->applications();
|
||||
$applicationImageRepos = collect($applications)->map(function ($app) {
|
||||
return $app->docker_registry_image_name ?? $app->uuid;
|
||||
})->unique()->values();
|
||||
|
||||
// Clean up old application images while preserving N most recent for rollback
|
||||
$applicationCleanupLog = $this->cleanupApplicationImages($server, $applications);
|
||||
$cleanupLog = array_merge($cleanupLog, $applicationCleanupLog);
|
||||
|
||||
// Build image prune command that excludes application images
|
||||
// This ensures we clean up non-Coolify images while preserving rollback images
|
||||
$imagePruneCmd = $this->buildImagePruneCommand($applicationImageRepos);
|
||||
|
||||
$commands = [
|
||||
'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"',
|
||||
'docker image prune -af --filter "label!=coolify.managed=true"',
|
||||
$imagePruneCmd,
|
||||
'docker builder prune -af',
|
||||
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
|
||||
"docker images --filter before=$realtimeImageWithVersion --filter reference=$realtimeImage | grep $realtimeImage | awk '{print $3}' | xargs -r docker rmi -f",
|
||||
|
|
@ -44,7 +59,6 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
|
|||
$commands[] = 'docker network prune -f';
|
||||
}
|
||||
|
||||
$cleanupLog = [];
|
||||
foreach ($commands as $command) {
|
||||
$commandOutput = instant_remote_process([$command], $server, false);
|
||||
if ($commandOutput !== null) {
|
||||
|
|
@ -57,4 +71,122 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
|
|||
|
||||
return $cleanupLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a docker image prune command that excludes application image repositories.
|
||||
*
|
||||
* Since docker image prune doesn't support excluding by repository name directly,
|
||||
* we use a shell script approach to delete unused images while preserving application images.
|
||||
*/
|
||||
private function buildImagePruneCommand($applicationImageRepos): string
|
||||
{
|
||||
// Step 1: Always prune dangling images (untagged)
|
||||
$commands = ['docker image prune -f'];
|
||||
|
||||
if ($applicationImageRepos->isEmpty()) {
|
||||
// No applications, add original prune command for all unused images
|
||||
$commands[] = 'docker image prune -af --filter "label!=coolify.managed=true"';
|
||||
} else {
|
||||
// Build grep pattern to exclude application image repositories
|
||||
$excludePatterns = $applicationImageRepos->map(function ($repo) {
|
||||
// Escape special characters for grep extended regex (ERE)
|
||||
// ERE special chars: . \ + * ? [ ^ ] $ ( ) { } |
|
||||
return preg_replace('/([.\\\\+*?\[\]^$(){}|])/', '\\\\$1', $repo);
|
||||
})->implode('|');
|
||||
|
||||
// Delete unused images that:
|
||||
// - Are not application images (don't match app repos)
|
||||
// - Don't have coolify.managed=true label
|
||||
// Images in use by containers will fail silently with docker rmi
|
||||
// Pattern matches both uuid:tag and uuid_servicename:tag (Docker Compose with build)
|
||||
$commands[] = "docker images --format '{{.Repository}}:{{.Tag}}' | ".
|
||||
"grep -v -E '^({$excludePatterns})[_:].+' | ".
|
||||
"grep -v '<none>' | ".
|
||||
"xargs -r -I {} sh -c 'docker inspect --format \"{{{{index .Config.Labels \\\"coolify.managed\\\"}}}}\" \"{}\" 2>/dev/null | grep -q true || docker rmi \"{}\" 2>/dev/null' || true";
|
||||
}
|
||||
|
||||
return implode(' && ', $commands);
|
||||
}
|
||||
|
||||
private function cleanupApplicationImages(Server $server, $applications = null): array
|
||||
{
|
||||
$cleanupLog = [];
|
||||
|
||||
if ($applications === null) {
|
||||
$applications = $server->applications();
|
||||
}
|
||||
|
||||
$disableRetention = $server->settings->disable_application_image_retention ?? false;
|
||||
|
||||
foreach ($applications as $application) {
|
||||
$imagesToKeep = $disableRetention ? 0 : ($application->settings->docker_images_to_keep ?? 2);
|
||||
$imageRepository = $application->docker_registry_image_name ?? $application->uuid;
|
||||
|
||||
// Get the currently running image tag
|
||||
$currentTagCommand = "docker inspect --format='{{.Config.Image}}' {$application->uuid} 2>/dev/null | grep -oP '(?<=:)[^:]+$' || true";
|
||||
$currentTag = instant_remote_process([$currentTagCommand], $server, false);
|
||||
$currentTag = trim($currentTag ?? '');
|
||||
|
||||
// List all images for this application with their creation timestamps
|
||||
// Use wildcard to match both uuid:tag and uuid_servicename:tag (Docker Compose with build)
|
||||
$listCommand = "docker images --format '{{.Repository}}:{{.Tag}}#{{.CreatedAt}}' --filter reference='{$imageRepository}*' 2>/dev/null || true";
|
||||
$output = instant_remote_process([$listCommand], $server, false);
|
||||
|
||||
if (empty($output)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$images = collect(explode("\n", trim($output)))
|
||||
->filter()
|
||||
->map(function ($line) {
|
||||
$parts = explode('#', $line);
|
||||
$imageRef = $parts[0] ?? '';
|
||||
$tagParts = explode(':', $imageRef);
|
||||
|
||||
return [
|
||||
'repository' => $tagParts[0] ?? '',
|
||||
'tag' => $tagParts[1] ?? '',
|
||||
'created_at' => $parts[1] ?? '',
|
||||
'image_ref' => $imageRef,
|
||||
];
|
||||
})
|
||||
->filter(fn ($image) => ! empty($image['tag']));
|
||||
|
||||
// Separate images into categories
|
||||
// PR images (pr-*) and build images (*-build) are excluded from retention
|
||||
// Build images will be cleaned up by docker image prune -af
|
||||
$prImages = $images->filter(fn ($image) => str_starts_with($image['tag'], 'pr-'));
|
||||
$regularImages = $images->filter(fn ($image) => ! str_starts_with($image['tag'], 'pr-') && ! str_ends_with($image['tag'], '-build'));
|
||||
|
||||
// Always delete all PR images
|
||||
foreach ($prImages as $image) {
|
||||
$deleteCommand = "docker rmi {$image['image_ref']} 2>/dev/null || true";
|
||||
$deleteOutput = instant_remote_process([$deleteCommand], $server, false);
|
||||
$cleanupLog[] = [
|
||||
'command' => $deleteCommand,
|
||||
'output' => $deleteOutput ?? 'PR image removed or was in use',
|
||||
];
|
||||
}
|
||||
|
||||
// Filter out current running image from regular images and sort by creation date
|
||||
$sortedRegularImages = $regularImages
|
||||
->filter(fn ($image) => $image['tag'] !== $currentTag)
|
||||
->sortByDesc('created_at')
|
||||
->values();
|
||||
|
||||
// Keep only N images (imagesToKeep), delete the rest
|
||||
$imagesToDelete = $sortedRegularImages->skip($imagesToKeep);
|
||||
|
||||
foreach ($imagesToDelete as $image) {
|
||||
$deleteCommand = "docker rmi {$image['image_ref']} 2>/dev/null || true";
|
||||
$deleteOutput = instant_remote_process([$deleteCommand], $server, false);
|
||||
$cleanupLog[] = [
|
||||
'command' => $deleteCommand,
|
||||
'output' => $deleteOutput ?? 'Image removed or was in use',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $cleanupLog;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ public function handle(Server $server)
|
|||
$command = $command->merge([$this->getRhelDockerInstallCommand()]);
|
||||
} elseif ($supported_os_type->contains('sles')) {
|
||||
$command = $command->merge([$this->getSuseDockerInstallCommand()]);
|
||||
} elseif ($supported_os_type->contains('arch')) {
|
||||
$command = $command->merge([$this->getArchDockerInstallCommand()]);
|
||||
} else {
|
||||
$command = $command->merge([$this->getGenericDockerInstallCommand()]);
|
||||
}
|
||||
|
|
@ -146,8 +148,27 @@ private function getSuseDockerInstallCommand(): string
|
|||
')';
|
||||
}
|
||||
|
||||
private function getArchDockerInstallCommand(): string
|
||||
{
|
||||
return 'pacman -Syyy --noconfirm && '.
|
||||
'pacman -S docker docker-compose --noconfirm && '.
|
||||
'systemctl start docker && '.
|
||||
'systemctl enable docker';
|
||||
}
|
||||
|
||||
private function getGenericDockerInstallCommand(): string
|
||||
{
|
||||
return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion}";
|
||||
}
|
||||
|
||||
private function getArchDockerInstallCommand(): string
|
||||
{
|
||||
// Use -Syu to perform full system upgrade before installing Docker
|
||||
// Partial upgrades (-Sy without -u) are discouraged on Arch Linux
|
||||
// as they can lead to broken dependencies and system instability
|
||||
// Use --needed to skip reinstalling packages that are already up-to-date (idempotent)
|
||||
return 'pacman -Syu --noconfirm --needed docker docker-compose && '.
|
||||
'systemctl enable docker.service && '.
|
||||
'systemctl start docker.service';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,13 @@ public function handle(Server $server)
|
|||
'command -v git >/dev/null || zypper install -y git',
|
||||
'command -v jq >/dev/null || zypper install -y jq',
|
||||
]);
|
||||
} elseif ($supported_os_type->contains('arch')) {
|
||||
// Use -Syu for full system upgrade to avoid partial upgrade issues on Arch Linux
|
||||
// --needed flag skips packages that are already installed and up-to-date
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Prerequisites for Arch Linux...'",
|
||||
'pacman -Syu --noconfirm --needed curl wget git jq',
|
||||
]);
|
||||
} else {
|
||||
throw new \Exception('Unsupported OS type for prerequisites installation');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,18 +20,43 @@ public function handle(Server $server, string $osId, ?string $package = null, ?s
|
|||
'error' => 'Server is not reachable or not ready.',
|
||||
];
|
||||
}
|
||||
|
||||
// Validate that package name is provided when not updating all packages
|
||||
if (! $all && ($package === null || $package === '')) {
|
||||
return [
|
||||
'error' => "Package name required when 'all' is false.",
|
||||
];
|
||||
}
|
||||
|
||||
// Sanitize package name to prevent command injection
|
||||
// Only allow alphanumeric characters, hyphens, underscores, periods, plus signs, and colons
|
||||
// These are valid characters in package names across most package managers
|
||||
$sanitizedPackage = '';
|
||||
if ($package !== null && ! $all) {
|
||||
if (! preg_match('/^[a-zA-Z0-9._+:-]+$/', $package)) {
|
||||
return [
|
||||
'error' => 'Invalid package name. Package names can only contain alphanumeric characters, hyphens, underscores, periods, plus signs, and colons.',
|
||||
];
|
||||
}
|
||||
$sanitizedPackage = escapeshellarg($package);
|
||||
}
|
||||
|
||||
switch ($packageManager) {
|
||||
case 'zypper':
|
||||
$commandAll = 'zypper update -y';
|
||||
$commandInstall = 'zypper install -y '.$package;
|
||||
$commandInstall = 'zypper install -y '.$sanitizedPackage;
|
||||
break;
|
||||
case 'dnf':
|
||||
$commandAll = 'dnf update -y';
|
||||
$commandInstall = 'dnf update -y '.$package;
|
||||
$commandInstall = 'dnf update -y '.$sanitizedPackage;
|
||||
break;
|
||||
case 'apt':
|
||||
$commandAll = 'apt update && apt upgrade -y';
|
||||
$commandInstall = 'apt install -y '.$package;
|
||||
$commandInstall = 'apt install -y '.$sanitizedPackage;
|
||||
break;
|
||||
case 'pacman':
|
||||
$commandAll = 'pacman -Syu --noconfirm';
|
||||
$commandInstall = 'pacman -S --noconfirm '.$sanitizedPackage;
|
||||
break;
|
||||
default:
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
namespace App\Actions\Service;
|
||||
|
||||
use App\Actions\Server\CleanupDocker;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class StopService
|
||||
{
|
||||
|
|
@ -17,6 +19,17 @@ class StopService
|
|||
public function handle(Service $service, bool $deleteConnectedNetworks = false, bool $dockerCleanup = true)
|
||||
{
|
||||
try {
|
||||
// Cancel any in-progress deployment activities so status doesn't stay stuck at "starting"
|
||||
Activity::where('properties->type_uuid', $service->uuid)
|
||||
->where(function ($q) {
|
||||
$q->where('properties->status', ProcessStatus::IN_PROGRESS->value)
|
||||
->orWhere('properties->status', ProcessStatus::QUEUED->value);
|
||||
})
|
||||
->each(function ($activity) {
|
||||
$activity->properties = $activity->properties->put('status', ProcessStatus::CANCELLED->value);
|
||||
$activity->save();
|
||||
});
|
||||
|
||||
$server = $service->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
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\CleanupOrphanedPreviewContainersJob;
|
||||
use App\Jobs\PullChangelog;
|
||||
use App\Jobs\PullTemplatesFromCDN;
|
||||
use App\Jobs\RegenerateSslCertJob;
|
||||
|
|
@ -18,7 +18,6 @@
|
|||
use App\Models\Team;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
|
|
@ -88,6 +87,9 @@ protected function schedule(Schedule $schedule): void
|
|||
|
||||
$this->scheduleInstance->command('cleanup:database --yes')->daily();
|
||||
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
|
||||
|
||||
// Cleanup orphaned PR preview containers daily
|
||||
$this->scheduleInstance->job(new CleanupOrphanedPreviewContainersJob)->daily()->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,17 +102,7 @@ private function pullImages(): void
|
|||
} else {
|
||||
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
try {
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$this->scheduleInstance->job(function () use ($server) {
|
||||
CheckAndStartSentinelJob::dispatch($server);
|
||||
})->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error pulling images: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
// Sentinel update checks are now handled by ServerManagerJob
|
||||
$this->scheduleInstance->job(new CheckHelperImageJob)
|
||||
->cron($this->updateCheckFrequency)
|
||||
->timezone($this->instanceTimezone)
|
||||
|
|
|
|||
|
|
@ -14,12 +14,15 @@ class ProxyStatusChangedUI implements ShouldBroadcast
|
|||
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct(?int $teamId = null)
|
||||
public ?int $activityId = null;
|
||||
|
||||
public function __construct(?int $teamId = null, ?int $activityId = null)
|
||||
{
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
$this->activityId = $activityId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ public static function generateScpCommand(Server $server, string $source, string
|
|||
return $scp_command;
|
||||
}
|
||||
|
||||
public static function generateSshCommand(Server $server, string $command)
|
||||
public static function generateSshCommand(Server $server, string $command, bool $disableMultiplexing = false)
|
||||
{
|
||||
if ($server->settings->force_disabled) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
|
|
@ -168,7 +168,7 @@ public static function generateSshCommand(Server $server, string $command)
|
|||
$ssh_command = "timeout $timeout ssh ";
|
||||
|
||||
$multiplexingSuccessful = false;
|
||||
if (self::isMultiplexingEnabled()) {
|
||||
if (! $disableMultiplexing && self::isMultiplexingEnabled()) {
|
||||
try {
|
||||
$multiplexingSuccessful = self::ensureMultiplexedConnection($server);
|
||||
if ($multiplexingSuccessful) {
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
|
||||
class MagicController extends Controller
|
||||
{
|
||||
public function servers()
|
||||
{
|
||||
return response()->json([
|
||||
'servers' => Server::isUsable()->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destinations()
|
||||
{
|
||||
return response()->json([
|
||||
'destinations' => Server::destinationsByServer(request()->query('server_id'))->sortBy('name'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function projects()
|
||||
{
|
||||
return response()->json([
|
||||
'projects' => Project::ownedByCurrentTeam()->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function environments()
|
||||
{
|
||||
$project = Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->first();
|
||||
if (! $project) {
|
||||
return response()->json([
|
||||
'environments' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'environments' => $project->environments,
|
||||
]);
|
||||
}
|
||||
|
||||
public function newProject()
|
||||
{
|
||||
$project = Project::firstOrCreate(
|
||||
['name' => request()->query('name') ?? generate_random_name()],
|
||||
['team_id' => currentTeam()->id]
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'project_uuid' => $project->uuid,
|
||||
]);
|
||||
}
|
||||
|
||||
public function newEnvironment()
|
||||
{
|
||||
$environment = Environment::firstOrCreate(
|
||||
['name' => request()->query('name') ?? generate_random_name()],
|
||||
['project_id' => Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->firstOrFail()->id]
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'environment_name' => $environment->name,
|
||||
]);
|
||||
}
|
||||
|
||||
public function newTeam()
|
||||
{
|
||||
$team = Team::create(
|
||||
[
|
||||
'name' => request()->query('name') ?? generate_random_name(),
|
||||
'personal_team' => false,
|
||||
],
|
||||
);
|
||||
auth()->user()->teams()->attach($team, ['role' => 'admin']);
|
||||
refreshSession();
|
||||
|
||||
return redirect(request()->header('Referer'));
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Livewire\Project\Service\Storage;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
|
|
@ -15,23 +15,6 @@ class Bitbucket extends Controller
|
|||
public function manual(Request $request)
|
||||
{
|
||||
try {
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Bitbicket::manual_bitbucket", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
$return_payloads = collect([]);
|
||||
$payload = $request->collect();
|
||||
$headers = $request->headers->all();
|
||||
|
|
@ -185,9 +168,10 @@ public function manual(Request $request)
|
|||
if ($x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
$container_name = generateApplicationContainerName($application, $pull_request_id);
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
// Use comprehensive cleanup that cancels active deployments,
|
||||
// kills helper containers, and removes all PR containers
|
||||
CleanupPreviewDeployment::run($application, $pull_request_id, $found);
|
||||
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
|
@ -18,30 +18,6 @@ public function manual(Request $request)
|
|||
try {
|
||||
$return_payloads = collect([]);
|
||||
$x_gitea_delivery = request()->header('X-Gitea-Delivery');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$files = Storage::disk('webhooks-during-maintenance')->files();
|
||||
$gitea_delivery_found = collect($files)->filter(function ($file) use ($x_gitea_delivery) {
|
||||
return Str::contains($file, $x_gitea_delivery);
|
||||
})->first();
|
||||
if ($gitea_delivery_found) {
|
||||
return;
|
||||
}
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Gitea::manual_{$x_gitea_delivery}", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
$x_gitea_event = Str::lower($request->header('X-Gitea-Event'));
|
||||
$x_hub_signature_256 = Str::after($request->header('X-Hub-Signature-256'), 'sha256=');
|
||||
$content_type = $request->header('Content-Type');
|
||||
|
|
@ -217,9 +193,10 @@ public function manual(Request $request)
|
|||
if ($action === 'closed') {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
$container_name = generateApplicationContainerName($application, $pull_request_id);
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
// Use comprehensive cleanup that cancels active deployments,
|
||||
// kills helper containers, and removes all PR containers
|
||||
CleanupPreviewDeployment::run($application, $pull_request_id, $found);
|
||||
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\ApplicationPullRequestUpdateJob;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Jobs\GithubAppPermissionJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
|
@ -25,30 +24,6 @@ public function manual(Request $request)
|
|||
try {
|
||||
$return_payloads = collect([]);
|
||||
$x_github_delivery = request()->header('X-GitHub-Delivery');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$files = Storage::disk('webhooks-during-maintenance')->files();
|
||||
$github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) {
|
||||
return Str::contains($file, $x_github_delivery);
|
||||
})->first();
|
||||
if ($github_delivery_found) {
|
||||
return;
|
||||
}
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::manual_{$x_github_delivery}", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
$x_github_event = Str::lower($request->header('X-GitHub-Event'));
|
||||
$x_hub_signature_256 = Str::after($request->header('X-Hub-Signature-256'), 'sha256=');
|
||||
$content_type = $request->header('Content-Type');
|
||||
|
|
@ -246,41 +221,10 @@ public function manual(Request $request)
|
|||
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();
|
||||
// Use comprehensive cleanup that cancels active deployments,
|
||||
// kills helper containers, and removes all PR containers
|
||||
CleanupPreviewDeployment::run($application, $pull_request_id, $found);
|
||||
|
||||
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,
|
||||
'status' => 'success',
|
||||
|
|
@ -310,30 +254,6 @@ public function normal(Request $request)
|
|||
$return_payloads = collect([]);
|
||||
$id = null;
|
||||
$x_github_delivery = $request->header('X-GitHub-Delivery');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$files = Storage::disk('webhooks-during-maintenance')->files();
|
||||
$github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) {
|
||||
return Str::contains($file, $x_github_delivery);
|
||||
})->first();
|
||||
if ($github_delivery_found) {
|
||||
return;
|
||||
}
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::normal_{$x_github_delivery}", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
$x_github_event = Str::lower($request->header('X-GitHub-Event'));
|
||||
$x_github_hook_installation_target_id = $request->header('X-GitHub-Hook-Installation-Target-Id');
|
||||
$x_hub_signature_256 = Str::after($request->header('X-Hub-Signature-256'), 'sha256=');
|
||||
|
|
@ -515,53 +435,12 @@ public function normal(Request $request)
|
|||
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) {
|
||||
$container_name = data_get($container, 'Names');
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
});
|
||||
}
|
||||
|
||||
// Delete the PR comment on GitHub (GitHub-specific feature)
|
||||
ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED);
|
||||
|
||||
DeleteResourceJob::dispatch($found);
|
||||
// Use comprehensive cleanup that cancels active deployments,
|
||||
// kills helper containers, and removes all PR containers
|
||||
CleanupPreviewDeployment::run($application, $pull_request_id, $found);
|
||||
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
|
|
@ -624,23 +503,6 @@ public function install(Request $request)
|
|||
{
|
||||
try {
|
||||
$installation_id = $request->get('installation_id');
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::install_{$installation_id}", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
$source = $request->get('source');
|
||||
$setup_action = $request->get('setup_action');
|
||||
$github_app = GithubApp::where('uuid', $source)->firstOrFail();
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
|
@ -16,24 +16,6 @@ class Gitlab extends Controller
|
|||
public function manual(Request $request)
|
||||
{
|
||||
try {
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Gitlab::manual_gitlab", $json);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$return_payloads = collect([]);
|
||||
$payload = $request->collect();
|
||||
$headers = $request->headers->all();
|
||||
|
|
@ -243,22 +225,22 @@ public function manual(Request $request)
|
|||
} elseif ($action === 'closed' || $action === 'close' || $action === 'merge') {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
$container_name = generateApplicationContainerName($application, $pull_request_id);
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
// Use comprehensive cleanup that cancels active deployments,
|
||||
// kills helper containers, and removes all PR containers
|
||||
CleanupPreviewDeployment::run($application, $pull_request_id, $found);
|
||||
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview Deployment closed',
|
||||
'message' => 'Preview deployment closed.',
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'No preview deployment found.',
|
||||
]);
|
||||
|
||||
return response($return_payloads);
|
||||
}
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'No Preview Deployment found',
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use App\Jobs\StripeProcessJob;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Stripe extends Controller
|
||||
{
|
||||
|
|
@ -20,23 +19,6 @@ public function events(Request $request)
|
|||
$signature,
|
||||
$webhookSecret
|
||||
);
|
||||
if (app()->isDownForMaintenance()) {
|
||||
$epoch = now()->valueOf();
|
||||
$data = [
|
||||
'attributes' => $request->attributes->all(),
|
||||
'request' => $request->request->all(),
|
||||
'query' => $request->query->all(),
|
||||
'server' => $request->server->all(),
|
||||
'files' => $request->files->all(),
|
||||
'cookies' => $request->cookies->all(),
|
||||
'headers' => $request->headers->all(),
|
||||
'content' => $request->getContent(),
|
||||
];
|
||||
$json = json_encode($data);
|
||||
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
|
||||
|
||||
return response('Webhook received. Cool cool cool cool cool.', 200);
|
||||
}
|
||||
StripeProcessJob::dispatch($event);
|
||||
|
||||
return response('Webhook received. Cool cool cool cool cool.', 200);
|
||||
|
|
|
|||
|
|
@ -620,7 +620,7 @@ private function deploy_docker_compose_buildpack()
|
|||
$this->application_deployment_queue->addLogEntry('Build secrets are configured. Ensure your docker-compose file includes build.secrets configuration for services that need them.');
|
||||
}
|
||||
} else {
|
||||
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
|
||||
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'), commit: $this->commit);
|
||||
// Always add .env file to services
|
||||
$services = collect(data_get($composeFile, 'services', []));
|
||||
$services = $services->map(function ($service, $name) {
|
||||
|
|
@ -670,13 +670,20 @@ private function deploy_docker_compose_buildpack()
|
|||
$build_command = "DOCKER_BUILDKIT=1 {$build_command}";
|
||||
}
|
||||
|
||||
// Append build arguments if not using build secrets (matching default behavior)
|
||||
// Inject build arguments after build subcommand if not using build secrets
|
||||
if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
|
||||
$build_args_string = $this->build_args->implode(' ');
|
||||
// Escape single quotes for bash -c context used by executeInDocker
|
||||
$build_args_string = str_replace("'", "'\\''", $build_args_string);
|
||||
$build_command .= " {$build_args_string}";
|
||||
$this->application_deployment_queue->addLogEntry('Adding build arguments to custom Docker Compose build command.');
|
||||
|
||||
// Inject build args right after 'build' subcommand (not at the end)
|
||||
$original_command = $build_command;
|
||||
$build_command = injectDockerComposeBuildArgs($build_command, $build_args_string);
|
||||
|
||||
// Only log if build args were actually injected (command was modified)
|
||||
if ($build_command !== $original_command) {
|
||||
$this->application_deployment_queue->addLogEntry('Adding build arguments to custom Docker Compose build command.');
|
||||
}
|
||||
}
|
||||
|
||||
$this->execute_remote_command(
|
||||
|
|
@ -1806,9 +1813,9 @@ private function health_check()
|
|||
$this->application->update(['status' => 'running']);
|
||||
$this->application_deployment_queue->addLogEntry('New container is healthy.');
|
||||
break;
|
||||
}
|
||||
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
|
||||
} elseif (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
|
||||
$this->newVersionIsHealthy = false;
|
||||
$this->application_deployment_queue->addLogEntry('New container is unhealthy.', type: 'error');
|
||||
$this->query_logs();
|
||||
break;
|
||||
}
|
||||
|
|
@ -2274,13 +2281,13 @@ private function generate_nixpacks_env_variables()
|
|||
$this->env_nixpacks_args = collect([]);
|
||||
if ($this->pull_request_id === 0) {
|
||||
foreach ($this->application->nixpacks_environment_variables as $env) {
|
||||
if (! is_null($env->real_value)) {
|
||||
if (! is_null($env->real_value) && $env->real_value !== '') {
|
||||
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
|
||||
if (! is_null($env->real_value)) {
|
||||
if (! is_null($env->real_value) && $env->real_value !== '') {
|
||||
$this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}");
|
||||
}
|
||||
}
|
||||
|
|
@ -2289,7 +2296,10 @@ private function generate_nixpacks_env_variables()
|
|||
// Add COOLIFY_* environment variables to Nixpacks build context
|
||||
$coolify_envs = $this->generate_coolify_env_variables(forBuildTime: true);
|
||||
$coolify_envs->each(function ($value, $key) {
|
||||
$this->env_nixpacks_args->push("--env {$key}={$value}");
|
||||
// Only add environment variables with non-null and non-empty values
|
||||
if (! is_null($value) && $value !== '') {
|
||||
$this->env_nixpacks_args->push("--env {$key}={$value}");
|
||||
}
|
||||
});
|
||||
|
||||
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' ');
|
||||
|
|
@ -3180,6 +3190,18 @@ private function stop_running_container(bool $force = false)
|
|||
$this->graceful_shutdown_container($this->container_name);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// If new version is healthy, this is just cleanup - don't fail the deployment
|
||||
if ($this->newVersionIsHealthy || $force) {
|
||||
$this->application_deployment_queue->addLogEntry(
|
||||
"Warning: Could not remove old container: {$e->getMessage()}",
|
||||
'stderr',
|
||||
hidden: true
|
||||
);
|
||||
|
||||
return; // Don't re-throw - cleanup failures shouldn't fail successful deployments
|
||||
}
|
||||
|
||||
// Only re-throw if deployment hasn't succeeded yet
|
||||
throw new DeploymentException("Failed to stop running container: {$e->getMessage()}", $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Server\TraefikVersionOutdated;
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
|
@ -38,6 +39,8 @@ public function handle(): void
|
|||
$this->server->update(['detected_traefik_version' => $currentVersion]);
|
||||
|
||||
if (! $currentVersion) {
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -48,16 +51,22 @@ public function handle(): void
|
|||
|
||||
// Handle empty/null response from SSH command
|
||||
if (empty(trim($imageTag))) {
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_contains(strtolower(trim($imageTag)), ':latest')) {
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse current version to extract major.minor.patch
|
||||
$current = ltrim($currentVersion, 'v');
|
||||
if (! preg_match('/^(\d+\.\d+)\.(\d+)$/', $current, $matches)) {
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +86,8 @@ public function handle(): void
|
|||
$this->server->update(['traefik_outdated_info' => null]);
|
||||
}
|
||||
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +107,9 @@ public function handle(): void
|
|||
// Fully up to date
|
||||
$this->server->update(['traefik_outdated_info' => null]);
|
||||
}
|
||||
|
||||
// Dispatch UI update event so warning state refreshes in real-time
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
214
app/Jobs/CleanupOrphanedPreviewContainersJob.php
Normal file
214
app/Jobs/CleanupOrphanedPreviewContainersJob.php
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Scheduled job to clean up orphaned PR preview containers.
|
||||
*
|
||||
* This job acts as a safety net for containers that weren't properly cleaned up
|
||||
* when a PR was closed (e.g., due to webhook failures, race conditions, etc.).
|
||||
*
|
||||
* It scans all functional servers for containers with the `coolify.pullRequestId` label
|
||||
* and removes any where the corresponding ApplicationPreview record no longer exists.
|
||||
*/
|
||||
class CleanupOrphanedPreviewContainersJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 600; // 10 minutes max
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('cleanup-orphaned-preview-containers'))->expireAfter(600)->dontRelease()];
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$servers = $this->getServersToCheck();
|
||||
|
||||
foreach ($servers as $server) {
|
||||
$this->cleanupOrphanedContainersOnServer($server);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('CleanupOrphanedPreviewContainersJob failed: '.$e->getMessage());
|
||||
send_internal_notification('CleanupOrphanedPreviewContainersJob failed with error: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all functional servers to check for orphaned containers.
|
||||
*/
|
||||
private function getServersToCheck(): \Illuminate\Support\Collection
|
||||
{
|
||||
$query = Server::whereRelation('settings', 'is_usable', true)
|
||||
->whereRelation('settings', 'is_reachable', true)
|
||||
->where('ip', '!=', '1.2.3.4');
|
||||
|
||||
if (isCloud()) {
|
||||
$query = $query->whereRelation('team.subscription', 'stripe_invoice_paid', true);
|
||||
}
|
||||
|
||||
return $query->get()->filter(fn ($server) => $server->isFunctional());
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and clean up orphaned PR containers on a specific server.
|
||||
*/
|
||||
private function cleanupOrphanedContainersOnServer(Server $server): void
|
||||
{
|
||||
try {
|
||||
$prContainers = $this->getPRContainersOnServer($server);
|
||||
|
||||
if ($prContainers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$orphanedCount = 0;
|
||||
foreach ($prContainers as $container) {
|
||||
if ($this->isOrphanedContainer($container)) {
|
||||
$this->removeContainer($container, $server);
|
||||
$orphanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($orphanedCount > 0) {
|
||||
Log::info("CleanupOrphanedPreviewContainersJob - Removed {$orphanedCount} orphaned PR containers", [
|
||||
'server' => $server->name,
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning("CleanupOrphanedPreviewContainersJob - Error on server {$server->name}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all PR containers on a server (containers with pullRequestId > 0).
|
||||
*/
|
||||
private function getPRContainersOnServer(Server $server): \Illuminate\Support\Collection
|
||||
{
|
||||
try {
|
||||
$output = instant_remote_process([
|
||||
"docker ps -a --filter 'label=coolify.pullRequestId' --format '{{json .}}'",
|
||||
], $server, false);
|
||||
|
||||
if (empty($output)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return format_docker_command_output_to_json($output)
|
||||
->filter(function ($container) {
|
||||
// Only include PR containers (pullRequestId > 0)
|
||||
$prId = $this->extractPullRequestId($container);
|
||||
|
||||
return $prId !== null && $prId > 0;
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
Log::debug("Failed to get PR containers on server {$server->name}: {$e->getMessage()}");
|
||||
|
||||
return collect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract pull request ID from container labels.
|
||||
*/
|
||||
private function extractPullRequestId($container): ?int
|
||||
{
|
||||
$labels = data_get($container, 'Labels', '');
|
||||
if (preg_match('/coolify\.pullRequestId=(\d+)/', $labels, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract application ID from container labels.
|
||||
*/
|
||||
private function extractApplicationId($container): ?int
|
||||
{
|
||||
$labels = data_get($container, 'Labels', '');
|
||||
if (preg_match('/coolify\.applicationId=(\d+)/', $labels, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a container is orphaned (no corresponding ApplicationPreview record).
|
||||
*/
|
||||
private function isOrphanedContainer($container): bool
|
||||
{
|
||||
$applicationId = $this->extractApplicationId($container);
|
||||
$pullRequestId = $this->extractPullRequestId($container);
|
||||
|
||||
if ($applicationId === null || $pullRequestId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if ApplicationPreview record exists (including soft-deleted)
|
||||
$previewExists = ApplicationPreview::withTrashed()
|
||||
->where('application_id', $applicationId)
|
||||
->where('pull_request_id', $pullRequestId)
|
||||
->exists();
|
||||
|
||||
// If preview exists (even soft-deleted), container should be handled by DeleteResourceJob
|
||||
// If preview doesn't exist at all, it's truly orphaned
|
||||
return ! $previewExists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an orphaned container from the server.
|
||||
*/
|
||||
private function removeContainer($container, Server $server): void
|
||||
{
|
||||
$containerName = data_get($container, 'Names');
|
||||
|
||||
if (empty($containerName)) {
|
||||
Log::warning('CleanupOrphanedPreviewContainersJob - Cannot remove container: missing container name', [
|
||||
'container_data' => $container,
|
||||
'server' => $server->name,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$applicationId = $this->extractApplicationId($container);
|
||||
$pullRequestId = $this->extractPullRequestId($container);
|
||||
|
||||
Log::info('CleanupOrphanedPreviewContainersJob - Removing orphaned container', [
|
||||
'container' => $containerName,
|
||||
'application_id' => $applicationId,
|
||||
'pull_request_id' => $pullRequestId,
|
||||
'server' => $server->name,
|
||||
]);
|
||||
|
||||
$escapedContainerName = escapeshellarg($containerName);
|
||||
|
||||
try {
|
||||
instant_remote_process(
|
||||
["docker rm -f {$escapedContainerName}"],
|
||||
$server,
|
||||
false
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning("Failed to remove orphaned container {$containerName}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -121,7 +121,7 @@ public function handle(): void
|
|||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName.'-'.$this->container_name;
|
||||
$commands[] = "docker exec $this->container_name env | grep POSTGRES_";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||
$envs = str($envs)->explode("\n");
|
||||
|
||||
$user = $envs->filter(function ($env) {
|
||||
|
|
@ -152,7 +152,7 @@ public function handle(): void
|
|||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName.'-'.$this->container_name;
|
||||
$commands[] = "docker exec $this->container_name env | grep MYSQL_";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||
$envs = str($envs)->explode("\n");
|
||||
|
||||
$rootPassword = $envs->filter(function ($env) {
|
||||
|
|
@ -175,7 +175,7 @@ public function handle(): void
|
|||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName.'-'.$this->container_name;
|
||||
$commands[] = "docker exec $this->container_name env";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||
$envs = str($envs)->explode("\n");
|
||||
$rootPassword = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MARIADB_ROOT_PASSWORD=');
|
||||
|
|
@ -217,7 +217,7 @@ public function handle(): void
|
|||
try {
|
||||
$commands = [];
|
||||
$commands[] = "docker exec $this->container_name env | grep MONGO_INITDB_";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||
|
||||
if (filled($envs)) {
|
||||
$envs = str($envs)->explode("\n");
|
||||
|
|
@ -508,7 +508,7 @@ private function backup_standalone_mongodb(string $databaseWithCollections): voi
|
|||
}
|
||||
}
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
$this->backup_output = null;
|
||||
|
|
@ -537,7 +537,7 @@ private function backup_standalone_postgresql(string $database): void
|
|||
}
|
||||
|
||||
$commands[] = $backupCommand;
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
$this->backup_output = null;
|
||||
|
|
@ -560,7 +560,7 @@ private function backup_standalone_mysql(string $database): void
|
|||
$escapedDatabase = escapeshellarg($database);
|
||||
$commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" $escapedDatabase > $this->backup_location";
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
$this->backup_output = null;
|
||||
|
|
@ -583,7 +583,7 @@ private function backup_standalone_mariadb(string $database): void
|
|||
$escapedDatabase = escapeshellarg($database);
|
||||
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" $escapedDatabase > $this->backup_location";
|
||||
}
|
||||
$this->backup_output = instant_remote_process($commands, $this->server);
|
||||
$this->backup_output = instant_remote_process($commands, $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->backup_output = trim($this->backup_output);
|
||||
if ($this->backup_output === '') {
|
||||
$this->backup_output = null;
|
||||
|
|
@ -614,7 +614,7 @@ private function add_to_error_output($output): void
|
|||
|
||||
private function calculate_size()
|
||||
{
|
||||
return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false);
|
||||
return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false, false, null, disableMultiplexing: true);
|
||||
}
|
||||
|
||||
private function upload_to_s3(): void
|
||||
|
|
@ -637,9 +637,9 @@ private function upload_to_s3(): void
|
|||
|
||||
$fullImageName = $this->getFullImageName();
|
||||
|
||||
$containerExists = instant_remote_process(["docker ps -a -q -f name=backup-of-{$this->backup_log_uuid}"], $this->server, false);
|
||||
$containerExists = instant_remote_process(["docker ps -a -q -f name=backup-of-{$this->backup_log_uuid}"], $this->server, false, false, null, disableMultiplexing: true);
|
||||
if (filled($containerExists)) {
|
||||
instant_remote_process(["docker rm -f backup-of-{$this->backup_log_uuid}"], $this->server, false);
|
||||
instant_remote_process(["docker rm -f backup-of-{$this->backup_log_uuid}"], $this->server, false, false, null, disableMultiplexing: true);
|
||||
}
|
||||
|
||||
if (isDev()) {
|
||||
|
|
@ -661,7 +661,7 @@ private function upload_to_s3(): void
|
|||
|
||||
$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);
|
||||
instant_remote_process($commands, $this->server, true, false, null, disableMultiplexing: true);
|
||||
|
||||
$this->s3_uploaded = true;
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -670,7 +670,7 @@ private function upload_to_s3(): void
|
|||
throw $e;
|
||||
} finally {
|
||||
$command = "docker rm -f backup-of-{$this->backup_log_uuid}";
|
||||
instant_remote_process([$command], $this->server);
|
||||
instant_remote_process([$command], $this->server, true, false, null, disableMultiplexing: true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -300,8 +300,9 @@ private function aggregateMultiContainerStatuses()
|
|||
}
|
||||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// Use preserveRestarting: true so applications show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0);
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0, preserveRestarting: true);
|
||||
|
||||
// Update application status with aggregated result
|
||||
if ($aggregatedStatus && $application->status !== $aggregatedStatus) {
|
||||
|
|
@ -360,8 +361,9 @@ private function aggregateServiceContainerStatuses()
|
|||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
// NOTE: Sentinel does NOT provide restart count data, so maxRestartCount is always 0
|
||||
// Use preserveRestarting: true so individual sub-resources show "Restarting" instead of "Degraded"
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0);
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($relevantStatuses, 0, preserveRestarting: true);
|
||||
|
||||
// Update service sub-resource status with aggregated result
|
||||
if ($aggregatedStatus && $subResource->status !== $aggregatedStatus) {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Proxy\StopProxy;
|
||||
use App\Actions\Proxy\GetProxyConfiguration;
|
||||
use App\Actions\Proxy\SaveProxyConfiguration;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Models\Server;
|
||||
use App\Services\ProxyDashboardCacheService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
|
@ -19,11 +22,13 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
public $tries = 1;
|
||||
|
||||
public $timeout = 60;
|
||||
public $timeout = 120;
|
||||
|
||||
public ?int $activity_id = null;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('restart-proxy-'.$this->server->uuid))->expireAfter(60)->dontRelease()];
|
||||
return [(new WithoutOverlapping('restart-proxy-'.$this->server->uuid))->expireAfter(120)->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
|
@ -31,15 +36,125 @@ public function __construct(public Server $server) {}
|
|||
public function handle()
|
||||
{
|
||||
try {
|
||||
StopProxy::run($this->server, restarting: true);
|
||||
|
||||
// Set status to restarting
|
||||
$this->server->proxy->status = 'restarting';
|
||||
$this->server->proxy->force_stop = false;
|
||||
$this->server->save();
|
||||
|
||||
StartProxy::run($this->server, force: true, restarting: true);
|
||||
// Build combined stop + start commands for a single activity
|
||||
$commands = $this->buildRestartCommands();
|
||||
|
||||
// Create activity and dispatch immediately - returns Activity right away
|
||||
// The remote_process runs asynchronously, so UI gets activity ID instantly
|
||||
$activity = remote_process(
|
||||
$commands,
|
||||
$this->server,
|
||||
callEventOnFinish: 'ProxyStatusChanged',
|
||||
callEventData: $this->server->id
|
||||
);
|
||||
|
||||
// Store activity ID and notify UI immediately with it
|
||||
$this->activity_id = $activity->id;
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id, $this->activity_id);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// Set error status
|
||||
$this->server->proxy->status = 'error';
|
||||
$this->server->save();
|
||||
|
||||
// Notify UI of error
|
||||
ProxyStatusChangedUI::dispatch($this->server->team_id);
|
||||
|
||||
// Clear dashboard cache on error
|
||||
ProxyDashboardCacheService::clearCache($this->server);
|
||||
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build combined stop + start commands for proxy restart.
|
||||
* This creates a single command sequence that shows all logs in one activity.
|
||||
*/
|
||||
private function buildRestartCommands(): array
|
||||
{
|
||||
$proxyType = $this->server->proxyType();
|
||||
$containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||
$proxy_path = $this->server->proxyPath();
|
||||
$stopTimeout = 30;
|
||||
|
||||
// Get proxy configuration
|
||||
$configuration = GetProxyConfiguration::run($this->server);
|
||||
if (! $configuration) {
|
||||
throw new \Exception('Configuration is not synced');
|
||||
}
|
||||
SaveProxyConfiguration::run($this->server, $configuration);
|
||||
$docker_compose_yml_base64 = base64_encode($configuration);
|
||||
$this->server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
|
||||
$this->server->save();
|
||||
|
||||
$commands = collect([]);
|
||||
|
||||
// === STOP PHASE ===
|
||||
$commands = $commands->merge([
|
||||
"echo 'Stopping proxy...'",
|
||||
"docker stop -t=$stopTimeout $containerName 2>/dev/null || true",
|
||||
"docker rm -f $containerName 2>/dev/null || true",
|
||||
'# Wait for container to be fully removed',
|
||||
'for i in {1..15}; do',
|
||||
" if ! docker ps -a --format \"{{.Names}}\" | grep -q \"^$containerName$\"; then",
|
||||
" echo 'Container removed successfully.'",
|
||||
' break',
|
||||
' fi',
|
||||
' echo "Waiting for container to be removed... ($i/15)"',
|
||||
' sleep 1',
|
||||
' # Force remove on each iteration in case it got stuck',
|
||||
" docker rm -f $containerName 2>/dev/null || true",
|
||||
'done',
|
||||
'# Final verification and force cleanup',
|
||||
"if docker ps -a --format \"{{.Names}}\" | grep -q \"^$containerName$\"; then",
|
||||
" echo 'Container still exists after wait, forcing removal...'",
|
||||
" docker rm -f $containerName 2>/dev/null || true",
|
||||
' sleep 2',
|
||||
'fi',
|
||||
"echo 'Proxy stopped successfully.'",
|
||||
]);
|
||||
|
||||
// === START PHASE ===
|
||||
if ($this->server->isSwarmManager()) {
|
||||
$commands = $commands->merge([
|
||||
"echo 'Starting proxy (Swarm mode)...'",
|
||||
"mkdir -p $proxy_path/dynamic",
|
||||
"cd $proxy_path",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker stack deploy --detach=true -c docker-compose.yml coolify-proxy',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
} else {
|
||||
if (isDev() && $proxyType === ProxyTypes::CADDY->value) {
|
||||
$proxy_path = '/data/coolify/proxy/caddy';
|
||||
}
|
||||
$caddyfile = 'import /dynamic/*.caddy';
|
||||
$commands = $commands->merge([
|
||||
"echo 'Starting proxy...'",
|
||||
"mkdir -p $proxy_path/dynamic",
|
||||
"cd $proxy_path",
|
||||
"echo '$caddyfile' > $proxy_path/dynamic/Caddyfile",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Pulling docker image.'",
|
||||
'docker compose pull',
|
||||
]);
|
||||
// Ensure required networks exist BEFORE docker compose up
|
||||
$commands = $commands->merge(ensureProxyNetworksExist($this->server));
|
||||
$commands = $commands->merge([
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker compose up -d --wait --remove-orphans',
|
||||
"echo 'Successfully started coolify-proxy.'",
|
||||
]);
|
||||
$commands = $commands->merge(connectProxyToNetworks($this->server));
|
||||
}
|
||||
|
||||
return $commands->toArray();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,9 @@ public function handle(): void
|
|||
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) {
|
||||
$cmd = "sh -c '".str_replace("'", "'\''", $this->task->command)."'";
|
||||
$exec = "docker exec {$containerName} {$cmd}";
|
||||
$this->task_output = instant_remote_process([$exec], $this->server, true);
|
||||
// Disable SSH multiplexing to prevent race conditions when multiple tasks run concurrently
|
||||
// See: https://github.com/coollabsio/coolify/issues/6736
|
||||
$this->task_output = instant_remote_process([$exec], $this->server, true, false, $this->timeout, disableMultiplexing: true);
|
||||
$this->task_log->update([
|
||||
'status' => 'success',
|
||||
'message' => $this->task_output,
|
||||
|
|
|
|||
|
|
@ -111,34 +111,48 @@ private function processScheduledTasks(Collection $servers): void
|
|||
|
||||
private function processServerTasks(Server $server): void
|
||||
{
|
||||
// Get server timezone (used for all scheduled tasks)
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
// Check if we should run sentinel-based checks
|
||||
$lastSentinelUpdate = $server->sentinel_updated_at;
|
||||
$waitTime = $server->waitBeforeDoingSshCheck();
|
||||
$sentinelOutOfSync = Carbon::parse($lastSentinelUpdate)->isBefore($this->executionTime->subSeconds($waitTime));
|
||||
$sentinelOutOfSync = Carbon::parse($lastSentinelUpdate)->isBefore($this->executionTime->copy()->subSeconds($waitTime));
|
||||
|
||||
if ($sentinelOutOfSync) {
|
||||
// Dispatch jobs if Sentinel is out of sync
|
||||
if ($this->shouldRunNow($this->checkFrequency)) {
|
||||
// Dispatch ServerCheckJob if Sentinel is out of sync
|
||||
if ($this->shouldRunNow($this->checkFrequency, $serverTimezone)) {
|
||||
ServerCheckJob::dispatch($server);
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch ServerStorageCheckJob if due
|
||||
$serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *');
|
||||
$isSentinelEnabled = $server->isSentinelEnabled();
|
||||
$shouldRestartSentinel = $isSentinelEnabled && $this->shouldRunNow('0 0 * * *', $serverTimezone);
|
||||
// Dispatch Sentinel restart if due (daily for Sentinel-enabled servers)
|
||||
|
||||
if ($shouldRestartSentinel) {
|
||||
dispatch(function () use ($server) {
|
||||
$server->restartContainer('coolify-sentinel');
|
||||
});
|
||||
}
|
||||
|
||||
// Dispatch ServerStorageCheckJob if due (only when Sentinel is out of sync or disabled)
|
||||
// When Sentinel is active, PushServerUpdateJob handles storage checks with real-time data
|
||||
if ($sentinelOutOfSync) {
|
||||
$serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 23 * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) {
|
||||
$serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency];
|
||||
}
|
||||
$shouldRunStorageCheck = $this->shouldRunNow($serverDiskUsageCheckFrequency);
|
||||
$shouldRunStorageCheck = $this->shouldRunNow($serverDiskUsageCheckFrequency, $serverTimezone);
|
||||
|
||||
if ($shouldRunStorageCheck) {
|
||||
ServerStorageCheckJob::dispatch($server);
|
||||
}
|
||||
}
|
||||
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
// Dispatch ServerPatchCheckJob if due (weekly)
|
||||
$shouldRunPatchCheck = $this->shouldRunNow('0 0 * * 0', $serverTimezone);
|
||||
|
||||
|
|
@ -146,14 +160,13 @@ private function processServerTasks(Server $server): void
|
|||
ServerPatchCheckJob::dispatch($server);
|
||||
}
|
||||
|
||||
// Dispatch Sentinel restart if due (daily for Sentinel-enabled servers)
|
||||
$isSentinelEnabled = $server->isSentinelEnabled();
|
||||
$shouldRestartSentinel = $isSentinelEnabled && $this->shouldRunNow('0 0 * * *', $serverTimezone);
|
||||
// Check for sentinel updates hourly (independent of user-configurable update_check_frequency)
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$shouldCheckSentinel = $this->shouldRunNow('0 * * * *', $serverTimezone);
|
||||
|
||||
if ($shouldRestartSentinel) {
|
||||
dispatch(function () use ($server) {
|
||||
$server->restartContainer('coolify-sentinel');
|
||||
});
|
||||
if ($shouldCheckSentinel) {
|
||||
CheckAndStartSentinelJob::dispatch($server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use Illuminate\Foundation\Events\MaintenanceModeDisabled as EventsMaintenanceModeDisabled;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
|
||||
|
||||
class MaintenanceModeDisabledNotification
|
||||
{
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(EventsMaintenanceModeDisabled $event): void
|
||||
{
|
||||
$files = Storage::disk('webhooks-during-maintenance')->files();
|
||||
$files = collect($files);
|
||||
$files = $files->sort();
|
||||
foreach ($files as $file) {
|
||||
$content = Storage::disk('webhooks-during-maintenance')->get($file);
|
||||
$data = json_decode($content, true);
|
||||
$symfonyRequest = new SymfonyRequest(
|
||||
$data['query'],
|
||||
$data['request'],
|
||||
$data['attributes'],
|
||||
$data['cookies'],
|
||||
$data['files'],
|
||||
$data['server'],
|
||||
$data['content']
|
||||
);
|
||||
|
||||
foreach ($data['headers'] as $key => $value) {
|
||||
$symfonyRequest->headers->set($key, $value);
|
||||
}
|
||||
$request = Request::createFromBase($symfonyRequest);
|
||||
$endpoint = str($file)->after('_')->beforeLast('_')->value();
|
||||
$class = "App\Http\Controllers\Webhook\\".ucfirst(str($endpoint)->before('::')->value());
|
||||
$method = str($endpoint)->after('::')->value();
|
||||
try {
|
||||
$instance = new $class;
|
||||
$instance->$method($request);
|
||||
} catch (\Throwable $th) {
|
||||
} finally {
|
||||
Storage::disk('webhooks-during-maintenance')->delete($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use Illuminate\Foundation\Events\MaintenanceModeEnabled as EventsMaintenanceModeEnabled;
|
||||
|
||||
class MaintenanceModeEnabledNotification
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*/
|
||||
public function handle(EventsMaintenanceModeEnabled $event): void {}
|
||||
}
|
||||
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Events\ProxyStatusChangedUI;
|
||||
use App\Jobs\CheckTraefikVersionForServerJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProxyStatusChangedNotification implements ShouldQueueAfterCommit
|
||||
{
|
||||
|
|
@ -32,6 +35,19 @@ public function handle(ProxyStatusChanged $event)
|
|||
$server->setupDynamicProxyConfiguration();
|
||||
$server->proxy->force_stop = false;
|
||||
$server->save();
|
||||
|
||||
// Check Traefik version after proxy is running
|
||||
if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
|
||||
$traefikVersions = get_traefik_versions();
|
||||
if ($traefikVersions !== null) {
|
||||
CheckTraefikVersionForServerJob::dispatch($server, $traefikVersions);
|
||||
} else {
|
||||
Log::warning('Traefik version check skipped after proxy status change: versions.json data unavailable', [
|
||||
'server_id' => $server->id,
|
||||
'server_name' => $server->name,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($status === 'created') {
|
||||
instant_remote_process([
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ class Dashboard extends Component
|
|||
|
||||
public function mount()
|
||||
{
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
$this->servers = Server::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeam()->get();
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeamCached();
|
||||
$this->servers = Server::ownedByCurrentTeamCached();
|
||||
$this->projects = Project::ownedByCurrentTeam()->with('environments')->get();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class DeploymentsIndicator extends Component
|
|||
#[Computed]
|
||||
public function deployments()
|
||||
{
|
||||
$servers = Server::ownedByCurrentTeam()->get();
|
||||
$servers = Server::ownedByCurrentTeamCached();
|
||||
|
||||
return ApplicationDeploymentQueue::with(['application.environment.project'])
|
||||
->whereIn('status', ['in_progress', 'queued'])
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class Show extends Component
|
|||
|
||||
public $isKeepAliveOn = true;
|
||||
|
||||
public bool $is_debug_enabled = false;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
|
@ -56,9 +58,23 @@ public function mount()
|
|||
$this->application_deployment_queue = $application_deployment_queue;
|
||||
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
|
||||
$this->deployment_uuid = $deploymentUuid;
|
||||
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
|
||||
$this->isKeepAliveOn();
|
||||
}
|
||||
|
||||
public function toggleDebug()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->application->settings->is_debug_enabled = ! $this->application->settings->is_debug_enabled;
|
||||
$this->application->settings->save();
|
||||
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
|
||||
$this->application_deployment_queue->refresh();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refreshQueue()
|
||||
{
|
||||
$this->application_deployment_queue->refresh();
|
||||
|
|
|
|||
|
|
@ -521,7 +521,7 @@ public function instantSave()
|
|||
}
|
||||
}
|
||||
|
||||
public function loadComposeFile($isInit = false, $showToast = true)
|
||||
public function loadComposeFile($isInit = false, $showToast = true, ?string $restoreBaseDirectory = null, ?string $restoreDockerComposeLocation = null)
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
|
@ -530,7 +530,7 @@ public function loadComposeFile($isInit = false, $showToast = true)
|
|||
return;
|
||||
}
|
||||
|
||||
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit);
|
||||
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit, $restoreBaseDirectory, $restoreDockerComposeLocation);
|
||||
if (is_null($this->parsedServices)) {
|
||||
$showToast && $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
||||
|
||||
|
|
@ -606,13 +606,6 @@ public function generateDomain(string $serviceName)
|
|||
}
|
||||
}
|
||||
|
||||
public function updatedBaseDirectory()
|
||||
{
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
$this->loadComposeFile();
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedIsStatic($value)
|
||||
{
|
||||
if ($value) {
|
||||
|
|
@ -786,11 +779,13 @@ public function submit($showToaster = true)
|
|||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
$this->resetErrorBag();
|
||||
$this->validate();
|
||||
|
||||
$oldPortsExposes = $this->application->ports_exposes;
|
||||
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$oldDockerComposeLocation = $this->initialDockerComposeLocation;
|
||||
$oldBaseDirectory = $this->application->base_directory;
|
||||
|
||||
// Process FQDN with intermediate variable to avoid Collection/string confusion
|
||||
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
|
||||
|
|
@ -821,6 +816,42 @@ public function submit($showToaster = true)
|
|||
return; // Stop if there are conflicts and user hasn't confirmed
|
||||
}
|
||||
|
||||
// Normalize paths BEFORE validation
|
||||
if ($this->baseDirectory && $this->baseDirectory !== '/') {
|
||||
$this->baseDirectory = rtrim($this->baseDirectory, '/');
|
||||
$this->application->base_directory = $this->baseDirectory;
|
||||
}
|
||||
if ($this->publishDirectory && $this->publishDirectory !== '/') {
|
||||
$this->publishDirectory = rtrim($this->publishDirectory, '/');
|
||||
$this->application->publish_directory = $this->publishDirectory;
|
||||
}
|
||||
|
||||
// Validate docker compose file path BEFORE saving to database
|
||||
// This prevents invalid paths from being persisted when validation fails
|
||||
if ($this->buildPack === 'dockercompose' &&
|
||||
($oldDockerComposeLocation !== $this->dockerComposeLocation ||
|
||||
$oldBaseDirectory !== $this->baseDirectory)) {
|
||||
// Pass original values to loadComposeFile so it can restore them on failure
|
||||
// The finally block in Application::loadComposeFile will save these original
|
||||
// values if validation fails, preventing invalid paths from being persisted
|
||||
$compose_return = $this->loadComposeFile(
|
||||
isInit: false,
|
||||
showToast: false,
|
||||
restoreBaseDirectory: $oldBaseDirectory,
|
||||
restoreDockerComposeLocation: $oldDockerComposeLocation
|
||||
);
|
||||
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
|
||||
// Validation failed - restore original values to component properties
|
||||
$this->baseDirectory = $oldBaseDirectory;
|
||||
$this->dockerComposeLocation = $oldDockerComposeLocation;
|
||||
// The model was saved by loadComposeFile's finally block with original values
|
||||
// Refresh to sync component with database state
|
||||
$this->application->refresh();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->application->save();
|
||||
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
|
||||
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
|
||||
|
|
@ -828,13 +859,6 @@ public function submit($showToaster = true)
|
|||
$this->application->save();
|
||||
}
|
||||
|
||||
if ($this->buildPack === 'dockercompose' && $oldDockerComposeLocation !== $this->dockerComposeLocation) {
|
||||
$compose_return = $this->loadComposeFile(showToast: false);
|
||||
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) {
|
||||
$this->resetDefaultLabels();
|
||||
}
|
||||
|
|
@ -855,14 +879,6 @@ public function submit($showToaster = true)
|
|||
$this->application->ports_exposes = $port;
|
||||
}
|
||||
}
|
||||
if ($this->baseDirectory && $this->baseDirectory !== '/') {
|
||||
$this->baseDirectory = rtrim($this->baseDirectory, '/');
|
||||
$this->application->base_directory = $this->baseDirectory;
|
||||
}
|
||||
if ($this->publishDirectory && $this->publishDirectory !== '/') {
|
||||
$this->publishDirectory = rtrim($this->publishDirectory, '/');
|
||||
$this->application->publish_directory = $this->publishDirectory;
|
||||
}
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||
if ($this->application->isDirty('docker_compose_domains')) {
|
||||
|
|
@ -1018,11 +1034,27 @@ public function getDockerComposeBuildCommandPreviewProperty(): string
|
|||
// Use relative path for clarity in preview (e.g., ./backend/docker-compose.yaml)
|
||||
// Actual deployment uses absolute path: /artifacts/{deployment_uuid}{base_directory}{docker_compose_location}
|
||||
// Build-time env path references ApplicationDeploymentJob::BUILD_TIME_ENV_PATH as source of truth
|
||||
return injectDockerComposeFlags(
|
||||
$command = injectDockerComposeFlags(
|
||||
$this->dockerComposeCustomBuildCommand,
|
||||
".{$normalizedBase}{$this->dockerComposeLocation}",
|
||||
\App\Jobs\ApplicationDeploymentJob::BUILD_TIME_ENV_PATH
|
||||
);
|
||||
|
||||
// Inject build args if not using build secrets
|
||||
if (! $this->application->settings->use_build_secrets) {
|
||||
$buildTimeEnvs = $this->application->environment_variables()
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
|
||||
if ($buildTimeEnvs->isNotEmpty()) {
|
||||
$buildArgs = generateDockerBuildArgs($buildTimeEnvs);
|
||||
$buildArgsString = $buildArgs->implode(' ');
|
||||
|
||||
$command = injectDockerComposeBuildArgs($command, $buildArgsString);
|
||||
}
|
||||
}
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
public function getDockerComposeStartCommandPreviewProperty(): string
|
||||
|
|
|
|||
|
|
@ -96,8 +96,7 @@ public function generate()
|
|||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{port}}', $port, $preview_fqdn);
|
||||
$preview_fqdns[] = "$schema://$preview_fqdn";
|
||||
$preview_fqdns[] = "$schema://$preview_fqdn{$port}";
|
||||
}
|
||||
|
||||
$preview_fqdn = implode(',', $preview_fqdns);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Models\Application;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
|
@ -19,9 +20,30 @@ class Rollback extends Component
|
|||
|
||||
public array $parameters;
|
||||
|
||||
#[Validate(['integer', 'min:0', 'max:100'])]
|
||||
public int $dockerImagesToKeep = 2;
|
||||
|
||||
public bool $serverRetentionDisabled = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->dockerImagesToKeep = $this->application->settings->docker_images_to_keep ?? 2;
|
||||
$server = $this->application->destination->server;
|
||||
$this->serverRetentionDisabled = $server->settings->disable_application_image_retention ?? false;
|
||||
}
|
||||
|
||||
public function saveSettings()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->validate();
|
||||
$this->application->settings->docker_images_to_keep = $this->dockerImagesToKeep;
|
||||
$this->application->settings->save();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function rollbackImage($commit)
|
||||
|
|
@ -66,14 +88,12 @@ public function loadImages($showToast = false)
|
|||
return str($item)->contains($image);
|
||||
})->map(function ($item) {
|
||||
$item = str($item)->explode('#');
|
||||
if ($item[1] === $this->current) {
|
||||
// $is_current = true;
|
||||
}
|
||||
$is_current = $item[1] === $this->current;
|
||||
|
||||
return [
|
||||
'tag' => $item[1],
|
||||
'created_at' => $item[2],
|
||||
'is_current' => $is_current ?? null,
|
||||
'is_current' => $is_current,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ class Index extends Component
|
|||
|
||||
public function mount()
|
||||
{
|
||||
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeam()->get();
|
||||
$this->servers = Server::ownedByCurrentTeam()->count();
|
||||
$this->private_keys = PrivateKey::ownedByCurrentTeamCached();
|
||||
$this->projects = Project::ownedByCurrentTeamCached();
|
||||
$this->servers = Server::ownedByCurrentTeamCached();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
|
|
|||
|
|
@ -75,16 +75,6 @@ public function mount()
|
|||
$this->github_apps = GithubApp::private();
|
||||
}
|
||||
|
||||
public function updatedBaseDirectory()
|
||||
{
|
||||
if ($this->base_directory) {
|
||||
$this->base_directory = rtrim($this->base_directory, '/');
|
||||
if (! str($this->base_directory)->startsWith('/')) {
|
||||
$this->base_directory = '/'.$this->base_directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedBuildPack()
|
||||
{
|
||||
if ($this->build_pack === 'nixpacks') {
|
||||
|
|
|
|||
|
|
@ -107,26 +107,6 @@ public function mount()
|
|||
$this->query = request()->query();
|
||||
}
|
||||
|
||||
public function updatedBaseDirectory()
|
||||
{
|
||||
if ($this->base_directory) {
|
||||
$this->base_directory = rtrim($this->base_directory, '/');
|
||||
if (! str($this->base_directory)->startsWith('/')) {
|
||||
$this->base_directory = '/'.$this->base_directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedDockerComposeLocation()
|
||||
{
|
||||
if ($this->docker_compose_location) {
|
||||
$this->docker_compose_location = rtrim($this->docker_compose_location, '/');
|
||||
if (! str($this->docker_compose_location)->startsWith('/')) {
|
||||
$this->docker_compose_location = '/'.$this->docker_compose_location;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedBuildPack()
|
||||
{
|
||||
if ($this->build_pack === 'nixpacks') {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,21 @@ public function instantSave()
|
|||
}
|
||||
}
|
||||
|
||||
public function instantSaveSettings()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
// Save checkbox states without port validation
|
||||
$this->application->is_gzip_enabled = $this->isGzipEnabled;
|
||||
$this->application->is_stripprefix_enabled = $this->isStripprefixEnabled;
|
||||
$this->application->exclude_from_status = $this->excludeFromStatus;
|
||||
$this->application->save();
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSaveAdvanced()
|
||||
{
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -39,10 +39,14 @@ class GetLogs extends Component
|
|||
|
||||
public ?bool $streamLogs = false;
|
||||
|
||||
public ?bool $showTimeStamps = false;
|
||||
public ?bool $showTimeStamps = true;
|
||||
|
||||
public ?int $numberOfLines = 100;
|
||||
|
||||
public bool $expandByDefault = false;
|
||||
|
||||
public bool $collapsible = true;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (! is_null($this->resource)) {
|
||||
|
|
@ -92,12 +96,33 @@ public function instantSave()
|
|||
}
|
||||
}
|
||||
|
||||
public function toggleTimestamps()
|
||||
{
|
||||
$previousValue = $this->showTimeStamps;
|
||||
$this->showTimeStamps = ! $this->showTimeStamps;
|
||||
|
||||
try {
|
||||
$this->instantSave();
|
||||
$this->getLogs(true);
|
||||
} catch (\Throwable $e) {
|
||||
// Revert the flag to its previous value on failure
|
||||
$this->showTimeStamps = $previousValue;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleStreamLogs()
|
||||
{
|
||||
$this->streamLogs = ! $this->streamLogs;
|
||||
}
|
||||
|
||||
public function getLogs($refresh = false)
|
||||
{
|
||||
if (! $this->server->isFunctional()) {
|
||||
return;
|
||||
}
|
||||
if (! $refresh && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) {
|
||||
if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) {
|
||||
return;
|
||||
}
|
||||
if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public function mount()
|
|||
$parameters = get_route_parameters();
|
||||
$this->projectUuid = data_get($parameters, 'project_uuid');
|
||||
$this->environmentUuid = data_get($parameters, 'environment_uuid');
|
||||
$this->projects = Project::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeamCached();
|
||||
$this->servers = currentTeam()->servers->filter(fn ($server) => ! $server->isBuildServer());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class Add extends Component
|
|||
'command' => 'required|string',
|
||||
'frequency' => 'required|string',
|
||||
'container' => 'nullable|string',
|
||||
'timeout' => 'required|integer|min:60|max:3600',
|
||||
'timeout' => 'required|integer|min:60|max:36000',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class Show extends Component
|
|||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $container = null;
|
||||
|
||||
#[Validate(['integer', 'required', 'min:60', 'max:3600'])]
|
||||
#[Validate(['integer', 'required', 'min:60', 'max:36000'])]
|
||||
public $timeout = 300;
|
||||
|
||||
#[Locked]
|
||||
|
|
@ -72,7 +72,7 @@ public function mount(string $task_uuid, string $project_uuid, string $environme
|
|||
} elseif ($service_uuid) {
|
||||
$this->type = 'service';
|
||||
$this->service_uuid = $service_uuid;
|
||||
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $service_uuid)->firstOrFail();
|
||||
$this->resource = Service::ownedByCurrentTeamCached()->where('uuid', $service_uuid)->firstOrFail();
|
||||
}
|
||||
$this->parameters = [
|
||||
'environment_uuid' => $environment_uuid,
|
||||
|
|
|
|||
|
|
@ -11,20 +11,6 @@ class Terminal extends Component
|
|||
{
|
||||
public bool $hasShell = true;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'closeTerminal',
|
||||
];
|
||||
}
|
||||
|
||||
public function closeTerminal()
|
||||
{
|
||||
$this->dispatch('reloadWindow');
|
||||
}
|
||||
|
||||
private function checkShellAvailability(Server $server, string $container): bool
|
||||
{
|
||||
$escapedContainer = escapeshellarg($container);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class Create extends Component
|
|||
|
||||
public function mount()
|
||||
{
|
||||
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
$this->private_keys = PrivateKey::ownedByCurrentTeamCached();
|
||||
if (! isCloud()) {
|
||||
$this->limit_reached = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ class DockerCleanup extends Component
|
|||
#[Validate('boolean')]
|
||||
public bool $deleteUnusedNetworks = false;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $disableApplicationImageRetention = false;
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
|
|
@ -52,6 +55,7 @@ public function syncData(bool $toModel = false)
|
|||
$this->server->settings->docker_cleanup_threshold = $this->dockerCleanupThreshold;
|
||||
$this->server->settings->delete_unused_volumes = $this->deleteUnusedVolumes;
|
||||
$this->server->settings->delete_unused_networks = $this->deleteUnusedNetworks;
|
||||
$this->server->settings->disable_application_image_retention = $this->disableApplicationImageRetention;
|
||||
$this->server->settings->save();
|
||||
} else {
|
||||
$this->forceDockerCleanup = $this->server->settings->force_docker_cleanup;
|
||||
|
|
@ -59,6 +63,7 @@ public function syncData(bool $toModel = false)
|
|||
$this->dockerCleanupThreshold = $this->server->settings->docker_cleanup_threshold;
|
||||
$this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes;
|
||||
$this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks;
|
||||
$this->disableApplicationImageRetention = $this->server->settings->disable_application_image_retention;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class Index extends Component
|
|||
|
||||
public function mount()
|
||||
{
|
||||
$this->servers = Server::ownedByCurrentTeam()->get();
|
||||
$this->servers = Server::ownedByCurrentTeamCached();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
|
|
|||
|
|
@ -6,11 +6,10 @@
|
|||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Proxy\StopProxy;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Jobs\CheckTraefikVersionForServerJob;
|
||||
use App\Jobs\RestartProxyJob;
|
||||
use App\Models\Server;
|
||||
use App\Services\ProxyDashboardCacheService;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Component;
|
||||
|
||||
class Navbar extends Component
|
||||
|
|
@ -29,6 +28,10 @@ class Navbar extends Component
|
|||
|
||||
public ?string $proxyStatus = 'unknown';
|
||||
|
||||
public ?string $lastNotifiedStatus = null;
|
||||
|
||||
public bool $restartInitiated = false;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
|
@ -63,27 +66,19 @@ public function restart()
|
|||
{
|
||||
try {
|
||||
$this->authorize('manageProxy', $this->server);
|
||||
StopProxy::run($this->server, restarting: true);
|
||||
|
||||
$this->server->proxy->force_stop = false;
|
||||
$this->server->save();
|
||||
|
||||
$activity = StartProxy::run($this->server, force: true, restarting: true);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
|
||||
// Check Traefik version after restart to provide immediate feedback
|
||||
if ($this->server->proxyType() === ProxyTypes::TRAEFIK->value) {
|
||||
$traefikVersions = get_traefik_versions();
|
||||
if ($traefikVersions !== null) {
|
||||
CheckTraefikVersionForServerJob::dispatch($this->server, $traefikVersions);
|
||||
} else {
|
||||
Log::warning('Traefik version check skipped: versions.json data unavailable', [
|
||||
'server_id' => $this->server->id,
|
||||
'server_name' => $this->server->name,
|
||||
]);
|
||||
}
|
||||
// Prevent duplicate restart calls
|
||||
if ($this->restartInitiated) {
|
||||
return;
|
||||
}
|
||||
$this->restartInitiated = true;
|
||||
|
||||
// Always use background job for all servers
|
||||
RestartProxyJob::dispatch($this->server);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->restartInitiated = false;
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
|
@ -137,12 +132,27 @@ public function checkProxyStatus()
|
|||
}
|
||||
}
|
||||
|
||||
public function showNotification()
|
||||
public function showNotification($event = null)
|
||||
{
|
||||
$previousStatus = $this->proxyStatus;
|
||||
$this->server->refresh();
|
||||
$this->proxyStatus = $this->server->proxy->status ?? 'unknown';
|
||||
|
||||
// If event contains activityId, open activity monitor
|
||||
if ($event && isset($event['activityId'])) {
|
||||
$this->dispatch('activityMonitor', $event['activityId']);
|
||||
}
|
||||
|
||||
// Reset restart flag when proxy reaches a stable state
|
||||
if (in_array($this->proxyStatus, ['running', 'exited', 'error'])) {
|
||||
$this->restartInitiated = false;
|
||||
}
|
||||
|
||||
// Skip notification if we already notified about this status (prevents duplicates)
|
||||
if ($this->lastNotifiedStatus === $this->proxyStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->proxyStatus) {
|
||||
case 'running':
|
||||
$this->loadProxyConfiguration();
|
||||
|
|
@ -150,6 +160,7 @@ public function showNotification()
|
|||
// Don't show during normal start/restart flows (starting, restarting, stopping)
|
||||
if (in_array($previousStatus, ['exited', 'stopped', 'unknown', null])) {
|
||||
$this->dispatch('success', 'Proxy is running.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
}
|
||||
break;
|
||||
case 'exited':
|
||||
|
|
@ -157,19 +168,30 @@ public function showNotification()
|
|||
// Don't show during normal stop/restart flows (stopping, restarting)
|
||||
if (in_array($previousStatus, ['running'])) {
|
||||
$this->dispatch('info', 'Proxy has exited.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
}
|
||||
break;
|
||||
case 'stopping':
|
||||
$this->dispatch('info', 'Proxy is stopping.');
|
||||
// $this->dispatch('info', 'Proxy is stopping.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
break;
|
||||
case 'starting':
|
||||
$this->dispatch('info', 'Proxy is starting.');
|
||||
// $this->dispatch('info', 'Proxy is starting.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
break;
|
||||
case 'restarting':
|
||||
// $this->dispatch('info', 'Proxy is restarting.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
break;
|
||||
case 'error':
|
||||
$this->dispatch('error', 'Proxy restart failed. Check logs.');
|
||||
$this->lastNotifiedStatus = $this->proxyStatus;
|
||||
break;
|
||||
case 'unknown':
|
||||
$this->dispatch('info', 'Proxy status is unknown.');
|
||||
// Don't notify for unknown status - too noisy
|
||||
break;
|
||||
default:
|
||||
$this->dispatch('info', 'Proxy status updated.');
|
||||
// Don't notify for other statuses
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class Index extends Component
|
|||
|
||||
public function mount()
|
||||
{
|
||||
$this->projects = Project::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeamCached();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class Index extends Component
|
|||
|
||||
public function mount()
|
||||
{
|
||||
$this->projects = Project::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeamCached();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ public function mount()
|
|||
$github_app_uuid = request()->github_app_uuid;
|
||||
$this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
|
||||
$this->github_app->makeVisible(['client_secret', 'webhook_secret']);
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeamCached();
|
||||
|
||||
$this->applications = $this->github_app->applications;
|
||||
$settings = instanceSettings();
|
||||
|
|
|
|||
|
|
@ -338,11 +338,25 @@ public static function ownedByCurrentTeamAPI(int $teamId)
|
|||
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for applications owned by current team.
|
||||
* If you need all applications without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return Application::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all applications owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return Application::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
public function getContainersToStop(Server $server, bool $previewDeployments = false): array
|
||||
{
|
||||
$containers = $previewDeployments
|
||||
|
|
@ -1500,10 +1514,10 @@ public function oldRawParser()
|
|||
instant_remote_process($commands, $this->destination->server, false);
|
||||
}
|
||||
|
||||
public function parse(int $pull_request_id = 0, ?int $preview_id = null)
|
||||
public function parse(int $pull_request_id = 0, ?int $preview_id = null, ?string $commit = null)
|
||||
{
|
||||
if ((int) $this->compose_parsing_version >= 3) {
|
||||
return applicationParser($this, $pull_request_id, $preview_id);
|
||||
return applicationParser($this, $pull_request_id, $preview_id, $commit);
|
||||
} elseif ($this->docker_compose_raw) {
|
||||
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
|
||||
} else {
|
||||
|
|
@ -1511,9 +1525,11 @@ public function parse(int $pull_request_id = 0, ?int $preview_id = null)
|
|||
}
|
||||
}
|
||||
|
||||
public function loadComposeFile($isInit = false)
|
||||
public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory = null, ?string $restoreDockerComposeLocation = null)
|
||||
{
|
||||
$initialDockerComposeLocation = $this->docker_compose_location;
|
||||
// Use provided restore values or capture current values as fallback
|
||||
$initialDockerComposeLocation = $restoreDockerComposeLocation ?? $this->docker_compose_location;
|
||||
$initialBaseDirectory = $restoreBaseDirectory ?? $this->base_directory;
|
||||
if ($isInit && $this->docker_compose_raw) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1580,6 +1596,7 @@ public function loadComposeFile($isInit = false)
|
|||
throw new \RuntimeException($e->getMessage());
|
||||
} finally {
|
||||
$this->docker_compose_location = $initialDockerComposeLocation;
|
||||
$this->base_directory = $initialBaseDirectory;
|
||||
$this->save();
|
||||
$commands = collect([
|
||||
"rm -rf /tmp/{$uuid}",
|
||||
|
|
|
|||
|
|
@ -145,11 +145,13 @@ public function generate_preview_fqdn_compose()
|
|||
$template = $this->application->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
$portInt = $url->getPort();
|
||||
$port = $portInt !== null ? ':'.$portInt : '';
|
||||
$random = new Cuid2;
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = "$schema://$preview_fqdn";
|
||||
$preview_fqdn = "$schema://$preview_fqdn{$port}";
|
||||
$preview_domains[] = $preview_fqdn;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class ApplicationSetting extends Model
|
|||
'is_git_submodules_enabled' => 'boolean',
|
||||
'is_git_lfs_enabled' => 'boolean',
|
||||
'is_git_shallow_clone_enabled' => 'boolean',
|
||||
'docker_images_to_keep' => 'integer',
|
||||
];
|
||||
|
||||
protected $guarded = [];
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ protected static function booted()
|
|||
'value' => $environment_variable->value,
|
||||
'is_multiline' => $environment_variable->is_multiline ?? false,
|
||||
'is_literal' => $environment_variable->is_literal ?? false,
|
||||
'is_runtime' => $environment_variable->is_runtime ?? false,
|
||||
'is_buildtime' => $environment_variable->is_buildtime ?? false,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $environment_variable->resourceable_id,
|
||||
'is_preview' => true,
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ public function getPublicKey()
|
|||
return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for private keys owned by current team.
|
||||
* If you need all private keys without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam(array $select = ['*'])
|
||||
{
|
||||
$teamId = currentTeam()->id;
|
||||
|
|
@ -88,6 +92,16 @@ public static function ownedByCurrentTeam(array $select = ['*'])
|
|||
return self::whereTeamId($teamId)->select($selectArray->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all private keys owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return PrivateKey::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedAndOnlySShKeys(array $select = ['*'])
|
||||
{
|
||||
$teamId = currentTeam()->id;
|
||||
|
|
|
|||
|
|
@ -30,11 +30,25 @@ class Project extends BaseModel
|
|||
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* Get query builder for projects owned by current team.
|
||||
* If you need all projects without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all projects owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return Project::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($project) {
|
||||
|
|
|
|||
|
|
@ -242,6 +242,10 @@ public static function isReachable()
|
|||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for servers owned by current team.
|
||||
* If you need all servers without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam(array $select = ['*'])
|
||||
{
|
||||
$teamId = currentTeam()->id;
|
||||
|
|
@ -250,6 +254,16 @@ public static function ownedByCurrentTeam(array $select = ['*'])
|
|||
return Server::whereTeamId($teamId)->with('settings', 'swarmDockers', 'standaloneDockers')->select($selectArray->all())->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all servers owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return Server::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
public static function isUsable()
|
||||
{
|
||||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false)->whereRelation('settings', 'force_disabled', false);
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ class ServerSetting extends Model
|
|||
'is_reachable' => 'boolean',
|
||||
'is_usable' => 'boolean',
|
||||
'is_terminal_enabled' => 'boolean',
|
||||
'disable_application_image_retention' => 'boolean',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -153,11 +153,25 @@ public function tags()
|
|||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for services owned by current team.
|
||||
* If you need all services without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return Service::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all services owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return Service::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
public function deleteConfigurations()
|
||||
{
|
||||
$server = data_get($this, 'destination.server');
|
||||
|
|
@ -712,6 +726,84 @@ public function extraFields()
|
|||
|
||||
$fields->put('MinIO', $data->toArray());
|
||||
break;
|
||||
case $image->contains('garage'):
|
||||
$data = collect([]);
|
||||
$s3_api_url = $this->environment_variables()->where('key', 'GARAGE_S3_API_URL')->first();
|
||||
$web_url = $this->environment_variables()->where('key', 'GARAGE_WEB_URL')->first();
|
||||
$admin_url = $this->environment_variables()->where('key', 'GARAGE_ADMIN_URL')->first();
|
||||
$admin_token = $this->environment_variables()->where('key', 'GARAGE_ADMIN_TOKEN')->first();
|
||||
if (is_null($admin_token)) {
|
||||
$admin_token = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GARAGE')->first();
|
||||
}
|
||||
$rpc_secret = $this->environment_variables()->where('key', 'GARAGE_RPC_SECRET')->first();
|
||||
if (is_null($rpc_secret)) {
|
||||
$rpc_secret = $this->environment_variables()->where('key', 'SERVICE_HEX_32_RPCSECRET')->first();
|
||||
}
|
||||
$metrics_token = $this->environment_variables()->where('key', 'GARAGE_METRICS_TOKEN')->first();
|
||||
if (is_null($metrics_token)) {
|
||||
$metrics_token = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GARAGEMETRICS')->first();
|
||||
}
|
||||
|
||||
if ($s3_api_url) {
|
||||
$data = $data->merge([
|
||||
'S3 API URL' => [
|
||||
'key' => data_get($s3_api_url, 'key'),
|
||||
'value' => data_get($s3_api_url, 'value'),
|
||||
'rules' => 'required|url',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($web_url) {
|
||||
$data = $data->merge([
|
||||
'Web URL' => [
|
||||
'key' => data_get($web_url, 'key'),
|
||||
'value' => data_get($web_url, 'value'),
|
||||
'rules' => 'required|url',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($admin_url) {
|
||||
$data = $data->merge([
|
||||
'Admin URL' => [
|
||||
'key' => data_get($admin_url, 'key'),
|
||||
'value' => data_get($admin_url, 'value'),
|
||||
'rules' => 'required|url',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($admin_token) {
|
||||
$data = $data->merge([
|
||||
'Admin Token' => [
|
||||
'key' => data_get($admin_token, 'key'),
|
||||
'value' => data_get($admin_token, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($rpc_secret) {
|
||||
$data = $data->merge([
|
||||
'RPC Secret' => [
|
||||
'key' => data_get($rpc_secret, 'key'),
|
||||
'value' => data_get($rpc_secret, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($metrics_token) {
|
||||
$data = $data->merge([
|
||||
'Metrics Token' => [
|
||||
'key' => data_get($metrics_token, 'key'),
|
||||
'value' => data_get($metrics_token, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$fields->put('Garage', $data->toArray());
|
||||
break;
|
||||
case $image->contains('weblate'):
|
||||
$data = collect([]);
|
||||
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
|
||||
|
|
|
|||
|
|
@ -37,11 +37,25 @@ public static function ownedByCurrentTeamAPI(int $teamId)
|
|||
return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for service applications owned by current team.
|
||||
* If you need all service applications without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all service applications owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return ServiceApplication::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
public function isRunning()
|
||||
{
|
||||
return str($this->status)->contains('running');
|
||||
|
|
|
|||
|
|
@ -30,11 +30,25 @@ public static function ownedByCurrentTeamAPI(int $teamId)
|
|||
return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for service databases owned by current team.
|
||||
* If you need all service databases without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all service databases owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return ServiceDatabase::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
public function restart()
|
||||
{
|
||||
$container_id = $this->name.'-'.$this->service->uuid;
|
||||
|
|
|
|||
|
|
@ -44,11 +44,25 @@ protected static function booted()
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for ClickHouse databases owned by current team.
|
||||
* If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneClickhouse::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all ClickHouse databases owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return StandaloneClickhouse::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
|
|
|||
|
|
@ -44,11 +44,25 @@ protected static function booted()
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for Dragonfly databases owned by current team.
|
||||
* If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneDragonfly::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Dragonfly databases owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return StandaloneDragonfly::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
|
|
|||
|
|
@ -44,11 +44,25 @@ protected static function booted()
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for KeyDB databases owned by current team.
|
||||
* If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneKeydb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all KeyDB databases owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return StandaloneKeydb::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
|
|
|||
|
|
@ -45,11 +45,25 @@ protected static function booted()
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for MariaDB databases owned by current team.
|
||||
* If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneMariadb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all MariaDB databases owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return StandaloneMariadb::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
|
|
|||
|
|
@ -47,11 +47,25 @@ protected static function booted()
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for MongoDB databases owned by current team.
|
||||
* If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneMongodb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all MongoDB databases owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return StandaloneMongodb::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
|
|
|||
|
|
@ -45,11 +45,25 @@ protected static function booted()
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for MySQL databases owned by current team.
|
||||
* If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneMysql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all MySQL databases owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return StandaloneMysql::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
|
|
|||
|
|
@ -45,11 +45,25 @@ protected static function booted()
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for PostgreSQL databases owned by current team.
|
||||
* If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandalonePostgresql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all PostgreSQL databases owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return StandalonePostgresql::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
public function workdir()
|
||||
{
|
||||
return database_configuration_dir()."/{$this->uuid}";
|
||||
|
|
|
|||
|
|
@ -46,11 +46,25 @@ protected static function booted()
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for Redis databases owned by current team.
|
||||
* If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneRedis::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Redis databases owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return StandaloneRedis::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
|
|
|||
|
|
@ -43,9 +43,19 @@ public function toMail($notifiable = null): MailMessage
|
|||
$mail = new MailMessage;
|
||||
$count = $this->servers->count();
|
||||
|
||||
// Transform servers to include URLs
|
||||
$serversWithUrls = $this->servers->map(function ($server) {
|
||||
return [
|
||||
'name' => $server->name,
|
||||
'uuid' => $server->uuid,
|
||||
'url' => base_url().'/server/'.$server->uuid.'/proxy',
|
||||
'outdatedInfo' => $server->outdatedInfo ?? [],
|
||||
];
|
||||
});
|
||||
|
||||
$mail->subject("Coolify: Traefik proxy outdated on {$count} server(s)");
|
||||
$mail->view('emails.traefik-version-outdated', [
|
||||
'servers' => $this->servers,
|
||||
'servers' => $serversWithUrls,
|
||||
'count' => $count,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Listeners\MaintenanceModeDisabledNotification;
|
||||
use App\Listeners\MaintenanceModeEnabledNotification;
|
||||
use Illuminate\Foundation\Events\MaintenanceModeDisabled;
|
||||
use Illuminate\Foundation\Events\MaintenanceModeEnabled;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use SocialiteProviders\Authentik\AuthentikExtendSocialite;
|
||||
use SocialiteProviders\Azure\AzureExtendSocialite;
|
||||
|
|
@ -19,12 +15,6 @@
|
|||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected $listen = [
|
||||
MaintenanceModeEnabled::class => [
|
||||
MaintenanceModeEnabledNotification::class,
|
||||
],
|
||||
MaintenanceModeDisabled::class => [
|
||||
MaintenanceModeDisabledNotification::class,
|
||||
],
|
||||
SocialiteWasCalled::class => [
|
||||
AzureExtendSocialite::class.'@handle',
|
||||
AuthentikExtendSocialite::class.'@handle',
|
||||
|
|
|
|||
|
|
@ -16,14 +16,23 @@
|
|||
* UI components transform this to human-readable format (e.g., "Running (Healthy)").
|
||||
*
|
||||
* State Priority (highest to lowest):
|
||||
* 1. Restarting → degraded:unhealthy
|
||||
* 2. Crash Loop (exited with restarts) → degraded:unhealthy
|
||||
* 3. Mixed (running + exited) → degraded:unhealthy
|
||||
* 4. Running → running:healthy/unhealthy/unknown
|
||||
* 5. Dead/Removing → degraded:unhealthy
|
||||
* 6. Paused → paused:unknown
|
||||
* 7. Starting/Created → starting:unknown
|
||||
* 8. Exited → exited
|
||||
* 1. Degraded (from sub-resources) → degraded:unhealthy
|
||||
* 2. Restarting → degraded:unhealthy (or restarting:unknown if preserveRestarting=true)
|
||||
* 3. Crash Loop (exited with restarts) → degraded:unhealthy
|
||||
* 4. Mixed (running + exited) → degraded:unhealthy
|
||||
* 5. Mixed (running + starting) → starting:unknown
|
||||
* 6. Running → running:healthy/unhealthy/unknown
|
||||
* 7. Dead/Removing → degraded:unhealthy
|
||||
* 8. Paused → paused:unknown
|
||||
* 9. Starting/Created → starting:unknown
|
||||
* 10. Exited → exited
|
||||
*
|
||||
* The $preserveRestarting parameter controls whether "restarting" containers should be
|
||||
* reported as "restarting:unknown" (true) or "degraded:unhealthy" (false, default).
|
||||
* - Use preserveRestarting=true for individual sub-resources (ServiceApplication/ServiceDatabase)
|
||||
* so they show "Restarting" in the UI.
|
||||
* - Use preserveRestarting=false for overall Service status aggregation where any restarting
|
||||
* container should mark the entire service as "Degraded".
|
||||
*/
|
||||
class ContainerStatusAggregator
|
||||
{
|
||||
|
|
@ -32,9 +41,10 @@ class ContainerStatusAggregator
|
|||
*
|
||||
* @param Collection $containerStatuses Collection of status strings (e.g., "running (healthy)", "running:healthy")
|
||||
* @param int $maxRestartCount Maximum restart count across containers (for crash loop detection)
|
||||
* @param bool $preserveRestarting If true, "restarting" containers return "restarting:unknown" instead of "degraded:unhealthy"
|
||||
* @return string Aggregated status in colon format (e.g., "running:healthy")
|
||||
*/
|
||||
public function aggregateFromStrings(Collection $containerStatuses, int $maxRestartCount = 0): string
|
||||
public function aggregateFromStrings(Collection $containerStatuses, int $maxRestartCount = 0, bool $preserveRestarting = false): string
|
||||
{
|
||||
// Validate maxRestartCount parameter
|
||||
if ($maxRestartCount < 0) {
|
||||
|
|
@ -64,10 +74,16 @@ public function aggregateFromStrings(Collection $containerStatuses, int $maxRest
|
|||
$hasStarting = false;
|
||||
$hasPaused = false;
|
||||
$hasDead = false;
|
||||
$hasDegraded = false;
|
||||
|
||||
// Parse each status string and set flags
|
||||
foreach ($containerStatuses as $status) {
|
||||
if (str($status)->contains('restarting')) {
|
||||
if (str($status)->contains('degraded')) {
|
||||
$hasDegraded = true;
|
||||
if (str($status)->contains('unhealthy')) {
|
||||
$hasUnhealthy = true;
|
||||
}
|
||||
} elseif (str($status)->contains('restarting')) {
|
||||
$hasRestarting = true;
|
||||
} elseif (str($status)->contains('running')) {
|
||||
$hasRunning = true;
|
||||
|
|
@ -98,7 +114,9 @@ public function aggregateFromStrings(Collection $containerStatuses, int $maxRest
|
|||
$hasStarting,
|
||||
$hasPaused,
|
||||
$hasDead,
|
||||
$maxRestartCount
|
||||
$hasDegraded,
|
||||
$maxRestartCount,
|
||||
$preserveRestarting
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -107,9 +125,10 @@ public function aggregateFromStrings(Collection $containerStatuses, int $maxRest
|
|||
*
|
||||
* @param Collection $containers Collection of Docker container objects with State property
|
||||
* @param int $maxRestartCount Maximum restart count across containers (for crash loop detection)
|
||||
* @param bool $preserveRestarting If true, "restarting" containers return "restarting:unknown" instead of "degraded:unhealthy"
|
||||
* @return string Aggregated status in colon format (e.g., "running:healthy")
|
||||
*/
|
||||
public function aggregateFromContainers(Collection $containers, int $maxRestartCount = 0): string
|
||||
public function aggregateFromContainers(Collection $containers, int $maxRestartCount = 0, bool $preserveRestarting = false): string
|
||||
{
|
||||
// Validate maxRestartCount parameter
|
||||
if ($maxRestartCount < 0) {
|
||||
|
|
@ -175,7 +194,9 @@ public function aggregateFromContainers(Collection $containers, int $maxRestartC
|
|||
$hasStarting,
|
||||
$hasPaused,
|
||||
$hasDead,
|
||||
$maxRestartCount
|
||||
false, // $hasDegraded - not applicable for container objects, only for status strings
|
||||
$maxRestartCount,
|
||||
$preserveRestarting
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +211,9 @@ public function aggregateFromContainers(Collection $containers, int $maxRestartC
|
|||
* @param bool $hasStarting Has at least one starting/created container
|
||||
* @param bool $hasPaused Has at least one paused container
|
||||
* @param bool $hasDead Has at least one dead/removing container
|
||||
* @param bool $hasDegraded Has at least one degraded container
|
||||
* @param int $maxRestartCount Maximum restart count (for crash loop detection)
|
||||
* @param bool $preserveRestarting If true, return "restarting:unknown" instead of "degraded:unhealthy" for restarting containers
|
||||
* @return string Status in colon format (e.g., "running:healthy")
|
||||
*/
|
||||
private function resolveStatus(
|
||||
|
|
@ -202,24 +225,40 @@ private function resolveStatus(
|
|||
bool $hasStarting,
|
||||
bool $hasPaused,
|
||||
bool $hasDead,
|
||||
int $maxRestartCount
|
||||
bool $hasDegraded,
|
||||
int $maxRestartCount,
|
||||
bool $preserveRestarting = false
|
||||
): string {
|
||||
// Priority 1: Restarting containers (degraded state)
|
||||
if ($hasRestarting) {
|
||||
// Priority 1: Degraded containers from sub-resources (highest priority)
|
||||
// If any service/application within a service stack is degraded, the entire stack is degraded
|
||||
if ($hasDegraded) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 2: Crash loop detection (exited with restart count > 0)
|
||||
// Priority 2: Restarting containers
|
||||
// When preserveRestarting is true (for individual sub-resources), keep as "restarting"
|
||||
// When false (for overall service status), mark as "degraded"
|
||||
if ($hasRestarting) {
|
||||
return $preserveRestarting ? 'restarting:unknown' : 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 3: Crash loop detection (exited with restart count > 0)
|
||||
if ($hasExited && $maxRestartCount > 0) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 3: Mixed state (some running, some exited = degraded)
|
||||
// Priority 4: Mixed state (some running, some exited = degraded)
|
||||
if ($hasRunning && $hasExited) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 4: Running containers (check health status)
|
||||
// Priority 5: Mixed state (some running, some starting = still starting)
|
||||
// If any component is still starting, the entire service stack is not fully ready
|
||||
if ($hasRunning && $hasStarting) {
|
||||
return 'starting:unknown';
|
||||
}
|
||||
|
||||
// Priority 6: Running containers (check health status)
|
||||
if ($hasRunning) {
|
||||
if ($hasUnhealthy) {
|
||||
return 'running:unhealthy';
|
||||
|
|
@ -230,22 +269,22 @@ private function resolveStatus(
|
|||
}
|
||||
}
|
||||
|
||||
// Priority 5: Dead or removing containers
|
||||
// Priority 7: Dead or removing containers
|
||||
if ($hasDead) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
// Priority 6: Paused containers
|
||||
// Priority 8: Paused containers
|
||||
if ($hasPaused) {
|
||||
return 'paused:unknown';
|
||||
}
|
||||
|
||||
// Priority 7: Starting/created containers
|
||||
// Priority 9: Starting/created containers
|
||||
if ($hasStarting) {
|
||||
return 'starting:unknown';
|
||||
}
|
||||
|
||||
// Priority 8: All containers exited (no restart count = truly stopped)
|
||||
// Priority 10: All containers exited (no restart count = truly stopped)
|
||||
return 'exited';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,10 +68,16 @@ function queue_application_deployment(Application $application, string $deployme
|
|||
]);
|
||||
|
||||
if ($no_questions_asked) {
|
||||
$deployment->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
ApplicationDeploymentJob::dispatch(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
);
|
||||
} elseif (next_queuable($server_id, $application_id, $commit, $pull_request_id)) {
|
||||
$deployment->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
ApplicationDeploymentJob::dispatch(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@
|
|||
'influxdb',
|
||||
'clickhouse/clickhouse-server',
|
||||
'timescaledb/timescaledb',
|
||||
'timescaledb', // Matches timescale/timescaledb
|
||||
'timescaledb-ha', // Matches timescale/timescaledb-ha
|
||||
'pgvector/pgvector',
|
||||
];
|
||||
const SPECIFIC_SERVICES = [
|
||||
|
|
@ -56,6 +58,7 @@
|
|||
'ghcr.io/coollabsio/minio',
|
||||
'coollabsio/minio',
|
||||
'svhd/logto',
|
||||
'dxflrs/garage',
|
||||
];
|
||||
|
||||
// Based on /etc/os-release
|
||||
|
|
|
|||
|
|
@ -312,6 +312,36 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
|
|||
$LOGTO_ADMIN_ENDPOINT->value.':3002',
|
||||
]);
|
||||
break;
|
||||
case $type?->contains('garage'):
|
||||
$GARAGE_S3_API_URL = $variables->where('key', 'GARAGE_S3_API_URL')->first();
|
||||
$GARAGE_WEB_URL = $variables->where('key', 'GARAGE_WEB_URL')->first();
|
||||
$GARAGE_ADMIN_URL = $variables->where('key', 'GARAGE_ADMIN_URL')->first();
|
||||
|
||||
if (is_null($GARAGE_S3_API_URL) || is_null($GARAGE_WEB_URL) || is_null($GARAGE_ADMIN_URL)) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
if (str($GARAGE_S3_API_URL->value ?? '')->isEmpty()) {
|
||||
$GARAGE_S3_API_URL->update([
|
||||
'value' => generateUrl(server: $server, random: 's3-'.$uuid, forceHttps: true),
|
||||
]);
|
||||
}
|
||||
if (str($GARAGE_WEB_URL->value ?? '')->isEmpty()) {
|
||||
$GARAGE_WEB_URL->update([
|
||||
'value' => generateUrl(server: $server, random: 'web-'.$uuid, forceHttps: true),
|
||||
]);
|
||||
}
|
||||
if (str($GARAGE_ADMIN_URL->value ?? '')->isEmpty()) {
|
||||
$GARAGE_ADMIN_URL->update([
|
||||
'value' => generateUrl(server: $server, random: 'admin-'.$uuid, forceHttps: true),
|
||||
]);
|
||||
}
|
||||
$payload = collect([
|
||||
$GARAGE_S3_API_URL->value.':3900',
|
||||
$GARAGE_WEB_URL->value.':3902',
|
||||
$GARAGE_ADMIN_URL->value.':3903',
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
|
|
@ -770,10 +800,26 @@ function isDatabaseImage(?string $image = null, ?array $serviceConfig = null)
|
|||
}
|
||||
$imageName = $image->before(':');
|
||||
|
||||
// First check if it's a known database image
|
||||
// Extract base image name (ignore registry prefix)
|
||||
// Examples:
|
||||
// docker.io/library/postgres -> postgres
|
||||
// ghcr.io/postgrest/postgrest -> postgrest
|
||||
// postgres -> postgres
|
||||
// postgrest/postgrest -> postgrest
|
||||
$baseImageName = $imageName;
|
||||
if (str($imageName)->contains('/')) {
|
||||
$baseImageName = str($imageName)->afterLast('/');
|
||||
}
|
||||
|
||||
// Check if base image name exactly matches a known database image
|
||||
$isKnownDatabase = false;
|
||||
foreach (DATABASE_DOCKER_IMAGES as $database_docker_image) {
|
||||
if (str($imageName)->contains($database_docker_image)) {
|
||||
// Extract base name from database pattern for comparison
|
||||
$databaseBaseName = str($database_docker_image)->contains('/')
|
||||
? str($database_docker_image)->afterLast('/')
|
||||
: $database_docker_image;
|
||||
|
||||
if ($baseImageName == $databaseBaseName) {
|
||||
$isKnownDatabase = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -1376,3 +1422,62 @@ function injectDockerComposeFlags(string $command, string $composeFilePath, stri
|
|||
// Replace only first occurrence to avoid modifying comments/strings/chained commands
|
||||
return preg_replace('/docker\s+compose/', $dockerComposeReplacement, $command, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject build arguments right after build-related subcommands in docker/docker compose commands.
|
||||
* This ensures build args are only applied to build operations, not to push, pull, up, etc.
|
||||
*
|
||||
* Supports:
|
||||
* - docker compose build
|
||||
* - docker buildx build
|
||||
* - docker builder build
|
||||
* - docker build (legacy)
|
||||
*
|
||||
* Examples:
|
||||
* - Input: "docker compose -f file.yml build"
|
||||
* Output: "docker compose -f file.yml build --build-arg X --build-arg Y"
|
||||
*
|
||||
* - Input: "docker buildx build --platform linux/amd64"
|
||||
* Output: "docker buildx build --build-arg X --build-arg Y --platform linux/amd64"
|
||||
*
|
||||
* - Input: "docker builder build --tag myimage:latest"
|
||||
* Output: "docker builder build --build-arg X --build-arg Y --tag myimage:latest"
|
||||
*
|
||||
* - Input: "docker compose build && docker compose push"
|
||||
* Output: "docker compose build --build-arg X --build-arg Y && docker compose push"
|
||||
*
|
||||
* - Input: "docker compose push"
|
||||
* Output: "docker compose push" (unchanged - no build command found)
|
||||
*
|
||||
* @param string $command The docker command
|
||||
* @param string $buildArgsString The build arguments to inject (e.g., "--build-arg X --build-arg Y")
|
||||
* @return string The modified command with build args injected after build subcommand
|
||||
*/
|
||||
function injectDockerComposeBuildArgs(string $command, string $buildArgsString): string
|
||||
{
|
||||
// Early return if no build args to inject
|
||||
if (empty(trim($buildArgsString))) {
|
||||
return $command;
|
||||
}
|
||||
|
||||
// Match build-related commands:
|
||||
// - ' builder build' (docker builder build)
|
||||
// - ' buildx build' (docker buildx build)
|
||||
// - ' build' (docker compose build, docker build)
|
||||
// Followed by either:
|
||||
// - whitespace (allowing service names, flags, or any valid arguments)
|
||||
// - end of string ($)
|
||||
// This regex ensures we match build subcommands, not "build" in other contexts
|
||||
// IMPORTANT: Order matters - check longer patterns first (builder build, buildx build) before ' build'
|
||||
$pattern = '/( builder build| buildx build| build)(?=\s|$)/';
|
||||
|
||||
// Replace the first occurrence of build command with build command + build-args
|
||||
$modifiedCommand = preg_replace(
|
||||
$pattern,
|
||||
'$1 '.$buildArgsString,
|
||||
$command,
|
||||
1 // Only replace first occurrence
|
||||
);
|
||||
|
||||
return $modifiedCommand ?? $command;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -358,7 +358,7 @@ function parseDockerVolumeString(string $volumeString): array
|
|||
];
|
||||
}
|
||||
|
||||
function applicationParser(Application $resource, int $pull_request_id = 0, ?int $preview_id = null): Collection
|
||||
function applicationParser(Application $resource, int $pull_request_id = 0, ?int $preview_id = null, ?string $commit = null): Collection
|
||||
{
|
||||
$uuid = data_get($resource, 'uuid');
|
||||
$compose = data_get($resource, 'docker_compose_raw');
|
||||
|
|
@ -1145,11 +1145,13 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
$template = $resource->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
$portInt = $url->getPort();
|
||||
$port = $portInt !== null ? ':'.$portInt : '';
|
||||
$random = new Cuid2;
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $pullRequestId, $preview_fqdn);
|
||||
$preview_fqdn = "$schema://$preview_fqdn";
|
||||
$preview_fqdn = "$schema://$preview_fqdn{$port}";
|
||||
$preview->fqdn = $preview_fqdn;
|
||||
$preview->save();
|
||||
|
||||
|
|
@ -1324,6 +1326,20 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
->values();
|
||||
|
||||
$payload['env_file'] = $envFiles;
|
||||
|
||||
// Inject commit-based image tag for services with build directive (for rollback support)
|
||||
// Only inject if service has build but no explicit image defined
|
||||
$hasBuild = data_get($service, 'build') !== null;
|
||||
$hasImage = data_get($service, 'image') !== null;
|
||||
if ($hasBuild && ! $hasImage && $commit) {
|
||||
$imageTag = str($commit)->substr(0, 128)->value();
|
||||
if ($isPullRequest) {
|
||||
$imageTag = "pr-{$pullRequestId}";
|
||||
}
|
||||
$imageRepo = "{$uuid}_{$serviceName}";
|
||||
$payload['image'] = "{$imageRepo}:{$imageTag}";
|
||||
}
|
||||
|
||||
if ($isPullRequest) {
|
||||
$serviceName = addPreviewDeploymentSuffix($serviceName, $pullRequestId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ function () use ($server, $command_string) {
|
|||
);
|
||||
}
|
||||
|
||||
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
|
||||
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false, ?int $timeout = null, bool $disableMultiplexing = false): ?string
|
||||
{
|
||||
$command = $command instanceof Collection ? $command->toArray() : $command;
|
||||
|
||||
|
|
@ -126,11 +126,12 @@ function instant_remote_process(Collection|array $command, Server $server, bool
|
|||
$command = parseCommandsByLineForSudo(collect($command), $server);
|
||||
}
|
||||
$command_string = implode("\n", $command);
|
||||
$effectiveTimeout = $timeout ?? config('constants.ssh.command_timeout');
|
||||
|
||||
return \App\Helpers\SshRetryHandler::retry(
|
||||
function () use ($server, $command_string) {
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string);
|
||||
$process = Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand);
|
||||
function () use ($server, $command_string, $effectiveTimeout, $disableMultiplexing) {
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string, $disableMultiplexing);
|
||||
$process = Process::timeout($effectiveTimeout)->run($sshCommand);
|
||||
|
||||
$output = trim($process->output());
|
||||
$exitCode = $process->exitCode();
|
||||
|
|
|
|||
|
|
@ -5,39 +5,44 @@
|
|||
|
||||
function isSubscriptionActive()
|
||||
{
|
||||
if (! isCloud()) {
|
||||
return false;
|
||||
}
|
||||
$team = currentTeam();
|
||||
if (! $team) {
|
||||
return false;
|
||||
}
|
||||
$subscription = $team?->subscription;
|
||||
return once(function () {
|
||||
if (! isCloud()) {
|
||||
return false;
|
||||
}
|
||||
$team = currentTeam();
|
||||
if (! $team) {
|
||||
return false;
|
||||
}
|
||||
$subscription = $team?->subscription;
|
||||
|
||||
if (is_null($subscription)) {
|
||||
return false;
|
||||
}
|
||||
if (isStripe()) {
|
||||
return $subscription->stripe_invoice_paid === true;
|
||||
}
|
||||
if (is_null($subscription)) {
|
||||
return false;
|
||||
}
|
||||
if (isStripe()) {
|
||||
return $subscription->stripe_invoice_paid === true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function isSubscriptionOnGracePeriod()
|
||||
{
|
||||
$team = currentTeam();
|
||||
if (! $team) {
|
||||
return false;
|
||||
}
|
||||
$subscription = $team?->subscription;
|
||||
if (! $subscription) {
|
||||
return false;
|
||||
}
|
||||
if (isStripe()) {
|
||||
return $subscription->stripe_cancel_at_period_end;
|
||||
}
|
||||
return once(function () {
|
||||
$team = currentTeam();
|
||||
if (! $team) {
|
||||
return false;
|
||||
}
|
||||
$subscription = $team?->subscription;
|
||||
if (! $subscription) {
|
||||
return false;
|
||||
}
|
||||
if (isStripe()) {
|
||||
return $subscription->stripe_cancel_at_period_end;
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
function subscriptionProvider()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.452',
|
||||
'version' => '4.0.0-beta.453',
|
||||
'helper_version' => '1.0.12',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
|
|
|||
|
|
@ -35,13 +35,6 @@
|
|||
'throw' => false,
|
||||
],
|
||||
|
||||
'webhooks-during-maintenance' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/webhooks-during-maintenance'),
|
||||
'visibility' => 'private',
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
'driver' => env('SESSION_DRIVER', 'redis'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->integer('docker_images_to_keep')->default(2);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('docker_images_to_keep');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->boolean('disable_application_image_retention')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('disable_application_image_retention');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Index definitions: [table, columns, index_name]
|
||||
*/
|
||||
private array $indexes = [
|
||||
['servers', ['team_id'], 'idx_servers_team_id'],
|
||||
['private_keys', ['team_id'], 'idx_private_keys_team_id'],
|
||||
['projects', ['team_id'], 'idx_projects_team_id'],
|
||||
['subscriptions', ['team_id'], 'idx_subscriptions_team_id'],
|
||||
['cloud_init_scripts', ['team_id'], 'idx_cloud_init_scripts_team_id'],
|
||||
['cloud_provider_tokens', ['team_id'], 'idx_cloud_provider_tokens_team_id'],
|
||||
['application_deployment_queues', ['status', 'server_id'], 'idx_deployment_queues_status_server'],
|
||||
['application_deployment_queues', ['application_id', 'status', 'pull_request_id', 'created_at'], 'idx_deployment_queues_app_status_pr_created'],
|
||||
['environments', ['project_id'], 'idx_environments_project_id'],
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
foreach ($this->indexes as [$table, $columns, $indexName]) {
|
||||
if (! $this->indexExists($indexName)) {
|
||||
$columnList = implode(', ', array_map(fn ($col) => "\"$col\"", $columns));
|
||||
DB::statement("CREATE INDEX \"{$indexName}\" ON \"{$table}\" ({$columnList})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach ($this->indexes as [, , $indexName]) {
|
||||
DB::statement("DROP INDEX IF EXISTS \"{$indexName}\"");
|
||||
}
|
||||
}
|
||||
|
||||
private function indexExists(string $indexName): bool
|
||||
{
|
||||
$result = DB::selectOne(
|
||||
'SELECT 1 FROM pg_indexes WHERE indexname = ?',
|
||||
[$indexName]
|
||||
);
|
||||
|
||||
return $result !== null;
|
||||
}
|
||||
};
|
||||
|
|
@ -11,7 +11,6 @@ services:
|
|||
- /data/coolify/databases:/var/www/html/storage/app/databases
|
||||
- /data/coolify/services:/var/www/html/storage/app/services
|
||||
- /data/coolify/backups:/var/www/html/storage/app/backups
|
||||
- /data/coolify/webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
|
||||
environment:
|
||||
- APP_ENV=${APP_ENV:-production}
|
||||
- PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT:-256M}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ services:
|
|||
- ./databases:/var/www/html/storage/app/databases
|
||||
- ./services:/var/www/html/storage/app/services
|
||||
- ./backups:/var/www/html/storage/app/backups
|
||||
- ./webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
|
@ -75,13 +74,7 @@ services:
|
|||
POSTGRES_PASSWORD: "${DB_PASSWORD}"
|
||||
POSTGRES_DB: "${DB_DATABASE:-coolify}"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"pg_isready -U ${DB_USERNAME}",
|
||||
"-d",
|
||||
"${DB_DATABASE:-coolify}"
|
||||
]
|
||||
test: [ "CMD-SHELL", "pg_isready -U ${DB_USERNAME}", "-d", "${DB_DATABASE:-coolify}" ]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
|
|
@ -121,7 +114,7 @@ services:
|
|||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"]
|
||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ services:
|
|||
- /data/coolify/databases:/var/www/html/storage/app/databases
|
||||
- /data/coolify/services:/var/www/html/storage/app/services
|
||||
- /data/coolify/backups:/var/www/html/storage/app/backups
|
||||
- /data/coolify/webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
|
||||
environment:
|
||||
- APP_ENV=${APP_ENV:-production}
|
||||
- PHP_MEMORY_LIMIT=${PHP_MEMORY_LIMIT:-256M}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ services:
|
|||
- ./databases:/var/www/html/storage/app/databases
|
||||
- ./services:/var/www/html/storage/app/services
|
||||
- ./backups:/var/www/html/storage/app/backups
|
||||
- ./webhooks-during-maintenance:/var/www/html/storage/app/webhooks-during-maintenance
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
|
@ -75,13 +74,7 @@ services:
|
|||
POSTGRES_PASSWORD: "${DB_PASSWORD}"
|
||||
POSTGRES_DB: "${DB_DATABASE:-coolify}"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"pg_isready -U ${DB_USERNAME}",
|
||||
"-d",
|
||||
"${DB_DATABASE:-coolify}"
|
||||
]
|
||||
test: [ "CMD-SHELL", "pg_isready -U ${DB_USERNAME}", "-d", "${DB_DATABASE:-coolify}" ]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
|
|
@ -121,7 +114,7 @@ services:
|
|||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"]
|
||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ if [ "$WARNING_SPACE" = true ]; then
|
|||
sleep 5
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel}
|
||||
mkdir -p /data/coolify/ssh/{keys,mux}
|
||||
mkdir -p /data/coolify/proxy/dynamic
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue