diff --git a/.dockerignore b/.dockerignore
index 2eba3cb46..0adca0b32 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -24,3 +24,4 @@ yarn-error.log
/.ssh
.ignition.json
.env.dusk.local
+docker/coolify-realtime/node_modules
diff --git a/.gitattributes b/.gitattributes
index fcb21d396..c48a5898b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -8,4 +8,4 @@
/.github export-ignore
CHANGELOG.md export-ignore
-.styleci.yml export-ignore
+.styleci.yml export-ignore
\ No newline at end of file
diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml
index 7017b4897..5271143ec 100644
--- a/.github/workflows/coolify-production-build.yml
+++ b/.github/workflows/coolify-production-build.yml
@@ -11,7 +11,7 @@ on:
- docker/coolify-helper/Dockerfile
- docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile
- - templates/service-templates.json
+ - templates/**
env:
GITHUB_REGISTRY: ghcr.io
@@ -41,7 +41,7 @@ jobs:
- name: Get Version
id: version
run: |
- echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT]
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
@@ -76,7 +76,7 @@ jobs:
- name: Get Version
id: version
run: |
- echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT]
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image
uses: docker/build-push-action@v6
diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml
index 7e937d17a..ef247170f 100644
--- a/.github/workflows/coolify-realtime-next.yml
+++ b/.github/workflows/coolify-realtime-next.yml
@@ -8,6 +8,7 @@ on:
- docker/coolify-realtime/Dockerfile
- docker/coolify-realtime/terminal-server.js
- docker/coolify-realtime/package.json
+ - docker/coolify-realtime/package-lock.json
- docker/coolify-realtime/soketi-entrypoint.sh
env:
diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml
index 97bfd52eb..9654a21b0 100644
--- a/.github/workflows/coolify-realtime.yml
+++ b/.github/workflows/coolify-realtime.yml
@@ -8,6 +8,7 @@ on:
- docker/coolify-realtime/Dockerfile
- docker/coolify-realtime/terminal-server.js
- docker/coolify-realtime/package.json
+ - docker/coolify-realtime/package-lock.json
- docker/coolify-realtime/soketi-entrypoint.sh
env:
diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml
index 6e4d4adc3..2c57a36a3 100644
--- a/.github/workflows/coolify-staging-build.yml
+++ b/.github/workflows/coolify-staging-build.yml
@@ -11,7 +11,7 @@ on:
- docker/coolify-helper/Dockerfile
- docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile
- - templates/service-templates.json
+ - templates/**
env:
GITHUB_REGISTRY: ghcr.io
diff --git a/.gitignore b/.gitignore
index 1a021ab3e..d7ee7e96c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,5 @@ _ide_helper_models.php
scripts/load-test/*
.ignition.json
.env.dusk.local
+docker/coolify-realtime/node_modules
+.DS_Store
diff --git a/.gitpod.yml b/.gitpod.yml
deleted file mode 100644
index 6fd6797b5..000000000
--- a/.gitpod.yml
+++ /dev/null
@@ -1,65 +0,0 @@
-tasks:
- - name: Setup Spin environment and Composer dependencies
- # Fix because of https://github.com/gitpod-io/gitpod/issues/16614
- before: sudo curl -o /usr/local/bin/docker-compose -fsSL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-$(uname -m)
- init: |
- cp .env.development.example .env &&
- sed -i "s#APP_URL=http://localhost#APP_URL=$(gp url 8000)#g" .env
- sed -i "s#USERID=#USERID=33333#g" .env
- sed -i "s#GROUPID=#GROUPID=33333#g" .env
- composer install --ignore-platform-reqs
- ./vendor/bin/spin up -d
- ./vendor/bin/spin exec -u webuser coolify php artisan key:generate
- ./vendor/bin/spin exec -u webuser coolify php artisan storage:link
- ./vendor/bin/spin exec -u webuser coolify php artisan migrate:fresh --seed
- cat .coolify-logo
- gp sync-done spin-is-ready
-
- - name: Install Node dependencies and run Vite
- command: |
- echo "Waiting for Sail environment to boot up."
- gp sync-await spin-is-ready
- ./vendor/bin/spin exec vite npm install
- ./vendor/bin/spin exec vite npm run dev -- --host
-
- - name: Laravel Queue Worker, listening to code changes
- command: |
- echo "Waiting for Sail environment to boot up."
- gp sync-await spin-is-ready
- ./vendor/bin/spin exec -u webuser coolify php artisan queue:listen
-
-ports:
- - port: 5432
- onOpen: ignore
- name: PostgreSQL
- visibility: public
- - port: 5173
- onOpen: ignore
- visibility: public
- name: Node Server for Vite
- - port: 8000
- onOpen: ignore
- visibility: public
- name: Coolify
-
-# Configure vscode
-vscode:
- extensions:
- - bmewburn.vscode-intelephense-client
- - ikappas.composer
- - ms-azuretools.vscode-docker
- - ecmel.vscode-html-css
- - MehediDracula.php-namespace-resolver
- - wmaurer.change-case
- - Equinusocio.vsc-community-material-theme
- - EditorConfig.EditorConfig
- - streetsidesoftware.code-spell-checker
- - rangav.vscode-thunder-client
- - PKief.material-icon-theme
- - cierra.livewire-vscode
- - lennardv.livewire-goto-updated
- - bradlc.vscode-tailwindcss
- - heybourn.headwind
- - adrianwilczynski.alpine-js-intellisense
- - amiralizadeh9480.laravel-extra-intellisense
- - shufo.vscode-blade-formatter
diff --git a/README.md b/README.md
index 14a741088..dac48d127 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
# About the Project
-Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
+Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
It helps you manage your servers, applications, and databases on your own hardware; you only need an SSH connection. You can manage VPS, Bare Metal, Raspberry PIs, and anything else.
@@ -22,6 +22,9 @@ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
```
You can find the installation script source [here](./scripts/install.sh).
+> [!NOTE]
+> Please refer to the [docs](https://coolify.io/docs/installation) for more information about the installation.
+
# Support
Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
@@ -37,21 +40,21 @@ Special thanks to our biggest sponsors!
### Special Sponsors
-
+
* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry.
* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions.
* [Logto](https://logto.io/?ref=coolify) - An open-source authentication and authorization solution for building secure login systems and managing user identities.
+* [Tolgee](https://tolgee.io/?ref=coolify) - Developer & translator friendly web-based localization platform.
* [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions and online business growth strategies.
* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
+* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
-* [Fractal Networks](https://fractalnetworks.co/?ref=coolify.io) - A decentralized network infrastructure company focusing on secure and private communication solutions.
* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies.
* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers.
-* [Latitude](https://latitude.sh/?ref=coolify.io) - A cloud computing platform offering bare metal servers and cloud instances for developers and businesses.
* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities.
* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries.
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
@@ -60,6 +63,7 @@ Special thanks to our biggest sponsors!
* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses.
* [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly.
* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
+* [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - Fast web hosting provider.
## Github Sponsors ($40+)
@@ -87,7 +91,11 @@ Special thanks to our biggest sponsors!
-
+
+
+
+
+
## Organizations
@@ -121,7 +129,6 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
- Better support
- Less maintenance for you
-
# Recognitions
@@ -138,6 +145,13 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
+# Core Maintainers
+
+| Andras Bacsai | 🏔️ Peak |
+|------------|------------|
+|
|
|
+|
|
|
+
# Repo Activity

diff --git a/RELEASE.md b/RELEASE.md
index d9f05f17d..bc159b040 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,6 +1,6 @@
# Coolify Release Guide
-This guide outlines the release process for Coolify, intended for developers and those interested in understanding how releases are managed and deployed.
+This guide outlines the release process for Coolify, intended for developers and those interested in understanding how Coolify releases are managed and deployed.
## Table of Contents
- [Release Process](#release-process)
@@ -19,19 +19,19 @@ This guide outlines the release process for Coolify, intended for developers and
- Improvements, fixes, and new features are developed on the `next` branch or separate feature branches.
2. **Merging to `main`**
- - Once ready, changes are merged from the `next` branch into the `main` branch.
+ - Once ready, changes are merged from the `next` branch into the `main` branch (via a pull request).
3. **Building the Release**
- - After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry with the version tag and the `latest` tag.
+ - After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry and Docker Hub with the specific version tag and the `latest` tag.
4. **Creating a GitHub Release**
- A new GitHub release is manually created with details of the changes made in the version.
5. **Updating the CDN**
- - To make a new version publicly available, the version information on the CDN needs to be updated: [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json)
+ - To make a new version publicly available, the version information on the CDN needs to be updated manually. After that the new version number will be available at [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json).
> [!NOTE]
-> The CDN update may not occur immediately after the GitHub release. It can take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated.**
+> The CDN update may not occur immediately after the GitHub release. It can take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated. After the CDN is updated, a discord announcement will be made in the Production Release channel.**
## Version Types
@@ -39,10 +39,10 @@ This guide outlines the release process for Coolify, intended for developers and
Stable (coming soon)
- **Stable**
- - The production version suitable for stable, production environments (generally recommended).
- - **Update Frequency:** Every 2 to 4 weeks, with more frequent possible hotfixes.
+ - The production version suitable for stable, production environments (recommended).
+ - **Update Frequency:** Every 2 to 4 weeks, with more frequent possible fixes.
- **Release Size:** Larger but less frequent releases. Multiple nightly versions are consolidated into a single stable release.
- - **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`).
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`, `4.1.0`, etc.).
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
@@ -57,7 +57,7 @@ This guide outlines the release process for Coolify, intended for developers and
- The latest development version, suitable for testing the latest changes and experimenting with new features.
- **Update Frequency:** Daily or bi-weekly updates.
- **Release Size:** Smaller, more frequent releases.
- - **Versioning Scheme:** TO BE DETERMINED
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-nightly.1`, `4.1.0-nightly.2`, etc.).
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify-nightly/install.sh | bash -s next
@@ -73,11 +73,11 @@ This guide outlines the release process for Coolify, intended for developers and
- **Purpose:** Allows users to test and provide feedback on new features and changes before they become stable.
- **Update Frequency:** Available if we think beta testing is necessary.
- **Release Size:** Same size as stable release as it will become the next stabe release after some time.
- - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`).
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`, `4.1.0-beta.2`, etc.).
- **Installation Command:**
- ```bash
+ ```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
- ```
+ ```
@@ -117,12 +117,15 @@ When a new version is released and a new GitHub release is created, it doesn't i
> [!IMPORTANT]
> The cloud version of Coolify may be several versions behind the latest GitHub releases even if the CDN is updated. This is intentional to ensure stability and reliability for cloud users and Andras will manully update the cloud version when the update is ready.
-## Manually Update to Specific Versions
+## Manually Update/ Downgrade to Specific Versions
> [!CAUTION]
-> Updating to unreleased versions is not recommended and may cause issues. Use at your own risk!
+> Updating to unreleased versions is not recommended and can cause issues.
-To update your Coolify instance to a specific (unreleased) version, use the following command:
+> [!IMPORTANT]
+> Downgrading is supported but not recommended and can cause issues because of database migrations and other changes.
+
+To update your Coolify instance to a specific version, use the following command:
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s
diff --git a/SECURITY.md b/SECURITY.md
index ad3a4addd..0711bf5b5 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,15 +2,24 @@
## Supported Versions
-Use this section to tell people about which versions of your project are
-currently being supported with security updates.
+Currently supported, maintained and updated versions:
-| Version | Supported |
-| ------- | ------------------ |
-| > 4 | :white_check_mark: |
-| 3 | :x: |
+| Version | Supported | Support Status |
+| ------- | ------------------ | -------------- |
+| 4.x | :white_check_mark: | Active Development & Security Updates |
+| < 4.0 | :x: | End of Life (no security updates) |
+## Security Updates
+
+We take security seriously. Security updates are released as soon as possible after a vulnerability is discovered and verified.
## Reporting a Vulnerability
-If you have any vulnerability please report at hi@coollabs.io
+If you discover a security vulnerability, please follow these steps:
+
+1. **DO NOT** disclose the vulnerability publicly.
+2. Send a detailed report to: `hi@coollabs.io`.
+3. Include in your report:
+ - A description of the vulnerability
+ - Steps to reproduce the issue
+ - Potential impact
diff --git a/app/Actions/Application/IsHorizonQueueEmpty.php b/app/Actions/Application/IsHorizonQueueEmpty.php
new file mode 100644
index 000000000..17966b8a0
--- /dev/null
+++ b/app/Actions/Application/IsHorizonQueueEmpty.php
@@ -0,0 +1,37 @@
+getRecent();
+ if ($recent) {
+ $running = $recent->filter(function ($job) use ($hostname) {
+ $payload = json_decode($job->payload);
+ $tags = data_get($payload, 'tags');
+
+ return $job->status != 'completed' &&
+ $job->status != 'failed' &&
+ isset($tags) &&
+ is_array($tags) &&
+ in_array('server:'.$hostname, $tags);
+ });
+ if ($running->count() > 0) {
+ echo 'false';
+
+ return false;
+ }
+ }
+ echo 'true';
+
+ return true;
+ }
+}
diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php
index cab7e45f0..642b4ba45 100644
--- a/app/Actions/Application/StopApplication.php
+++ b/app/Actions/Application/StopApplication.php
@@ -10,6 +10,8 @@ class StopApplication
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{
try {
diff --git a/app/Actions/CoolifyTask/PrepareCoolifyTask.php b/app/Actions/CoolifyTask/PrepareCoolifyTask.php
index 6676b7937..3f76a2e3c 100644
--- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php
+++ b/app/Actions/CoolifyTask/PrepareCoolifyTask.php
@@ -3,7 +3,6 @@
namespace App\Actions\CoolifyTask;
use App\Data\CoolifyTaskArgs;
-use App\Enums\ActivityTypes;
use App\Jobs\CoolifyTask;
use Spatie\Activitylog\Models\Activity;
@@ -47,11 +46,7 @@ class PrepareCoolifyTask
call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish,
call_event_data: $this->remoteProcessArgs->call_event_data,
);
- if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) {
- dispatch($job)->onQueue('high');
- } else {
- dispatch($job);
- }
+ dispatch($job);
$this->activity->refresh();
return $this->activity;
diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php
index 51c5a1f9e..981b81378 100644
--- a/app/Actions/CoolifyTask/RunRemoteProcess.php
+++ b/app/Actions/CoolifyTask/RunRemoteProcess.php
@@ -9,6 +9,7 @@ use App\Jobs\ApplicationDeploymentJob;
use App\Models\Server;
use Illuminate\Process\ProcessResult;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Spatie\Activitylog\Models\Activity;
@@ -124,6 +125,7 @@ class RunRemoteProcess
]));
}
} catch (\Throwable $e) {
+ Log::error('Error calling event: '.$e->getMessage());
}
}
diff --git a/app/Actions/Database/StartDatabase.php b/app/Actions/Database/StartDatabase.php
index 869a88521..e2fa6fc87 100644
--- a/app/Actions/Database/StartDatabase.php
+++ b/app/Actions/Database/StartDatabase.php
@@ -16,6 +16,8 @@ class StartDatabase
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = $database->destination->server;
@@ -49,7 +51,7 @@ class StartDatabase
break;
}
if ($database->is_public && $database->public_port) {
- StartDatabaseProxy::dispatch($database)->onQueue('high');
+ StartDatabaseProxy::dispatch($database);
}
return $activity;
diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php
index d7a3bc697..3ddf6c036 100644
--- a/app/Actions/Database/StartDatabaseProxy.php
+++ b/app/Actions/Database/StartDatabaseProxy.php
@@ -18,6 +18,8 @@ class StartDatabaseProxy
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
{
$internalPort = null;
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 6d67bdde6..26a0f82d0 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -25,6 +25,10 @@ class StartMongodb
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
+ if (isDev()) {
+ $this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
+ }
+
$this->commands = [
"echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php
index 0a166d24a..9ee794351 100644
--- a/app/Actions/Database/StopDatabaseProxy.php
+++ b/app/Actions/Database/StopDatabaseProxy.php
@@ -18,6 +18,8 @@ class StopDatabaseProxy
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = data_get($database, 'destination.server');
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index 95c22efc1..8aae910a9 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -7,7 +7,6 @@ use App\Actions\Shared\ComplexStatusCheck;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceDatabase;
-use App\Notifications\Container\ContainerRestarted;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -16,6 +15,8 @@ class GetContainersStatus
{
use AsAction;
+ public string $jobQueue = 'high';
+
public $applications;
public ?Collection $containers;
@@ -107,6 +108,8 @@ class GetContainersStatus
$statusFromDb = $preview->status;
if ($statusFromDb !== $containerStatus) {
$preview->update(['status' => $containerStatus]);
+ } else {
+ $preview->update(['last_online_at' => now()]);
}
} else {
//Notify user that this container should not be there.
@@ -118,6 +121,8 @@ class GetContainersStatus
$statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) {
$application->update(['status' => $containerStatus]);
+ } else {
+ $application->update(['last_online_at' => now()]);
}
} else {
//Notify user that this container should not be there.
@@ -160,7 +165,10 @@ class GetContainersStatus
$statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
+ } else {
+ $database->update(['last_online_at' => now()]);
}
+
if ($isPublic) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
@@ -171,7 +179,7 @@ class GetContainersStatus
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
- $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
+ // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
}
}
} else {
@@ -202,6 +210,8 @@ class GetContainersStatus
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]);
+ } else {
+ $service->update(['last_online_at' => now()]);
}
}
}
diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php
deleted file mode 100644
index 26a1ff7bf..000000000
--- a/app/Actions/License/CheckResaleLicense.php
+++ /dev/null
@@ -1,66 +0,0 @@
-update([
- 'is_resale_license_active' => true,
- ]);
-
- return;
- }
- // if (!$settings->resale_license) {
- // return;
- // }
- $base_url = config('coolify.license_url');
- $instance_id = config('app.id');
- $data = Http::withHeaders([
- 'Accept' => 'application/json',
- ])->get("$base_url/lemon/validate", [
- 'license_key' => $settings->resale_license,
- 'instance_id' => $instance_id,
- ])->json();
- if (data_get($data, 'valid') === true && data_get($data, 'license_key.status') === 'active') {
- $settings->update([
- 'is_resale_license_active' => true,
- ]);
-
- return;
- }
- $data = Http::withHeaders([
- 'Accept' => 'application/json',
- ])->get("$base_url/lemon/activate", [
- 'license_key' => $settings->resale_license,
- 'instance_id' => $instance_id,
- ])->json();
- if (data_get($data, 'activated') === true) {
- $settings->update([
- 'is_resale_license_active' => true,
- ]);
-
- return;
- }
- if (data_get($data, 'license_key.status') === 'active') {
- throw new \Exception('Invalid license key.');
- }
- throw new \Exception('Cannot activate license key.');
- } catch (\Throwable $e) {
- $settings->update([
- 'resale_license' => null,
- 'is_resale_license_active' => false,
- ]);
- throw $e;
- }
- }
-}
diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php
index d64804758..6c8dd5234 100644
--- a/app/Actions/Proxy/CheckProxy.php
+++ b/app/Actions/Proxy/CheckProxy.php
@@ -4,6 +4,7 @@ namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Models\Server;
+use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -29,7 +30,7 @@ class CheckProxy
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
return false;
}
- ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
+ ['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
if (! $uptime) {
throw new \Exception($error);
}
@@ -88,6 +89,7 @@ class CheckProxy
$portsToCheck = [];
}
} catch (\Exception $e) {
+ Log::error('Error checking proxy: '.$e->getMessage());
}
if (count($portsToCheck) === 0) {
return false;
diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php
index dc6ac12bf..0349ead89 100644
--- a/app/Actions/Server/CleanupDocker.php
+++ b/app/Actions/Server/CleanupDocker.php
@@ -9,11 +9,13 @@ class CleanupDocker
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Server $server)
{
$settings = instanceSettings();
$helperImageVersion = data_get($settings, 'helper_version');
- $helperImage = config('coolify.helper_image');
+ $helperImage = config('constants.coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion";
$commands = [
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index ba6c23ffc..cbcb20368 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -12,7 +12,7 @@ class InstallDocker
public function handle(Server $server)
{
- $dockerVersion = config('constants.docker_install_version');
+ $dockerVersion = config('constants.docker.minimum_required_version');
$supported_os_type = $server->validateOS();
if (! $supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.');
diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php
index d9de1464e..5f9a1e357 100644
--- a/app/Actions/Server/ServerCheck.php
+++ b/app/Actions/Server/ServerCheck.php
@@ -14,7 +14,7 @@ use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
-use Arr;
+use Illuminate\Support\Arr;
use Lorisleiva\Actions\Concerns\AsAction;
class ServerCheck
@@ -130,10 +130,10 @@ class ServerCheck
if ($foundLogDrainContainer) {
$status = data_get($foundLogDrainContainer, 'State.Status');
if ($status !== 'running') {
- StartLogDrain::dispatch($this->server)->onQueue('high');
+ StartLogDrain::dispatch($this->server);
}
} else {
- StartLogDrain::dispatch($this->server)->onQueue('high');
+ StartLogDrain::dispatch($this->server);
}
}
@@ -259,7 +259,7 @@ class ServerCheck
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
- $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
+ // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
}
}
}
diff --git a/app/Actions/Server/StartLogDrain.php b/app/Actions/Server/StartLogDrain.php
index 0e8036cd9..0d28a0099 100644
--- a/app/Actions/Server/StartLogDrain.php
+++ b/app/Actions/Server/StartLogDrain.php
@@ -9,6 +9,8 @@ class StartLogDrain
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Server $server)
{
if ($server->settings->is_logdrain_newrelic_enabled) {
@@ -169,7 +171,7 @@ Files:
');
$license_key = $server->settings->logdrain_newrelic_license_key;
$base_uri = $server->settings->logdrain_newrelic_base_uri;
- $base_path = config('coolify.base_config_path');
+ $base_path = config('constants.coolify.base_config_path');
$config_path = $base_path.'/log-drains';
$fluent_bit_config = $config_path.'/fluent-bit.conf';
diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php
index 3185c22b7..be9b4062c 100644
--- a/app/Actions/Server/UpdateCoolify.php
+++ b/app/Actions/Server/UpdateCoolify.php
@@ -4,6 +4,7 @@ namespace App\Actions\Server;
use App\Jobs\PullHelperImageJob;
use App\Models\Server;
+use Illuminate\Support\Sleep;
use Lorisleiva\Actions\Concerns\AsAction;
class UpdateCoolify
@@ -18,14 +19,19 @@ class UpdateCoolify
public function handle($manual_update = false)
{
+ if (isDev()) {
+ Sleep::for(10)->seconds();
+
+ return;
+ }
$settings = instanceSettings();
$this->server = Server::find(0);
if (! $this->server) {
return;
}
- CleanupDocker::dispatch($this->server)->onQueue('high');
+ CleanupDocker::dispatch($this->server);
$this->latestVersion = get_latest_version_of_coolify();
- $this->currentVersion = config('version');
+ $this->currentVersion = config('constants.coolify.version');
if (! $manual_update) {
if (! $settings->is_auto_update_enabled) {
return;
@@ -44,19 +50,7 @@ class UpdateCoolify
private function update()
{
- if (isDev()) {
- remote_process([
- 'sleep 10',
- ], $this->server);
-
- return;
- }
-
- $all_servers = Server::all();
- $servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
- foreach ($servers as $server) {
- PullHelperImageJob::dispatch($server);
- }
+ PullHelperImageJob::dispatch($this->server);
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
diff --git a/app/Actions/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php
index d0a4cd6be..55b37a77c 100644
--- a/app/Actions/Server/ValidateServer.php
+++ b/app/Actions/Server/ValidateServer.php
@@ -9,6 +9,8 @@ class ValidateServer
{
use AsAction;
+ public string $jobQueue = 'high';
+
public ?string $uptime = null;
public ?string $error = null;
diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php
index 9c4b0349c..9b87454da 100644
--- a/app/Actions/Service/DeleteService.php
+++ b/app/Actions/Service/DeleteService.php
@@ -4,6 +4,7 @@ namespace App\Actions\Service;
use App\Actions\Server\CleanupDocker;
use App\Models\Service;
+use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
class DeleteService
@@ -39,7 +40,8 @@ class DeleteService
if (! empty($commands)) {
foreach ($commands as $command) {
$result = instant_remote_process([$command], $server, false);
- if ($result !== 0) {
+ if ($result !== null && $result !== 0) {
+ Log::error('Error deleting volumes: '.$result);
}
}
}
diff --git a/app/Actions/Service/RestartService.php b/app/Actions/Service/RestartService.php
index 1b6a5c32c..4151ea947 100644
--- a/app/Actions/Service/RestartService.php
+++ b/app/Actions/Service/RestartService.php
@@ -9,6 +9,8 @@ class RestartService
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Service $service)
{
StopService::run($service);
diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php
index 82de066d7..1dfaf6c49 100644
--- a/app/Actions/Service/StartService.php
+++ b/app/Actions/Service/StartService.php
@@ -10,6 +10,8 @@ class StartService
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Service $service)
{
$service->saveComposeConfigs();
diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php
index 046d94ced..95b08b437 100644
--- a/app/Actions/Service/StopService.php
+++ b/app/Actions/Service/StopService.php
@@ -10,6 +10,8 @@ class StopService
{
use AsAction;
+ public string $jobQueue = 'high';
+
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
try {
diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php
index 5fc2b4e61..e16a82be4 100644
--- a/app/Console/Commands/CleanupRedis.php
+++ b/app/Console/Commands/CleanupRedis.php
@@ -13,7 +13,6 @@ class CleanupRedis extends Command
public function handle()
{
- echo "Cleanup Redis keys.\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php
index 9d36ce9b8..def3d5a2c 100644
--- a/app/Console/Commands/CleanupStuckedResources.php
+++ b/app/Console/Commands/CleanupStuckedResources.php
@@ -30,7 +30,6 @@ class CleanupStuckedResources extends Command
public function handle()
{
- echo "Running cleanup stucked resources.\n";
$this->cleanup_stucked_resources();
}
diff --git a/app/Console/Commands/CloudCheckSubscription.php b/app/Console/Commands/CloudCheckSubscription.php
new file mode 100644
index 000000000..6e237e84b
--- /dev/null
+++ b/app/Console/Commands/CloudCheckSubscription.php
@@ -0,0 +1,49 @@
+get();
+ foreach ($activeSubscribers as $team) {
+ $stripeSubscriptionId = $team->subscription->stripe_subscription_id;
+ $stripeInvoicePaid = $team->subscription->stripe_invoice_paid;
+ $stripeCustomerId = $team->subscription->stripe_customer_id;
+ if (! $stripeSubscriptionId) {
+ echo "Team {$team->id} has no subscription, but invoice status is: {$stripeInvoicePaid}\n";
+ echo "Link on Stripe: https://dashboard.stripe.com/customers/{$stripeCustomerId}\n";
+
+ continue;
+ }
+ $subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId);
+ if ($subscription->status === 'active') {
+ continue;
+ }
+ echo "Subscription {$stripeSubscriptionId} is not active ({$subscription->status})\n";
+ echo "Link on Stripe: https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}\n";
+ }
+ }
+}
diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php
index f5f1233fe..962000d07 100644
--- a/app/Console/Commands/Dev.php
+++ b/app/Console/Commands/Dev.php
@@ -6,6 +6,7 @@ use App\Models\InstanceSettings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Process;
+use Symfony\Component\Yaml\Yaml;
class Dev extends Command
{
@@ -31,19 +32,32 @@ class Dev extends Command
{
// Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n";
- $process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
+ // https://github.com/OAI/OpenAPI-Specification/releases
+ $process = Process::run([
+ '/var/www/html/vendor/bin/openapi',
+ 'app',
+ '-o',
+ 'openapi.yaml',
+ '--version',
+ '3.1.0',
+ ]);
$error = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error);
echo $error;
echo $process->output();
+ // Convert YAML to JSON
+ $yaml = file_get_contents('openapi.yaml');
+ $json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
+ file_put_contents('openapi.json', $json);
+ echo "Converted OpenAPI YAML to JSON.\n";
}
public function init()
{
// Generate APP_KEY if not exists
- if (empty(env('APP_KEY'))) {
+ if (empty(config('app.key'))) {
echo "Generating APP_KEY.\n";
Artisan::call('key:generate');
}
diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php
index cda4ca84f..f0e0e7fa0 100644
--- a/app/Console/Commands/Emails.php
+++ b/app/Console/Commands/Emails.php
@@ -187,7 +187,7 @@ class Emails extends Command
'team_id' => 0,
]);
}
- $this->mail = (new BackupSuccess($backup, $db))->toMail();
+ // $this->mail = (new BackupSuccess($backup->frequency, $db->name))->toMail();
$this->sendEmail();
break;
// case 'invitation-link':
diff --git a/app/Console/Commands/Horizon.php b/app/Console/Commands/Horizon.php
index 65a142d6e..655729ec9 100644
--- a/app/Console/Commands/Horizon.php
+++ b/app/Console/Commands/Horizon.php
@@ -12,8 +12,8 @@ class Horizon extends Command
public function handle()
{
- if (config('coolify.is_horizon_enabled')) {
- $this->info('Horizon is enabled. Starting.');
+ if (config('constants.horizon.is_horizon_enabled')) {
+ $this->info('[x]: Horizon is enabled. Starting.');
$this->call('horizon');
exit(0);
} else {
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index c802fb116..216262819 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -2,9 +2,9 @@
namespace App\Console\Commands;
-use App\Actions\Server\StopSentinel;
use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus;
+use App\Jobs\CheckHelperImageJob;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\ScheduledDatabaseBackup;
@@ -12,6 +12,7 @@ use App\Models\Server;
use App\Models\StandalonePostgresql;
use App\Models\User;
use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
@@ -25,6 +26,8 @@ class Init extends Command
public function handle()
{
+ $this->optimize();
+
if (isCloud() && ! $this->option('force-cloud')) {
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
@@ -39,7 +42,6 @@ class Init extends Command
}
// Backward compatibility
- // $this->disable_metrics();
$this->replace_slash_in_environment_name();
$this->restore_coolify_db_backup();
$this->update_user_emails();
@@ -53,16 +55,32 @@ class Init extends Command
} else {
$this->cleanup_in_progress_application_deployments();
}
+ echo "[3]: Cleanup Redis keys.\n";
$this->call('cleanup:redis');
+
+ echo "[4]: Cleanup stucked resources.\n";
$this->call('cleanup:stucked-resources');
+ try {
+ $this->pullHelperImage();
+ } catch (\Throwable $e) {
+ //
+ }
+
if (isCloud()) {
- $response = Http::retry(3, 1000)->get(config('constants.services.official'));
- if ($response->successful()) {
- $services = $response->json();
- File::put(base_path('templates/service-templates.json'), json_encode($services));
+ try {
+ $this->pullTemplatesFromCDN();
+ } catch (\Throwable $e) {
+ echo "Could not pull templates from CDN: {$e->getMessage()}\n";
+ }
+ }
+
+ if (! isCloud()) {
+ try {
+ $this->pullTemplatesFromCDN();
+ } catch (\Throwable $e) {
+ echo "Could not pull templates from CDN: {$e->getMessage()}\n";
}
- } else {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
@@ -70,8 +88,8 @@ class Init extends Command
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
$settings = instanceSettings();
- if (! is_null(env('AUTOUPDATE', null))) {
- if (env('AUTOUPDATE') == true) {
+ if (! is_null(config('constants.coolify.autoupdate', null))) {
+ if (config('constants.coolify.autoupdate') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
@@ -80,19 +98,26 @@ class Init extends Command
}
}
- // private function disable_metrics()
- // {
- // if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
- // foreach ($this->servers as $server) {
- // if ($server->settings->is_metrics_enabled === true) {
- // $server->settings->update(['is_metrics_enabled' => false]);
- // }
- // if ($server->isFunctional()) {
- // StopSentinel::dispatch($server)->onQueue('high');
- // }
- // }
- // }
- // }
+ private function pullHelperImage()
+ {
+ CheckHelperImageJob::dispatch();
+ }
+
+ private function pullTemplatesFromCDN()
+ {
+ $response = Http::retry(3, 1000)->get(config('constants.services.official'));
+ if ($response->successful()) {
+ $services = $response->json();
+ File::put(base_path('templates/service-templates.json'), json_encode($services));
+ }
+ }
+
+ private function optimize()
+ {
+ echo "[1]: Optimizing Laravel (caching config, routes, views).\n";
+ Artisan::call('optimize:clear');
+ Artisan::call('optimize');
+ }
private function update_user_emails()
{
@@ -175,7 +200,7 @@ class Init extends Command
private function restore_coolify_db_backup()
{
- if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
+ if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) {
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
@@ -203,19 +228,19 @@ class Init extends Command
private function send_alive_signal()
{
$id = config('app.id');
- $version = config('version');
+ $version = config('constants.coolify.version');
$settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
- echo "Skipping alive as do_not_track is enabled\n";
+ echo "[2]: Skipping sending live signal as do_not_track is enabled\n";
return;
}
try {
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
- echo "I am alive!\n";
+ echo "[2]: Sending live signal!\n";
} catch (\Throwable $e) {
- echo "Error in alive: {$e->getMessage()}\n";
+ echo "[2]: Error in sending live signal: {$e->getMessage()}\n";
}
}
@@ -239,7 +264,7 @@ class Init extends Command
private function replace_slash_in_environment_name()
{
- if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
+ if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) {
$environments = Environment::all();
foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) {
diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/OpenApi.php
index e248aa2c0..6cbcb310c 100644
--- a/app/Console/Commands/OpenApi.php
+++ b/app/Console/Commands/OpenApi.php
@@ -15,7 +15,15 @@ class OpenApi extends Command
{
// Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n";
- $process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
+ // https://github.com/OAI/OpenAPI-Specification/releases
+ $process = Process::run([
+ '/var/www/html/vendor/bin/openapi',
+ 'app',
+ '-o',
+ 'openapi.yaml',
+ '--version',
+ '3.1.0',
+ ]);
$error = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error);
diff --git a/app/Console/Commands/Scheduler.php b/app/Console/Commands/Scheduler.php
index 304cb357d..9ee7b06e6 100644
--- a/app/Console/Commands/Scheduler.php
+++ b/app/Console/Commands/Scheduler.php
@@ -12,8 +12,8 @@ class Scheduler extends Command
public function handle()
{
- if (config('coolify.is_scheduler_enabled')) {
- $this->info('Scheduler is enabled. Starting.');
+ if (config('constants.horizon.is_scheduler_enabled')) {
+ $this->info('[x]: Scheduler is enabled. Starting.');
$this->call('schedule:work');
exit(0);
} else {
diff --git a/app/Console/Commands/ServicesDelete.php b/app/Console/Commands/ServicesDelete.php
index 1e5d5808c..b5a74166a 100644
--- a/app/Console/Commands/ServicesDelete.php
+++ b/app/Console/Commands/ServicesDelete.php
@@ -96,7 +96,7 @@ class ServicesDelete extends Command
if (! $confirmed) {
break;
}
- DeleteResourceJob::dispatch($toDelete)->onQueue('high');
+ DeleteResourceJob::dispatch($toDelete);
}
}
}
@@ -122,7 +122,7 @@ class ServicesDelete extends Command
if (! $confirmed) {
return;
}
- DeleteResourceJob::dispatch($toDelete)->onQueue('high');
+ DeleteResourceJob::dispatch($toDelete);
}
}
}
@@ -148,7 +148,7 @@ class ServicesDelete extends Command
if (! $confirmed) {
return;
}
- DeleteResourceJob::dispatch($toDelete)->onQueue('high');
+ DeleteResourceJob::dispatch($toDelete);
}
}
}
diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php
index 1559e5f6d..b45707c5c 100644
--- a/app/Console/Commands/ServicesGenerate.php
+++ b/app/Console/Commands/ServicesGenerate.php
@@ -20,7 +20,10 @@ class ServicesGenerate extends Command
public function handle(): int
{
- $serviceTemplatesJson = collect(glob(base_path('templates/compose/*.yaml')))
+ $serviceTemplatesJson = collect(array_merge(
+ glob(base_path('templates/compose/*.yaml')),
+ glob(base_path('templates/compose/*.yml'))
+ ))
->mapWithKeys(function ($file): array {
$file = basename($file);
$parsed = $this->processFile($file);
@@ -68,7 +71,7 @@ class ServicesGenerate extends Command
'slogan' => $data->get('slogan', str($file)->headline()),
'compose' => $compose,
'tags' => $tags,
- 'logo' => $data->get('logo', 'svgs/coolify.png'),
+ 'logo' => $data->get('logo', 'svgs/default.webp'),
'minversion' => $data->get('minversion', '0.0.0'),
];
diff --git a/app/Console/Commands/SyncBunny.php b/app/Console/Commands/SyncBunny.php
index 228467f88..df1903828 100644
--- a/app/Console/Commands/SyncBunny.php
+++ b/app/Console/Commands/SyncBunny.php
@@ -57,7 +57,7 @@ class SyncBunny extends Command
PendingRequest::macro('storage', function ($fileName) use ($that) {
$headers = [
- 'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
+ 'AccessKey' => config('constants.bunny.storage_api_key'),
'Accept' => 'application/json',
'Content-Type' => 'application/octet-stream',
];
@@ -69,7 +69,7 @@ class SyncBunny extends Command
});
PendingRequest::macro('purge', function ($url) use ($that) {
$headers = [
- 'AccessKey' => env('BUNNY_API_KEY'),
+ 'AccessKey' => config('constants.bunny.api_key'),
'Accept' => 'application/json',
];
$that->info('Purging: '.$url);
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 3e3ae0834..19d22ae21 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -28,85 +28,100 @@ class Kernel extends ConsoleKernel
{
private $allServers;
+ private Schedule $scheduleInstance;
+
private InstanceSettings $settings;
+ private string $updateCheckFrequency;
+
+ private string $instanceTimezone;
+
protected function schedule(Schedule $schedule): void
{
+ $this->scheduleInstance = $schedule;
$this->allServers = Server::where('ip', '!=', '1.2.3.4');
$this->settings = instanceSettings();
+ $this->updateCheckFrequency = $this->settings->update_check_frequency ?: '0 * * * *';
- $schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
+ $this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone');
+
+ if (validate_timezone($this->instanceTimezone) === false) {
+ $this->instanceTimezone = config('app.timezone');
+ }
+
+ // $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
if (isDev()) {
// Instance Jobs
- $schedule->command('horizon:snapshot')->everyMinute();
- $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
- $schedule->job(new CheckHelperImageJob)->everyFiveMinutes()->onOneServer();
+ $this->scheduleInstance->command('horizon:snapshot')->everyMinute();
+ $this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
+ $this->scheduleInstance->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer();
// Server Jobs
- $this->checkResources($schedule);
+ $this->checkResources();
- $this->checkScheduledBackups($schedule);
- $this->checkScheduledTasks($schedule);
+ $this->checkScheduledBackups();
+ $this->checkScheduledTasks();
- $schedule->command('uploads:clear')->everyTwoMinutes();
+ $this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
} else {
// Instance Jobs
- $schedule->command('horizon:snapshot')->everyFiveMinutes();
- $schedule->command('cleanup:unreachable-servers')->daily()->onOneServer();
- $schedule->job(new PullTemplatesFromCDN)->cron($this->settings->update_check_frequency)->timezone($this->settings->instance_timezone)->onOneServer();
- $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
- $this->scheduleUpdates($schedule);
+ $this->scheduleInstance->command('horizon:snapshot')->everyFiveMinutes();
+ $this->scheduleInstance->command('cleanup:unreachable-servers')->daily()->onOneServer();
+
+ $this->scheduleInstance->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
+
+ $this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
+ $this->scheduleUpdates();
// Server Jobs
- $this->checkResources($schedule);
+ $this->checkResources();
- $this->pullImages($schedule);
+ $this->pullImages();
- $this->checkScheduledBackups($schedule);
- $this->checkScheduledTasks($schedule);
+ $this->checkScheduledBackups();
+ $this->checkScheduledTasks();
- $schedule->command('cleanup:database --yes')->daily();
- $schedule->command('uploads:clear')->everyTwoMinutes();
+ $this->scheduleInstance->command('cleanup:database --yes')->daily();
+ $this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
}
}
- private function pullImages($schedule): void
+ private function pullImages(): void
{
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
foreach ($servers as $server) {
if ($server->isSentinelEnabled()) {
- $schedule->job(function () use ($server) {
+ $this->scheduleInstance->job(function () use ($server) {
CheckAndStartSentinelJob::dispatch($server);
- })->cron($this->settings->update_check_frequency)->timezone($this->settings->instance_timezone)->onOneServer();
+ })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
}
}
- $schedule->job(new CheckHelperImageJob)
- ->cron($this->settings->update_check_frequency)
- ->timezone($this->settings->instance_timezone)
+ $this->scheduleInstance->job(new CheckHelperImageJob)
+ ->cron($this->updateCheckFrequency)
+ ->timezone($this->instanceTimezone)
->onOneServer();
}
- private function scheduleUpdates($schedule): void
+ private function scheduleUpdates(): void
{
- $updateCheckFrequency = $this->settings->update_check_frequency;
- $schedule->job(new CheckForUpdatesJob)
- ->cron($updateCheckFrequency)
- ->timezone($this->settings->instance_timezone)
+ $this->scheduleInstance->job(new CheckForUpdatesJob)
+ ->cron($this->updateCheckFrequency)
+ ->timezone($this->instanceTimezone)
->onOneServer();
if ($this->settings->is_auto_update_enabled) {
$autoUpdateFrequency = $this->settings->auto_update_frequency;
- $schedule->job(new UpdateCoolifyJob)
+ $this->scheduleInstance->job(new UpdateCoolifyJob)
->cron($autoUpdateFrequency)
- ->timezone($this->settings->instance_timezone)
+ ->timezone($this->instanceTimezone)
->onOneServer();
}
}
- private function checkResources($schedule): void
+ private function checkResources(): void
{
if (isCloud()) {
$servers = $this->allServers->whereHas('team.subscription')->get();
@@ -115,40 +130,46 @@ class Kernel extends ConsoleKernel
} else {
$servers = $this->allServers->get();
}
- // $schedule->job(new \App\Jobs\ResourcesCheck)->everyMinute()->onOneServer();
foreach ($servers as $server) {
- $serverTimezone = $server->settings->server_timezone;
+ $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
// Sentinel check
$lastSentinelUpdate = $server->sentinel_updated_at;
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
// Check container status every minute if Sentinel does not activated
- $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
- // $schedule->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer();
+ if (validate_timezone($serverTimezone) === false) {
+ $serverTimezone = config('app.timezone');
+ }
+ if (isCloud()) {
+ $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer();
+ } else {
+ $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer();
+ }
+ // $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer();
// Check storage usage every 10 minutes if Sentinel does not activated
- $schedule->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer();
+ $this->scheduleInstance->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer();
}
if ($server->settings->force_docker_cleanup) {
- $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
+ $this->scheduleInstance->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
} else {
- $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
+ $this->scheduleInstance->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
}
// Cleanup multiplexed connections every hour
- $schedule->job(new ServerCleanupMux($server))->hourly()->onOneServer();
+ // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
// Temporary solution until we have better memory management for Sentinel
if ($server->isSentinelEnabled()) {
- $schedule->job(function () use ($server) {
+ $this->scheduleInstance->job(function () use ($server) {
$server->restartContainer('coolify-sentinel');
})->daily()->onOneServer();
}
}
}
- private function checkScheduledBackups($schedule): void
+ private function checkScheduledBackups(): void
{
$scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get();
if ($scheduled_backups->isEmpty()) {
@@ -166,18 +187,17 @@ class Kernel extends ConsoleKernel
if (is_null($server)) {
continue;
}
- $serverTimezone = $server->settings->server_timezone;
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
- $schedule->job(new DatabaseBackupJob(
+ $this->scheduleInstance->job(new DatabaseBackupJob(
backup: $scheduled_backup
- ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
+ ))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->onOneServer();
}
}
- private function checkScheduledTasks($schedule): void
+ private function checkScheduledTasks(): void
{
$scheduled_tasks = ScheduledTask::where('enabled', true)->get();
if ($scheduled_tasks->isEmpty()) {
@@ -207,14 +227,13 @@ class Kernel extends ConsoleKernel
if (! $server) {
continue;
}
- $serverTimezone = $server->settings->server_timezone ?: config('app.timezone');
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}
- $schedule->job(new ScheduledTaskJob(
+ $this->scheduleInstance->job(new ScheduledTaskJob(
task: $scheduled_task
- ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
+ ))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer();
}
}
diff --git a/app/Events/DatabaseProxyStopped.php b/app/Events/DatabaseProxyStopped.php
index b457dc6a0..96b35a5ca 100644
--- a/app/Events/DatabaseProxyStopped.php
+++ b/app/Events/DatabaseProxyStopped.php
@@ -18,7 +18,7 @@ class DatabaseProxyStopped implements ShouldBroadcast
public function __construct($teamId = null)
{
if (is_null($teamId)) {
- $teamId = Auth::user()->currentTeam()->id ?? null;
+ $teamId = Auth::user()?->currentTeam()?->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
diff --git a/app/Events/ScheduledTaskDone.php b/app/Events/ScheduledTaskDone.php
new file mode 100644
index 000000000..c8b5547f6
--- /dev/null
+++ b/app/Events/ScheduledTaskDone.php
@@ -0,0 +1,34 @@
+user()->currentTeam()->id ?? null;
+ }
+ if (is_null($teamId)) {
+ throw new \Exception('Team id is null');
+ }
+ $this->teamId = $teamId;
+ }
+
+ public function broadcastOn(): array
+ {
+ return [
+ new PrivateChannel("team.{$this->teamId}"),
+ ];
+ }
+}
diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php
index 1a2146799..8da476b9e 100644
--- a/app/Helpers/SshMultiplexingHelper.php
+++ b/app/Helpers/SshMultiplexingHelper.php
@@ -21,17 +21,14 @@ class SshMultiplexingHelper
];
}
- public static function ensureMultiplexedConnection(Server $server)
+ public static function ensureMultiplexedConnection(Server $server): bool
{
if (! self::isMultiplexingEnabled()) {
- return;
+ return false;
}
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
- $sshKeyLocation = $sshConfig['sshKeyLocation'];
-
- self::validateSshKey($sshKeyLocation);
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -41,16 +38,17 @@ class SshMultiplexingHelper
$process = Process::run($checkCommand);
if ($process->exitCode() !== 0) {
- self::establishNewMultiplexedConnection($server);
+ return self::establishNewMultiplexedConnection($server);
}
+
+ return true;
}
- public static function establishNewMultiplexedConnection(Server $server)
+ public static function establishNewMultiplexedConnection(Server $server): bool
{
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
-
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
@@ -60,15 +58,14 @@ class SshMultiplexingHelper
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
-
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
$establishCommand .= "{$server->user}@{$server->ip}";
-
$establishProcess = Process::run($establishCommand);
-
if ($establishProcess->exitCode() !== 0) {
- throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
+ return false;
}
+
+ return true;
}
public static function removeMuxFile(Server $server)
@@ -97,9 +94,8 @@ class SshMultiplexingHelper
if ($server->isIpv6()) {
$scp_command .= '-6 ';
}
- if (self::isMultiplexingEnabled()) {
+ if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
- self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -120,6 +116,9 @@ class SshMultiplexingHelper
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
+
+ self::validateSshKey($server->privateKey);
+
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
@@ -127,9 +126,8 @@ class SshMultiplexingHelper
$ssh_command = "timeout $timeout ssh ";
- if (self::isMultiplexingEnabled()) {
+ if (self::isMultiplexingEnabled() && self::ensureMultiplexedConnection($server)) {
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
- self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
@@ -151,16 +149,17 @@ class SshMultiplexingHelper
private static function isMultiplexingEnabled(): bool
{
- return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
+ return config('constants.ssh.mux_enabled') && ! config('constants.coolify.is_windows_docker_desktop');
}
- private static function validateSshKey(string $sshKeyLocation): void
+ private static function validateSshKey(PrivateKey $privateKey): void
{
- $checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
+ $keyLocation = $privateKey->getKeyLocation();
+ $checkKeyCommand = "ls $keyLocation 2>/dev/null";
$keyCheckProcess = Process::run($checkKeyCommand);
if ($keyCheckProcess->exitCode() !== 0) {
- throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
+ $privateKey->storeInFileSystem();
}
}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index f0eeb56d8..c69640970 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -636,7 +636,7 @@ class ApplicationsController extends Controller
private function create_application(Request $request, $type)
{
- $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image'];
+ $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -676,6 +676,27 @@ class ApplicationsController extends Controller
$githubAppUuid = $request->github_app_uuid;
$useBuildServer = $request->use_build_server;
$isStatic = $request->is_static;
+ $customNginxConfiguration = $request->custom_nginx_configuration;
+
+ if (! is_null($customNginxConfiguration)) {
+ if (! isBase64Encoded($customNginxConfiguration)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $customNginxConfiguration = base64_decode($customNginxConfiguration);
+ if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ }
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
@@ -1203,7 +1224,7 @@ class ApplicationsController extends Controller
$service->name = "service-$service->uuid";
$service->parse(isNew: true);
if ($instantDeploy) {
- StartService::dispatch($service)->onQueue('high');
+ StartService::dispatch($service);
}
return response()->json(serializeApiResponse([
@@ -1358,7 +1379,7 @@ class ApplicationsController extends Controller
deleteVolumes: $request->query->get('delete_volumes', true),
dockerCleanup: $request->query->get('docker_cleanup', true),
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
- )->onQueue('high');
+ );
return response()->json([
'message' => 'Application deletion request queued.',
@@ -1500,7 +1521,7 @@ class ApplicationsController extends Controller
], 404);
}
$server = $application->destination->server;
- $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server'];
+ $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration'];
$validationRules = [
'name' => 'string|max:255',
@@ -1512,6 +1533,7 @@ class ApplicationsController extends Controller
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
+ 'custom_nginx_configuration' => 'string|nullable',
];
$validationRules = array_merge($validationRules, sharedDataApplications());
$validator = customApiValidator($request->all(), $validationRules);
@@ -1530,6 +1552,25 @@ class ApplicationsController extends Controller
}
}
}
+ if ($request->has('custom_nginx_configuration')) {
+ if (! isBase64Encoded($request->custom_nginx_configuration)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $customNginxConfiguration = base64_decode($request->custom_nginx_configuration);
+ if (mb_detect_encoding($customNginxConfiguration, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'custom_nginx_configuration' => 'The custom_nginx_configuration should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ }
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
@@ -2482,7 +2523,7 @@ class ApplicationsController extends Controller
if (! $application) {
return response()->json(['message' => 'Application not found.'], 404);
}
- StopApplication::dispatch($application)->onQueue('high');
+ StopApplication::dispatch($application);
return response()->json(
[
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index eaa542a83..ce658d2a2 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -497,9 +497,9 @@ class DatabasesController extends Controller
$database->update($request->all());
if ($whatToDoWithDatabaseProxy === 'start') {
- StartDatabaseProxy::dispatch($database)->onQueue('high');
+ StartDatabaseProxy::dispatch($database);
} elseif ($whatToDoWithDatabaseProxy === 'stop') {
- StopDatabaseProxy::dispatch($database)->onQueue('high');
+ StopDatabaseProxy::dispatch($database);
}
return response()->json([
@@ -1151,7 +1151,7 @@ class DatabasesController extends Controller
}
$database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) {
- StartDatabase::dispatch($database)->onQueue('high');
+ StartDatabase::dispatch($database);
}
$database->refresh();
$payload = [
@@ -1206,7 +1206,7 @@ class DatabasesController extends Controller
}
$database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) {
- StartDatabase::dispatch($database)->onQueue('high');
+ StartDatabase::dispatch($database);
}
$database->refresh();
@@ -1264,7 +1264,7 @@ class DatabasesController extends Controller
}
$database = create_standalone_mysql($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) {
- StartDatabase::dispatch($database)->onQueue('high');
+ StartDatabase::dispatch($database);
}
$database->refresh();
@@ -1320,7 +1320,7 @@ class DatabasesController extends Controller
}
$database = create_standalone_redis($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) {
- StartDatabase::dispatch($database)->onQueue('high');
+ StartDatabase::dispatch($database);
}
$database->refresh();
@@ -1357,7 +1357,7 @@ class DatabasesController extends Controller
removeUnnecessaryFieldsFromRequest($request);
$database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) {
- StartDatabase::dispatch($database)->onQueue('high');
+ StartDatabase::dispatch($database);
}
return response()->json(serializeApiResponse([
@@ -1406,7 +1406,7 @@ class DatabasesController extends Controller
}
$database = create_standalone_keydb($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) {
- StartDatabase::dispatch($database)->onQueue('high');
+ StartDatabase::dispatch($database);
}
$database->refresh();
@@ -1442,7 +1442,7 @@ class DatabasesController extends Controller
removeUnnecessaryFieldsFromRequest($request);
$database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) {
- StartDatabase::dispatch($database)->onQueue('high');
+ StartDatabase::dispatch($database);
}
$database->refresh();
@@ -1500,7 +1500,7 @@ class DatabasesController extends Controller
}
$database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all());
if ($instantDeploy) {
- StartDatabase::dispatch($database)->onQueue('high');
+ StartDatabase::dispatch($database);
}
$database->refresh();
@@ -1593,7 +1593,7 @@ class DatabasesController extends Controller
deleteVolumes: $request->query->get('delete_volumes', true),
dockerCleanup: $request->query->get('docker_cleanup', true),
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
- )->onQueue('high');
+ );
return response()->json([
'message' => 'Database deletion request queued.',
@@ -1666,7 +1666,7 @@ class DatabasesController extends Controller
if (str($database->status)->contains('running')) {
return response()->json(['message' => 'Database is already running.'], 400);
}
- StartDatabase::dispatch($database)->onQueue('high');
+ StartDatabase::dispatch($database);
return response()->json(
[
@@ -1742,7 +1742,7 @@ class DatabasesController extends Controller
if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) {
return response()->json(['message' => 'Database is already stopped.'], 400);
}
- StopDatabase::dispatch($database)->onQueue('high');
+ StopDatabase::dispatch($database);
return response()->json(
[
@@ -1815,7 +1815,7 @@ class DatabasesController extends Controller
if (! $database) {
return response()->json(['message' => 'Database not found.'], 404);
}
- RestartDatabase::dispatch($database)->onQueue('high');
+ RestartDatabase::dispatch($database);
return response()->json(
[
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
index 59b199d87..666dc55a5 100644
--- a/app/Http/Controllers/Api/DeployController.php
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -307,7 +307,7 @@ class DeployController extends Controller
break;
default:
// Database resource
- StartDatabase::dispatch($resource)->onQueue('high');
+ StartDatabase::dispatch($resource);
$resource->update([
'started_at' => now(),
]);
diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php
index 062cc04e7..303e6535d 100644
--- a/app/Http/Controllers/Api/OtherController.php
+++ b/app/Http/Controllers/Api/OtherController.php
@@ -37,7 +37,7 @@ class OtherController extends Controller
)]
public function version(Request $request)
{
- return response(config('version'));
+ return response(config('constants.coolify.version'));
}
#[OA\Get(
@@ -147,7 +147,7 @@ class OtherController extends Controller
public function feedback(Request $request)
{
$content = $request->input('content');
- $webhook_url = config('coolify.feedback_discord_webhook');
+ $webhook_url = config('constants.webhooks.feedback_discord_webhook');
if ($webhook_url) {
Http::post($webhook_url, [
'content' => $content,
diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php
index 491179d5d..1d89c82ed 100644
--- a/app/Http/Controllers/Api/ProjectController.php
+++ b/app/Http/Controllers/Api/ProjectController.php
@@ -116,7 +116,7 @@ class ProjectController extends Controller
responses: [
new OA\Response(
response: 200,
- description: 'Project details',
+ description: 'Environment details',
content: new OA\JsonContent(ref: '#/components/schemas/Environment')),
new OA\Response(
response: 401,
@@ -422,7 +422,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
- if ($project->resource_count() > 0) {
+ if (! $project->isEmpty()) {
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
}
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index bb474aed3..b7190ab1e 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -81,15 +81,8 @@ class SecurityController extends Controller
new OA\Response(
response: 200,
description: 'Get all private keys.',
- content: [
- new OA\MediaType(
- mediaType: 'application/json',
- schema: new OA\Schema(
- type: 'array',
- items: new OA\Items(ref: '#/components/schemas/PrivateKey')
- )
- ),
- ]),
+ content: new OA\JsonContent(ref: '#/components/schemas/PrivateKey')
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index 024ef35fa..8c13b1a01 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -426,6 +426,7 @@ class ServersController extends Controller
'private_key_uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'example' => false, 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Instant validate.'],
+ 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'example' => 'traefik', 'description' => 'The proxy type.'],
],
),
),
@@ -461,7 +462,7 @@ class ServersController extends Controller
)]
public function create_server(Request $request)
{
- $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
+ $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -481,6 +482,7 @@ class ServersController extends Controller
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
+ 'proxy_type' => 'string|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -512,6 +514,14 @@ class ServersController extends Controller
if (is_null($request->instant_validate)) {
$request->offsetSet('instant_validate', false);
}
+ if ($request->proxy_type) {
+ $validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
+ return str($proxyType->value)->lower();
+ });
+ if (! $validProxyTypes->contains(str($request->proxy_type)->lower())) {
+ return response()->json(['message' => 'Invalid proxy type.'], 422);
+ }
+ }
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
if (! $privateKey) {
return response()->json(['message' => 'Private key not found.'], 404);
@@ -521,6 +531,8 @@ class ServersController extends Controller
return response()->json(['message' => 'Server with this IP already exists.'], 400);
}
+ $proxyType = $request->proxy_type ? str($request->proxy_type)->upper() : ProxyTypes::TRAEFIK->value;
+
$server = ModelsServer::create([
'name' => $request->name,
'description' => $request->description,
@@ -530,7 +542,7 @@ class ServersController extends Controller
'private_key_id' => $privateKey->id,
'team_id' => $teamId,
'proxy' => [
- 'type' => ProxyTypes::TRAEFIK->value,
+ 'type' => $proxyType,
'status' => ProxyStatus::EXITED->value,
],
]);
@@ -538,7 +550,7 @@ class ServersController extends Controller
'is_build_server' => $request->is_build_server,
]);
if ($request->instant_validate) {
- ValidateServer::dispatch($server)->onQueue('high');
+ ValidateServer::dispatch($server);
}
return response()->json([
@@ -555,6 +567,9 @@ class ServersController extends Controller
['bearerAuth' => []],
],
tags: ['Servers'],
+ parameters: [
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
+ ],
requestBody: new OA\RequestBody(
required: true,
description: 'Server updated.',
@@ -571,6 +586,7 @@ class ServersController extends Controller
'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'],
+ 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'],
],
),
),
@@ -583,8 +599,7 @@ class ServersController extends Controller
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
- type: 'array',
- items: new OA\Items(ref: '#/components/schemas/Server')
+ ref: '#/components/schemas/Server'
)
),
]),
@@ -604,7 +619,7 @@ class ServersController extends Controller
)]
public function update_server(Request $request)
{
- $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
+ $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -624,6 +639,7 @@ class ServersController extends Controller
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
+ 'proxy_type' => 'string|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -644,6 +660,16 @@ class ServersController extends Controller
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
+ if ($request->proxy_type) {
+ $validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
+ return str($proxyType->value)->lower();
+ });
+ if ($validProxyTypes->contains(str($request->proxy_type)->lower())) {
+ $server->changeProxy($request->proxy_type, async: true);
+ } else {
+ return response()->json(['message' => 'Invalid proxy type.'], 422);
+ }
+ }
$server->update($request->only(['name', 'description', 'ip', 'port', 'user']));
if ($request->is_build_server) {
$server->settings()->update([
@@ -651,10 +677,12 @@ class ServersController extends Controller
]);
}
if ($request->instant_validate) {
- ValidateServer::dispatch($server)->onQueue('high');
+ ValidateServer::dispatch($server);
}
- return response()->json(serializeApiResponse($server))->setStatusCode(201);
+ return response()->json([
+ 'uuid' => $server->uuid,
+ ])->setStatusCode(201);
}
#[OA\Delete(
@@ -787,7 +815,7 @@ class ServersController extends Controller
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
- ValidateServer::dispatch($server)->onQueue('high');
+ ValidateServer::dispatch($server);
return response()->json(['message' => 'Validation started.']);
}
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index bdb5612ad..bf90322e2 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -342,7 +342,7 @@ class ServicesController extends Controller
}
$service->parse(isNew: true);
if ($instantDeploy) {
- StartService::dispatch($service)->onQueue('high');
+ StartService::dispatch($service);
}
$domains = $service->applications()->get()->pluck('fqdn')->sort();
$domains = $domains->map(function ($domain) {
@@ -487,7 +487,7 @@ class ServicesController extends Controller
deleteVolumes: $request->query->get('delete_volumes', true),
dockerCleanup: $request->query->get('docker_cleanup', true),
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
- )->onQueue('high');
+ );
return response()->json([
'message' => 'Service deletion request queued.',
@@ -1076,7 +1076,7 @@ class ServicesController extends Controller
if (str($service->status())->contains('running')) {
return response()->json(['message' => 'Service is already running.'], 400);
}
- StartService::dispatch($service)->onQueue('high');
+ StartService::dispatch($service);
return response()->json(
[
@@ -1154,7 +1154,7 @@ class ServicesController extends Controller
if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) {
return response()->json(['message' => 'Service is already stopped.'], 400);
}
- StopService::dispatch($service)->onQueue('high');
+ StopService::dispatch($service);
return response()->json(
[
@@ -1229,7 +1229,7 @@ class ServicesController extends Controller
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
- RestartService::dispatch($service)->onQueue('high');
+ RestartService::dispatch($service);
return response()->json(
[
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 718ba5ed4..9f1e4eeb8 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -110,13 +110,19 @@ class Controller extends BaseController
return redirect()->route('login')->with('error', 'Invalid credentials.');
}
- public function accept_invitation()
+ public function acceptInvitation()
{
$resetPassword = request()->query('reset-password');
$invitationUuid = request()->route('uuid');
+
$invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail();
$user = User::whereEmail($invitation->email)->firstOrFail();
+
+ if (Auth::id() !== $user->id) {
+ abort(400, 'You are not allowed to accept this invitation.');
+ }
$invitationValid = $invitation->isValid();
+
if ($invitationValid) {
if ($resetPassword) {
$user->update([
@@ -131,14 +137,12 @@ class Controller extends BaseController
}
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
$invitation->delete();
- if (Auth::id() !== $user->id) {
- return redirect()->route('login');
- }
+
refreshSession($invitation->team);
return redirect()->route('team.index');
} else {
- abort(401);
+ abort(400, 'Invitation expired.');
}
}
diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php
index f56711bad..d8dcc0c3b 100644
--- a/app/Http/Controllers/Webhook/Gitlab.php
+++ b/app/Http/Controllers/Webhook/Gitlab.php
@@ -33,6 +33,7 @@ class Gitlab extends Controller
return;
}
+
$return_payloads = collect([]);
$payload = $request->collect();
$headers = $request->headers->all();
@@ -48,6 +49,15 @@ class Gitlab extends Controller
return response($return_payloads);
}
+ if (empty($x_gitlab_token)) {
+ $return_payloads->push([
+ 'status' => 'failed',
+ 'message' => 'Invalid signature.',
+ ]);
+
+ return response($return_payloads);
+ }
+
if ($x_gitlab_event === 'push') {
$branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'project.path_with_namespace');
diff --git a/app/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php
index 5d297b242..83ba16699 100644
--- a/app/Http/Controllers/Webhook/Stripe.php
+++ b/app/Http/Controllers/Webhook/Stripe.php
@@ -3,23 +3,26 @@
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
-use App\Jobs\ServerLimitCheckJob;
-use App\Jobs\SubscriptionInvoiceFailedJob;
-use App\Jobs\SubscriptionTrialEndedJob;
-use App\Jobs\SubscriptionTrialEndsSoonJob;
-use App\Models\Subscription;
-use App\Models\Team;
+use App\Jobs\StripeProcessJob;
use App\Models\Webhook;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Str;
class Stripe extends Controller
{
+ protected $webhook;
+
public function events(Request $request)
{
try {
+ $webhookSecret = config('subscription.stripe_webhook_secret');
+ $signature = $request->header('Stripe-Signature');
+ $event = \Stripe\Webhook::constructEvent(
+ $request->getContent(),
+ $signature,
+ $webhookSecret
+ );
if (app()->isDownForMaintenance()) {
$epoch = now()->valueOf();
$data = [
@@ -35,276 +38,17 @@ class Stripe extends Controller
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
- return;
+ return response('Webhook received. Cool cool cool cool cool.', 200);
}
- $webhookSecret = config('subscription.stripe_webhook_secret');
- $signature = $request->header('Stripe-Signature');
- $excludedPlans = config('subscription.stripe_excluded_plans');
- $event = \Stripe\Webhook::constructEvent(
- $request->getContent(),
- $signature,
- $webhookSecret
- );
- $webhook = Webhook::create([
+ $this->webhook = Webhook::create([
'type' => 'stripe',
'payload' => $request->getContent(),
]);
- $type = data_get($event, 'type');
- $data = data_get($event, 'data.object');
- switch ($type) {
- case 'radar.early_fraud_warning.created':
- $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
- $id = data_get($data, 'id');
- $charge = data_get($data, 'charge');
- if ($charge) {
- $stripe->refunds->create(['charge' => $charge]);
- }
- $pi = data_get($data, 'payment_intent');
- $piData = $stripe->paymentIntents->retrieve($pi, []);
- $customerId = data_get($piData, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if ($subscription) {
- $subscriptionId = data_get($subscription, 'stripe_subscription_id');
- $stripe->subscriptions->cancel($subscriptionId, []);
- $subscription->update([
- 'stripe_invoice_paid' => false,
- ]);
- send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
- } else {
- send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ StripeProcessJob::dispatch($event);
- return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400);
- }
- break;
- case 'checkout.session.completed':
- $clientReferenceId = data_get($data, 'client_reference_id');
- if (is_null($clientReferenceId)) {
- send_internal_notification('Checkout session completed without client reference id.');
- break;
- }
- $userId = Str::before($clientReferenceId, ':');
- $teamId = Str::after($clientReferenceId, ':');
- $subscriptionId = data_get($data, 'subscription');
- $customerId = data_get($data, 'customer');
- $team = Team::find($teamId);
- $found = $team->members->where('id', $userId)->first();
- if (! $found->isAdmin()) {
- send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
-
- return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400);
- }
- $subscription = Subscription::where('team_id', $teamId)->first();
- if ($subscription) {
- // send_internal_notification('Old subscription activated for team: '.$teamId);
- $subscription->update([
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => true,
- ]);
- } else {
- // send_internal_notification('New subscription for team: '.$teamId);
- Subscription::create([
- 'team_id' => $teamId,
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => true,
- ]);
- }
- break;
- case 'invoice.paid':
- $customerId = data_get($data, 'customer');
- $planId = data_get($data, 'lines.data.0.plan.id');
- if (Str::contains($excludedPlans, $planId)) {
- // send_internal_notification('Subscription excluded.');
- break;
- }
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if ($subscription) {
- $subscription->update([
- 'stripe_invoice_paid' => true,
- ]);
- } else {
- return response("No subscription found for customer: {$customerId}", 400);
- }
- break;
- case 'invoice.payment_failed':
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if (! $subscription) {
- // send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
-
- return response('No subscription found in Coolify.');
- }
- $team = data_get($subscription, 'team');
- if (! $team) {
- // send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
-
- return response('No team found in Coolify.');
- }
- if (! $subscription->stripe_invoice_paid) {
- SubscriptionInvoiceFailedJob::dispatch($team);
- // send_internal_notification('Invoice payment failed: '.$customerId);
- } else {
- // send_internal_notification('Invoice payment failed but already paid: '.$customerId);
- }
- break;
- case 'payment_intent.payment_failed':
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if (! $subscription) {
- // send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
-
- return response('No subscription found in Coolify.');
- }
- if ($subscription->stripe_invoice_paid) {
- // send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
-
- return;
- }
- send_internal_notification('Subscription payment failed for customer: '.$customerId);
- break;
- case 'customer.subscription.created':
- $customerId = data_get($data, 'customer');
- $subscriptionId = data_get($data, 'id');
- $teamId = data_get($data, 'metadata.team_id');
- $userId = data_get($data, 'metadata.user_id');
- if (! $teamId || ! $userId) {
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if ($subscription) {
- return response("Subscription already exists for customer: {$customerId}", 200);
- }
-
- return response('No team id or user id found', 400);
- }
- $team = Team::find($teamId);
- $found = $team->members->where('id', $userId)->first();
- if (! $found->isAdmin()) {
- send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
-
- return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400);
- }
- $subscription = Subscription::where('team_id', $teamId)->first();
- if ($subscription) {
- return response("Subscription already exists for team: {$teamId}", 200);
- } else {
- Subscription::create([
- 'team_id' => $teamId,
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => false,
- ]);
-
- return response('Subscription created');
- }
- case 'customer.subscription.updated':
- $teamId = data_get($data, 'metadata.team_id');
- $userId = data_get($data, 'metadata.user_id');
- $customerId = data_get($data, 'customer');
- $status = data_get($data, 'status');
- $subscriptionId = data_get($data, 'items.data.0.subscription');
- $planId = data_get($data, 'items.data.0.plan.id');
- if (Str::contains($excludedPlans, $planId)) {
- // send_internal_notification('Subscription excluded.');
- break;
- }
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if (! $subscription) {
- if ($status === 'incomplete_expired') {
- return response('Subscription incomplete expired', 200);
- }
- if ($teamId) {
- $subscription = Subscription::create([
- 'team_id' => $teamId,
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => false,
- ]);
- } else {
- return response('No subscription and team id found', 400);
- }
- }
- $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
- $feedback = data_get($data, 'cancellation_details.feedback');
- $comment = data_get($data, 'cancellation_details.comment');
- $lookup_key = data_get($data, 'items.data.0.price.lookup_key');
- if (str($lookup_key)->contains('dynamic')) {
- $quantity = data_get($data, 'items.data.0.quantity', 2);
- $team = data_get($subscription, 'team');
- if ($team) {
- $team->update([
- 'custom_server_limit' => $quantity,
- ]);
- }
- ServerLimitCheckJob::dispatch($team);
- }
- $subscription->update([
- 'stripe_feedback' => $feedback,
- 'stripe_comment' => $comment,
- 'stripe_plan_id' => $planId,
- 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
- ]);
- if ($status === 'paused' || $status === 'incomplete_expired') {
- $subscription->update([
- 'stripe_invoice_paid' => false,
- ]);
- }
- if ($feedback) {
- $reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
- if ($comment) {
- $reason .= ' with comment: \''.$comment."'";
- }
- }
- break;
- case 'customer.subscription.deleted':
- // End subscription
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
- $team = data_get($subscription, 'team');
- if ($team) {
- $team->trialEnded();
- }
- $subscription->update([
- 'stripe_subscription_id' => null,
- 'stripe_plan_id' => null,
- 'stripe_cancel_at_period_end' => false,
- 'stripe_invoice_paid' => false,
- 'stripe_trial_already_ended' => false,
- ]);
- // send_internal_notification('customer.subscription.deleted for customer: '.$customerId);
- break;
- case 'customer.subscription.trial_will_end':
- // Not used for now
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
- $team = data_get($subscription, 'team');
- if (! $team) {
- return response('No team found for subscription: '.$subscription->id, 400);
- }
- SubscriptionTrialEndsSoonJob::dispatch($team);
- break;
- case 'customer.subscription.paused':
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
- $team = data_get($subscription, 'team');
- if (! $team) {
- return response('No team found for subscription: '.$subscription->id, 400);
- }
- $team->trialEnded();
- $subscription->update([
- 'stripe_trial_already_ended' => true,
- 'stripe_invoice_paid' => false,
- ]);
- SubscriptionTrialEndedJob::dispatch($team);
- // send_internal_notification('Subscription paused for customer: '.$customerId);
- break;
- default:
- // Unhandled event type
- }
+ return response('Webhook received. Cool cool cool cool cool.', 200);
} catch (Exception $e) {
- if ($type !== 'payment_intent.payment_failed') {
- send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage());
- }
- $webhook->update([
+ $this->webhook->update([
'status' => 'failed',
'failure_reason' => $e->getMessage(),
]);
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 5ceed332a..6a66bb56d 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -166,6 +166,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(int $application_deployment_queue_id)
{
+ $this->onQueue('high');
+
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
@@ -225,6 +227,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
+ public function tags(): array
+ {
+ return ['server:'.gethostname()];
+ }
+
public function handle(): void
{
$this->application_deployment_queue->update([
@@ -344,8 +351,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function post_deployment()
{
if ($this->server->isProxyShouldRun()) {
- GetContainersStatus::dispatch($this->server)->onQueue('high');
- // dispatch(new ContainerStatusJob($this->server));
+ GetContainersStatus::dispatch($this->server);
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) {
@@ -457,7 +463,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables();
if (! is_null($this->env_filename)) {
- $services = collect($composeFile['services']);
+ $services = collect(data_get($composeFile, 'services', []));
$services = $services->map(function ($service, $name) {
$service['env_file'] = [$this->env_filename];
@@ -1318,7 +1324,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function prepare_builder_image()
{
$settings = instanceSettings();
- $helperImage = config('coolify.helper_image');
+ $helperImage = config('constants.coolify.helper_image');
$helperImage = "{$helperImage}:{$settings->helper_version}";
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
@@ -1990,22 +1996,11 @@ COPY . .
RUN rm -f /usr/share/nginx/html/nginx.conf
RUN rm -f /usr/share/nginx/html/Dockerfile
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
- $nginx_config = base64_encode('server {
- listen 80;
- listen [::]:80;
- server_name localhost;
-
- location / {
- root /usr/share/nginx/html;
- index index.html;
- try_files $uri $uri.html $uri/index.html $uri/ /index.html =404;
+ if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
+ $nginx_config = base64_encode($this->application->custom_nginx_configuration);
+ } else {
+ $nginx_config = base64_encode(defaultNginxConfiguration());
}
-
- error_page 500 502 503 504 /50x.html;
- location = /50x.html {
- root /usr/share/nginx/html;
- }
- }');
} else {
if ($this->application->build_pack === 'nixpacks') {
$this->nixpacks_plan = base64_encode($this->nixpacks_plan);
@@ -2068,23 +2063,11 @@ WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid}
COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
-
- $nginx_config = base64_encode('server {
- listen 80;
- listen [::]:80;
- server_name localhost;
-
- location / {
- root /usr/share/nginx/html;
- index index.html;
- try_files $uri $uri.html $uri/index.html $uri/ /index.html =404;
+ if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
+ $nginx_config = base64_encode($this->application->custom_nginx_configuration);
+ } else {
+ $nginx_config = base64_encode(defaultNginxConfiguration());
}
-
- error_page 500 502 503 504 /50x.html;
- location = /50x.html {
- root /usr/share/nginx/html;
- }
- }');
}
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
$base64_build_command = base64_encode($build_command);
diff --git a/app/Jobs/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php
index 2eefc4dd2..ef8e6efb6 100755
--- a/app/Jobs/ApplicationPullRequestUpdateJob.php
+++ b/app/Jobs/ApplicationPullRequestUpdateJob.php
@@ -25,7 +25,9 @@ class ApplicationPullRequestUpdateJob implements ShouldBeEncrypted, ShouldQueue
public ApplicationPreview $preview,
public ProcessStatus $status,
public ?string $deployment_uuid = null
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
public function handle()
{
diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php
index f2348118a..1d3a345e1 100644
--- a/app/Jobs/CheckForUpdatesJob.php
+++ b/app/Jobs/CheckForUpdatesJob.php
@@ -27,7 +27,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
$versions = $response->json();
$latest_version = data_get($versions, 'coolify.v4.version');
- $current_version = config('version');
+ $current_version = config('constants.coolify.version');
if (version_compare($latest_version, $current_version, '>')) {
// New version available
diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php
index acbe82338..84f14ed02 100644
--- a/app/Jobs/CleanupInstanceStuffsJob.php
+++ b/app/Jobs/CleanupInstanceStuffsJob.php
@@ -3,14 +3,15 @@
namespace App\Jobs;
use App\Models\TeamInvitation;
-use App\Models\Waitlist;
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;
class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue
{
@@ -18,34 +19,21 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho
public function __construct() {}
- // public function uniqueId(): string
- // {
- // return $this->container_name;
- // }
+ public function middleware(): array
+ {
+ return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()];
+ }
public function handle(): void
{
try {
- // $this->cleanup_waitlist();
+ $this->cleanupInvitationLink();
} catch (\Throwable $e) {
- send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
- }
- try {
- $this->cleanup_invitation_link();
- } catch (\Throwable $e) {
- send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
+ Log::error('CleanupInstanceStuffsJob failed with error: '.$e->getMessage());
}
}
- private function cleanup_waitlist()
- {
- $waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get();
- foreach ($waitlist as $item) {
- $item->delete();
- }
- }
-
- private function cleanup_invitation_link()
+ private function cleanupInvitationLink()
{
$invitation = TeamInvitation::all();
foreach ($invitation as $item) {
diff --git a/app/Jobs/CoolifyTask.php b/app/Jobs/CoolifyTask.php
index c3692c30b..49a5ba8dd 100755
--- a/app/Jobs/CoolifyTask.php
+++ b/app/Jobs/CoolifyTask.php
@@ -23,7 +23,10 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue
public bool $ignore_errors,
public $call_event_on_finish,
public $call_event_data,
- ) {}
+ ) {
+
+ $this->onQueue('high');
+ }
/**
* Execute the job.
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index fcfe2fe3d..89674b255 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -60,12 +60,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct($backup)
{
+ $this->onQueue('high');
$this->backup = $backup;
}
public function handle(): void
{
try {
+ $databasesToBackup = null;
+
$this->team = Team::find($this->backup->team_id);
if (! $this->team) {
$this->backup->delete();
@@ -197,8 +200,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
}
-
- if (is_null($databasesToBackup)) {
+ if (blank($databasesToBackup)) {
if (str($databaseType)->contains('postgres')) {
$databasesToBackup = [$this->database->postgres_db];
} elseif (str($databaseType)->contains('mongodb')) {
@@ -319,12 +321,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
'filename' => null,
]);
}
- send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
}
}
} catch (\Throwable $e) {
- send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e;
} finally {
if ($this->team) {
@@ -524,7 +524,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
private function getFullImageName(): string
{
$settings = instanceSettings();
- $helperImage = config('coolify.helper_image');
+ $helperImage = config('constants.coolify.helper_image');
$latestVersion = $settings->helper_version;
return "{$helperImage}:{$latestVersion}";
diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php
index 2442d5b06..8b9228e5f 100644
--- a/app/Jobs/DeleteResourceJob.php
+++ b/app/Jobs/DeleteResourceJob.php
@@ -35,7 +35,9 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public bool $deleteVolumes = true,
public bool $dockerCleanup = true,
public bool $deleteConnectedNetworks = true
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
public function handle()
{
@@ -87,7 +89,6 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
$this->resource?->delete_connected_networks($this->resource->uuid);
}
} catch (\Throwable $e) {
- send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();
diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php
index 900bae99c..80542e03b 100644
--- a/app/Jobs/DockerCleanupJob.php
+++ b/app/Jobs/DockerCleanupJob.php
@@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
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;
@@ -23,6 +24,11 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $usageBefore = null;
+ public function middleware(): array
+ {
+ return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
+ }
+
public function __construct(public Server $server, public bool $manualCleanup = false) {}
public function handle(): void
diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php
index a92e44c6b..b92886d38 100644
--- a/app/Jobs/PullHelperImageJob.php
+++ b/app/Jobs/PullHelperImageJob.php
@@ -16,11 +16,14 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
- public function __construct(public Server $server) {}
+ public function __construct(public Server $server)
+ {
+ $this->onQueue('high');
+ }
public function handle(): void
{
- $helperImage = config('coolify.helper_image');
+ $helperImage = config('constants.coolify.helper_image');
$latest_version = instanceSettings()->helper_version;
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
}
diff --git a/app/Jobs/PullTemplatesFromCDN.php b/app/Jobs/PullTemplatesFromCDN.php
index bde5e6c7a..45c536e06 100644
--- a/app/Jobs/PullTemplatesFromCDN.php
+++ b/app/Jobs/PullTemplatesFromCDN.php
@@ -17,7 +17,10 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
public $timeout = 10;
- public function __construct() {}
+ public function __construct()
+ {
+ $this->onQueue('high');
+ }
public function handle(): void
{
diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php
index 9822ca071..24f8d1e6b 100644
--- a/app/Jobs/PushServerUpdateJob.php
+++ b/app/Jobs/PushServerUpdateJob.php
@@ -360,7 +360,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
private function checkLogDrainContainer()
{
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
- StartLogDrain::dispatch($this->server)->onQueue('high');
+ StartLogDrain::dispatch($this->server);
}
}
}
diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php
index 827641e8c..00575e187 100644
--- a/app/Jobs/ScheduledTaskJob.php
+++ b/app/Jobs/ScheduledTaskJob.php
@@ -2,6 +2,7 @@
namespace App\Jobs;
+use App\Events\ScheduledTaskDone;
use App\Models\Application;
use App\Models\ScheduledTask;
use App\Models\ScheduledTaskExecution;
@@ -19,7 +20,7 @@ class ScheduledTaskJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public ?Team $team = null;
+ public Team $team;
public Server $server;
@@ -39,6 +40,8 @@ class ScheduledTaskJob implements ShouldQueue
public function __construct($task)
{
+ $this->onQueue('high');
+
$this->task = $task;
if ($service = $task->service()->first()) {
$this->resource = $service;
@@ -47,7 +50,7 @@ class ScheduledTaskJob implements ShouldQueue
} else {
throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
}
- $this->team = Team::find($task->team_id);
+ $this->team = Team::findOrFail($task->team_id);
$this->server_timezone = $this->getServerTimezone();
}
@@ -125,6 +128,7 @@ class ScheduledTaskJob implements ShouldQueue
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
} finally {
+ ScheduledTaskDone::dispatch($this->team->id);
}
}
}
diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php
index 5b406f50f..99aeaeea2 100644
--- a/app/Jobs/SendMessageToDiscordJob.php
+++ b/app/Jobs/SendMessageToDiscordJob.php
@@ -32,7 +32,9 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(
public DiscordMessage $message,
public string $webhookUrl
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
/**
* Execute the job.
diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php
index bf52b782f..85f4fc934 100644
--- a/app/Jobs/SendMessageToTelegramJob.php
+++ b/app/Jobs/SendMessageToTelegramJob.php
@@ -33,7 +33,9 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
public string $token,
public string $chatId,
public ?string $topicId = null,
- ) {}
+ ) {
+ $this->onQueue('high');
+ }
/**
* Execute the job.
@@ -70,7 +72,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
}
$response = Http::post($url, $payload);
if ($response->failed()) {
- throw new \Exception('Telegram notification failed with '.$response->status().' status code.'.$response->body());
+ throw new \RuntimeException('Telegram notification failed with '.$response->status().' status code.'.$response->body());
}
}
}
diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php
index c584f493d..49d8dfe08 100644
--- a/app/Jobs/ServerCheckJob.php
+++ b/app/Jobs/ServerCheckJob.php
@@ -28,10 +28,15 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array
{
- return [(new WithoutOverlapping($this->server->id))->dontRelease()];
+ return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
- public function __construct(public Server $server) {}
+ public function __construct(public Server $server)
+ {
+ if (isDev()) {
+ $this->handle();
+ }
+ }
public function handle()
{
@@ -94,10 +99,10 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
if ($foundLogDrainContainer) {
$status = data_get($foundLogDrainContainer, 'State.Status');
if ($status !== 'running') {
- StartLogDrain::dispatch($this->server)->onQueue('high');
+ StartLogDrain::dispatch($this->server);
}
} else {
- StartLogDrain::dispatch($this->server)->onQueue('high');
+ StartLogDrain::dispatch($this->server);
}
}
}
diff --git a/app/Jobs/ServerCheckNewJob.php b/app/Jobs/ServerCheckNewJob.php
index 9ce52759c..3e8e60a31 100644
--- a/app/Jobs/ServerCheckNewJob.php
+++ b/app/Jobs/ServerCheckNewJob.php
@@ -2,6 +2,7 @@
namespace App\Jobs;
+use App\Actions\Server\ResourcesCheck;
use App\Actions\Server\ServerCheck;
use App\Models\Server;
use Illuminate\Bus\Queueable;
@@ -25,6 +26,7 @@ class ServerCheckNewJob implements ShouldBeEncrypted, ShouldQueue
{
try {
ServerCheck::run($this->server);
+ ResourcesCheck::dispatch($this->server);
} catch (\Throwable $e) {
return handleError($e);
}
diff --git a/app/Jobs/ServerFilesFromServerJob.php b/app/Jobs/ServerFilesFromServerJob.php
index 769dfc004..58455df2f 100644
--- a/app/Jobs/ServerFilesFromServerJob.php
+++ b/app/Jobs/ServerFilesFromServerJob.php
@@ -16,7 +16,10 @@ class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) {}
+ public function __construct(public ServiceApplication|ServiceDatabase|Application $resource)
+ {
+ $this->onQueue('high');
+ }
public function handle()
{
diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php
index 0723ffcee..9a8d86be1 100644
--- a/app/Jobs/ServerStorageCheckJob.php
+++ b/app/Jobs/ServerStorageCheckJob.php
@@ -25,7 +25,7 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
return isDev() ? 1 : 3;
}
- public function __construct(public Server $server, public ?int $percentage = null) {}
+ public function __construct(public Server $server, public int|string|null $percentage = null) {}
public function handle()
{
diff --git a/app/Jobs/ServerStorageSaveJob.php b/app/Jobs/ServerStorageSaveJob.php
index 526cd5375..17a293f94 100644
--- a/app/Jobs/ServerStorageSaveJob.php
+++ b/app/Jobs/ServerStorageSaveJob.php
@@ -14,7 +14,10 @@ class ServerStorageSaveJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(public LocalFileVolume $localFileVolume) {}
+ public function __construct(public LocalFileVolume $localFileVolume)
+ {
+ $this->onQueue('high');
+ }
public function handle()
{
diff --git a/app/Jobs/StripeProcessJob.php b/app/Jobs/StripeProcessJob.php
new file mode 100644
index 000000000..00c9b6d18
--- /dev/null
+++ b/app/Jobs/StripeProcessJob.php
@@ -0,0 +1,246 @@
+onQueue('high');
+ }
+
+ public function handle(): void
+ {
+ try {
+ $excludedPlans = config('subscription.stripe_excluded_plans');
+
+ $type = data_get($this->event, 'type');
+ $this->type = $type;
+ $data = data_get($this->event, 'data.object');
+ switch ($type) {
+ case 'radar.early_fraud_warning.created':
+ $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
+ $id = data_get($data, 'id');
+ $charge = data_get($data, 'charge');
+ if ($charge) {
+ $stripe->refunds->create(['charge' => $charge]);
+ }
+ $pi = data_get($data, 'payment_intent');
+ $piData = $stripe->paymentIntents->retrieve($pi, []);
+ $customerId = data_get($piData, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ $subscriptionId = data_get($subscription, 'stripe_subscription_id');
+ $stripe->subscriptions->cancel($subscriptionId, []);
+ $subscription->update([
+ 'stripe_invoice_paid' => false,
+ ]);
+ send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ } else {
+ send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ throw new \RuntimeException("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ }
+ break;
+ case 'checkout.session.completed':
+ $clientReferenceId = data_get($data, 'client_reference_id');
+ if (is_null($clientReferenceId)) {
+ send_internal_notification('Checkout session completed without client reference id.');
+ break;
+ }
+ $userId = Str::before($clientReferenceId, ':');
+ $teamId = Str::after($clientReferenceId, ':');
+ $subscriptionId = data_get($data, 'subscription');
+ $customerId = data_get($data, 'customer');
+ $team = Team::find($teamId);
+ $found = $team->members->where('id', $userId)->first();
+ if (! $found->isAdmin()) {
+ send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
+ throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
+ }
+ $subscription = Subscription::where('team_id', $teamId)->first();
+ if ($subscription) {
+ send_internal_notification('Old subscription activated for team: '.$teamId);
+ $subscription->update([
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => true,
+ ]);
+ } else {
+ send_internal_notification('New subscription for team: '.$teamId);
+ Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => true,
+ ]);
+ }
+ break;
+ case 'invoice.paid':
+ $customerId = data_get($data, 'customer');
+ $planId = data_get($data, 'lines.data.0.plan.id');
+ if (Str::contains($excludedPlans, $planId)) {
+ send_internal_notification('Subscription excluded.');
+ break;
+ }
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ $subscription->update([
+ 'stripe_invoice_paid' => true,
+ ]);
+ } else {
+ throw new \RuntimeException("No subscription found for customer: {$customerId}");
+ }
+ break;
+ case 'invoice.payment_failed':
+ $customerId = data_get($data, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if (! $subscription) {
+ send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
+ throw new \RuntimeException("No subscription found for customer: {$customerId}");
+ }
+ $team = data_get($subscription, 'team');
+ if (! $team) {
+ send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
+ throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
+ }
+ if (! $subscription->stripe_invoice_paid) {
+ SubscriptionInvoiceFailedJob::dispatch($team);
+ send_internal_notification('Invoice payment failed: '.$customerId);
+ } else {
+ send_internal_notification('Invoice payment failed but already paid: '.$customerId);
+ }
+ break;
+ case 'payment_intent.payment_failed':
+ $customerId = data_get($data, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if (! $subscription) {
+ send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
+ throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}");
+ }
+ if ($subscription->stripe_invoice_paid) {
+ send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
+
+ return;
+ }
+ send_internal_notification('Subscription payment failed for customer: '.$customerId);
+ break;
+ case 'customer.subscription.created':
+ $customerId = data_get($data, 'customer');
+ $subscriptionId = data_get($data, 'id');
+ $teamId = data_get($data, 'metadata.team_id');
+ $userId = data_get($data, 'metadata.user_id');
+ if (! $teamId || ! $userId) {
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ throw new \RuntimeException("Subscription already exists for customer: {$customerId}");
+ }
+ throw new \RuntimeException('No team id or user id found');
+ }
+ $team = Team::find($teamId);
+ $found = $team->members->where('id', $userId)->first();
+ if (! $found->isAdmin()) {
+ send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
+ throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
+ }
+ $subscription = Subscription::where('team_id', $teamId)->first();
+ if ($subscription) {
+ send_internal_notification("Subscription already exists for team: {$teamId}");
+ throw new \RuntimeException("Subscription already exists for team: {$teamId}");
+ } else {
+ Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => false,
+ ]);
+ }
+ case 'customer.subscription.updated':
+ $teamId = data_get($data, 'metadata.team_id');
+ $userId = data_get($data, 'metadata.user_id');
+ $customerId = data_get($data, 'customer');
+ $status = data_get($data, 'status');
+ $subscriptionId = data_get($data, 'items.data.0.subscription');
+ $planId = data_get($data, 'items.data.0.plan.id');
+ if (Str::contains($excludedPlans, $planId)) {
+ send_internal_notification('Subscription excluded.');
+ break;
+ }
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if (! $subscription) {
+ if ($status === 'incomplete_expired') {
+ send_internal_notification('Subscription incomplete expired');
+ throw new \RuntimeException('Subscription incomplete expired');
+ }
+ if ($teamId) {
+ $subscription = Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => false,
+ ]);
+ } else {
+ send_internal_notification('No subscription and team id found');
+ throw new \RuntimeException('No subscription and team id found');
+ }
+ }
+ $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
+ $feedback = data_get($data, 'cancellation_details.feedback');
+ $comment = data_get($data, 'cancellation_details.comment');
+ $lookup_key = data_get($data, 'items.data.0.price.lookup_key');
+ if (str($lookup_key)->contains('dynamic')) {
+ $quantity = data_get($data, 'items.data.0.quantity', 2);
+ $team = data_get($subscription, 'team');
+ if ($team) {
+ $team->update([
+ 'custom_server_limit' => $quantity,
+ ]);
+ }
+ ServerLimitCheckJob::dispatch($team);
+ }
+ $subscription->update([
+ 'stripe_feedback' => $feedback,
+ 'stripe_comment' => $comment,
+ 'stripe_plan_id' => $planId,
+ 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
+ ]);
+ if ($status === 'paused' || $status === 'incomplete_expired') {
+ $subscription->update([
+ 'stripe_invoice_paid' => false,
+ ]);
+ }
+ if ($feedback) {
+ $reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
+ if ($comment) {
+ $reason .= ' with comment: \''.$comment."'";
+ }
+ }
+ break;
+ case 'customer.subscription.deleted':
+ // End subscription
+ $customerId = data_get($data, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
+ $team = data_get($subscription, 'team');
+ $team?->subscriptionEnded();
+ break;
+ default:
+ throw new \RuntimeException("Unhandled event type: {$type}");
+ }
+ } catch (\Exception $e) {
+ send_internal_notification('StripeProcessJob error: '.$e->getMessage());
+ }
+ }
+}
diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php
index aabeecef5..dc511f445 100755
--- a/app/Jobs/SubscriptionInvoiceFailedJob.php
+++ b/app/Jobs/SubscriptionInvoiceFailedJob.php
@@ -15,7 +15,10 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct(protected Team $team) {}
+ public function __construct(protected Team $team)
+ {
+ $this->onQueue('high');
+ }
public function handle()
{
diff --git a/app/Jobs/SubscriptionTrialEndedJob.php b/app/Jobs/SubscriptionTrialEndedJob.php
deleted file mode 100755
index 88a5e06be..000000000
--- a/app/Jobs/SubscriptionTrialEndedJob.php
+++ /dev/null
@@ -1,42 +0,0 @@
-team);
- $mail = new MailMessage;
- $mail->subject('Action required: You trial in Coolify Cloud ended.');
- $mail->view('emails.trial-ended', [
- 'stripeCustomerPortal' => $session->url,
- ]);
- $this->team->members()->each(function ($member) use ($mail) {
- if ($member->isAdmin()) {
- send_user_an_email($mail, $member->email);
- send_internal_notification('Trial reminder email sent to '.$member->email);
- }
- });
- } catch (\Throwable $e) {
- send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage());
- throw $e;
- }
- }
-}
diff --git a/app/Jobs/SubscriptionTrialEndsSoonJob.php b/app/Jobs/SubscriptionTrialEndsSoonJob.php
deleted file mode 100755
index 2a76a1097..000000000
--- a/app/Jobs/SubscriptionTrialEndsSoonJob.php
+++ /dev/null
@@ -1,42 +0,0 @@
-team);
- $mail = new MailMessage;
- $mail->subject('You trial in Coolify Cloud ends soon.');
- $mail->view('emails.trial-ends-soon', [
- 'stripeCustomerPortal' => $session->url,
- ]);
- $this->team->members()->each(function ($member) use ($mail) {
- if ($member->isAdmin()) {
- send_user_an_email($mail, $member->email);
- send_internal_notification('Trial reminder email sent to '.$member->email);
- }
- });
- } catch (\Throwable $e) {
- send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage());
- throw $e;
- }
- }
-}
diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php
index 1e5197b6f..f0e43cbc0 100644
--- a/app/Jobs/UpdateCoolifyJob.php
+++ b/app/Jobs/UpdateCoolifyJob.php
@@ -18,6 +18,11 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 600;
+ public function __construct()
+ {
+ $this->onQueue('high');
+ }
+
public function handle(): void
{
try {
diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php
index 2579c3db2..359db6329 100644
--- a/app/Livewire/Admin/Index.php
+++ b/app/Livewire/Admin/Index.php
@@ -2,8 +2,8 @@
namespace App\Livewire\Admin;
+use App\Models\Team;
use App\Models\User;
-use Illuminate\Container\Attributes\Auth as AttributesAuth;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
@@ -43,17 +43,13 @@ class Index extends Component
public function getSubscribers()
{
- $this->inactiveSubscribers = User::whereDoesntHave('teams', function ($query) {
- $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
- })->count();
- $this->activeSubscribers = User::whereHas('teams', function ($query) {
- $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
- })->count();
+ $this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count();
+ $this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count();
}
public function switchUser(int $user_id)
{
- if (AttributesAuth::id() !== 0) {
+ if (Auth::id() !== 0) {
return redirect()->route('dashboard');
}
$user = User::find($user_id);
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index c9c3092b3..b6c799c4e 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -66,11 +66,15 @@ class Index extends Component
public bool $serverReachable = true;
+ public ?string $minDockerVersion = null;
+
public function mount()
{
if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
return redirect()->route('dashboard');
}
+
+ $this->minDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.');
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
if (isDev()) {
diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php
index 8cf58ec41..337f1d067 100644
--- a/app/Livewire/Destination/New/Docker.php
+++ b/app/Livewire/Destination/New/Docker.php
@@ -35,9 +35,11 @@ class Docker extends Component
$this->network = new Cuid2;
$this->servers = Server::isUsable()->get();
if ($server_id) {
- $this->selectedServer = $this->servers->find($server_id);
+ $this->selectedServer = $this->servers->find($server_id) ?: $this->servers->first();
+ $this->serverId = $this->selectedServer->id;
} else {
$this->selectedServer = $this->servers->first();
+ $this->serverId = $this->selectedServer->id;
}
$this->generateName();
}
diff --git a/app/Livewire/Dev/Compose.php b/app/Livewire/Dev/Compose.php
deleted file mode 100644
index a5cd53fc2..000000000
--- a/app/Livewire/Dev/Compose.php
+++ /dev/null
@@ -1,37 +0,0 @@
-services = get_service_templates();
- }
-
- public function setService(string $selected)
- {
- $this->base64 = data_get($this->services, $selected.'.compose');
- if ($this->base64) {
- $this->compose = base64_decode($this->base64);
- }
- }
-
- public function updatedCompose($value)
- {
- $this->base64 = base64_encode($value);
- }
-
- public function render()
- {
- return view('livewire.dev.compose');
- }
-}
diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php
index cc5d78f60..e97cceb0d 100644
--- a/app/Livewire/NavbarDeleteTeam.php
+++ b/app/Livewire/NavbarDeleteTeam.php
@@ -3,7 +3,6 @@
namespace App\Livewire;
use App\Models\InstanceSettings;
-use Illuminate\Container\Attributes\Auth as AttributesAuth;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
@@ -32,7 +31,7 @@ class NavbarDeleteTeam extends Component
$currentTeam->delete();
$currentTeam->members->each(function ($user) use ($currentTeam) {
- if ($user->id === AttributesAuth::id()) {
+ if ($user->id === Auth::id()) {
return;
}
$user->teams()->detach($currentTeam);
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index b49638b48..fcedf1305 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -50,22 +50,22 @@ class Email extends Component
public ?int $smtpTimeout = null;
#[Validate(['boolean'])]
- public bool $smtpNotificationsTest;
+ public bool $smtpNotificationsTest = false;
#[Validate(['boolean'])]
- public bool $smtpNotificationsDeployments;
+ public bool $smtpNotificationsDeployments = false;
#[Validate(['boolean'])]
- public bool $smtpNotificationsStatusChanges;
+ public bool $smtpNotificationsStatusChanges = false;
#[Validate(['boolean'])]
- public bool $smtpNotificationsDatabaseBackups;
+ public bool $smtpNotificationsDatabaseBackups = false;
#[Validate(['boolean'])]
- public bool $smtpNotificationsScheduledTasks;
+ public bool $smtpNotificationsScheduledTasks = false;
#[Validate(['boolean'])]
- public bool $smtpNotificationsServerDiskUsage;
+ public bool $smtpNotificationsServerDiskUsage = false;
#[Validate(['boolean'])]
public bool $resendEnabled;
@@ -73,6 +73,9 @@ class Email extends Component
#[Validate(['nullable', 'string'])]
public ?string $resendApiKey = null;
+ #[Validate(['required', 'email'])]
+ public string $testEmailAddress = '';
+
public function mount()
{
try {
@@ -132,14 +135,21 @@ class Email extends Component
}
}
- public function sendTestNotification()
+ public function sendTestEmail()
{
try {
+ $this->validate([
+ 'testEmailAddress' => 'required|email',
+ ], [
+ 'testEmailAddress.required' => 'Test email address is required.',
+ 'testEmailAddress.email' => 'Please enter a valid email address.',
+ ]);
+
$executed = RateLimiter::attempt(
'test-email:'.$this->team->id,
$perMinute = 0,
function () {
- $this->team?->notify(new Test($this->emails));
+ $this->team?->notify(new Test($this->testEmailAddress));
$this->dispatch('success', 'Test Email sent.');
},
$decaySeconds = 10,
@@ -187,6 +197,7 @@ class Email extends Component
{
try {
$this->validate([
+ 'resendApiKey' => 'required',
], [
'resendApiKey.required' => 'Resend API Key is required.',
]);
diff --git a/app/Livewire/Project/Application/Configuration.php b/app/Livewire/Project/Application/Configuration.php
index d4ec8f581..5e7f83772 100644
--- a/app/Livewire/Project/Application/Configuration.php
+++ b/app/Livewire/Project/Application/Configuration.php
@@ -16,24 +16,33 @@ class Configuration extends Component
public function mount()
{
- $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
- if (! $project) {
- return redirect()->route('dashboard');
- }
- $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
- if (! $environment) {
- return redirect()->route('dashboard');
- }
- $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
- if (! $application) {
- return redirect()->route('dashboard');
- }
+ $project = currentTeam()
+ ->projects()
+ ->select('id', 'uuid', 'team_id')
+ ->where('uuid', request()->route('project_uuid'))
+ ->firstOrFail();
+
+ $environment = $project->environments()
+ ->select('id', 'name', 'project_id')
+ ->where('name', request()->route('environment_name'))
+ ->firstOrFail();
+
+ $application = $environment->applications()
+ ->with(['destination'])
+ ->where('uuid', request()->route('application_uuid'))
+ ->firstOrFail();
+
$this->application = $application;
- $mainServer = $this->application->destination->server;
- $servers = Server::ownedByCurrentTeam()->get();
- $this->servers = $servers->filter(function ($server) use ($mainServer) {
- return $server->id != $mainServer->id;
- });
+
+ if ($application->destination && $application->destination->server) {
+ $mainServer = $application->destination->server;
+ $this->servers = Server::ownedByCurrentTeam()
+ ->select('id', 'name')
+ ->where('id', '!=', $mainServer->id)
+ ->get();
+ } else {
+ $this->servers = collect();
+ }
}
public function render()
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index f1575a01f..ff29b74e9 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -84,6 +84,7 @@ class General extends Component
'application.pre_deployment_command_container' => 'nullable',
'application.post_deployment_command' => 'nullable',
'application.post_deployment_command_container' => 'nullable',
+ 'application.custom_nginx_configuration' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_container_label_escape_enabled' => 'boolean|required',
@@ -121,6 +122,7 @@ class General extends Component
'application.custom_docker_run_options' => 'Custom docker run commands',
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
+ 'application.custom_nginx_configuration' => 'Custom Nginx configuration',
'application.settings.is_static' => 'Is static',
'application.settings.is_build_server_enabled' => 'Is build server enabled',
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
@@ -241,6 +243,13 @@ class General extends Component
}
}
+ public function updatedApplicationSettingsIsStatic($value)
+ {
+ if ($value) {
+ $this->generateNginxConfiguration();
+ }
+ }
+
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
@@ -257,6 +266,7 @@ class General extends Component
if ($this->application->build_pack === 'static') {
$this->application->ports_exposes = $this->ports_exposes = 80;
$this->resetDefaultLabels(false);
+ $this->generateNginxConfiguration();
}
$this->submit();
$this->dispatch('buildPackUpdated');
@@ -274,6 +284,13 @@ class General extends Component
}
}
+ public function generateNginxConfiguration()
+ {
+ $this->application->custom_nginx_configuration = defaultNginxConfiguration();
+ $this->application->save();
+ $this->dispatch('success', 'Nginx configuration generated.');
+ }
+
public function resetDefaultLabels($manualReset = false)
{
try {
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index 1082b48cd..19a6145b7 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -36,7 +36,11 @@ class Heading extends Component
public function mount()
{
- $this->parameters = get_route_parameters();
+ $this->parameters = [
+ 'project_uuid' => $this->application->project()->uuid,
+ 'environment_name' => $this->application->environment->name,
+ 'application_uuid' => $this->application->uuid,
+ ];
$lastDeployment = $this->application->get_last_successful_deployment();
$this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7).' '.data_get($lastDeployment, 'commit_message');
$this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit'));
@@ -45,13 +49,11 @@ class Heading extends Component
public function check_status($showNotification = false)
{
if ($this->application->destination->server->isFunctional()) {
- GetContainersStatus::dispatch($this->application->destination->server)->onQueue('high');
+ GetContainersStatus::dispatch($this->application->destination->server);
}
if ($showNotification) {
$this->dispatch('success', 'Success', 'Application status updated.');
}
- // Removed because it caused flickering
- // $this->dispatch('configurationChanged');
}
public function force_deploy_without_cache()
diff --git a/app/Livewire/Project/Application/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php
index c7b2e8184..edcab44c8 100644
--- a/app/Livewire/Project/Application/Preview/Form.php
+++ b/app/Livewire/Project/Application/Preview/Form.php
@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Application\Preview;
use App\Models\Application;
+use Livewire\Attributes\Validate;
use Livewire\Component;
use Spatie\Url\Url;
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index bcf2f959e..b3a54f0ab 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -46,8 +46,8 @@ class BackupEdit extends Component
#[Validate(['required', 'boolean'])]
public bool $saveS3 = false;
- #[Validate(['required', 'integer'])]
- public int $s3StorageId = 1;
+ #[Validate(['nullable', 'integer'])]
+ public ?int $s3StorageId = 1;
#[Validate(['nullable', 'string'])]
public ?string $databasesToBackup = null;
diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php
index 0903efdfd..01108c290 100644
--- a/app/Livewire/Project/Database/CreateScheduledBackup.php
+++ b/app/Livewire/Project/Database/CreateScheduledBackup.php
@@ -21,8 +21,8 @@ class CreateScheduledBackup extends Component
public bool $enabled = true;
- #[Validate(['required', 'integer'])]
- public int $s3StorageId;
+ #[Validate(['nullable', 'integer'])]
+ public ?int $s3StorageId = null;
public Collection $definedS3s;
@@ -49,6 +49,7 @@ class CreateScheduledBackup extends Component
return;
}
+
$payload = [
'enabled' => true,
'frequency' => $this->frequency,
@@ -58,6 +59,7 @@ class CreateScheduledBackup extends Component
'database_type' => $this->database->getMorphClass(),
'team_id' => currentTeam()->id,
];
+
if ($this->database->type() === 'standalone-postgresql') {
$payload['databases_to_backup'] = $this->database->postgres_db;
} elseif ($this->database->type() === 'standalone-mysql') {
@@ -72,11 +74,11 @@ class CreateScheduledBackup extends Component
} else {
$this->dispatch('refreshScheduledBackups');
}
+
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->frequency = '';
- $this->saveToS3 = true;
}
}
}
diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php
index 6d8c3aff7..1ee5de269 100644
--- a/app/Livewire/Project/DeleteEnvironment.php
+++ b/app/Livewire/Project/DeleteEnvironment.php
@@ -37,6 +37,6 @@ class DeleteEnvironment extends Component
return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]);
}
- return $this->dispatch('error', 'Environment has defined resources, please delete them first.');
+ return $this->dispatch('error', "Environment {$environment->name} has defined resources, please delete them first.");
}
}
diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php
index 360fad10a..f320a19b0 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -27,11 +27,12 @@ class DeleteProject extends Component
'project_id' => 'required|int',
]);
$project = Project::findOrFail($this->project_id);
- if ($project->applications->count() > 0) {
- return $this->dispatch('error', 'Project has resources defined, please delete them first.');
- }
- $project->delete();
+ if ($project->isEmpty()) {
+ $project->delete();
- return redirect()->route('project.index');
+ return redirect()->route('project.index');
+ }
+
+ return $this->dispatch('error', "Project {$project->name} has resources defined, please delete them first.");
}
}
diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index a562d8b66..0b6d075a4 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -91,9 +91,16 @@ class Select extends Component
{
$services = get_service_templates(true);
$services = collect($services)->map(function ($service, $key) {
+ $default_logo = 'images/default.webp';
+ $logo = data_get($service, 'logo', $default_logo);
+ $local_logo_path = public_path($logo);
+
return [
'name' => str($key)->headline(),
- 'logo' => asset(data_get($service, 'logo', 'svgs/coolify.png')),
+ 'logo' => asset($logo),
+ 'logo_github_url' => file_exists($local_logo_path)
+ ? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo
+ : asset($default_logo),
] + (array) $service;
})->all();
$gitBasedApplications = [
@@ -141,14 +148,14 @@ class Select extends Component
'id' => 'postgresql',
'name' => 'PostgreSQL',
'description' => 'PostgreSQL is an object-relational database known for its robustness, advanced features, and strong standards compliance.',
- 'logo' => '
+ 'logo' => '
',
],
[
'id' => 'mysql',
'name' => 'MySQL',
'description' => 'MySQL is an open-source relational database management system. ',
- 'logo' => '