Merge branch 'next' into next

This commit is contained in:
Andras Bacsai
2024-09-27 17:06:52 +02:00
committed by GitHub
57 changed files with 632 additions and 800 deletions

View File

@@ -1,44 +0,0 @@
name: Docker Image CI
on:
# push:
# branches: [ "main" ]
# pull_request:
# branches: [ "*" ]
push:
branches: ["this-does-not-exist"]
pull_request:
branches: ["this-does-not-exist"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: |
/usr/local/share/ca-certificates
/var/cache/apt/archives
/var/lib/apt/lists
~/.cache
key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
restore-keys: |
${{ runner.os }}-docker-
- name: Build the Docker image
run: |
cp .env.example .env
docker run --rm -u "$(id -u):$(id -g)" \
-v "$(pwd):/app" \
-w /app composer:2 \
composer install --ignore-platform-reqs
./vendor/bin/spin build
- name: Start the stack
run: |
./vendor/bin/spin up -d
./vendor/bin/spin exec coolify php artisan key:generate
./vendor/bin/spin exec coolify php artisan migrate:fresh --seed
- name: Test (missing E2E tests)
run: |
./vendor/bin/spin exec coolify php artisan test

View File

@@ -1,25 +0,0 @@
name: Fix PHP code style issues
on: [push]
permissions:
contents: write
jobs:
php-code-styling:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- name: Fix PHP code style issues
uses: aglipanci/laravel-pint-action@2.4
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Fix styling

View File

@@ -0,0 +1,17 @@
name: Lock closed Issues, Discussions, and PRs
on:
schedule:
- cron: '0 1 * * *'
jobs:
lock-threads:
runs-on: ubuntu-latest
steps:
- name: Lock threads after 30 days of inactivity
uses: dessant/lock-threads@v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: '30'
pr-inactive-days: '30'
discussion-inactive-days: '30'

View File

@@ -0,0 +1,28 @@
name: Manage Stale Issues and PRs
on:
schedule:
- cron: '0 2 * * *'
jobs:
manage-stale:
runs-on: ubuntu-latest
steps:
- name: Manage stale issues and PRs
uses: actions/stale@v9
id: stale
with:
stale-issue-message: 'This issue will be automatically closed in a few days if no response is received. Please provide an update with the requested information.'
stale-pr-message: 'This pull request will be automatically closed in a few days if no response is received. Please update your PR or comment if you would like to continue working on it.'
close-issue-message: 'This issue has been automatically closed due to inactivity.'
close-pr-message: 'This pull request has been automatically closed due to inactivity.'
days-before-stale: 14
days-before-close: 7
stale-issue-label: '⏱︎ Stale'
stale-pr-label: '⏱︎ Stale'
only-labels: '💤 Waiting for feedback'
remove-stale-when-updated: true
operations-per-run: 100
labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback'
close-issue-reason: 'not_planned'
exempt-all-milestones: false

View File

@@ -1,93 +0,0 @@
name: PR Build (v4)
on:
pull_request:
types:
- opened
branches-ignore: ["main", "v3"]
paths-ignore:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
actions: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
context: .
file: docker/prod/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}
aarch64:
runs-on: [self-hosted, arm64]
permissions:
contents: read
packages: write
attestations: write
id-token: write
actions: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
context: .
file: docker/prod/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}-aarch64
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
actions: write
needs: [amd64, aarch64]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -2,35 +2,120 @@
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 releases are managed and deployed.
## Table of Contents
- [Release Process](#release-process)
- [Version Types](#version-types)
- [Stable](#stable)
- [Nightly](#nightly)
- [Beta](#beta)
- [Version Availability](#version-availability)
- [Self-Hosted](#self-hosted)
- [Cloud](#cloud)
- [Manually Update to Specific Versions](#manually-update-to-specific-versions)
## Release Process ## Release Process
1. **Development on `next` or separate branches** 1. **Development on `next` or Feature Branches**
- Changes, fixes and new features are developed on the `next` or even separate 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 changes are ready, they are merged from `next` into the `main` branch. - Once ready, changes are merged from the `next` branch into the `main` branch.
3. **Building the release** 3. **Building the Release**
- After merging to `main`, a new release is built. - 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.
- Note: A push to `main` does not automatically mean a new version is released.
4. **Creating a GitHub release** 4. **Creating a GitHub Release**
- A new release is created on GitHub with the new version details. - A new GitHub release is manually created with details of the changes made in the version.
5. **Updating the CDN** 5. **Updating the CDN**
- The final step is updating the version information on 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)
[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 happen hours or even days later due to additional testing, stability checks, or potential hotfixes. > 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.**
## Version Types
<details>
<summary><strong>Stable (coming soon)</strong></summary>
- **Stable**
- The production version suitable for stable, production environments (generally recommended).
- **Update Frequency:** Every 2 to 4 weeks, with more frequent possible hotfixes.
- **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`).
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
```
</details>
<details>
<summary><strong>Nightly</strong></summary>
- **Nightly**
- The latest development version, suitable for testing the latest changes and experimenting with new features.
- **Update Frequency:** Daily or bi-weekly updates.
- **Release Size:** Smaller, more frequent releases.
- **Versioning Scheme:** TO BE DETERMINED
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify-nightly/install.sh | bash -s next
```
</details>
<details>
<summary><strong>Beta</strong></summary>
- **Beta**
- Test releases for the upcoming stable version.
- **Purpose:** Allows users to test and provide feedback on new features and changes before they become stable.
- **Update Frequency:** Available if we think beta testing is necessary.
- **Release Size:** Same size as stable release as it will become the next stabe release after some time.
- **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`).
- **Installation Command:**
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
```
</details>
> [!WARNING]
> Do not use nightly/beta builds in production as there is no guarantee of stability.
## Version Availability ## Version Availability
It's important to understand that a new version released on GitHub may not immediately become available for users to update (through manual or auto-update). When a new version is released and a new GitHub release is created, it doesn't immediately become available for your instance. Here's how version availability works for different instance types.
### Self-Hosted
- **Update Frequency:** More frequent updates, especially on the nightly release channel.
- **Update Availability:** New versions are available once the CDN has been updated.
- **Update Methods:**
1. **Manual Update in Instance Settings:**
- Go to `Settings > Update Check Frequency` and click the `Check Manually` button.
- If an update is available, an upgrade button will appear on the sidebar.
2. **Automatic Update:**
- If enabled, the instance will update automatically at the time set in the settings.
3. **Re-run Installation Script:**
- Run the installation script again to upgrade to the latest version available on the CDN:
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
```
> [!IMPORTANT] > [!IMPORTANT]
> If you see a new release on GitHub but haven't received the update, it's likely because the CDN hasn't been updated yet. This is intentional and ensures stability and allows for hotfixes before the new version is officially released. > If a new release is available on GitHub but your instance hasn't updated yet or no upgrade button is shown in the UI, the CDN might not have been updated yet. This intentional delay ensures stability and allows for hotfixes before official release.
### Cloud
- **Update Frequency:** Less frequent as it's a managed service.
- **Update Availability:** New versions are available once Andras has updated the cloud version manually.
- **Update Method:**
- Updates are managed by Andras, who ensures each cloud version is thoroughly tested and stable before releasing it.
> [!IMPORTANT]
> The cloud version of Coolify may be several versions behind the latest GitHub releases even if the CDN is updated. This is intentional to ensure stability and reliability for cloud users and Andras will manully update the cloud version when the update is ready.
## Manually Update to Specific Versions ## Manually Update to Specific Versions
@@ -42,4 +127,4 @@ To update your Coolify instance to a specific (unreleased) version, use the foll
```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>
``` ```
-> Replace `<version>` with the version you want to update to (for example `4.0.0-beta.332`). Replace `<version>` with the version you want to update to (for example `4.0.0-beta.332`).

View File

@@ -651,8 +651,9 @@ class GetContainersStatus
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
} }
// Check if proxy is running if (! $this->server->proxySet() || $this->server->proxy->force_stop) {
$this->server->proxyType(); return;
}
$foundProxyContainer = $this->containers->filter(function ($value, $key) { $foundProxyContainer = $this->containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';

View File

@@ -35,7 +35,7 @@ class StartProxy
"echo 'Creating required Docker Compose file.'", "echo 'Creating required Docker Compose file.'",
"echo 'Starting coolify-proxy.'", "echo 'Starting coolify-proxy.'",
'docker stack deploy -c docker-compose.yml coolify-proxy', 'docker stack deploy -c docker-compose.yml coolify-proxy',
"echo 'Proxy started successfully.'", "echo 'Successfully started coolify-proxy.'",
]); ]);
} else { } else {
$caddfile = 'import /dynamic/*.caddy'; $caddfile = 'import /dynamic/*.caddy';
@@ -46,12 +46,14 @@ class StartProxy
"echo 'Creating required Docker Compose file.'", "echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'", "echo 'Pulling docker image.'",
'docker compose pull', 'docker compose pull',
"echo 'Stopping existing coolify-proxy.'", 'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then',
'docker stop -t 10 coolify-proxy || true', " echo 'Stopping and removing existing coolify-proxy.'",
'docker rm coolify-proxy || true', ' docker rm -f coolify-proxy || true',
" echo 'Successfully stopped and removed existing coolify-proxy.'",
'fi',
"echo 'Starting coolify-proxy.'", "echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans', 'docker compose up -d --remove-orphans',
"echo 'Proxy started successfully.'", "echo 'Successfully started coolify-proxy.'",
]); ]);
$commands = $commands->merge(connectProxyToNetworks($server)); $commands = $commands->merge(connectProxyToNetworks($server));
} }

View File

@@ -12,28 +12,29 @@ class CleanupDocker
public function handle(Server $server) public function handle(Server $server)
{ {
$settings = InstanceSettings::get();
$helperImageVersion = data_get($settings, 'helper_version');
$helperImage = config('coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion";
$commands = $this->getCommands(); $commands = [
'docker container prune -f --filter "label=coolify.managed=true"',
'docker image prune -af --filter "label!=coolify.managed=true"',
'docker builder prune -af',
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
];
$serverSettings = $server->settings;
if ($serverSettings->delete_unused_volumes) {
$commands[] = 'docker volume prune -af';
}
if ($serverSettings->delete_unused_networks) {
$commands[] = 'docker network prune -f';
}
foreach ($commands as $command) { foreach ($commands as $command) {
instant_remote_process([$command], $server, false); instant_remote_process([$command], $server, false);
} }
} }
private function getCommands(): array
{
$settings = InstanceSettings::get();
$helperImageVersion = data_get($settings, 'helper_version');
$helperImage = config('coolify.helper_image');
$helperImageWithVersion = config('coolify.helper_image').':'.$helperImageVersion;
$commonCommands = [
'docker container prune -f --filter "label=coolify.managed=true"',
'docker image prune -af --filter "label!=coolify.managed=true"',
'docker builder prune -af',
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi",
];
return $commonCommands;
}
} }

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Enums;
enum ContainerStatusTypes: string
{
case PAUSED = 'paused';
case RESTARTING = 'restarting';
case REMOVING = 'removing';
case RUNNING = 'running';
case DEAD = 'dead';
case CREATED = 'created';
case EXITED = 'exited';
}

View File

@@ -23,7 +23,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $usageBefore = null; public ?string $usageBefore = null;
public function __construct(public Server $server) {} public function __construct(public Server $server, public bool $manualCleanup = false) {}
public function handle(): void public function handle(): void
{ {
@@ -31,8 +31,9 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
if (! $this->server->isFunctional()) { if (! $this->server->isFunctional()) {
return; return;
} }
if ($this->server->settings->force_docker_cleanup) {
Log::info('DockerCleanupJob force cleanup on '.$this->server->name); if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
Log::info('DockerCleanupJob '.($this->manualCleanup ? 'manual' : 'force').' cleanup on '.$this->server->name);
CleanupDocker::run(server: $this->server); CleanupDocker::run(server: $this->server);
return; return;

View File

@@ -69,8 +69,10 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
return 'No containers found.'; return 'No containers found.';
} }
GetContainersStatus::run($this->server, $this->containers, $containerReplicates); GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
if ($this->server->isLogDrainEnabled()) {
$this->checkLogDrainContainer(); $this->checkLogDrainContainer();
} }
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e->getMessage()); ray($e->getMessage());
@@ -115,9 +117,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
private function checkLogDrainContainer() private function checkLogDrainContainer()
{ {
if (! $this->server->isLogDrainEnabled()) {
return;
}
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) { $foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain'; return data_get($value, 'Name') === '/coolify-log-drain';
})->first(); })->first();

View File

@@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Security; namespace App\Livewire\Security;
use App\Models\InstanceSettings;
use Livewire\Component; use Livewire\Component;
class ApiTokens extends Component class ApiTokens extends Component
@@ -16,6 +17,8 @@ class ApiTokens extends Component
public array $permissions = ['read-only']; public array $permissions = ['read-only'];
public $isApiEnabled;
public function render() public function render()
{ {
return view('livewire.security.api-tokens'); return view('livewire.security.api-tokens');
@@ -23,6 +26,7 @@ class ApiTokens extends Component
public function mount() public function mount()
{ {
$this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
$this->tokens = auth()->user()->tokens->sortByDesc('created_at'); $this->tokens = auth()->user()->tokens->sortByDesc('created_at');
} }

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Server;
use App\Actions\Server\StartSentinel; use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel; use App\Actions\Server\StopSentinel;
use App\Jobs\DockerCleanupJob;
use App\Jobs\PullSentinelImageJob; use App\Jobs\PullSentinelImageJob;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -24,6 +25,10 @@ class Form extends Component
public $timezones; public $timezones;
public $delete_unused_volumes = false;
public $delete_unused_networks = false;
public function getListeners() public function getListeners()
{ {
$teamId = auth()->user()->currentTeam()->id; $teamId = auth()->user()->currentTeam()->id;
@@ -58,6 +63,8 @@ class Form extends Component
'server.settings.force_docker_cleanup' => 'required|boolean', 'server.settings.force_docker_cleanup' => 'required|boolean',
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string', 'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100', 'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
'server.settings.delete_unused_volumes' => 'boolean',
'server.settings.delete_unused_networks' => 'boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@@ -79,6 +86,8 @@ class Form extends Component
'server.settings.metrics_history_days' => 'Metrics History', 'server.settings.metrics_history_days' => 'Metrics History',
'server.settings.is_server_api_enabled' => 'Server API', 'server.settings.is_server_api_enabled' => 'Server API',
'server.settings.server_timezone' => 'Server Timezone', 'server.settings.server_timezone' => 'Server Timezone',
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
]; ];
public function mount(Server $server) public function mount(Server $server)
@@ -88,6 +97,8 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain; $this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold; $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency; $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
$this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes;
$this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks;
} }
public function updated($field) public function updated($field)
@@ -137,6 +148,7 @@ class Form extends Component
try { try {
refresh_server_connection($this->server->privateKey); refresh_server_connection($this->server->privateKey);
$this->validateServer(false); $this->validateServer(false);
$this->server->settings->save(); $this->server->settings->save();
$this->server->save(); $this->server->save();
$this->dispatch('success', 'Server updated.'); $this->dispatch('success', 'Server updated.');
@@ -154,6 +166,7 @@ class Form extends Component
ray('Sentinel is not enabled'); ray('Sentinel is not enabled');
StopSentinel::dispatch($this->server); StopSentinel::dispatch($this->server);
} }
$this->server->settings->save();
// $this->checkPortForServerApi(); // $this->checkPortForServerApi();
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -234,9 +247,9 @@ class Form extends Component
$this->server->settings->server_timezone = $newTimezone; $this->server->settings->server_timezone = $newTimezone;
$this->server->settings->save(); $this->server->settings->save();
} }
$this->server->settings->save(); $this->server->settings->save();
$this->server->save(); $this->server->save();
$this->dispatch('success', 'Server updated.'); $this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
@@ -250,6 +263,16 @@ class Form extends Component
$this->dispatch('success', 'Server timezone updated.'); $this->dispatch('success', 'Server timezone updated.');
} }
public function manualCleanup()
{
try {
DockerCleanupJob::dispatch($this->server, true);
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function manualCloudflareConfig() public function manualCloudflareConfig()
{ {
$this->server->settings->is_cloudflare_tunnel = true; $this->server->settings->is_cloudflare_tunnel = true;

View File

@@ -49,6 +49,10 @@ class Status extends Component
if ($this->server->proxy->status === 'running') { if ($this->server->proxy->status === 'running') {
$this->polling = false; $this->polling = false;
$notification && $this->dispatch('success', 'Proxy is running.'); $notification && $this->dispatch('success', 'Proxy is running.');
} elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) {
$notification && $this->dispatch('error', 'Proxy has exited.');
} elseif ($this->server->proxy->force_stop) {
$notification && $this->dispatch('error', 'Proxy is stopped manually.');
} else { } else {
$notification && $this->dispatch('error', 'Proxy is not running.'); $notification && $this->dispatch('error', 'Proxy is not running.');
} }

View File

@@ -36,6 +36,8 @@ use Symfony\Component\Yaml\Yaml;
'validation_logs' => ['type' => 'string'], 'validation_logs' => ['type' => 'string'],
'log_drain_notification_sent' => ['type' => 'boolean'], 'log_drain_notification_sent' => ['type' => 'boolean'],
'swarm_cluster' => ['type' => 'string'], 'swarm_cluster' => ['type' => 'string'],
'delete_unused_volumes' => ['type' => 'boolean'],
'delete_unused_networks' => ['type' => 'boolean'],
] ]
)] )]
@@ -105,6 +107,8 @@ class Server extends BaseModel
'proxy' => SchemalessAttributes::class, 'proxy' => SchemalessAttributes::class,
'logdrain_axiom_api_key' => 'encrypted', 'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted', 'logdrain_newrelic_license_key' => 'encrypted',
'delete_unused_volumes' => 'boolean',
'delete_unused_networks' => 'boolean',
]; ];
protected $schemalessAttributes = [ protected $schemalessAttributes = [

View File

@@ -22,6 +22,7 @@ class Input extends Component
public bool $allowToPeak = true, public bool $allowToPeak = true,
public bool $isMultiline = false, public bool $isMultiline = false,
public string $defaultClass = 'input', public string $defaultClass = 'input',
public string $autocomplete = 'off',
) {} ) {}
public function render(): View|Closure|string public function render(): View|Closure|string

View File

@@ -96,6 +96,8 @@ function connectProxyToNetworks(Server $server)
"echo 'Connecting coolify-proxy to $network network...'", "echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
"echo 'Successfully connected coolify-proxy to $network network.'",
"echo 'Proxy started and configured successfully!'",
]; ];
}); });
} else { } else {
@@ -104,6 +106,8 @@ function connectProxyToNetworks(Server $server)
"echo 'Connecting coolify-proxy to $network network...'", "echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
"echo 'Successfully connected coolify-proxy to $network network.'",
"echo 'Proxy started and configured successfully!'",
]; ];
}); });
} }
@@ -217,7 +221,6 @@ function generate_default_proxy_configuration(Server $server)
} }
} elseif ($proxy_type === 'CADDY') { } elseif ($proxy_type === 'CADDY') {
$config = [ $config = [
'version' => '3.8',
'networks' => $array_of_networks->toArray(), 'networks' => $array_of_networks->toArray(),
'services' => [ 'services' => [
'caddy' => [ 'caddy' => [
@@ -236,12 +239,9 @@ function generate_default_proxy_configuration(Server $server)
'80:80', '80:80',
'443:443', '443:443',
], ],
// "healthcheck" => [ 'labels' => [
// "test" => "wget -qO- http://localhost:80|| exit 1", 'coolify.managed=true',
// "interval" => "4s", ],
// "timeout" => "2s",
// "retries" => 5,
// ],
'volumes' => [ 'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro', '/var/run/docker.sock:/var/run/docker.sock:ro',
"{$proxy_path}/dynamic:/dynamic", "{$proxy_path}/dynamic:/dynamic",

View File

@@ -7,7 +7,7 @@ return [
// 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.346', 'release' => '4.0.0-beta.347',
// 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.346'; return '4.0.0-beta.347';

View File

@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('server_settings', function (Blueprint $table) {
$table->boolean('delete_unused_volumes')->default(false);
$table->boolean('delete_unused_networks')->default(false);
});
}
public function down()
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('delete_unused_volumes');
$table->dropColumn('delete_unused_networks');
});
}
};

View File

@@ -0,0 +1,18 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('instance_settings', function (Blueprint $table) {
$table->boolean('is_api_enabled')->default(false)->change();
});
}
};

View File

@@ -1,20 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class ApplicationPreviewSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// $application_1 = Application::find(1);
// ApplicationPreview::create([
// 'application_id' => $application_1->id,
// 'pull_request_id' => 1
// ]);
}
}

View File

@@ -17,22 +17,14 @@ class DatabaseSeeder extends Seeder
ServerSeeder::class, ServerSeeder::class,
ServerSettingSeeder::class, ServerSettingSeeder::class,
ProjectSeeder::class, ProjectSeeder::class,
ProjectSettingSeeder::class,
EnvironmentSeeder::class,
StandaloneDockerSeeder::class, StandaloneDockerSeeder::class,
SwarmDockerSeeder::class,
KubernetesSeeder::class,
GithubAppSeeder::class, GithubAppSeeder::class,
GitlabAppSeeder::class, GitlabAppSeeder::class,
ApplicationSeeder::class, ApplicationSeeder::class,
ApplicationSettingsSeeder::class, ApplicationSettingsSeeder::class,
ApplicationPreviewSeeder::class,
EnvironmentVariableSeeder::class,
LocalPersistentVolumeSeeder::class, LocalPersistentVolumeSeeder::class,
S3StorageSeeder::class, S3StorageSeeder::class,
StandalonePostgresqlSeeder::class, StandalonePostgresqlSeeder::class,
ScheduledDatabaseBackupSeeder::class,
ScheduledDatabaseBackupExecutionSeeder::class,
OauthSettingSeeder::class, OauthSettingSeeder::class,
]); ]);
} }

View File

@@ -1,13 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class EnvironmentSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void {}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class EnvironmentVariableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// EnvironmentVariable::create([
// 'key' => 'NODE_ENV',
// 'value' => 'production',
// 'is_build_time' => true,
// 'application_id' => 1,
// ]);
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace Database\Seeders;
use App\Models\Git;
use Illuminate\Database\Seeder;
class GitSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// $project = Project::find(1);
// $private_key_1 = PrivateKey::find(1);
// Git::create([
// 'api_url' => 'https://api.github.com',
// 'html_url' => 'https://github.com',
// 'is_public' => false,
// 'private_key_id' => $private_key_1->id,
// 'project_id' => $project->id,
// ]);
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class KubernetesSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class LocalFileVolumeSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -1,15 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class ProjectSettingSeeder extends Seeder
{
public function run(): void
{
// $first_project = Project::find(1);
// $first_project->settings->wildcard_domain = 'wildcard.example.com';
// $first_project->settings->save();
}
}

View File

@@ -1,28 +0,0 @@
<?php
namespace Database\Seeders;
use App\Models\ScheduledDatabaseBackupExecution;
use Illuminate\Database\Seeder;
class ScheduledDatabaseBackupExecutionSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// ScheduledDatabaseBackupExecution::create([
// 'status' => 'success',
// 'message' => 'Backup created successfully.',
// 'size' => '10243467789556',
// 'scheduled_database_backup_id' => 1,
// ]);
// ScheduledDatabaseBackupExecution::create([
// 'status' => 'failed',
// 'message' => 'Backup failed.',
// 'size' => '10243456',
// 'scheduled_database_backup_id' => 1,
// ]);
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace Database\Seeders;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Database\Seeder;
class ScheduledDatabaseBackupSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// ScheduledDatabaseBackup::create([
// 'enabled' => true,
// 'frequency' => '* * * * *',
// 'number_of_backups_locally' => 2,
// 'database_id' => 1,
// 'database_type' => 'App\Models\StandalonePostgresql',
// 's3_storage_id' => 1,
// 'team_id' => 0,
// ]);
// ScheduledDatabaseBackup::create([
// 'enabled' => true,
// 'frequency' => '* * * * *',
// 'number_of_backups_locally' => 3,
// 'database_id' => 1,
// 'database_type' => 'App\Models\StandalonePostgresql',
// 'team_id' => 0,
// ]);
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class ServiceApplicationSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class ServiceDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class ServiceSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class SubscriptionSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -1,20 +0,0 @@
<?php
namespace Database\Seeders;
use App\Models\SwarmDocker;
use Illuminate\Database\Seeder;
class SwarmDockerSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// SwarmDocker::create([
// 'name' => 'Swarm Docker 1',
// 'server_id' => 1,
// ]);
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class WaitlistSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class WebhookSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View File

@@ -68,7 +68,11 @@ wss.on('connection', (ws) => {
const messageHandlers = { const messageHandlers = {
message: (session, data) => session.ptyProcess.write(data), message: (session, data) => session.ptyProcess.write(data),
resize: (session, { cols, rows }) => session.ptyProcess.resize(cols, rows), resize: (session, { cols, rows }) => {
cols = cols > 0 ? cols : 80;
rows = rows > 0 ? rows : 30;
session.ptyProcess.resize(cols, rows)
},
pause: (session) => session.ptyProcess.pause(), pause: (session) => session.ptyProcess.pause(),
resume: (session) => session.ptyProcess.resume(), resume: (session) => session.ptyProcess.resume(),
checkActive: (session, data) => { checkActive: (session, data) => {
@@ -140,6 +144,7 @@ async function handleCommand(ws, command, userId) {
ptyProcess.onData((data) => ws.send(data)); ptyProcess.onData((data) => ws.send(data));
// when parent closes
ptyProcess.onExit(({ exitCode, signal }) => { ptyProcess.onExit(({ exitCode, signal }) => {
console.error(`Process exited with code ${exitCode} and signal ${signal}`); console.error(`Process exited with code ${exitCode} and signal ${signal}`);
userSession.isActive = false; userSession.isActive = false;

View File

@@ -404,10 +404,10 @@ if [ ! -f ~/.ssh/authorized_keys ]; then
fi fi
set +e set +e
IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l) IF_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l)
set -e set -e
if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then if [ "$IF_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
echo " - Generating SSH key." echo " - Generating SSH key."
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify
chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal

View File

@@ -1,10 +1,11 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.346"
"version": "4.0.0-beta.347"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.347" "version": "4.0.0-beta.348"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.1"

View File

@@ -5,17 +5,13 @@
// app.component("magic-bar", MagicBar); // app.component("magic-bar", MagicBar);
// app.mount("#vue"); // app.mount("#vue");
import { Terminal } from '@xterm/xterm'; import { initializeTerminalComponent } from './terminal.js';
import '@xterm/xterm/css/xterm.css';
import { FitAddon } from '@xterm/addon-fit';
if (!window.term) { ['livewire:navigated', 'alpine:init'].forEach((event) => {
window.term = new Terminal({ document.addEventListener(event, () => {
cols: 80, // tree-shaking
rows: 30, if (document.getElementById('terminal-container')) {
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', initializeTerminalComponent()
cursorBlink: true,
});
window.fitAddon = new FitAddon();
window.term.loadAddon(window.fitAddon);
} }
});
});

228
resources/js/terminal.js Normal file
View File

@@ -0,0 +1,228 @@
import { Terminal } from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css';
import { FitAddon } from '@xterm/addon-fit';
export function initializeTerminalComponent() {
function terminalData() {
return {
fullscreen: false,
terminalActive: false,
message: '(connection closed)',
term: null,
fitAddon: null,
socket: null,
commandBuffer: '',
pendingWrites: 0,
paused: false,
MAX_PENDING_WRITES: 5,
keepAliveInterval: null,
init() {
this.setupTerminal();
this.initializeWebSocket();
this.setupTerminalEventListeners();
this.$wire.on('send-back-command', (command) => {
this.socket.send(JSON.stringify({
command: command
}));
});
this.keepAliveInterval = setInterval(this.keepAlive.bind(this), 30000);
this.$watch('terminalActive', (active) => {
if (!active && this.keepAliveInterval) {
clearInterval(this.keepAliveInterval);
}
this.$nextTick(() => {
if (active) {
this.$refs.terminalWrapper.style.display = 'block';
this.resizeTerminal();
} else {
this.$refs.terminalWrapper.style.display = 'none';
}
});
});
['livewire:navigated', 'beforeunload'].forEach((event) => {
document.addEventListener(event, () => {
this.checkIfProcessIsRunningAndKillIt();
clearInterval(this.keepAliveInterval);
}, { once: true });
});
window.onresize = () => {
this.resizeTerminal()
};
},
setupTerminal() {
const terminalElement = document.getElementById('terminal');
if (terminalElement) {
this.term = new Terminal({
cols: 80,
rows: 30,
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
cursorBlink: true,
});
this.fitAddon = new FitAddon();
this.term.loadAddon(this.fitAddon);
}
},
initializeWebSocket() {
if (!this.socket || this.socket.readyState === WebSocket.CLOSED) {
const predefined = window.terminalConfig
const connectionString = {
protocol: window.location.protocol === 'https:' ? 'wss' : 'ws',
host: window.location.hostname,
port: ":6002",
path: '/terminal/ws'
}
if (!window.location.port) {
connectionString.port = ''
}
if (predefined.host) {
connectionString.host = predefined.host
}
if (predefined.port) {
connectionString.port = `:${predefined.port}`
}
if (predefined.protocol) {
connectionString.protocol = predefined.protocol
}
const url =
`${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}`
this.socket = new WebSocket(url);
this.socket.onmessage = this.handleSocketMessage.bind(this);
this.socket.onerror = (e) => {
console.error('WebSocket error:', e);
};
this.socket.onclose = () => {
console.log('WebSocket connection closed');
};
}
},
handleSocketMessage(event) {
this.message = '(connection closed)';
if (event.data === 'pty-ready') {
if (!this.term._initialized) {
this.term.open(document.getElementById('terminal'));
this.term._initialized = true;
} else {
this.term.reset();
}
this.terminalActive = true;
this.term.focus();
document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded');
this.resizeTerminal();
} else if (event.data === 'unprocessable') {
if (this.term) this.term.reset();
this.terminalActive = false;
this.message = '(sorry, something went wrong, please try again)';
} else {
this.pendingWrites++;
this.term.write(event.data, this.flowControlCallback.bind(this));
}
},
flowControlCallback() {
this.pendingWrites--;
if (this.pendingWrites > this.MAX_PENDING_WRITES && !this.paused) {
this.paused = true;
this.socket.send(JSON.stringify({ pause: true }));
} else if (this.pendingWrites <= this.MAX_PENDING_WRITES && this.paused) {
this.paused = false;
this.socket.send(JSON.stringify({ resume: true }));
}
},
setupTerminalEventListeners() {
if (!this.term) return;
this.term.onData((data) => {
this.socket.send(JSON.stringify({ message: data }));
// Handle CTRL + D or exit command
if (data === '\x04' || (data === '\r' && this.stripAnsiCommands(this.commandBuffer).trim().includes('exit'))) {
this.checkIfProcessIsRunningAndKillIt();
setTimeout(() => {
this.terminalActive = false;
this.term.reset();
}, 500);
this.commandBuffer = '';
} else if (data === '\r') {
this.commandBuffer = '';
} else {
this.commandBuffer += data;
}
});
// Copy and paste functionality
this.term.attachCustomKeyEventHandler((arg) => {
if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") {
navigator.clipboard.readText()
.then(text => {
this.socket.send(JSON.stringify({ message: text }));
});
return false;
}
if (arg.ctrlKey && arg.code === "KeyC" && arg.type === "keydown") {
const selection = this.term.getSelection();
if (selection) {
navigator.clipboard.writeText(selection);
return false;
}
}
return true;
});
},
stripAnsiCommands(input) {
return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
},
keepAlive() {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({ ping: true }));
}
},
checkIfProcessIsRunningAndKillIt() {
if (this.socket && this.socket.readyState == WebSocket.OPEN) {
this.socket.send(JSON.stringify({ checkActive: 'force' }));
}
},
makeFullscreen() {
this.fullscreen = !this.fullscreen;
this.$nextTick(() => {
this.resizeTerminal();
});
},
resizeTerminal() {
if (!this.terminalActive || !this.term || !this.fitAddon) return;
this.fitAddon.fit();
const height = this.$refs.terminalWrapper.clientHeight;
const width = this.$refs.terminalWrapper.clientWidth;
const rows = Math.floor(height / this.term._core._renderService._charSizeService.height) - 1;
const cols = Math.floor(width / this.term._core._renderService._charSizeService.width) - 1;
const termWidth = cols;
const termHeight = rows;
this.term.resize(termWidth, termHeight);
this.socket.send(JSON.stringify({
resize: { cols: termWidth, rows: termHeight }
}));
},
};
}
window.Alpine.data('terminalData', terminalData);
}

View File

@@ -25,7 +25,8 @@
</svg> </svg>
</div> </div>
@endif @endif
<input value="{{ $value }}" {{ $attributes->merge(['class' => $defaultClass]) }} @required($required) <input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
@if ($id !== 'null') wire:model={{ $id }} @endif @if ($id !== 'null') wire:model={{ $id }} @endif
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled"
@@ -35,7 +36,7 @@
</div> </div>
@else @else
<input @if ($value) value="{{ $value }}" @endif <input autocomplete="{{ $autocomplete }}" @if ($value) value="{{ $value }}" @endif
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly) {{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly)
@if ($id !== 'null') wire:model={{ $id }} @endif @if ($id !== 'null') wire:model={{ $id }} @endif
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'

View File

@@ -1,4 +1,4 @@
<div x-data="data()"> <div id="terminal-container" x-data="terminalData()">
{{-- <div x-show="!terminalActive" class="flex items-center justify-center w-full py-4 mx-auto h-[510px]"> {{-- <div x-show="!terminalActive" class="flex items-center justify-center w-full py-4 mx-auto h-[510px]">
<div class="p-1 w-full h-full rounded border dark:bg-coolgray-100 dark:border-coolgray-300"> <div class="p-1 w-full h-full rounded border dark:bg-coolgray-100 dark:border-coolgray-300">
<span class="font-mono text-sm text-gray-500" x-text="message"></span> <span class="font-mono text-sm text-gray-500" x-text="message"></span>
@@ -22,219 +22,14 @@
</g> </g>
</svg></button> </svg></button>
</div> </div>
@script @script
<script> <script>
const MAX_PENDING_WRITES = 5; // expose terminal config to the terminal.js file
let pendingWrites = 0; window.terminalConfig = {
let paused = false;
let socket;
let commandBuffer = '';
function keepAlive() {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
ping: true
}));
}
}
const keepAliveInterval = setInterval(keepAlive, 30000);
// Clear the interval when the component is destroyed
document.addEventListener('livewire:navigating', () => {
clearInterval(keepAliveInterval);
});
function initializeWebSocket() {
if (!socket || socket.readyState === WebSocket.CLOSED) {
const predefined = {
protocol: "{{ env('TERMINAL_PROTOCOL') }}", protocol: "{{ env('TERMINAL_PROTOCOL') }}",
host: "{{ env('TERMINAL_HOST') }}", host: "{{ env('TERMINAL_HOST') }}",
port: "{{ env('TERMINAL_PORT') }}" port: "{{ env('TERMINAL_PORT') }}"
} }
const connectionString = {
protocol: window.location.protocol === 'https:' ? 'wss' : 'ws',
host: window.location.hostname,
port: ":6002",
path: '/terminal/ws'
}
if (!window.location.port) {
connectionString.port = ''
}
if (predefined.host) {
connectionString.host = predefined.host
}
if (predefined.port) {
connectionString.port = `:${predefined.port}`
}
if (predefined.protocol) {
connectionString.protocol = predefined.protocol
}
const url =
`${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}`
socket = new WebSocket(url);
socket.onmessage = handleSocketMessage;
socket.onerror = (e) => {
console.error('WebSocket error:', e);
};
socket.onclose = () => {
console.log('WebSocket connection closed');
};
}
}
function handleSocketMessage(event) {
$data.message = '(connection closed)';
// Initialize Terminal
if (event.data === 'pty-ready') {
term.open(document.getElementById('terminal'));
$data.terminalActive = true;
term.reset();
term.focus();
document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded')
$data.resizeTerminal()
} else if (event.data === 'unprocessable') {
term.reset();
$data.terminalActive = false;
$data.message = '(sorry, something went wrong, please try again)';
} else {
pendingWrites++;
term.write(event.data, flowControlCallback);
}
}
function flowControlCallback() {
pendingWrites--;
if (pendingWrites > MAX_PENDING_WRITES && !paused) {
paused = true;
socket.send(JSON.stringify({
pause: true
}));
return;
}
if (pendingWrites <= MAX_PENDING_WRITES && paused) {
paused = false;
socket.send(JSON.stringify({
resume: true
}));
return;
}
}
term.onData((data) => {
socket.send(JSON.stringify({
message: data
}));
// Type CTRL + D or exit in the terminal
if (data === '\x04' || (data === '\r' && stripAnsiCommands(commandBuffer).trim().includes('exit'))) {
checkIfProcessIsRunningAndKillIt();
setTimeout(() => {
$data.terminalActive = false;
term.reset();
}, 500);
commandBuffer = '';
} else if (data === '\r') {
commandBuffer = '';
} else {
commandBuffer += data;
}
});
function stripAnsiCommands(input) {
return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
}
// Copy and paste
// Enables ctrl + c and ctrl + v
// defaults otherwise to ctrl + insert, shift + insert
term.attachCustomKeyEventHandler((arg) => {
if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") {
navigator.clipboard.readText()
.then(text => {
socket.send(JSON.stringify({
message: text
}));
})
};
if (arg.ctrlKey && arg.code === "KeyC" && arg.type === "keydown") {
const selection = term.getSelection();
if (selection) {
navigator.clipboard.writeText(selection);
return false;
}
}
return true;
});
$wire.on('send-back-command', function(command) {
socket.send(JSON.stringify({
command: command
}));
});
window.addEventListener('beforeunload', function(e) {
checkIfProcessIsRunningAndKillIt();
});
function checkIfProcessIsRunningAndKillIt() {
socket.send(JSON.stringify({
checkActive: 'force'
}));
}
window.onresize = function() {
$data.resizeTerminal()
};
Alpine.data('data', () => ({
fullscreen: false,
terminalActive: false,
message: '(connection closed)',
init() {
this.$watch('terminalActive', (value) => {
this.$nextTick(() => {
if (value) {
$refs.terminalWrapper.style.display = 'block';
this.resizeTerminal();
} else {
$refs.terminalWrapper.style.display = 'none';
}
});
});
},
makeFullscreen() {
this.fullscreen = !this.fullscreen;
$nextTick(() => {
this.resizeTerminal()
})
},
resizeTerminal() {
if (!this.terminalActive) return;
fitAddon.fit();
const height = $refs.terminalWrapper.clientHeight;
const rows = height / term._core._renderService._charSizeService.height - 1;
var termWidth = term.cols;
var termHeight = parseInt(rows.toString(), 10);
term.resize(termWidth, termHeight);
socket.send(JSON.stringify({
resize: {
cols: termWidth,
rows: termHeight
}
}));
}
}));
initializeWebSocket();
</script> </script>
@endscript @endscript
</div> </div>

View File

@@ -5,17 +5,21 @@
<x-security.navbar /> <x-security.navbar />
<div class="pb-4"> <div class="pb-4">
<h2>API Tokens</h2> <h2>API Tokens</h2>
@if (!$isApiEnabled)
<div>API is disabled. If you want to use the API, please enable it in the Settings menu.</div>
@else
<div>Tokens are created with the current team as scope. You will only have access to this team's resources. <div>Tokens are created with the current team as scope. You will only have access to this team's resources.
</div> </div>
</div> </div>
<h3>New Token</h3> <h3>New Token</h3>
<form class="flex flex-col gap-2 pt-4" wire:submit='addNewToken'> <form class="flex flex-col gap-2 pt-4" wire:submit='addNewToken'>
<div class="flex items-end gap-2"> <div class="flex gap-2 items-end">
<x-forms.input required id="description" label="Description" /> <x-forms.input required id="description" label="Description" />
<x-forms.button type="submit">Create New Token</x-forms.button> <x-forms.button type="submit">Create New Token</x-forms.button>
</div> </div>
<div class="flex"> <div class="flex">
Permissions <x-helper class="px-1" helper="These permissions will be granted to the token." /><span Permissions
<x-helper class="px-1" helper="These permissions will be granted to the token." /><span
class="pr-1">:</span> class="pr-1">:</span>
<div class="flex gap-1 font-bold dark:text-white"> <div class="flex gap-1 font-bold dark:text-white">
@if ($permissions) @if ($permissions)
@@ -56,18 +60,15 @@
@endif @endif
</div> </div>
<x-modal-confirmation <x-modal-confirmation title="Confirm API Token Revocation?" isErrorButton buttonTitle="Revoke token"
title="Confirm API Token Revocation?" submitAction="revoke({{ data_get($token, 'id') }})" :actions="[
isErrorButton 'This API Token will be revoked and permanently deleted.',
buttonTitle="Revoke token" 'Any API call made with this token will fail.',
submitAction="revoke({{ data_get($token, 'id') }})" ]"
:actions="['This API Token will be revoked and permanently deleted.', 'Any API call made with this token will fail.']"
confirmationText="{{ $token->name }}" confirmationText="{{ $token->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the API Token Description below" confirmationLabel="Please confirm the execution of the actions by entering the API Token Description below"
shortConfirmationLabel="API Token Description" shortConfirmationLabel="API Token Description" :confirmWithPassword="false"
:confirmWithPassword="false" step2ButtonText="Revoke API Token" />
step2ButtonText="Revoke API Token"
/>
</div> </div>
@empty @empty
<div> <div>
@@ -75,5 +76,5 @@
</div> </div>
@endforelse @endforelse
</div> </div>
@endif
</div> </div>

View File

@@ -16,7 +16,7 @@
<x-modal-confirmation title="Confirm Server Deletion?" isErrorButton buttonTitle="Delete" <x-modal-confirmation title="Confirm Server Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="['This server will be permanently deleted.']" confirmationText="{{ $server->name }}" submitAction="delete" :actions="['This server will be permanently deleted.']" confirmationText="{{ $server->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Server Name below" confirmationLabel="Please confirm the execution of the actions by entering the Server Name below"
shortConfirmationLabel="Server Name" step2ButtonText="Permanently Delete" /> shortConfirmationLabel="Server Name" step2ButtonText="Continue" step3ButtonText="Permanently Delete" />
@endif @endif
@endif @endif
</div> </div>

View File

@@ -194,14 +194,39 @@
@if ($server->isFunctional()) @if ($server->isFunctional())
<h3 class="pt-4">Settings</h3> <h3 class="pt-4">Settings</h3>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex flex-col flex-wrap gap-2 sm:flex-nowrap"> <div class="flex flex-wrap items-center gap-4">
<div class="w-64"> <div class="w-64">
<x-forms.checkbox <x-forms.checkbox
helper="Enable force Docker Cleanup. This will cleanup build caches / unused images / etc." helper="Enabling Force Docker Cleanup or manually triggering a cleanup will perform the following actions:
<ul class='list-disc pl-4 mt-2'>
<li>Removes stopped containers manged by Coolify (as containers are none persistent, no data will be lost).</li>
<li>Deletes unused images.</li>
<li>Clears build cache.</li>
<li>Removes old versions of the Coolify helper image.</li>
<li>Optionally delete unused volumes (if enabled in advanced options).</li>
<li>Optionally remove unused networks (if enabled in advanced options).</li>
</ul>"
instantSave id="server.settings.force_docker_cleanup" label="Force Docker Cleanup" /> instantSave id="server.settings.force_docker_cleanup" label="Force Docker Cleanup" />
</div> </div>
<x-modal-confirmation
title="Confirm Docker Cleanup?"
buttonTitle="Trigger Docker Cleanup"
submitAction="manualCleanup"
:actions="[
'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)',
'Permanently deletes all unused images',
'Clears build cache',
'Removes old versions of the Coolify helper image',
'Optionally permanently deletes all unused volumes (if enabled in advanced options).',
'Optionally permanently deletes all unused networks (if enabled in advanced options).'
]"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Trigger Docker Cleanup"
/>
</div>
@if ($server->settings->force_docker_cleanup) @if ($server->settings->force_docker_cleanup)
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency" <x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
label="Docker cleanup frequency" required label="Docker cleanup frequency" required
@@ -211,9 +236,36 @@
label="Docker cleanup threshold (%)" required label="Docker cleanup threshold (%)" required
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." /> helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
@endif @endif
<div x-data="{ open: false }" class="mt-4 max-w-md">
<button @click="open = !open" type="button" class="flex items-center justify-between w-full text-left text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100">
<span>Advanced Options</span>
<svg :class="{'rotate-180': open}" class="w-5 h-5 transition-transform duration-200" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
<div x-show="open" class="mt-2 space-y-2">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2"><strong>Warning: Enable these options only if you fully understand their implications and consequences!</strong><br>Improper use will result in data loss and could cause functional issues.</p>
<x-forms.checkbox instantSave id="server.settings.delete_unused_volumes" label="Delete Unused Volumes"
helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data form stopped containers will be lost!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'>
<li>Volumes not attached to running containers will be deleted and data will be permanently lost (stopped containers are affected).</li>
<li>Data from stopped containers volumes will be permanently lost.</li>
<li>No way to recover deleted volume data.</li>
</ul>"
/>
<x-forms.checkbox instantSave id="server.settings.delete_unused_networks" label="Delete Unused Networks"
helper="This option will remove all unused Docker networks during cleanup.<br><br><strong>Warning: Functionality may be lost and containers may not be able to communicate with each other!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'>
<li>Networks not attached to running containers will be permanently deleted (stopped containers are affected).</li>
<li>Custom networks for stopped containers will be permanently deleted.</li>
<li>Functionality may be lost and containers may not be able to communicate with each other.</li>
</ul>"
/>
</div> </div>
</div> </div>
<div class="flex flex-wrap gap-2 sm:flex-nowrap"> </div>
<div class="flex flex-wrap gap-4 sm:flex-nowrap">
<x-forms.input id="server.settings.concurrent_builds" label="Number of concurrent builds" required <x-forms.input id="server.settings.concurrent_builds" label="Number of concurrent builds" required
helper="You can specify the number of simultaneous build processes/deployments that should run concurrently." /> helper="You can specify the number of simultaneous build processes/deployments that should run concurrently." />
<x-forms.input id="server.settings.dynamic_timeout" label="Deployment timeout (seconds)" required <x-forms.input id="server.settings.dynamic_timeout" label="Deployment timeout (seconds)" required

View File

@@ -3,10 +3,12 @@
<x-status.running status="Proxy Running" /> <x-status.running status="Proxy Running" />
@elseif (data_get($server, 'proxy.status') === 'restarting') @elseif (data_get($server, 'proxy.status') === 'restarting')
<x-status.restarting status="Proxy Restarting" /> <x-status.restarting status="Proxy Restarting" />
@else @elseif (data_get($server, 'proxy.force_stop'))
<x-status.stopped status="Proxy Stopped" /> <x-status.stopped status="Proxy Stopped" />
@elseif (data_get($server, 'proxy.status') === 'exited')
<x-status.stopped status="Proxy Exited" />
@else
<x-status.stopped status="Proxy Not Running" />
@endif @endif
@if (data_get($server, 'proxy.status') === 'running')
<x-forms.button wire:click='checkProxy(true)'>Refresh</x-forms.button> <x-forms.button wire:click='checkProxy(true)'>Refresh</x-forms.button>
@endif
</div> </div>

View File

@@ -8,7 +8,7 @@ services:
twenty: twenty:
image: 'twentycrm/twenty:latest' image: 'twentycrm/twenty:latest'
environment: environment:
- SERVICE_FQDN_TRIGGER_3000 - SERVICE_FQDN_TWENTY_3000
- SERVER_URL=$SERVICE_FQDN_TWENTY - SERVER_URL=$SERVICE_FQDN_TWENTY
- FRONT_BASE_URL=$SERVICE_FQDN_TWENTY - FRONT_BASE_URL=$SERVICE_FQDN_TWENTY
- ENABLE_DB_MIGRATIONS=true - ENABLE_DB_MIGRATIONS=true

View File

@@ -8,7 +8,7 @@ services:
uptime-kuma: uptime-kuma:
image: louislam/uptime-kuma:1 image: louislam/uptime-kuma:1
environment: environment:
- SERVICE_FQDN_UPTIME-KUMA_3001 - SERVICE_FQDN_UPTIMEKUMA_3001
volumes: volumes:
- uptime-kuma:/app/data - uptime-kuma:/app/data
healthcheck: healthcheck:

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.346" "version": "4.0.0-beta.347"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.347" "version": "4.0.0-beta.348"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.1"