Merge branch 'next' into main

This commit is contained in:
Marvin von Rappard
2024-11-14 12:54:04 +01:00
committed by GitHub
120 changed files with 11805 additions and 3231 deletions

2
.gitattributes vendored
View File

@@ -8,4 +8,4 @@
/.github export-ignore /.github export-ignore
CHANGELOG.md export-ignore CHANGELOG.md export-ignore
.styleci.yml export-ignore .styleci.yml export-ignore

View File

@@ -11,7 +11,7 @@ on:
- docker/coolify-helper/Dockerfile - docker/coolify-helper/Dockerfile
- docker/coolify-realtime/Dockerfile - docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile - docker/testing-host/Dockerfile
- templates/* - templates/**
env: env:
GITHUB_REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io

View File

@@ -11,7 +11,7 @@ on:
- docker/coolify-helper/Dockerfile - docker/coolify-helper/Dockerfile
- docker/coolify-realtime/Dockerfile - docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile - docker/testing-host/Dockerfile
- templates/* - templates/**
env: env:
GITHUB_REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io

1
.gitignore vendored
View File

@@ -35,3 +35,4 @@ scripts/load-test/*
.ignition.json .ignition.json
.env.dusk.local .env.dusk.local
docker/coolify-realtime/node_modules docker/coolify-realtime/node_modules
.DS_Store

View File

@@ -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

View File

@@ -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). You can find the installation script source [here](./scripts/install.sh).
> [!NOTE]
> Please refer to the [docs](https://coolify.io/docs/installation) for more information about the installation.
# Support # Support
Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact). Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
@@ -121,7 +124,6 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
- Better support - Better support
- Less maintenance for you - Less maintenance for you
# Recognitions # Recognitions
<p> <p>
@@ -138,6 +140,13 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
<a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
# Core Maintainers
| Andras Bacsai | Peak |
|------------|------------|
| <img src="https://github.com/andrasbacsai.png" width="200px" alt="Andras Bacsai" /> | <img src="https://github.com/peaklabs-dev.png" width="200px" alt="Peak Labs" /> |
| <a href="https://x.com/heyandras"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Twitter.svg" width="25px"></a> <a href="https://github.com/andrasbacsai"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Github.svg" width="25px"></a> | <a href="https://x.com/peaklabs_dev"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Twitter.svg" width="25px"></a> <a href="https://github.com/peaklabs-dev"><img src="https://raw.githubusercontent.com/gauravghongde/social-icons/master/SVG/Color/Github.svg" width="25px"></a> |
# Repo Activity # Repo Activity
![Alt](https://repobeats.axiom.co/api/embed/eab1c8066f9c59d0ad37b76c23ebb5ccac4278ae.svg "Repobeats analytics image") ![Alt](https://repobeats.axiom.co/api/embed/eab1c8066f9c59d0ad37b76c23ebb5ccac4278ae.svg "Repobeats analytics image")

View File

@@ -1,6 +1,6 @@
# Coolify Release Guide # 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 ## Table of Contents
- [Release Process](#release-process) - [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. - Improvements, fixes, and new features are developed on the `next` branch or separate feature branches.
2. **Merging to `main`** 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** 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** 4. **Creating a GitHub Release**
- A new GitHub release is manually created with details of the changes made in the version. - A new GitHub release is manually created with details of the changes made in the version.
5. **Updating the CDN** 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] > [!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 ## Version Types
@@ -39,10 +39,10 @@ This guide outlines the release process for Coolify, intended for developers and
<summary><strong>Stable (coming soon)</strong></summary> <summary><strong>Stable (coming soon)</strong></summary>
- **Stable** - **Stable**
- The production version suitable for stable, production environments (generally recommended). - The production version suitable for stable, production environments (recommended).
- **Update Frequency:** Every 2 to 4 weeks, with more frequent possible hotfixes. - **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. - **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:** - **Installation Command:**
```bash ```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | 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. - The latest development version, suitable for testing the latest changes and experimenting with new features.
- **Update Frequency:** Daily or bi-weekly updates. - **Update Frequency:** Daily or bi-weekly updates.
- **Release Size:** Smaller, more frequent releases. - **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:** - **Installation Command:**
```bash ```bash
curl -fsSL https://cdn.coollabs.io/coolify-nightly/install.sh | bash -s next 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. - **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. - **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. - **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:** - **Installation Command:**
```bash ```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
``` ```
</details> </details>
@@ -117,12 +117,15 @@ When a new version is released and a new GitHub release is created, it doesn't i
> [!IMPORTANT] > [!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. > 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] > [!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 ```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s <version> curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s <version>

View File

@@ -2,15 +2,24 @@
## Supported Versions ## Supported Versions
Use this section to tell people about which versions of your project are Currently supported, maintained and updated versions:
currently being supported with security updates.
| Version | Supported | | Version | Supported | Support Status |
| ------- | ------------------ | | ------- | ------------------ | -------------- |
| > 4 | :white_check_mark: | | 4.x | :white_check_mark: | Active Development & Security Updates |
| 3 | :x: | | < 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 ## 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

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Actions\Application;
use Laravel\Horizon\Contracts\JobRepository;
use Lorisleiva\Actions\Concerns\AsAction;
class IsHorizonQueueEmpty
{
use AsAction;
public function handle()
{
$hostname = gethostname();
$recent = app(JobRepository::class)->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;
}
}

View File

@@ -1,66 +0,0 @@
<?php
namespace App\Actions\License;
use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
class CheckResaleLicense
{
use AsAction;
public function handle()
{
try {
$settings = instanceSettings();
if (isDev()) {
$settings->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;
}
}
}

View File

@@ -13,7 +13,7 @@ class CleanupDocker
{ {
$settings = instanceSettings(); $settings = instanceSettings();
$helperImageVersion = data_get($settings, 'helper_version'); $helperImageVersion = data_get($settings, 'helper_version');
$helperImage = config('coolify.helper_image'); $helperImage = config('constants.coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion"; $helperImageWithVersion = "$helperImage:$helperImageVersion";
$commands = [ $commands = [

View File

@@ -12,7 +12,7 @@ class InstallDocker
public function handle(Server $server) public function handle(Server $server)
{ {
$dockerVersion = config('constants.docker_install_version'); $dockerVersion = config('constants.docker.minimum_required_version');
$supported_os_type = $server->validateOS(); $supported_os_type = $server->validateOS();
if (! $supported_os_type) { if (! $supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.'); throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');

View File

@@ -169,7 +169,7 @@ Files:
'); ');
$license_key = $server->settings->logdrain_newrelic_license_key; $license_key = $server->settings->logdrain_newrelic_license_key;
$base_uri = $server->settings->logdrain_newrelic_base_uri; $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'; $config_path = $base_path.'/log-drains';
$fluent_bit_config = $config_path.'/fluent-bit.conf'; $fluent_bit_config = $config_path.'/fluent-bit.conf';

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use App\Models\Team;
use Illuminate\Console\Command;
class CloudCheckSubscription extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cloud:check-subscription';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check Cloud subscriptions';
/**
* Execute the console command.
*/
public function handle()
{
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->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";
}
}
}

View File

@@ -6,6 +6,7 @@ use App\Models\InstanceSettings;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use Symfony\Component\Yaml\Yaml;
class Dev extends Command class Dev extends Command
{ {
@@ -31,19 +32,32 @@ class Dev extends Command
{ {
// Generate OpenAPI documentation // Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n"; 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 = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error); $error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error); $error = preg_replace('/^\h*\v+/m', '', $error);
echo $error; echo $error;
echo $process->output(); 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() public function init()
{ {
// Generate APP_KEY if not exists // Generate APP_KEY if not exists
if (empty(env('APP_KEY'))) { if (empty(config('app.key'))) {
echo "Generating APP_KEY.\n"; echo "Generating APP_KEY.\n";
Artisan::call('key:generate'); Artisan::call('key:generate');
} }

View File

@@ -12,7 +12,7 @@ class Horizon extends Command
public function handle() public function handle()
{ {
if (config('coolify.is_horizon_enabled')) { if (config('constants.horizon.is_horizon_enabled')) {
$this->info('Horizon is enabled. Starting.'); $this->info('Horizon is enabled. Starting.');
$this->call('horizon'); $this->call('horizon');
exit(0); exit(0);

View File

@@ -57,12 +57,19 @@ class Init extends Command
$this->call('cleanup:stucked-resources'); $this->call('cleanup:stucked-resources');
if (isCloud()) { if (isCloud()) {
$response = Http::retry(3, 1000)->get(config('constants.services.official')); try {
if ($response->successful()) { $this->pullTemplatesFromCDN();
$services = $response->json(); } catch (\Throwable $e) {
File::put(base_path('templates/service-templates.json'), json_encode($services)); 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 { try {
$localhost = $this->servers->where('id', 0)->first(); $localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration(); $localhost->setupDynamicProxyConfiguration();
@@ -70,8 +77,8 @@ class Init extends Command
echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
} }
$settings = instanceSettings(); $settings = instanceSettings();
if (! is_null(env('AUTOUPDATE', null))) { if (! is_null(config('constants.coolify.autoupdate', null))) {
if (env('AUTOUPDATE') == true) { if (config('constants.coolify.autoupdate') == true) {
$settings->update(['is_auto_update_enabled' => true]); $settings->update(['is_auto_update_enabled' => true]);
} else { } else {
$settings->update(['is_auto_update_enabled' => false]); $settings->update(['is_auto_update_enabled' => false]);
@@ -80,6 +87,14 @@ class Init extends Command
} }
} }
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 disable_metrics() // private function disable_metrics()
// { // {
// if (version_compare('4.0.0-beta.312', config('version'), '<=')) { // if (version_compare('4.0.0-beta.312', config('version'), '<=')) {

View File

@@ -15,7 +15,15 @@ class OpenApi extends Command
{ {
// Generate OpenAPI documentation // Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n"; 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 = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error); $error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error); $error = preg_replace('/^\h*\v+/m', '', $error);

View File

@@ -12,7 +12,7 @@ class Scheduler extends Command
public function handle() public function handle()
{ {
if (config('coolify.is_scheduler_enabled')) { if (config('constants.horizon.is_scheduler_enabled')) {
$this->info('Scheduler is enabled. Starting.'); $this->info('Scheduler is enabled. Starting.');
$this->call('schedule:work'); $this->call('schedule:work');
exit(0); exit(0);

View File

@@ -57,7 +57,7 @@ class SyncBunny extends Command
PendingRequest::macro('storage', function ($fileName) use ($that) { PendingRequest::macro('storage', function ($fileName) use ($that) {
$headers = [ $headers = [
'AccessKey' => env('BUNNY_STORAGE_API_KEY'), 'AccessKey' => config('constants.bunny.storage_api_key'),
'Accept' => 'application/json', 'Accept' => 'application/json',
'Content-Type' => 'application/octet-stream', 'Content-Type' => 'application/octet-stream',
]; ];
@@ -69,7 +69,7 @@ class SyncBunny extends Command
}); });
PendingRequest::macro('purge', function ($url) use ($that) { PendingRequest::macro('purge', function ($url) use ($that) {
$headers = [ $headers = [
'AccessKey' => env('BUNNY_API_KEY'), 'AccessKey' => config('constants.bunny.api_key'),
'Accept' => 'application/json', 'Accept' => 'application/json',
]; ];
$that->info('Purging: '.$url); $that->info('Purging: '.$url);

View File

@@ -28,6 +28,8 @@ class Kernel extends ConsoleKernel
{ {
private $allServers; private $allServers;
private Schedule $scheduleInstance;
private InstanceSettings $settings; private InstanceSettings $settings;
private string $updateCheckFrequency; private string $updateCheckFrequency;
@@ -36,82 +38,90 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule): void
{ {
$this->scheduleInstance = $schedule;
$this->allServers = Server::where('ip', '!=', '1.2.3.4'); $this->allServers = Server::where('ip', '!=', '1.2.3.4');
$this->settings = instanceSettings(); $this->settings = instanceSettings();
$this->updateCheckFrequency = $this->settings->update_check_frequency ?: '0 * * * *'; $this->updateCheckFrequency = $this->settings->update_check_frequency ?: '0 * * * *';
$this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone'); $this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone');
$schedule->job(new CleanupStaleMultiplexedConnections)->hourly(); if (validate_timezone($this->instanceTimezone) === false) {
$this->instanceTimezone = config('app.timezone');
}
$this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
if (isDev()) { if (isDev()) {
// Instance Jobs // Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute(); $this->scheduleInstance->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); $this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
$schedule->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer(); $this->scheduleInstance->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer();
// Server Jobs // Server Jobs
$this->checkResources($schedule); $this->checkResources();
$this->checkScheduledBackups($schedule); $this->checkScheduledBackups();
$this->checkScheduledTasks($schedule); $this->checkScheduledTasks();
$schedule->command('uploads:clear')->everyTwoMinutes(); $this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
} else { } else {
// Instance Jobs // Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $this->scheduleInstance->command('horizon:snapshot')->everyFiveMinutes();
$schedule->command('cleanup:unreachable-servers')->daily()->onOneServer(); $this->scheduleInstance->command('cleanup:unreachable-servers')->daily()->onOneServer();
$schedule->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); $this->scheduleInstance->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
$this->scheduleUpdates($schedule);
$this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$this->scheduleUpdates();
// Server Jobs // Server Jobs
$this->checkResources($schedule); $this->checkResources();
$this->pullImages($schedule); $this->pullImages();
$this->checkScheduledBackups($schedule); $this->checkScheduledBackups();
$this->checkScheduledTasks($schedule); $this->checkScheduledTasks();
$schedule->command('cleanup:database --yes')->daily(); $this->scheduleInstance->command('cleanup:database --yes')->daily();
$schedule->command('uploads:clear')->everyTwoMinutes(); $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(); $servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
foreach ($servers as $server) { foreach ($servers as $server) {
if ($server->isSentinelEnabled()) { if ($server->isSentinelEnabled()) {
$schedule->job(function () use ($server) { $this->scheduleInstance->job(function () use ($server) {
CheckAndStartSentinelJob::dispatch($server); CheckAndStartSentinelJob::dispatch($server);
})->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
} }
} }
$schedule->job(new CheckHelperImageJob) $this->scheduleInstance->job(new CheckHelperImageJob)
->cron($this->updateCheckFrequency) ->cron($this->updateCheckFrequency)
->timezone($this->instanceTimezone) ->timezone($this->instanceTimezone)
->onOneServer(); ->onOneServer();
} }
private function scheduleUpdates($schedule): void private function scheduleUpdates(): void
{ {
$schedule->job(new CheckForUpdatesJob) $this->scheduleInstance->job(new CheckForUpdatesJob)
->cron($this->updateCheckFrequency) ->cron($this->updateCheckFrequency)
->timezone($this->instanceTimezone) ->timezone($this->instanceTimezone)
->onOneServer(); ->onOneServer();
if ($this->settings->is_auto_update_enabled) { if ($this->settings->is_auto_update_enabled) {
$autoUpdateFrequency = $this->settings->auto_update_frequency; $autoUpdateFrequency = $this->settings->auto_update_frequency;
$schedule->job(new UpdateCoolifyJob) $this->scheduleInstance->job(new UpdateCoolifyJob)
->cron($autoUpdateFrequency) ->cron($autoUpdateFrequency)
->timezone($this->instanceTimezone) ->timezone($this->instanceTimezone)
->onOneServer(); ->onOneServer();
} }
} }
private function checkResources($schedule): void private function checkResources(): void
{ {
if (isCloud()) { if (isCloud()) {
$servers = $this->allServers->whereHas('team.subscription')->get(); $servers = $this->allServers->whereHas('team.subscription')->get();
@@ -128,31 +138,34 @@ class Kernel extends ConsoleKernel
$lastSentinelUpdate = $server->sentinel_updated_at; $lastSentinelUpdate = $server->sentinel_updated_at;
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
// Check container status every minute if Sentinel does not activated // Check container status every minute if Sentinel does not activated
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); if (validate_timezone($serverTimezone) === false) {
// $schedule->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer(); $serverTimezone = config('app.timezone');
}
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer();
// $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer();
// Check storage usage every 10 minutes if Sentinel does not activated // 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) { 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 { } 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 // 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 // Temporary solution until we have better memory management for Sentinel
if ($server->isSentinelEnabled()) { if ($server->isSentinelEnabled()) {
$schedule->job(function () use ($server) { $this->scheduleInstance->job(function () use ($server) {
$server->restartContainer('coolify-sentinel'); $server->restartContainer('coolify-sentinel');
})->daily()->onOneServer(); })->daily()->onOneServer();
} }
} }
} }
private function checkScheduledBackups($schedule): void private function checkScheduledBackups(): void
{ {
$scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get(); $scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get();
if ($scheduled_backups->isEmpty()) { if ($scheduled_backups->isEmpty()) {
@@ -174,13 +187,13 @@ class Kernel extends ConsoleKernel
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = 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 backup: $scheduled_backup
))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->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(); $scheduled_tasks = ScheduledTask::where('enabled', true)->get();
if ($scheduled_tasks->isEmpty()) { if ($scheduled_tasks->isEmpty()) {
@@ -214,7 +227,7 @@ class Kernel extends ConsoleKernel
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = 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 task: $scheduled_task
))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer(); ))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer();
} }

View File

@@ -151,7 +151,7 @@ class SshMultiplexingHelper
private static function isMultiplexingEnabled(): bool 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(string $sshKeyLocation): void

View File

@@ -147,7 +147,7 @@ class OtherController extends Controller
public function feedback(Request $request) public function feedback(Request $request)
{ {
$content = $request->input('content'); $content = $request->input('content');
$webhook_url = config('coolify.feedback_discord_webhook'); $webhook_url = config('constants.webhooks.feedback_discord_webhook');
if ($webhook_url) { if ($webhook_url) {
Http::post($webhook_url, [ Http::post($webhook_url, [
'content' => $content, 'content' => $content,

View File

@@ -116,7 +116,7 @@ class ProjectController extends Controller
responses: [ responses: [
new OA\Response( new OA\Response(
response: 200, response: 200,
description: 'Project details', description: 'Environment details',
content: new OA\JsonContent(ref: '#/components/schemas/Environment')), content: new OA\JsonContent(ref: '#/components/schemas/Environment')),
new OA\Response( new OA\Response(
response: 401, response: 401,

View File

@@ -81,15 +81,8 @@ class SecurityController extends Controller
new OA\Response( new OA\Response(
response: 200, response: 200,
description: 'Get all private keys.', description: 'Get all private keys.',
content: [ content: new OA\JsonContent(ref: '#/components/schemas/PrivateKey')
new OA\MediaType( ),
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/PrivateKey')
)
),
]),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',

View File

@@ -426,6 +426,7 @@ class ServersController extends Controller
'private_key_uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the private key.'], '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.'], 'is_build_server' => ['type' => 'boolean', 'example' => false, 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Instant validate.'], '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) 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(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
@@ -481,6 +482,7 @@ class ServersController extends Controller
'user' => 'string|nullable', 'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable', 'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable',
'proxy_type' => 'string|nullable',
]); ]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields); $extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -512,6 +514,14 @@ class ServersController extends Controller
if (is_null($request->instant_validate)) { if (is_null($request->instant_validate)) {
$request->offsetSet('instant_validate', false); $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(); $privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
if (! $privateKey) { if (! $privateKey) {
return response()->json(['message' => 'Private key not found.'], 404); 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); 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([ $server = ModelsServer::create([
'name' => $request->name, 'name' => $request->name,
'description' => $request->description, 'description' => $request->description,
@@ -530,7 +542,7 @@ class ServersController extends Controller
'private_key_id' => $privateKey->id, 'private_key_id' => $privateKey->id,
'team_id' => $teamId, 'team_id' => $teamId,
'proxy' => [ 'proxy' => [
'type' => ProxyTypes::TRAEFIK->value, 'type' => $proxyType,
'status' => ProxyStatus::EXITED->value, 'status' => ProxyStatus::EXITED->value,
], ],
]); ]);
@@ -571,6 +583,7 @@ class ServersController extends Controller
'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'], 'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'], 'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'], 'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'],
'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'],
], ],
), ),
), ),
@@ -604,7 +617,7 @@ class ServersController extends Controller
)] )]
public function update_server(Request $request) 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(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
@@ -624,6 +637,7 @@ class ServersController extends Controller
'user' => 'string|nullable', 'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable', 'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable',
'proxy_type' => 'string|nullable',
]); ]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields); $extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -644,6 +658,16 @@ class ServersController extends Controller
if (! $server) { if (! $server) {
return response()->json(['message' => 'Server not found.'], 404); 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'])); $server->update($request->only(['name', 'description', 'ip', 'port', 'user']));
if ($request->is_build_server) { if ($request->is_build_server) {
$server->settings()->update([ $server->settings()->update([
@@ -654,7 +678,9 @@ class ServersController extends Controller
ValidateServer::dispatch($server)->onQueue('high'); ValidateServer::dispatch($server)->onQueue('high');
} }
return response()->json(serializeApiResponse($server))->setStatusCode(201); return response()->json([
])->setStatusCode(201);
} }
#[OA\Delete( #[OA\Delete(

View File

@@ -33,6 +33,7 @@ class Gitlab extends Controller
return; return;
} }
$return_payloads = collect([]); $return_payloads = collect([]);
$payload = $request->collect(); $payload = $request->collect();
$headers = $request->headers->all(); $headers = $request->headers->all();
@@ -48,6 +49,15 @@ class Gitlab extends Controller
return response($return_payloads); 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') { if ($x_gitlab_event === 'push') {
$branch = data_get($payload, 'ref'); $branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'project.path_with_namespace'); $full_name = data_get($payload, 'project.path_with_namespace');

View File

@@ -5,8 +5,6 @@ namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Jobs\ServerLimitCheckJob; use App\Jobs\ServerLimitCheckJob;
use App\Jobs\SubscriptionInvoiceFailedJob; use App\Jobs\SubscriptionInvoiceFailedJob;
use App\Jobs\SubscriptionTrialEndedJob;
use App\Jobs\SubscriptionTrialEndsSoonJob;
use App\Models\Subscription; use App\Models\Subscription;
use App\Models\Team; use App\Models\Team;
use App\Models\Webhook; use App\Models\Webhook;
@@ -260,42 +258,7 @@ class Stripe extends Controller
$customerId = data_get($data, 'customer'); $customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team'); $team = data_get($subscription, 'team');
if ($team) { $team?->subscriptionEnded();
$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; break;
default: default:
// Unhandled event type // Unhandled event type

View File

@@ -225,6 +225,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
public function tags(): array
{
return ['server:'.gethostname()];
}
public function handle(): void public function handle(): void
{ {
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
@@ -1318,7 +1323,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function prepare_builder_image() private function prepare_builder_image()
{ {
$settings = instanceSettings(); $settings = instanceSettings();
$helperImage = config('coolify.helper_image'); $helperImage = config('constants.coolify.helper_image');
$helperImage = "{$helperImage}:{$settings->helper_version}"; $helperImage = "{$helperImage}:{$settings->helper_version}";
// Get user home directory // Get user home directory
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server); $this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);

View File

@@ -524,7 +524,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
private function getFullImageName(): string private function getFullImageName(): string
{ {
$settings = instanceSettings(); $settings = instanceSettings();
$helperImage = config('coolify.helper_image'); $helperImage = config('constants.coolify.helper_image');
$latestVersion = $settings->helper_version; $latestVersion = $settings->helper_version;
return "{$helperImage}:{$latestVersion}"; return "{$helperImage}:{$latestVersion}";

View File

@@ -26,7 +26,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array 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 bool $manualCleanup = false) {} public function __construct(public Server $server, public bool $manualCleanup = false) {}

View File

@@ -20,7 +20,7 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public function handle(): void public function handle(): void
{ {
$helperImage = config('coolify.helper_image'); $helperImage = config('constants.coolify.helper_image');
$latest_version = instanceSettings()->helper_version; $latest_version = instanceSettings()->helper_version;
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false); instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
} }

View File

@@ -28,7 +28,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array 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) {}

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndedJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->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;
}
}
}

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndsSoonJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->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;
}
}
}

View File

@@ -2,8 +2,8 @@
namespace App\Livewire\Admin; namespace App\Livewire\Admin;
use App\Models\Team;
use App\Models\User; use App\Models\User;
use Illuminate\Container\Attributes\Auth as AttributesAuth;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
@@ -43,17 +43,13 @@ class Index extends Component
public function getSubscribers() public function getSubscribers()
{ {
$this->inactiveSubscribers = User::whereDoesntHave('teams', function ($query) { $this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count();
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); $this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count();
})->count();
$this->activeSubscribers = User::whereHas('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->count();
} }
public function switchUser(int $user_id) public function switchUser(int $user_id)
{ {
if (AttributesAuth::id() !== 0) { if (Auth::id() !== 0) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$user = User::find($user_id); $user = User::find($user_id);

View File

@@ -66,11 +66,15 @@ class Index extends Component
public bool $serverReachable = true; public bool $serverReachable = true;
public ?string $minDockerVersion = null;
public function mount() public function mount()
{ {
if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) { if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$this->minDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.');
$this->privateKeyName = generate_random_name(); $this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name(); $this->remoteServerName = generate_random_name();
if (isDev()) { if (isDev()) {

View File

@@ -1,37 +0,0 @@
<?php
namespace App\Livewire\Dev;
use Livewire\Component;
class Compose extends Component
{
public string $compose = '';
public string $base64 = '';
public $services;
public function mount()
{
$this->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');
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Server;
use App\Actions\Proxy\CheckConfiguration; use App\Actions\Proxy\CheckConfiguration;
use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\SaveConfiguration;
use App\Actions\Proxy\StartProxy;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -44,14 +43,13 @@ class Proxy extends Component
public function selectProxy($proxy_type) public function selectProxy($proxy_type)
{ {
$this->server->proxy->set('status', 'exited'); try {
$this->server->proxy->set('type', $proxy_type); $this->server->changeProxy($proxy_type, async: false);
$this->server->save(); $this->selectedProxy = $this->server->proxy->type;
$this->selectedProxy = $this->server->proxy->type; $this->dispatch('proxyStatusUpdated');
if ($this->server->proxySet()) { } catch (\Throwable $e) {
StartProxy::run($this->server, false); return handleError($e, $this);
} }
$this->dispatch('proxyStatusUpdated');
} }
public function instantSave() public function instantSave()

View File

@@ -127,7 +127,14 @@ class Show extends Component
$this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl; $this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl;
$this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled; $this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled;
$this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled; $this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled;
$this->server->settings->server_timezone = $this->serverTimezone;
if (! validate_timezone($this->serverTimezone)) {
$this->serverTimezone = config('app.timezone');
throw new \Exception('Invalid timezone.');
} else {
$this->server->settings->server_timezone = $this->serverTimezone;
}
$this->server->settings->save(); $this->server->settings->save();
} else { } else {
$this->name = $this->server->name; $this->name = $this->server->name;

View File

@@ -159,7 +159,8 @@ class ValidateAndInstall extends Component
$this->dispatch('refreshBoardingIndex'); $this->dispatch('refreshBoardingIndex');
$this->dispatch('success', 'Server validated.'); $this->dispatch('success', 'Server validated.');
} else { } else {
$this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.'; $requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.');
$this->error = 'Minimum Docker Engine version '.$requiredDockerVersion.' is not instaled. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$this->server->update([ $this->server->update([
'validation_logs' => $this->error, 'validation_logs' => $this->error,
]); ]);

View File

@@ -139,6 +139,14 @@ class Index extends Component
$error_show = false; $error_show = false;
$this->server = Server::findOrFail(0); $this->server = Server::findOrFail(0);
$this->resetErrorBag(); $this->resetErrorBag();
if (! validate_timezone($this->instance_timezone)) {
$this->instance_timezone = config('app.timezone');
throw new \Exception('Invalid timezone.');
} else {
$this->settings->instance_timezone = $this->instance_timezone;
}
if ($this->settings->public_port_min > $this->settings->public_port_max) { if ($this->settings->public_port_min > $this->settings->public_port_max) {
$this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.'); $this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.');

View File

@@ -1,58 +0,0 @@
<?php
namespace App\Livewire\Settings;
use App\Actions\License\CheckResaleLicense;
use App\Models\InstanceSettings;
use Livewire\Component;
class License extends Component
{
public InstanceSettings $settings;
public ?string $instance_id = null;
protected $rules = [
'settings.resale_license' => 'nullable',
'settings.is_resale_license_active' => 'nullable',
];
protected $validationAttributes = [
'settings.resale_license' => 'License',
'instance_id' => 'Instance Id (Do not change this)',
'settings.is_resale_license_active' => 'Is License Active',
];
public function mount()
{
if (! isCloud()) {
abort(404);
}
if (! isInstanceAdmin()) {
return redirect()->route('home');
}
$this->instance_id = config('app.id');
$this->settings = instanceSettings();
}
public function render()
{
return view('livewire.settings.license');
}
public function submit()
{
$this->validate();
$this->settings->save();
if ($this->settings->resale_license) {
try {
CheckResaleLicense::run();
$this->dispatch('reloadWindow');
} catch (\Throwable $e) {
session()->flash('error', 'Something went wrong. Please contact support. <br>Error: '.$e->getMessage());
return redirect()->route('settings.license');
}
}
}
}

View File

@@ -14,13 +14,25 @@ class Index extends Component
public $containers = []; public $containers = [];
public bool $isLoadingContainers = true;
public function mount() public function mount()
{ {
if (! auth()->user()->isAdmin()) { if (! auth()->user()->isAdmin()) {
abort(403); abort(403);
} }
$this->servers = Server::isReachable()->get(); $this->servers = Server::isReachable()->get();
$this->containers = $this->getAllActiveContainers(); }
public function loadContainers()
{
try {
$this->containers = $this->getAllActiveContainers();
} catch (\Exception $e) {
return handleError($e, $this);
} finally {
$this->isLoadingContainers = false;
}
} }
private function getAllActiveContainers() private function getAllActiveContainers()

View File

@@ -27,7 +27,7 @@ class Index extends Component
public function mount() public function mount()
{ {
if (config('coolify.waitlist') == false) { if (config('constants.waitlist.enabled') == false) {
return redirect()->route('register'); return redirect()->route('register');
} }
$this->waitingInLine = Waitlist::whereVerified(true)->count(); $this->waitingInLine = Waitlist::whereVerified(true)->count();

View File

@@ -906,21 +906,7 @@ class Application extends BaseModel
public function customRepository() public function customRepository()
{ {
preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches); return convertGitUrl($this->git_repository, $this->deploymentType(), $this->source);
$port = 22;
if (count($matches) === 1) {
$port = $matches[0];
$gitHost = str($this->git_repository)->before(':');
$gitRepo = str($this->git_repository)->after('/');
$repository = "$gitHost:$gitRepo";
} else {
$repository = $this->git_repository;
}
return [
'repository' => $repository,
'port' => $port,
];
} }
public function generateBaseDir(string $uuid) public function generateBaseDir(string $uuid)
@@ -953,6 +939,122 @@ class Application extends BaseModel
return $git_clone_command; return $git_clone_command;
} }
public function getGitRemoteStatus(string $deployment_uuid)
{
try {
['commands' => $lsRemoteCommand] = $this->generateGitLsRemoteCommands(deployment_uuid: $deployment_uuid, exec_in_docker: false);
instant_remote_process([$lsRemoteCommand], $this->destination->server, true);
return [
'is_accessible' => true,
'error' => null,
];
} catch (\RuntimeException $ex) {
return [
'is_accessible' => false,
'error' => $ex->getMessage(),
];
}
}
public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_in_docker = true)
{
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
$commands = collect([]);
$base_command = 'git ls-remote';
if ($this->deploymentType() === 'source') {
$source_html_url = data_get($this, 'source.html_url');
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$base_command = "{$base_command} {$this->source->html_url}/{$customRepository}";
} else {
$github_access_token = generate_github_installation_token($this->source);
if ($exec_in_docker) {
$base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
} else {
$base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
}
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
} else {
$commands->push($base_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
}
}
if ($this->deploymentType() === 'deploy_key') {
$fullRepoUrl = $customRepository;
$private_key = data_get($this, 'private_key.private_key');
if (is_null($private_key)) {
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$base_comamnd = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$base_command} {$customRepository}";
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
]);
} else {
$commands = collect([
'mkdir -p /root/.ssh',
"echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null",
'chmod 600 /root/.ssh/id_rsa',
]);
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_comamnd));
} else {
$commands->push($base_comamnd);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
}
if ($this->deploymentType() === 'other') {
$fullRepoUrl = $customRepository;
$base_command = "{$base_command} {$customRepository}";
$base_command = $this->setGitImportSettings($deployment_uuid, $base_command, public: true);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
} else {
$commands->push($base_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
}
}
public function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null) public function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null)
{ {
$branch = $this->git_branch; $branch = $this->git_branch;
@@ -1214,6 +1316,11 @@ class Application extends BaseModel
$workdir = rtrim($this->base_directory, '/'); $workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location; $composeFile = $this->docker_compose_location;
$fileList = collect([".$workdir$composeFile"]); $fileList = collect([".$workdir$composeFile"]);
$gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid);
if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
}
$commands = collect([ $commands = collect([
"rm -rf /tmp/{$uuid}", "rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}", "mkdir -p /tmp/{$uuid}",

View File

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Actions\Proxy\StartProxy;
use App\Actions\Server\InstallDocker; use App\Actions\Server\InstallDocker;
use App\Actions\Server\StartSentinel; use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
@@ -26,22 +27,23 @@ use Symfony\Component\Yaml\Yaml;
description: 'Server model', description: 'Server model',
type: 'object', type: 'object',
properties: [ properties: [
'id' => ['type' => 'integer'], 'id' => ['type' => 'integer', 'description' => 'The server ID.'],
'uuid' => ['type' => 'string'], 'uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'name' => ['type' => 'string'], 'name' => ['type' => 'string', 'description' => 'The server name.'],
'description' => ['type' => 'string'], 'description' => ['type' => 'string', 'description' => 'The server description.'],
'ip' => ['type' => 'string'], 'ip' => ['type' => 'string', 'description' => 'The IP address.'],
'user' => ['type' => 'string'], 'user' => ['type' => 'string', 'description' => 'The user.'],
'port' => ['type' => 'integer'], 'port' => ['type' => 'integer', 'description' => 'The port number.'],
'proxy' => ['type' => 'object'], 'proxy' => ['type' => 'object', 'description' => 'The proxy configuration.'],
'high_disk_usage_notification_sent' => ['type' => 'boolean'], 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'],
'unreachable_notification_sent' => ['type' => 'boolean'], 'high_disk_usage_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the high disk usage notification has been sent.'],
'unreachable_count' => ['type' => 'integer'], 'unreachable_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unreachable notification has been sent.'],
'validation_logs' => ['type' => 'string'], 'unreachable_count' => ['type' => 'integer', 'description' => 'The unreachable count for your server.'],
'log_drain_notification_sent' => ['type' => 'boolean'], 'validation_logs' => ['type' => 'string', 'description' => 'The validation logs.'],
'swarm_cluster' => ['type' => 'string'], 'log_drain_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the log drain notification has been sent.'],
'delete_unused_volumes' => ['type' => 'boolean'], 'swarm_cluster' => ['type' => 'string', 'description' => 'The swarm cluster configuration.'],
'delete_unused_networks' => ['type' => 'boolean'], 'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'],
'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'],
] ]
)] )]
@@ -462,7 +464,7 @@ $schema://$host {
public function proxyPath() public function proxyPath()
{ {
$base_path = config('coolify.base_config_path'); $base_path = config('constants.coolify.base_config_path');
$proxyType = $this->proxyType(); $proxyType = $this->proxyType();
$proxy_path = "$base_path/proxy"; $proxy_path = "$base_path/proxy";
// TODO: should use /traefik for already exisiting configurations? // TODO: should use /traefik for already exisiting configurations?
@@ -1251,4 +1253,25 @@ $schema://$host {
{ {
return instant_remote_process(['docker restart '.$containerName], $this, false); return instant_remote_process(['docker restart '.$containerName], $this, false);
} }
public function changeProxy(string $proxyType, bool $async = true)
{
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
return str($proxyType->value)->lower();
});
if ($validProxyTypes->contains(str($proxyType)->lower())) {
$this->proxy->set('type', str($proxyType)->upper());
$this->proxy->set('status', 'exited');
$this->save();
if ($this->proxySet()) {
if ($async) {
StartProxy::dispatch($this);
} else {
StartProxy::run($this);
}
}
} else {
throw new \Exception('Invalid proxy type.');
}
}
} }

View File

@@ -1171,7 +1171,7 @@ class Service extends BaseModel
$services = get_service_templates(); $services = get_service_templates();
$service = data_get($services, str($this->name)->beforeLast('-')->value, []); $service = data_get($services, str($this->name)->beforeLast('-')->value, []);
return data_get($service, 'documentation', config('constants.docs.base_url')); return data_get($service, 'documentation', config('constants.urls.docs'));
} }
public function applications() public function applications()

View File

@@ -178,7 +178,7 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack
{ {
return Attribute::make( return Attribute::make(
get: function () { get: function () {
if (config('coolify.self_hosted') || $this->id === 0) { if (config('constants.coolify.self_hosted') || $this->id === 0) {
$subscription = 'self-hosted'; $subscription = 'self-hosted';
} else { } else {
$subscription = data_get($this, 'subscription'); $subscription = data_get($this, 'subscription');
@@ -263,8 +263,15 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack
return $this->hasMany(S3Storage::class)->where('is_usable', true); return $this->hasMany(S3Storage::class)->where('is_usable', true);
} }
public function trialEnded() public function subscriptionEnded()
{ {
$this->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,
]);
foreach ($this->servers as $server) { foreach ($this->servers as $server) {
$server->settings()->update([ $server->settings()->update([
'is_usable' => false, 'is_usable' => false,
@@ -273,16 +280,6 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack
} }
} }
public function trialEndedButSubscribed()
{
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => true,
'is_reachable' => true,
]);
}
}
public function isAnyNotificationEnabled() public function isAnyNotificationEnabled()
{ {
if (isCloud()) { if (isCloud()) {

View File

@@ -50,7 +50,7 @@ class FortifyServiceProvider extends ServiceProvider
if (! $settings->is_registration_enabled) { if (! $settings->is_registration_enabled) {
return redirect()->route('login'); return redirect()->route('login');
} }
if (config('coolify.waitlist')) { if (config('constants.waitlist.enabled')) {
return redirect()->route('waitlist.index'); return redirect()->route('waitlist.index');
} else { } else {
return view('auth.register', [ return view('auth.register', [

View File

@@ -109,7 +109,8 @@ function format_docker_envs_to_json($rawOutput)
function checkMinimumDockerEngineVersion($dockerVersion) function checkMinimumDockerEngineVersion($dockerVersion)
{ {
$majorDockerVersion = str($dockerVersion)->before('.')->value(); $majorDockerVersion = str($dockerVersion)->before('.')->value();
if ($majorDockerVersion <= 22) { $requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.')->value();
if ($majorDockerVersion < $requiredDockerVersion) {
$dockerVersion = null; $dockerVersion = null;
} }
@@ -225,15 +226,13 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
case $type?->contains('minio'): case $type?->contains('minio'):
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); $MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first(); $MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
return $payload; if (str($MINIO_BROWSER_REDIRECT_URL->value)->isEmpty()) {
}
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
$MINIO_BROWSER_REDIRECT_URL?->update([ $MINIO_BROWSER_REDIRECT_URL?->update([
'value' => generateFqdn($server, 'console-'.$uuid, true), 'value' => generateFqdn($server, 'console-'.$uuid, true),
]); ]);
} }
if (is_null($MINIO_SERVER_URL?->value)) { if (str($MINIO_SERVER_URL->value)->isEmpty()) {
$MINIO_SERVER_URL?->update([ $MINIO_SERVER_URL?->update([
'value' => generateFqdn($server, 'minio-'.$uuid, true), 'value' => generateFqdn($server, 'minio-'.$uuid, true),
]); ]);
@@ -246,15 +245,13 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
case $type?->contains('logto'): case $type?->contains('logto'):
$LOGTO_ENDPOINT = $variables->where('key', 'LOGTO_ENDPOINT')->first(); $LOGTO_ENDPOINT = $variables->where('key', 'LOGTO_ENDPOINT')->first();
$LOGTO_ADMIN_ENDPOINT = $variables->where('key', 'LOGTO_ADMIN_ENDPOINT')->first(); $LOGTO_ADMIN_ENDPOINT = $variables->where('key', 'LOGTO_ADMIN_ENDPOINT')->first();
if (is_null($LOGTO_ENDPOINT) || is_null($LOGTO_ADMIN_ENDPOINT)) {
return $payload; if (str($LOGTO_ENDPOINT?->value)->isEmpty()) {
}
if (is_null($LOGTO_ENDPOINT?->value)) {
$LOGTO_ENDPOINT?->update([ $LOGTO_ENDPOINT?->update([
'value' => generateFqdn($server, 'logto-'.$uuid), 'value' => generateFqdn($server, 'logto-'.$uuid),
]); ]);
} }
if (is_null($LOGTO_ADMIN_ENDPOINT?->value)) { if (str($LOGTO_ADMIN_ENDPOINT?->value)->isEmpty()) {
$LOGTO_ADMIN_ENDPOINT?->update([ $LOGTO_ADMIN_ENDPOINT?->update([
'value' => generateFqdn($server, 'logto-admin-'.$uuid), 'value' => generateFqdn($server, 'logto-admin-'.$uuid),
]); ]);
@@ -397,7 +394,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push("redir-ghost-{$uuid}");
} }
if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_non_www); $labels = $labels->merge($redirect_to_non_www);
@@ -420,7 +417,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push("redir-ghost-{$uuid}");
} }
if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_non_www); $labels = $labels->merge($redirect_to_non_www);
@@ -469,7 +466,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push("redir-ghost-{$uuid}");
} }
if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_non_www); $labels = $labels->merge($redirect_to_non_www);
@@ -492,7 +489,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push("redir-ghost-{$uuid}");
} }
if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) { if ($redirect_direction === 'non-www' && str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_non_www); $labels = $labels->merge($redirect_to_non_www);

View File

@@ -7,6 +7,7 @@ use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\EnvironmentVariable; use App\Models\EnvironmentVariable;
use App\Models\GithubApp;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\LocalFileVolume; use App\Models\LocalFileVolume;
use App\Models\LocalPersistentVolume; use App\Models\LocalPersistentVolume;
@@ -359,7 +360,7 @@ function isDev(): bool
function isCloud(): bool function isCloud(): bool
{ {
return !config('coolify.self_hosted'); return ! config('constants.coolify.self_hosted');
} }
function translate_cron_expression($expression_to_validate): string function translate_cron_expression($expression_to_validate): string
@@ -385,6 +386,11 @@ function validate_cron_expression($expression_to_validate): bool
return $isValid; return $isValid;
} }
function validate_timezone(string $timezone): bool
{
return in_array($timezone, timezone_identifiers_list());
}
function send_internal_notification(string $message): void function send_internal_notification(string $message): void
{ {
try { try {
@@ -994,7 +1000,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n
function getRealtime() function getRealtime()
{ {
$envDefined = env('PUSHER_PORT'); $envDefined = config('constants.pusher.port');
if (empty($envDefined)) { if (empty($envDefined)) {
$url = Url::fromString(Request::getSchemeAndHttpHost()); $url = Url::fromString(Request::getSchemeAndHttpHost());
$port = $url->getPort(); $port = $url->getPort();
@@ -4098,3 +4104,53 @@ function defaultNginxConfiguration(): string
} }
}'; }';
} }
function convertGitUrl(string $gitRepository, string $deploymentType, ?GithubApp $source = null): array
{
$repository = $gitRepository;
$providerInfo = [
'host' => null,
'user' => 'git',
'port' => 22,
'repository' => $gitRepository,
];
$sshMatches = [];
$matches = [];
// Let's try and parse the string to detect if it's a valid SSH string or not
preg_match('/((.*?)\:\/\/)?(.*@.*:.*)/', $gitRepository, $sshMatches);
if ($deploymentType === 'deploy_key' && empty($sshMatches) && $source) {
// If this happens, the user may have provided an HTTP URL when they needed an SSH one
// Let's try and fix that for known Git providers
switch ($source->getMorphClass()) {
case \App\Models\GithubApp::class:
$providerInfo['host'] = Url::fromString($source->html_url)->getHost();
$providerInfo['port'] = $source->custom_port;
$providerInfo['user'] = $source->custom_user;
break;
}
if (! empty($providerInfo['host'])) {
// Until we do not support more providers with App (like GithubApp), this will be always true, port will be 22
if ($providerInfo['port'] === 22) {
$repository = "{$providerInfo['user']}@{$providerInfo['host']}:{$providerInfo['repository']}";
} else {
$repository = "ssh://{$providerInfo['user']}@{$providerInfo['host']}:{$providerInfo['port']}/{$providerInfo['repository']}";
}
}
}
preg_match('/(?<=:)\d+(?=\/)/', $gitRepository, $matches);
if (count($matches) === 1) {
$providerInfo['port'] = $matches[0];
$gitHost = str($gitRepository)->before(':');
$gitRepo = str($gitRepository)->after('/');
$repository = "$gitHost:$gitRepo";
}
return [
'repository' => $repository,
'port' => $providerInfo['port'],
];
}

View File

@@ -12,14 +12,15 @@
], ],
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"3sidedcube/laravel-redoc": "^1.0",
"danharrin/livewire-rate-limiting": "^1.1", "danharrin/livewire-rate-limiting": "^1.1",
"doctrine/dbal": "^3.6", "doctrine/dbal": "^4.2",
"guzzlehttp/guzzle": "^7.5.0", "guzzlehttp/guzzle": "^7.5.0",
"laravel/fortify": "^1.16.0", "laravel/fortify": "^1.16.0",
"laravel/framework": "^11", "laravel/framework": "^11.0",
"laravel/horizon": "^5.29.1", "laravel/horizon": "^5.29.1",
"laravel/pail": "^1.1", "laravel/pail": "^1.1",
"laravel/prompts": "^0.1.6", "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
"laravel/socialite": "^5.14.0", "laravel/socialite": "^5.14.0",
"laravel/tinker": "^2.8.1", "laravel/tinker": "^2.8.1",
@@ -27,7 +28,7 @@
"lcobucci/jwt": "^5.0.0", "lcobucci/jwt": "^5.0.0",
"league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-aws-s3-v3": "^3.0",
"league/flysystem-sftp-v3": "^3.0", "league/flysystem-sftp-v3": "^3.0",
"livewire/livewire": "3.4.9", "livewire/livewire": "^3.5",
"log1x/laravel-webfonts": "^1.0", "log1x/laravel-webfonts": "^1.0",
"lorisleiva/laravel-actions": "^2.7", "lorisleiva/laravel-actions": "^2.7",
"nubs/random-name-generator": "^2.2", "nubs/random-name-generator": "^2.2",
@@ -36,17 +37,16 @@
"poliander/cron": "^3.0", "poliander/cron": "^3.0",
"purplepixie/phpdns": "^2.1", "purplepixie/phpdns": "^2.1",
"pusher/pusher-php-server": "^7.2", "pusher/pusher-php-server": "^7.2",
"resend/resend-laravel": "^0.13.0", "resend/resend-laravel": "^0.15.0",
"sentry/sentry-laravel": "^4.6", "sentry/sentry-laravel": "^4.6",
"socialiteproviders/microsoft-azure": "^5.1", "socialiteproviders/microsoft-azure": "^5.1",
"spatie/laravel-activitylog": "^4.7.3", "spatie/laravel-activitylog": "^4.7.3",
"spatie/laravel-data": "^3.4.3", "spatie/laravel-data": "^4.11",
"spatie/laravel-ray": "^1.32.4",
"spatie/laravel-schemaless-attributes": "^2.4", "spatie/laravel-schemaless-attributes": "^2.4",
"spatie/url": "^2.2", "spatie/url": "^2.2",
"stripe/stripe-php": "^12.0", "stripe/stripe-php": "^16.2.0",
"symfony/yaml": "^6.2", "symfony/yaml": "^7.1.6",
"visus/cuid2": "^2.0.0", "visus/cuid2": "^4.1.0",
"yosymfony/toml": "^1.0", "yosymfony/toml": "^1.0",
"zircote/swagger-php": "^4.10" "zircote/swagger-php": "^4.10"
}, },
@@ -58,12 +58,13 @@
"laravel/telescope": "^5.2", "laravel/telescope": "^5.2",
"mockery/mockery": "^1.5.1", "mockery/mockery": "^1.5.1",
"nunomaduro/collision": "^8.1", "nunomaduro/collision": "^8.1",
"pestphp/pest": "^2.16", "pestphp/pest": "^3.5",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.12.10",
"phpunit/phpunit": "^10.0.19", "phpunit/phpunit": "^11.4",
"serversideup/spin": "^1.1.0", "serversideup/spin": "^2.3",
"spatie/laravel-ignition": "^2.1.0", "spatie/laravel-ignition": "^2.1.0",
"symfony/http-client": "^6.2" "spatie/laravel-ray": "^1.37",
"symfony/http-client": "^7.1"
}, },
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true, "prefer-stable": true,
@@ -119,4 +120,4 @@
"@php artisan key:generate --ansi" "@php artisan key:generate --ansi"
] ]
} }
} }

3552
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,47 @@
<?php <?php
return [ return [
'docker_install_version' => '26.0', 'coolify' => [
'docs' => [ 'version' => '4.0.0-beta.368',
'base_url' => 'https://coolify.io/docs', 'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE', false),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper'),
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
],
'urls' => [
'docs' => 'https://coolify.io/docs',
'contact' => 'https://coolify.io/docs/contact', 'contact' => 'https://coolify.io/docs/contact',
], ],
'services' => [
// Temporary disabled until cache is implemented
// 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json',
],
'terminal' => [
'protocol' => env('TERMINAL_PROTOCOL'),
'host' => env('TERMINAL_HOST'),
'port' => env('TERMINAL_PORT'),
],
'pusher' => [
'host' => env('PUSHER_HOST'),
'port' => env('PUSHER_PORT'),
'app_key' => env('PUSHER_APP_KEY'),
],
'horizon' => [
'is_horizon_enabled' => env('HORIZON_ENABLED', true),
'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true),
],
'docker' => [
'minimum_required_version' => '26.0',
],
'ssh' => [ 'ssh' => [
'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true)), 'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true)),
'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', 3600), 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', 3600),
@@ -13,20 +49,14 @@ return [
'server_interval' => 20, 'server_interval' => 20,
'command_timeout' => 7200, 'command_timeout' => 7200,
], ],
'waitlist' => [
'expiration' => 10,
],
'invitation' => [ 'invitation' => [
'link' => [ 'link' => [
'base_url' => '/invitations/', 'base_url' => '/invitations/',
'expiration_days' => 3, 'expiration_days' => 3,
], ],
], ],
'services' => [
// Temporary disabled until cache is implemented
// 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json',
],
'limits' => [ 'limits' => [
'trial_period' => 0, 'trial_period' => 0,
'server' => [ 'server' => [
@@ -46,4 +76,23 @@ return [
'dynamic' => true, 'dynamic' => true,
], ],
], ],
'waitlist' => [
'enabled' => env('WAITLIST', false),
'expiration' => 10,
],
'sentry' => [
'sentry_dsn' => env('SENTRY_DSN'),
],
'webhooks' => [
'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'),
'dev_webhook' => env('SERVEO_URL'),
],
'bunny' => [
'storage_api_key' => env('BUNNY_STORAGE_API_KEY'),
'api_key' => env('BUNNY_API_KEY'),
],
]; ];

View File

@@ -1,17 +0,0 @@
<?php
return [
'sentry_dsn' => env('SENTRY_DSN'),
'docs' => 'https://coolify.io/docs/',
'contact' => 'https://coolify.io/docs/contact',
'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'),
'self_hosted' => env('SELF_HOSTED', true),
'waitlist' => env('WAITLIST', false),
'license_url' => 'https://licenses.coollabs.io',
'dev_webhook' => env('SERVEO_URL'),
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper'),
'is_horizon_enabled' => env('HORIZON_ENABLED', true),
'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true),
];

28
config/redoc.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Directory
|--------------------------------------------------------------------------
|
| The name of the directory where your OpenAPI definitions are stored.
|
*/
'directory' => '',
/*
|--------------------------------------------------------------------------
| Variables
|--------------------------------------------------------------------------
|
| You can automatically replace variables in your OpenAPI definitions by
| adding a key value pair to the array below. This will replace any
| instances of :key with the given value.
|
*/
'variables' => [],
];

View File

@@ -3,11 +3,11 @@
return [ return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => config('coolify.sentry_dsn'), 'dsn' => config('constants.sentry.sentry_dsn'),
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.367', 'release' => config('constants.coolify.version'),
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.367'; return '4.0.0-beta.368';

View File

@@ -24,9 +24,8 @@ class PopulateSshKeysDirectorySeeder extends Seeder
}); });
if (isDev()) { if (isDev()) {
$user = env('PUID').':'.env('PGID'); Process::run('chown -R 9999:9999 '.storage_path('app/ssh/keys'));
Process::run("chown -R $user ".storage_path('app/ssh/keys')); Process::run('chown -R 9999:9999 '.storage_path('app/ssh/mux'));
Process::run("chown -R $user ".storage_path('app/ssh/mux'));
} else { } else {
Process::run('chown -R 9999:root '.storage_path('app/ssh/keys')); Process::run('chown -R 9999:root '.storage_path('app/ssh/keys'));
Process::run('chown -R 9999:root '.storage_path('app/ssh/mux')); Process::run('chown -R 9999:root '.storage_path('app/ssh/mux'));

View File

@@ -100,7 +100,7 @@ class ProductionSeeder extends Seeder
} }
} }
if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) { if (! isCloud() && config('constants.coolify.is_windows_docker_desktop') == false) {
$coolify_key_name = '@host.docker.internal'; $coolify_key_name = '@host.docker.internal';
$ssh_keys_directory = Storage::disk('ssh-keys')->files(); $ssh_keys_directory = Storage::disk('ssh-keys')->files();
$coolify_key = collect($ssh_keys_directory)->firstWhere(fn ($item) => str($item)->contains($coolify_key_name)); $coolify_key = collect($ssh_keys_directory)->firstWhere(fn ($item) => str($item)->contains($coolify_key_name));
@@ -127,7 +127,7 @@ class ProductionSeeder extends Seeder
} }
} }
} }
if (config('coolify.is_windows_docker_desktop')) { if (config('constants.coolify.is_windows_docker_desktop')) {
PrivateKey::updateOrCreate( PrivateKey::updateOrCreate(
[ [
'id' => 0, 'id' => 0,

View File

@@ -60,7 +60,7 @@ services:
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"] entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"]
vite: vite:
image: node:20 image: node:20-alpine
pull_policy: always pull_policy: always
working_dir: /var/www/html working_dir: /var/www/html
environment: environment:
@@ -74,8 +74,9 @@ services:
networks: networks:
- coolify - coolify
testing-host: testing-host:
image: "ghcr.io/coollabsio/coolify-testing-host:latest" build:
pull_policy: always context: .
dockerfile: ./docker/testing-host/Dockerfile
init: true init: true
container_name: coolify-testing-host container_name: coolify-testing-host
volumes: volumes:
@@ -88,7 +89,7 @@ services:
networks: networks:
- coolify - coolify
mailpit: mailpit:
image: "axllent/mailpit:latest" image: axllent/mailpit:latest
pull_policy: always pull_policy: always
container_name: coolify-mail container_name: coolify-mail
ports: ports:

View File

@@ -86,7 +86,7 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
redis: redis:
image: redis:alpine image: redis:7-alpine
pull_policy: always pull_policy: always
container_name: coolify-redis container_name: coolify-redis
restart: always restart: always

View File

@@ -4,7 +4,7 @@ services:
restart: always restart: always
working_dir: /var/www/html working_dir: /var/www/html
extra_hosts: extra_hosts:
- 'host.docker.internal:host-gateway' - host.docker.internal:host-gateway
networks: networks:
- coolify - coolify
depends_on: depends_on:
@@ -18,7 +18,7 @@ services:
networks: networks:
- coolify - coolify
redis: redis:
image: redis:alpine image: redis:7-alpine
container_name: coolify-redis container_name: coolify-redis
restart: always restart: always
networks: networks:
@@ -26,7 +26,7 @@ services:
soketi: soketi:
container_name: coolify-realtime container_name: coolify-realtime
extra_hosts: extra_hosts:
- 'host.docker.internal:host-gateway' - host.docker.internal:host-gateway
restart: always restart: always
networks: networks:
- coolify - coolify

View File

@@ -1,16 +1,29 @@
FROM alpine:3.17 # Versions
# https://hub.docker.com/_/alpine
ARG TARGETPLATFORM ARG BASE_IMAGE=alpine:3.20
# https://download.docker.com/linux/static/stable/ # https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=26.1.3 ARG DOCKER_VERSION=27.3.1
# https://github.com/docker/compose/releases # https://github.com/docker/compose/releases
ARG DOCKER_COMPOSE_VERSION=2.27.1 ARG DOCKER_COMPOSE_VERSION=2.30.3
# https://github.com/docker/buildx/releases # https://github.com/docker/buildx/releases
ARG DOCKER_BUILDX_VERSION=0.14.1 ARG DOCKER_BUILDX_VERSION=0.18.0
# https://github.com/buildpacks/pack/releases # https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.35.1 ARG PACK_VERSION=0.35.1
# https://github.com/railwayapp/nixpacks/releases # https://github.com/railwayapp/nixpacks/releases
ARG NIXPACKS_VERSION=1.29.0 ARG NIXPACKS_VERSION=1.29.0
# https://hub.docker.com/r/minio/mc/tags
ARG MINIO_VERSION=RELEASE.2024-03-07T00-31-49Z
FROM minio/mc:${MINIO_VERSION} AS minio-client
FROM ${BASE_IMAGE} AS base
ARG TARGETPLATFORM
ARG DOCKER_VERSION
ARG DOCKER_COMPOSE_VERSION
ARG DOCKER_BUILDX_VERSION
ARG PACK_VERSION
ARG NIXPACKS_VERSION
USER root USER root
WORKDIR /artifacts WORKDIR /artifacts
@@ -34,9 +47,8 @@ RUN if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack /root/.docker/cli-plugins/docker-buildx \ chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack /root/.docker/cli-plugins/docker-buildx \
;fi ;fi
COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc COPY --from=minio-client /usr/bin/mc /usr/bin/mc
RUN chmod +x /usr/bin/mc RUN chmod +x /usr/bin/mc
ENTRYPOINT ["/sbin/tini", "--"] ENTRYPOINT ["/sbin/tini", "--"]
CMD ["tail", "-f", "/dev/null"] CMD ["tail", "-f", "/dev/null"]

View File

@@ -7,7 +7,7 @@
"dependencies": { "dependencies": {
"@xterm/addon-fit": "0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0", "@xterm/xterm": "5.5.0",
"axios": "1.7.5", "axios": "1.7.7",
"cookie": "1.0.1", "cookie": "1.0.1",
"dotenv": "16.4.5", "dotenv": "16.4.5",
"node-pty": "1.0.0", "node-pty": "1.0.0",
@@ -36,9 +36,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.5", "version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",

View File

@@ -5,7 +5,7 @@
"@xterm/addon-fit": "0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0", "@xterm/xterm": "5.5.0",
"cookie": "1.0.1", "cookie": "1.0.1",
"axios": "1.7.5", "axios": "1.7.7",
"dotenv": "16.4.5", "dotenv": "16.4.5",
"node-pty": "1.0.0", "node-pty": "1.0.0",
"ws": "8.18.0" "ws": "8.18.0"

View File

@@ -1,10 +1,21 @@
FROM serversideup/php:8.2-fpm-nginx-v2.2.1 # Versions
# https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx-alpine
ARG SERVERSIDEUP_PHP_VERSION=8.2-fpm-nginx-v2.2.1
# https://github.com/minio/mc/releases
ARG MINIO_VERSION=RELEASE.2024-11-05T11-29-45Z
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.11.0
# https://www.postgresql.org/support/versioning/ - Can not updated automatically so keep it at 15
ARG POSTGRES_VERSION=15
FROM minio/mc:${MINIO_VERSION} AS minio-client
FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION}
ARG TARGETPLATFORM ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases ARG CLOUDFLARED_VERSION
ARG CLOUDFLARED_VERSION=2024.4.1 ARG MINIO_VERSION
ARG POSTGRES_VERSION
ARG POSTGRES_VERSION=15
# Use build arguments for caching # Use build arguments for caching
ARG BUILDTIME_DEPS="dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl" ARG BUILDTIME_DEPS="dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl"
@@ -41,7 +52,7 @@ RUN --mount=type=cache,target=/root/.cache \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi" ;fi"
COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc COPY --from=minio-client /usr/bin/mc /usr/bin/mc
RUN chmod +x /usr/bin/mc RUN chmod +x /usr/bin/mc
RUN { \ RUN { \

View File

@@ -1,4 +1,15 @@
FROM serversideup/php:8.2-fpm-nginx-v2.2.1 AS base # Versions
# https://hub.docker.com/r/serversideup/php/tags?name=8.3-fpm-nginx-alpine
ARG SERVERSIDEUP_PHP_VERSION=8.2-fpm-nginx-v2.2.1
# https://github.com/minio/mc/releases
ARG MINIO_VERSION=RELEASE.2024-11-05T11-29-45Z
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.11.0
# https://www.postgresql.org/support/versioning/ - Can not updated automatically so keep it at 15
ARG POSTGRES_VERSION=15
FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION} AS base
WORKDIR /var/www/html WORKDIR /var/www/html
COPY composer.json composer.lock ./ COPY composer.json composer.lock ./
@@ -11,12 +22,13 @@ COPY --from=base --chown=9999:9999 /var/www/html .
RUN npm install RUN npm install
RUN npm run build RUN npm run build
FROM serversideup/php:8.2-fpm-nginx-v2.2.1 FROM minio/mc:${MINIO_VERSION} AS minio-client
FROM serversideup/php:${SERVERSIDEUP_PHP_VERSION}
ARG TARGETPLATFORM ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases ARG CLOUDFLARED_VERSION
ARG CLOUDFLARED_VERSION=2024.4.1 ARG POSTGRES_VERSION
ARG POSTGRES_VERSION=15
ARG CI=true ARG CI=true
WORKDIR /var/www/html WORKDIR /var/www/html
@@ -29,7 +41,7 @@ RUN curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmo
RUN echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list RUN echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list
RUN apt-get update RUN apt-get update
RUN apt-get install postgresql-client-$POSTGRES_VERSION -y RUN apt-get install postgresql-client-${POSTGRES_VERSION} -y
# Coolify requirements # Coolify requirements
RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof vim RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof vim
@@ -47,8 +59,10 @@ COPY --chmod=755 docker/prod/etc/s6-overlay/ /etc/s6-overlay/
RUN php artisan route:clear RUN php artisan route:clear
RUN php artisan view:clear RUN php artisan view:clear
RUN php artisan config:clear
RUN php artisan route:cache RUN php artisan route:cache
RUN php artisan view:cache RUN php artisan view:cache
RUN php artisan config:cache
RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc
RUN echo "alias a='php artisan'" >>/etc/bash.bashrc RUN echo "alias a='php artisan'" >>/etc/bash.bashrc
@@ -71,5 +85,5 @@ RUN { \
echo 'post_max_size=256M'; \ echo 'post_max_size=256M'; \
} > /etc/php/current_version/cli/conf.d/upload-limits.ini } > /etc/php/current_version/cli/conf.d/upload-limits.ini
COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc COPY --from=minio-client /usr/bin/mc /usr/bin/mc
RUN chmod +x /usr/bin/mc RUN chmod +x /usr/bin/mc

View File

@@ -1,16 +1,22 @@
# Versions
# https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=27.3.1
# https://github.com/docker/compose/releases
ARG DOCKER_COMPOSE_VERSION=2.30.3
# https://github.com/docker/buildx/releases
ARG DOCKER_BUILDX_VERSION=0.18.0
FROM debian:12-slim FROM debian:12-slim
ARG TARGETPLATFORM ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/ ARG DOCKER_VERSION
ARG DOCKER_VERSION=26.1.2 ARG DOCKER_COMPOSE_VERSION
# https://github.com/docker/compose/releases ARG DOCKER_BUILDX_VERSION
ARG DOCKER_COMPOSE_VERSION=2.27.0
# https://github.com/docker/buildx/releases
ARG DOCKER_BUILDX_VERSION=0.14.0
USER root USER root
WORKDIR /root WORKDIR /root
ENV PATH "$PATH:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin" ENV PATH="/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin:$PATH"
RUN apt update && apt -y install openssh-client openssh-server curl wget git jq jc RUN apt update && apt -y install openssh-client openssh-server curl wget git jq jc
RUN mkdir -p ~/.docker/cli-plugins RUN mkdir -p ~/.docker/cli-plugins

View File

@@ -1,23 +0,0 @@
# This is an example dynamic configuration.
http:
routers:
catchall:
entryPoints:
- http
- https
service: noop
rule: HostRegexp(`{catchall:.*}`)
priority: 1
middlewares:
- redirect-regexp
services:
noop:
loadBalancer:
servers:
- url: ''
middlewares:
redirect-regexp:
redirectRegex:
regex: '(.*)'
replacement: 'https://coolify.io'
permanent: false

View File

@@ -1,14 +0,0 @@
# This is an example dynamic configuration.
http:
routers:
coolify-http:
entryPoints:
- http
service: coolify
rule: Host(`coolify.io`)
services:
coolify:
loadBalancer:
servers:
-
url: 'http://coolify:80'

7985
openapi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
openapi: 3.0.0 openapi: 3.1.0
info: info:
title: Coolify title: Coolify
version: '0.1' version: '0.1'
@@ -3311,7 +3311,7 @@ paths:
type: string type: string
responses: responses:
'200': '200':
description: 'Project details' description: 'Environment details'
content: content:
application/json: application/json:
schema: schema:
@@ -3467,9 +3467,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: array $ref: '#/components/schemas/PrivateKey'
items:
$ref: '#/components/schemas/PrivateKey'
'401': '401':
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
'400': '400':
@@ -3579,6 +3577,11 @@ paths:
type: boolean type: boolean
example: false example: false
description: 'Instant validate.' description: 'Instant validate.'
proxy_type:
type: string
enum: [traefik, caddy, none]
example: traefik
description: 'The proxy type.'
type: object type: object
responses: responses:
'201': '201':
@@ -3699,6 +3702,10 @@ paths:
instant_validate: instant_validate:
type: boolean type: boolean
description: 'Instant validate.' description: 'Instant validate.'
proxy_type:
type: string
enum: [traefik, caddy, none]
description: 'The proxy type.'
type: object type: object
responses: responses:
'201': '201':
@@ -4759,6 +4766,10 @@ components:
compose_parsing_version: compose_parsing_version:
type: string type: string
description: 'How Coolify parse the compose file.' description: 'How Coolify parse the compose file.'
custom_nginx_configuration:
type: string
nullable: true
description: 'Custom Nginx configuration base64 encoded.'
type: object type: object
ApplicationDeploymentQueue: ApplicationDeploymentQueue:
description: 'Project model' description: 'Project model'
@@ -4909,36 +4920,59 @@ components:
properties: properties:
id: id:
type: integer type: integer
description: 'The server ID.'
uuid: uuid:
type: string type: string
description: 'The server UUID.'
name: name:
type: string type: string
description: 'The server name.'
description: description:
type: string type: string
description: 'The server description.'
ip: ip:
type: string type: string
description: 'The IP address.'
user: user:
type: string type: string
description: 'The user.'
port: port:
type: integer type: integer
description: 'The port number.'
proxy: proxy:
type: object type: object
description: 'The proxy configuration.'
proxy_type:
type: string
enum:
- traefik
- caddy
- none
description: 'The proxy type.'
high_disk_usage_notification_sent: high_disk_usage_notification_sent:
type: boolean type: boolean
description: 'The flag to indicate if the high disk usage notification has been sent.'
unreachable_notification_sent: unreachable_notification_sent:
type: boolean type: boolean
description: 'The flag to indicate if the unreachable notification has been sent.'
unreachable_count: unreachable_count:
type: integer type: integer
description: 'The unreachable count for your server.'
validation_logs: validation_logs:
type: string type: string
description: 'The validation logs.'
log_drain_notification_sent: log_drain_notification_sent:
type: boolean type: boolean
description: 'The flag to indicate if the log drain notification has been sent.'
swarm_cluster: swarm_cluster:
type: string type: string
description: 'The swarm cluster configuration.'
delete_unused_volumes: delete_unused_volumes:
type: boolean type: boolean
description: 'The flag to indicate if the unused volumes should be deleted.'
delete_unused_networks: delete_unused_networks:
type: boolean type: boolean
description: 'The flag to indicate if the unused networks should be deleted.'
type: object type: object
ServerSetting: ServerSetting:
description: 'Server Settings model' description: 'Server Settings model'
@@ -5136,6 +5170,9 @@ components:
smtp_notifications_database_backups: smtp_notifications_database_backups:
type: boolean type: boolean
description: 'Whether to send database backup notifications via SMTP.' description: 'Whether to send database backup notifications via SMTP.'
smtp_notifications_server_disk_usage:
type: boolean
description: 'Whether to send server disk usage notifications via SMTP.'
discord_enabled: discord_enabled:
type: boolean type: boolean
description: 'Whether Discord is enabled or not.' description: 'Whether Discord is enabled or not.'
@@ -5157,6 +5194,9 @@ components:
discord_notifications_scheduled_tasks: discord_notifications_scheduled_tasks:
type: boolean type: boolean
description: 'Whether to send scheduled task notifications via Discord.' description: 'Whether to send scheduled task notifications via Discord.'
discord_notifications_server_disk_usage:
type: boolean
description: 'Whether to send server disk usage notifications via Discord.'
show_boarding: show_boarding:
type: boolean type: boolean
description: 'Whether to show the boarding screen or not.' description: 'Whether to show the boarding screen or not.'

View File

@@ -9,7 +9,7 @@ CDN="https://cdn.coollabs.io/coolify-nightly"
DATE=$(date +"%Y%m%d-%H%M%S") DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.6" VERSION="1.6"
DOCKER_VERSION="26.0" DOCKER_VERSION="27.3"
# TODO: Ask for a user # TODO: Ask for a user
CURRENT_USER=$USER CURRENT_USER=$USER

1084
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
{ {
"name": "coolify",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -6,28 +7,24 @@
"build": "vite build" "build": "vite build"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "4.5.1", "@vitejs/plugin-vue": "5.2.0",
"autoprefixer": "10.4.19", "autoprefixer": "10.4.20",
"axios": "1.7.5", "axios": "1.7.7",
"laravel-echo": "1.16.1", "laravel-echo": "1.17.0",
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "1.0.6",
"postcss": "8.4.38", "postcss": "8.4.49",
"pusher-js": "8.4.0-rc2", "pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.4", "tailwind-scrollbar": "^3.1.0",
"vite": "4.5.5", "tailwindcss": "3.4.14",
"vue": "3.4.29" "vite": "5.4.11",
"vue": "3.5.12"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/forms": "0.5.7", "@tailwindcss/forms": "0.5.9",
"@tailwindcss/typography": "0.5.13", "@tailwindcss/typography": "0.5.15",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"alpinejs": "3.14.0", "alpinejs": "3.14.3",
"cookie": "^0.7.0", "ioredis": "5.4.1"
"dotenv": "^16.4.5",
"ioredis": "5.4.1",
"node-pty": "^1.0.0",
"tailwindcss-scrollbar": "0.1.0",
"ws": "^8.17.0"
} }
} }

View File

@@ -1,21 +0,0 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

View File

@@ -4,8 +4,6 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html, html,
body { body {
@apply h-full bg-neutral-50 text-neutral-800 dark:bg-base dark:text-neutral-400; @apply h-full bg-neutral-50 text-neutral-800 dark:bg-base dark:text-neutral-400;
@@ -172,10 +170,6 @@ section {
@apply bg-error; @apply bg-error;
} }
/* [type='checkbox']:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='black' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
} */
.menu { .menu {
@apply flex items-center gap-1; @apply flex items-center gap-1;
} }
@@ -197,7 +191,7 @@ section {
} }
.scrollbar { .scrollbar {
@apply scrollbar-thumb-coollabs-100 dark:scrollbar-track-coolgray-200 scrollbar-track-neutral-200 scrollbar-w-2; @apply scrollbar-thumb-coollabs-100 dark:scrollbar-track-coolgray-200 scrollbar-track-neutral-200 scrollbar-thin;
} }
.main { .main {

View File

@@ -1,32 +0,0 @@
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
// import axios from 'axios';
// window.axios = axios;
// window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// import Pusher from 'pusher-js';
// window.Pusher = Pusher;
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: import.meta.env.VITE_PUSHER_APP_KEY,
// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
// wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
// enabledTransports: ['ws', 'wss'],
// });

View File

@@ -18,7 +18,7 @@
@else @else
<div>Transactional emails are not active on this instance.</div> <div>Transactional emails are not active on this instance.</div>
<div>See how to set it in our <a class="dark:text-white" target="_blank" <div>See how to set it in our <a class="dark:text-white" target="_blank"
href="{{ config('constants.docs.base_url') }}">docs</a>, or how to href="{{ config('constants.urls.docs') }}">docs</a>, or how to
manually reset password. manually reset password.
</div> </div>
@endif @endif

View File

@@ -14,8 +14,8 @@
'w-full' => $fullWidth, 'w-full' => $fullWidth,
])> ])>
@if (!$hideLabel) @if (!$hideLabel)
<label @class(['flex gap-4 px-0 min-w-fit label', 'opacity-40' => $disabled])> <label @class(['flex gap-4 items-center px-0 min-w-fit label w-full cursor-pointer', 'opacity-40' => $disabled])>
<span class="flex gap-2"> <span class="flex flex-grow gap-2">
@if ($label) @if ($label)
{!! $label !!} {!! $label !!}
@else @else
@@ -25,11 +25,11 @@
<x-helper :helper="$helper" /> <x-helper :helper="$helper" />
@endif @endif
</span> </span>
@endif
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
wire:model={{ $id }} @else wire:model={{ $value ?? $id }} @endif />
@if (!$hideLabel)
</label> </label>
@endif @endif
<span class="flex-grow"></span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
wire:model={{ $id }} @else wire:model={{ $value ?? $id }} @endif />
</div> </div>

View File

@@ -28,7 +28,7 @@
$disableTwoStepConfirmation = data_get(InstanceSettings::get(), 'disable_two_step_confirmation'); $disableTwoStepConfirmation = data_get(InstanceSettings::get(), 'disable_two_step_confirmation');
@endphp @endphp
<div x-data="{ <div wire:ignore x-data="{
modalOpen: false, modalOpen: false,
step: {{ empty($checkboxes) ? 2 : 1 }}, step: {{ empty($checkboxes) ? 2 : 1 }},
initialStep: {{ empty($checkboxes) ? 2 : 1 }}, initialStep: {{ empty($checkboxes) ? 2 : 1 }},
@@ -106,8 +106,8 @@
this.selectedActions.push(id); this.selectedActions.push(id);
} }
} }
}" @keydown.escape.window="modalOpen = false; resetModal()" :class="{ 'z-40': modalOpen }" }" @keydown.escape.window="modalOpen = false; resetModal()"
class="relative w-auto h-auto"> :class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
@if ($customButton) @if ($customButton)
@if ($buttonFullWidth) @if ($buttonFullWidth)
<x-forms.button @click="modalOpen=true" class="w-full"> <x-forms.button @click="modalOpen=true" class="w-full">
@@ -302,7 +302,8 @@
</x-forms.button> </x-forms.button>
@endif @endif
<x-forms.button <x-forms.button
x-bind:disabled="!disableTwoStepConfirmation && confirmWithText && userConfirmationText !== confirmationText" x-bind:disabled="!disableTwoStepConfirmation && confirmWithText && userConfirmationText !==
confirmationText"
class="w-auto" isError class="w-auto" isError
@click=" @click="
if (dispatchEvent) { if (dispatchEvent) {
@@ -337,11 +338,14 @@
Your Password Your Password
</label> </label>
<form @submit.prevent="false" @keydown.enter.prevent> <form @submit.prevent="false" @keydown.enter.prevent>
<input type="text" name="username" autocomplete="username" value="{{ auth()->user()->email }}" style="display: none;"> <input type="text" name="username" autocomplete="username"
<input type="password" id="password-confirm-{{ $passwordConfirm }}" x-model="password" value="{{ auth()->user()->email }}" style="display: none;">
class="w-full input" placeholder="Enter your password" autocomplete="current-password"> <input type="password" id="password-confirm-{{ $passwordConfirm }}"
x-model="password" class="w-full input" placeholder="Enter your password"
autocomplete="current-password">
</form> </form>
<p x-show="passwordError" x-text="passwordError" class="mt-1 text-sm text-red-500"></p> <p x-show="passwordError" x-text="passwordError" class="mt-1 text-sm text-red-500">
</p>
@error('password') @error('password')
<p class="mt-1 text-sm text-red-500">{{ $message }}</p> <p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@enderror @enderror

View File

@@ -262,7 +262,7 @@
your self-hosted instance? your self-hosted instance?
<x-forms.button> <x-forms.button>
<a class="font-bold dark:text-white hover:no-underline" <a class="font-bold dark:text-white hover:no-underline"
href="{{ config('coolify.contact') }}">Contact href="{{ config('constants.urls.contact') }}">Contact
Us</a> Us</a>
</x-forms.button> </x-forms.button>
</div> </div>

View File

@@ -7,12 +7,6 @@
href="{{ route('settings.index') }}"> href="{{ route('settings.index') }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
@if (isCloud())
<a class="{{ request()->routeIs('settings.license') ? 'dark:text-white' : '' }}"
href="{{ route('settings.license') }}">
<button>Resale License</button>
</a>
@endif
<a class="{{ request()->routeIs('settings.backup') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('settings.backup') ? 'dark:text-white' : '' }}"
href="{{ route('settings.backup') }}"> href="{{ route('settings.backup') }}">
<button>Backup</button> <button>Backup</button>

View File

@@ -14,7 +14,7 @@
<a href="/"> <a href="/">
<x-forms.button>Go back home</x-forms.button> <x-forms.button>Go back home</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('coolify.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support support
<x-external-link /> <x-external-link />
</a> </a>

View File

@@ -9,7 +9,7 @@
<a href="/"> <a href="/">
<x-forms.button>Go back home</x-forms.button> <x-forms.button>Go back home</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('coolify.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support support
<x-external-link /> <x-external-link />
</a> </a>

View File

@@ -8,7 +8,7 @@
<a href="/"> <a href="/">
<x-forms.button>Go back home</x-forms.button> <x-forms.button>Go back home</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('coolify.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support support
<x-external-link /> <x-external-link />
</a> </a>

View File

@@ -9,7 +9,7 @@
<a href="/"> <a href="/">
<x-forms.button>Go back home</x-forms.button> <x-forms.button>Go back home</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('coolify.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support support
<x-external-link /> <x-external-link />
</a> </a>

View File

@@ -10,7 +10,7 @@
<a href="/"> <a href="/">
<x-forms.button>Go back home</x-forms.button> <x-forms.button>Go back home</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('coolify.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support support
<x-external-link /> <x-external-link />
</a> </a>

View File

@@ -10,7 +10,7 @@
<a href="/"> <a href="/">
<x-forms.button>Go back home</x-forms.button> <x-forms.button>Go back home</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('coolify.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support support
<x-external-link /> <x-external-link />
</a> </a>

View File

@@ -10,7 +10,7 @@
<a href="/"> <a href="/">
<x-forms.button>Go back home</x-forms.button> <x-forms.button>Go back home</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('coolify.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support support
<x-external-link /> <x-external-link />
</a> </a>

View File

@@ -2,7 +2,7 @@
<div class="flex flex-col items-center justify-center h-full"> <div class="flex flex-col items-center justify-center h-full">
<div> <div>
<p class="font-mono font-semibold text-red-500 text-7xl">500</p> <p class="font-mono font-semibold text-red-500 text-7xl">500</p>
<h1 class="mt-4 font-bold tracking-tight dark:text-white">Something is not okay, are you okay?</h1> <h1 class="mt-4 font-bold tracking-tight dark:text-white">Wait, this is not cool...</h1>
<p class="text-base leading-7 text-neutral-300">There has been an error, we are working on it. <p class="text-base leading-7 text-neutral-300">There has been an error, we are working on it.
</p> </p>
@if ($exception->getMessage() !== '') @if ($exception->getMessage() !== '')
@@ -13,7 +13,7 @@
<a href="/"> <a href="/">
<x-forms.button>Go back home</x-forms.button> <x-forms.button>Go back home</x-forms.button>
</a> </a>
<a target="_blank" class="text-xs" href="{{ config('coolify.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support support
<x-external-link /> <x-external-link />
</a> </a>

View File

@@ -7,7 +7,7 @@
patience. patience.
</p> </p>
<div class="flex items-center mt-10 gap-x-6"> <div class="flex items-center mt-10 gap-x-6">
<a target="_blank" class="text-xs" href="{{ config('coolify.contact') }}">Contact <a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
support support
<x-external-link /> <x-external-link />
</a> </a>

View File

@@ -84,9 +84,9 @@
window.Pusher = Pusher; window.Pusher = Pusher;
window.Echo = new Echo({ window.Echo = new Echo({
broadcaster: 'pusher', broadcaster: 'pusher',
cluster: "{{ env('PUSHER_HOST') }}" || window.location.hostname, cluster: "{{ config('constants.pusher.host') }}" || window.location.hostname,
key: "{{ env('PUSHER_APP_KEY') }}" || 'coolify', key: "{{ config('constants.pusher.app_key') }}" || 'coolify',
wsHost: "{{ env('PUSHER_HOST') }}" || window.location.hostname, wsHost: "{{ config('constants.pusher.host') }}" || window.location.hostname,
wsPort: "{{ getRealtime() }}", wsPort: "{{ getRealtime() }}",
wssPort: "{{ getRealtime() }}", wssPort: "{{ getRealtime() }}",
forceTLS: false, forceTLS: false,

View File

@@ -12,7 +12,7 @@
@if ($foundUsers->count() > 0) @if ($foundUsers->count() > 0)
<div class="flex flex-wrap gap-2 pt-4"> <div class="flex flex-wrap gap-2 pt-4">
@foreach ($foundUsers as $user) @foreach ($foundUsers as $user)
<div class="box w-64 group"> <div class="box w-64 group" wire:click="switchUser({{ $user->id }})">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="box-title">{{ $user->name }}</div> <div class="box-title">{{ $user->name }}</div>
<div class="box-description">{{ $user->email }}</div> <div class="box-description">{{ $user->email }}</div>

View File

@@ -323,7 +323,7 @@
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able <p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.<br><br>Minimum Docker Engine version is: 22<br><br>To manually install to run optimal.<br><br>Minimum Docker Engine version is: {{ $minDockerVersion }}<br><br>To manually install
Docker Docker
Engine, check <a target="_blank" class="underline dark:text-warning" Engine, check <a target="_blank" class="underline dark:text-warning"
href="https://docs.docker.com/engine/install/#server">this href="https://docs.docker.com/engine/install/#server">this

View File

@@ -1,13 +0,0 @@
<div class="pb-10" x-data>
<h1>Compose</h1>
<div>All kinds of compose files.</div>
<h3 class="pt-4">Services</h3>
@foreach ($services as $serviceName => $value)
<x-forms.button wire:click="setService('{{ $serviceName }}')">{{ Str::headline($serviceName) }}</x-forms.button>
@endforeach
<h3 class="pt-4">Base64 En/Decode</h3>
<x-forms.button x-on:click="copyToClipboard('{{ $base64 }}')">Copy Base64 Compose</x-forms.button>
<div class="pt-4">
<x-forms.textarea realtimeValidation rows="40" id="compose"></x-forms.textarea>
</div>
</div>

View File

@@ -1,15 +0,0 @@
<div>
<h2>S3 Test</h2>
<form wire:submit="save">
<input type="file" wire:model="file">
@error('file')
<span class="error">{{ $message }}</span>
@enderror
<div wire:loading wire:target="file">Uploading to server...</div>
@if ($file)
<x-forms.button type="submit">Upload file to s3:/files</x-forms.button>
@endif
</form>
<h4>Functions</h4>
<x-forms.button wire:click="get_files">Get s3:/files</x-forms.button>
</div>

View File

@@ -44,17 +44,108 @@
</nav> </nav>
</div> </div>
@if ($environment->isEmpty()) @if ($environment->isEmpty())
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_name' => data_get($parameters, 'environment_name')]) }} " <a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_name' => data_get($parameters, 'environment_name')]) }} "
class="items-center justify-center box">+ Add New Resource</a> class="items-center justify-center box">+ Add New Resource</a>
@else @else
<div x-data="searchComponent()"> <div x-data="searchComponent()">
<x-forms.input placeholder="Search for name, fqdn..." x-model="search" id="null" /> <x-forms.input placeholder="Search for name, fqdn..." x-model="search" id="null" />
<div class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3"> <template
<template x-if="allFilteredItems.length === 0"> x-if="filteredApplications.length === 0 && filteredDatabases.length === 0 && filteredServices.length === 0">
<div>No resource found with the search term "<span x-text="search"></span>".</div> <div>No resource found with the search term "<span x-text="search"></span>".</div>
</template> </template>
<template x-for="item in allFilteredItems" :key="item.uuid"> <template x-if="filteredApplications.length > 0">
<h2 class="pt-4">Applications</h2>
</template>
<div x-show="filteredApplications.length > 0"
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredApplications" :key="item.uuid">
<span>
<a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col w-full">
<div class="flex gap-2 px-4">
<div class="pb-2 truncate box-title" x-text="item.name"></div>
<div class="flex-1"></div>
<template x-if="item.status.startsWith('running')">
<div title="running" class="bg-success badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div title="exited" class="bg-error badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div title="restarting" class="bg-warning badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('degraded')">
<div title="degraded" class="bg-warning badge badge-absolute"></div>
</template>
</div>
<div class="max-w-full px-4 truncate box-description" x-text="item.description"></div>
<div class="max-w-full px-4 truncate box-description" x-text="item.fqdn"></div>
<template x-if="item.server_status == false">
<div class="px-4 text-xs font-bold text-error">The underlying server has problems
</div>
</template>
</div>
</a>
<div
class="flex flex-wrap gap-1 pt-1 group-hover:dark:text-white group-hover:text-black group min-h-6">
<template x-for="tag in item.tags">
<div class="tag" @click.prevent="gotoTag(tag.name)" x-text="tag.name"></div>
</template>
<div class="add-tag" @click.prevent="goto(item)">Add tag</div>
</div>
</span>
</template>
</div>
<template x-if="filteredDatabases.length > 0">
<h2 class="pt-4">Databases</h2>
</template>
<div x-show="filteredDatabases.length > 0"
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredDatabases" :key="item.uuid">
<span>
<a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col w-full">
<div class="flex gap-2 px-4">
<div class="pb-2 truncate box-title" x-text="item.name"></div>
<div class="flex-1"></div>
<template x-if="item.status.startsWith('running')">
<div title="running" class="bg-success badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div title="exited" class="bg-error badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div title="restarting" class="bg-warning badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('degraded')">
<div title="degraded" class="bg-warning badge badge-absolute"></div>
</template>
</div>
<div class="max-w-full px-4 truncate box-description" x-text="item.description"></div>
<div class="max-w-full px-4 truncate box-description" x-text="item.fqdn"></div>
<template x-if="item.server_status == false">
<div class="px-4 text-xs font-bold text-error">The underlying server has problems
</div>
</template>
</div>
</a>
<div
class="flex flex-wrap gap-1 pt-1 group-hover:dark:text-white group-hover:text-black group min-h-6">
<template x-for="tag in item.tags">
<div class="tag" @click.prevent="gotoTag(tag.name)" x-text="tag.name"></div>
</template>
<div class="add-tag" @click.prevent="goto(item)">Add tag</div>
</div>
</span>
</template>
</div>
<template x-if="filteredServices.length > 0">
<h2 class="pt-4">Services</h2>
</template>
<div x-show="filteredServices.length > 0"
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredServices" :key="item.uuid">
<span> <span>
<a class="h-24 box group" :href="item.hrefLink"> <a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
@@ -134,9 +225,11 @@
item.tags?.some(tag => tag.name.toLowerCase().includes(searchLower))); item.tags?.some(tag => tag.name.toLowerCase().includes(searchLower)));
}).sort(sortFn); }).sort(sortFn);
}, },
get allFilteredItems() { get filteredApplications() {
return this.filterAndSort(this.applications)
},
get filteredDatabases() {
return [ return [
this.applications,
this.postgresqls, this.postgresqls,
this.redis, this.redis,
this.mongodbs, this.mongodbs,
@@ -145,8 +238,10 @@
this.keydbs, this.keydbs,
this.dragonflies, this.dragonflies,
this.clickhouses, this.clickhouses,
this.services
].flatMap((items) => this.filterAndSort(items)) ].flatMap((items) => this.filterAndSort(items))
},
get filteredServices() {
return this.filterAndSort(this.services)
} }
}; };
} }

Some files were not shown because too many files have changed in this diff Show More