Merge branch 'next' into next

This commit is contained in:
Leonardo Cabeza
2024-09-29 20:58:27 -05:00
committed by GitHub
84 changed files with 962 additions and 1203 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

@@ -6,19 +6,16 @@ You can ask for guidance anytime on our [Discord server](https://coollabs.io/dis
## Table of Contents
- [Contributing to Coolify](#contributing-to-coolify)
- [Table of Contents](#table-of-contents)
- [1. Setup Development Environment](#1-setup-development-environment)
- [2. Verify Installation (Optional)](#2-verify-installation-optional)
- [3. Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
- [4. Set up Environment Variables](#4-set-up-environment-variables)
- [5. Start Coolify](#5-start-coolify)
- [6. Start Development](#6-start-development)
- [7. Development Notes](#7-development-notes)
- [8. Create a Pull Request](#8-create-a-pull-request)
- [Additional Contribution Guidelines](#additional-contribution-guidelines)
- [Contributing a New Service](#contributing-a-new-service)
- [Contributing to Documentation](#contributing-to-documentation)
1. [Setup Development Environment](#1-setup-development-environment)
2. [Verify Installation](#2-verify-installation-optional)
3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
4. [Set up Environment Variables](#4-set-up-environment-variables)
5. [Start Coolify](#5-start-coolify)
6. [Start Development](#6-start-development)
7. [Create a Pull Request](#7-create-a-pull-request)
8. [Development Notes](#development-notes)
9. [Resetting Development Environment](#resetting-development-environment)
10. [Additional Contribution Guidelines](#additional-contribution-guidelines)
## 1. Setup Development Environment
@@ -29,15 +26,15 @@ Follow the steps below for your operating system:
1. Install `docker-ce`, Docker Desktop (or similar):
- Docker CE (recommended):
- Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install)
- After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/)
- Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify)
- After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify)
- Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide
- Install Docker Desktop (easier):
- Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/)
- Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify)
- Ensure WSL2 backend is enabled in Docker Desktop settings
2. Install Spin:
- Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2)
- Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2?ref=coolify)
</details>
@@ -46,12 +43,12 @@ Follow the steps below for your operating system:
1. Install Orbstack, Docker Desktop (or similar):
- Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
- Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation)
- Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify)
- Docker Desktop:
- Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/)
- Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify)
2. Install Spin:
- Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin)
- Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin?ref=coolify)
</details>
@@ -60,12 +57,12 @@ Follow the steps below for your operating system:
1. Install Docker Engine, Docker Desktop (or similar):
- Docker Engine (recommended, as there is no VM overhead):
- Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/) for your Linux distribution
- Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution
- Docker Desktop:
- If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/)
- If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify)
2. Install Spin:
- Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions)
- Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions?ref=coolify)
</details>
@@ -89,14 +86,14 @@ After installing Docker (or Orbstack) and Spin, verify the installation:
| Editor | Platform | Download Link |
|--------|----------|---------------|
| Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download) |
| Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/) |
| Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download) |
| Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download?ref=coolify) |
| Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/?ref=coolify) |
| Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download?ref=coolify) |
3. Clone the Coolify Repository from your fork to your local machine
- Use `git clone` in the command line, or
- Use GitHub Desktop (recommended):
- Download and install from [https://desktop.github.com/](https://desktop.github.com/)
- Download and install from [https://desktop.github.com/](https://desktop.github.com/?ref=coolify)
- Open GitHub Desktop and login with your GitHub account
- Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone`
@@ -149,7 +146,36 @@ After installing Docker (or Orbstack) and Spin, verify the installation:
> TELESCOPE_ENABLED=true
> ```
## 7. Development Notes
## 7. Create a Pull Request
1. After making changes or adding a new service:
- Commit your changes to your forked repository.
- Push the changes to your GitHub account.
2. Creating the Pull Request (PR):
- Navigate to the main Coolify repository on GitHub.
- Click the "Pull requests" tab.
- Click the green "New pull request" button.
- Choose your fork and branch as the compare branch.
- Click "Create pull request".
3. Filling out the PR details:
- Give your PR a descriptive title.
- Use the Pull Request Template provided and fill in the details.
> [!IMPORTANT]
> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch.
4. Submit your PR:
- Review your changes one last time.
- Click "Create pull request" to submit.
> [!NOTE]
> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers.
After submission, maintainers will review your PR and may request changes or provide feedback.
## Development Notes
When working on Coolify, keep the following in mind:
@@ -168,35 +194,41 @@ When working on Coolify, keep the following in mind:
> [!IMPORTANT]
> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
## 8. Create a Pull Request
## Resetting Development Environment
1. After making changes or adding a new service:
- Commit your changes to your forked repository.
- Push the changes to your GitHub account.
If you encounter issues or break your database or something else, follow these steps to start from a clean slate (works since `v4.0.0-beta.342`):
2. Creating the Pull Request (PR):
- Navigate to the main Coolify repository on GitHub.
- Click the "Pull requests" tab.
- Click the green "New pull request" button.
- Choose your fork and branch as the compare branch.
- Click "Create pull request".
1. Stop all running containers `ctrl + c`.
3. Filling out the PR details:
- Give your PR a descriptive title.
- In the description, explain the changes you've made.
- Reference any related issues by using keywords like "Fixes #123" or "Closes #456".
2. Remove all Coolify containers:
```bash
docker rm coolify coolify-db coolify-redis coolify-realtime coolify-testing-host coolify-minio coolify-vite-1 coolify-mail
```
3. Remove Coolify volumes (it is possible that the volumes have no `coolify` prefix on your machine, in that case remove the prefix from the command):
```bash
docker volume rm coolify_dev_backups_data coolify_dev_postgres_data coolify_dev_redis_data coolify_dev_coolify_data coolify_dev_minio_data
```
4. Remove unused images:
```bash
docker image prune -a
```
5. Start Coolify again:
```bash
spin up
```
6. Run database migrations and seeders:
```bash
docker exec -it coolify php artisan migrate:fresh --seed
```
After completing these steps, you'll have a fresh development setup.
> [!IMPORTANT]
> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch.
4. Submit your PR:
- Review your changes one last time.
- Click "Create pull request" to submit.
> [!NOTE]
> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers.
After submission, maintainers will review your PR and may request changes or provide feedback.
> Always run database migrations and seeders after switching branches or pulling updates to ensure your local database structure matches the current codebase and includes necessary seed data.
## Additional Contribution Guidelines

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.
## 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
1. **Development on `next` or separate branches**
- Changes, fixes and new features are developed on the `next` or even separate branches.
1. **Development on `next` or Feature Branches**
- Improvements, fixes, and new features are developed on the `next` branch or separate feature branches.
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**
- After merging to `main`, a new release is built.
- Note: A push to `main` does not automatically mean a new version is released.
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.
4. **Creating a GitHub release**
- A new release is created on GitHub with the new version details.
4. **Creating a GitHub Release**
- A new GitHub release is manually created with details of the changes made in the version.
5. **Updating the CDN**
- The final step is updating the version information on the CDN:
[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: [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json)
> [!NOTE]
> The CDN update may not occur immediately after the GitHub release. It can 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
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]
> 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
@@ -42,4 +127,4 @@ To update your Coolify instance to a specific (unreleased) version, use the foll
```bash
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

@@ -543,7 +543,7 @@ class GetContainersStatus
}
}
}
$exitedServices = $exitedServices->unique('id');
$exitedServices = $exitedServices->unique('uuid');
foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) {
continue;
@@ -651,8 +651,9 @@ class GetContainersStatus
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
// Check if proxy is running
$this->server->proxyType();
if (! $this->server->proxySet() || $this->server->proxy->force_stop) {
return;
}
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';

View File

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

View File

@@ -12,28 +12,29 @@ class CleanupDocker
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" --filter "label!=coolify.proxy=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) {
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

@@ -1442,14 +1442,24 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->pull_request_id !== 0) {
$local_branch = "pull/{$this->pull_request_id}/head";
}
$private_key = $this->application->privateKey?->getKeyLocation();
$private_key = data_get($this->application, 'private_key.private_key');
if ($private_key) {
$private_key = base64_encode($private_key);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$private_key}\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'),
],
[
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
],
[
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
],
[
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
'hidden' => true,
'save' => 'git_commit_sha',
],
]
);
} else {
$this->execute_remote_command(

View File

@@ -1,62 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Team;
use App\Notifications\Database\DailyBackup;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class DatabaseBackupStatusJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public function __construct() {}
public function handle()
{
// $teams = Team::all();
// foreach ($teams as $team) {
// $scheduled_backups = $team->scheduledDatabaseBackups()->get();
// if ($scheduled_backups->isEmpty()) {
// continue;
// }
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// }
// }
// $scheduled_backups = ScheduledDatabaseBackup::all();
// $databases = collect();
// $teams = collect();
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// $database = $scheduled_backup->database;
// $team = $database->team();
// $teams->put($team->id, $team);
// $databases->put("{$team->id}:{$database->name}", [
// 'failed_count' => $failed->count(),
// ]);
// }
// foreach ($databases as $name => $database) {
// [$team_id, $name] = explode(':', $name);
// $team = $teams->get($team_id);
// $team?->notify(new DailyBackup($databases));
// }
}
}

View File

@@ -23,7 +23,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $usageBefore = null;
public function __construct(public Server $server) {}
public function __construct(public Server $server, public bool $manualCleanup = false) {}
public function handle(): void
{
@@ -31,8 +31,9 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
if (! $this->server->isFunctional()) {
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);
return;

View File

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

View File

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

View File

@@ -182,7 +182,7 @@ class BackupEdit extends Component
{
return view('livewire.project.database.backup-edit', [
'checkboxes' => [
['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
['id' => 'delete_associated_backups_locally', 'label' => __('database.delete_backups_locally')],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
],

View File

@@ -78,7 +78,7 @@ class Heading extends Component
{
return view('livewire.project.database.heading', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
]);
}

View File

@@ -33,7 +33,7 @@ class StackForm extends Component
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$rules = data_get($field, 'rules', 'nullable');
$isPassword = data_get($field, 'isPassword');
$isPassword = data_get($field, 'isPassword', false);
$this->fields->put($key, [
'serviceName' => $serviceName,
'key' => $key,
@@ -47,7 +47,15 @@ class StackForm extends Component
$this->validationAttributes["fields.$key.value"] = $fieldKey;
}
}
$this->fields = $this->fields->sortBy('name');
$this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
return $group->sortBy(function ($field) {
return data_get($field, 'isPassword') ? 1 : 0;
})->mapWithKeys(function ($field) {
return [$field['key'] => $field];
});
})->flatMap(function ($group) {
return $group;
});
}
public function saveCompose($raw)

View File

@@ -9,6 +9,20 @@ use Livewire\Component;
class Terminal extends Component
{
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'closeTerminal',
];
}
public function closeTerminal()
{
$this->dispatch('reloadWindow');
}
#[On('send-terminal-command')]
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
{

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Security;
use App\Models\InstanceSettings;
use Livewire\Component;
class ApiTokens extends Component
@@ -16,6 +17,8 @@ class ApiTokens extends Component
public array $permissions = ['read-only'];
public $isApiEnabled;
public function render()
{
return view('livewire.security.api-tokens');
@@ -23,6 +26,7 @@ class ApiTokens extends Component
public function mount()
{
$this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
$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\StopSentinel;
use App\Jobs\DockerCleanupJob;
use App\Jobs\PullSentinelImageJob;
use App\Models\Server;
use Livewire\Component;
@@ -24,6 +25,10 @@ class Form extends Component
public $timezones;
public $delete_unused_volumes = false;
public $delete_unused_networks = false;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@@ -58,6 +63,8 @@ class Form extends Component
'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_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 = [
@@ -79,6 +86,8 @@ class Form extends Component
'server.settings.metrics_history_days' => 'Metrics History',
'server.settings.is_server_api_enabled' => 'Server API',
'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)
@@ -88,6 +97,8 @@ class Form extends Component
$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_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)
@@ -137,6 +148,7 @@ class Form extends Component
try {
refresh_server_connection($this->server->privateKey);
$this->validateServer(false);
$this->server->settings->save();
$this->server->save();
$this->dispatch('success', 'Server updated.');
@@ -154,6 +166,7 @@ class Form extends Component
ray('Sentinel is not enabled');
StopSentinel::dispatch($this->server);
}
$this->server->settings->save();
// $this->checkPortForServerApi();
} catch (\Throwable $e) {
@@ -234,9 +247,9 @@ class Form extends Component
$this->server->settings->server_timezone = $newTimezone;
$this->server->settings->save();
}
$this->server->settings->save();
$this->server->save();
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -250,6 +263,16 @@ class Form extends Component
$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()
{
$this->server->settings->is_cloudflare_tunnel = true;

View File

@@ -49,6 +49,10 @@ class Status extends Component
if ($this->server->proxy->status === 'running') {
$this->polling = false;
$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 {
$notification && $this->dispatch('error', 'Proxy is not running.');
}

View File

@@ -36,6 +36,8 @@ use Symfony\Component\Yaml\Yaml;
'validation_logs' => ['type' => 'string'],
'log_drain_notification_sent' => ['type' => 'boolean'],
'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,
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
'delete_unused_volumes' => 'boolean',
'delete_unused_networks' => 'boolean',
];
protected $schemalessAttributes = [
@@ -1048,6 +1052,10 @@ $schema://$host {
return ['uptime' => false, 'error' => 'Server skipped.'];
}
try {
// Make sure the private key is stored
if ($server->privateKey) {
$server->privateKey->storeInFileSystem();
}
instant_remote_process(['ls /'], $server);
$server->settings()->update([
'is_reachable' => true,

View File

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

View File

@@ -96,6 +96,8 @@ function connectProxyToNetworks(Server $server)
"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 connect $network coolify-proxy >/dev/null 2>&1 || true",
"echo 'Successfully connected coolify-proxy to $network network.'",
"echo 'Proxy started and configured successfully!'",
];
});
} else {
@@ -104,6 +106,8 @@ function connectProxyToNetworks(Server $server)
"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 connect $network coolify-proxy >/dev/null 2>&1 || true",
"echo 'Successfully connected coolify-proxy to $network network.'",
"echo 'Proxy started and configured successfully!'",
];
});
}
@@ -144,6 +148,7 @@ function generate_default_proxy_configuration(Server $server)
'traefik.http.routers.traefik.service=api@internal',
'traefik.http.services.traefik.loadbalancer.server.port=8080',
'coolify.managed=true',
'coolify.proxy=true',
];
$config = [
'networks' => $array_of_networks->toArray(),
@@ -217,7 +222,6 @@ function generate_default_proxy_configuration(Server $server)
}
} elseif ($proxy_type === 'CADDY') {
$config = [
'version' => '3.8',
'networks' => $array_of_networks->toArray(),
'services' => [
'caddy' => [
@@ -236,12 +240,9 @@ function generate_default_proxy_configuration(Server $server)
'80:80',
'443:443',
],
// "healthcheck" => [
// "test" => "wget -qO- http://localhost:80|| exit 1",
// "interval" => "4s",
// "timeout" => "2s",
// "retries" => 5,
// ],
'labels' => [
'coolify.managed=true',
],
'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro',
"{$proxy_path}/dynamic:/dynamic",

View File

@@ -505,6 +505,12 @@ function sslip(Server $server)
return "http://$baseIp.sslip.io";
}
// ipv6
if (str($server->ip)->contains(':')) {
$ipv6 = str($server->ip)->replace(':', '-');
return "http://{$ipv6}.sslip.io";
}
return "http://{$server->ip}.sslip.io";
}
@@ -1230,8 +1236,6 @@ function parseLineForSudo(string $command, Server $server): string
function get_public_ips()
{
try {
echo "Refreshing public ips!\n";
$settings = \App\Models\InstanceSettings::get();
[$first, $second] = Process::concurrently(function (Pool $pool) {
$pool->path(__DIR__)->command('curl -4s https://ifconfig.io');
$pool->path(__DIR__)->command('curl -6s https://ifconfig.io');
@@ -1245,7 +1249,7 @@ function get_public_ips()
return;
}
$settings->update(['public_ipv4' => $ipv4]);
InstanceSettings::get()->update(['public_ipv4' => $ipv4]);
}
} catch (\Exception $e) {
echo "Error: {$e->getMessage()}\n";
@@ -1260,7 +1264,7 @@ function get_public_ips()
return;
}
$settings->update(['public_ipv6' => $ipv6]);
InstanceSettings::get()->update(['public_ipv6' => $ipv6]);
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
@@ -2928,7 +2932,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}
$parsedServices = collect([]);
ray()->clearAll();
// ray()->clearAll();
$allMagicEnvironments = collect([]);
foreach ($services as $serviceName => $service) {
@@ -3484,6 +3488,18 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$value = $value->after('?');
}
if ($originalValue->value() === $value->value()) {
// This means the variable does not have a default value, so it needs to be created in Coolify
$parsedKeyValue = replaceVariables($value);
$resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->firstOrCreate([
'key' => $parsedKeyValue,
$nameOfId => $resource->id,
], [
'is_build_time' => false,
'is_preview' => false,
]);
// Add the variable to the environment so it will be shown in the deployable compose file
$environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value;
continue;
}
$resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([
@@ -3576,6 +3592,17 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($environment->count() > 0) {
$environment = $environment->filter(function ($value, $key) {
return ! str($key)->startsWith('SERVICE_FQDN_');
})->map(function ($value, $key) use ($resource) {
// if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used
if (str($value)->isEmpty()) {
if ($resource->environment_variables()->where('key', $key)->exists()) {
$value = $resource->environment_variables()->where('key', $key)->first()->value;
} else {
$value = null;
}
}
return $value;
});
}
$serviceLabels = $labels->merge($defaultLabels);

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.342',
'release' => '4.0.0-beta.348',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.342';
return '4.0.0-beta.348';

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,
ServerSettingSeeder::class,
ProjectSeeder::class,
ProjectSettingSeeder::class,
EnvironmentSeeder::class,
StandaloneDockerSeeder::class,
SwarmDockerSeeder::class,
KubernetesSeeder::class,
GithubAppSeeder::class,
GitlabAppSeeder::class,
ApplicationSeeder::class,
ApplicationSettingsSeeder::class,
ApplicationPreviewSeeder::class,
EnvironmentVariableSeeder::class,
LocalPersistentVolumeSeeder::class,
S3StorageSeeder::class,
StandalonePostgresqlSeeder::class,
ScheduledDatabaseBackupSeeder::class,
ScheduledDatabaseBackupExecutionSeeder::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

@@ -19,7 +19,6 @@ class PopulateSshKeysDirectorySeeder extends Seeder
PrivateKey::chunk(100, function ($keys) {
foreach ($keys as $key) {
echo 'Storing key: '.$key->name."\n";
$key->storeInFileSystem();
}
});
@@ -29,8 +28,8 @@ class PopulateSshKeysDirectorySeeder extends Seeder
Process::run("chown -R $user ".storage_path('app/ssh/keys'));
Process::run("chown -R $user ".storage_path('app/ssh/mux'));
} else {
Process::run('chown -R 9999:9999 '.storage_path('app/ssh/keys'));
Process::run('chown -R 9999:9999 '.storage_path('app/ssh/mux'));
Process::run('chown -R 9999:root '.storage_path('app/ssh/keys'));
Process::run('chown -R 9999:root '.storage_path('app/ssh/mux'));
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";

View File

@@ -65,55 +65,52 @@ class ProductionSeeder extends Seeder
]);
}
// Add Coolify host (localhost) as Server if it doesn't exist
if (Server::find(0) == null) {
$server_details = [
'id' => 0,
'name' => 'localhost',
'description' => "This is the server where Coolify is running on. Don't delete this!",
'user' => 'root',
'ip' => 'host.docker.internal',
'team_id' => 0,
'private_key_id' => 0,
];
$server_details['proxy'] = ServerMetadata::from([
'type' => ProxyTypes::TRAEFIK->value,
'status' => ProxyStatus::EXITED->value,
]);
$server = Server::create($server_details);
$server->settings->is_reachable = true;
$server->settings->is_usable = true;
$server->settings->save();
} else {
$server = Server::find(0);
$server->settings->is_reachable = true;
$server->settings->is_usable = true;
$server->settings->save();
}
if (StandaloneDocker::find(0) == null) {
StandaloneDocker::create([
'id' => 0,
'name' => 'localhost-coolify',
'network' => 'coolify',
'server_id' => 0,
]);
if (! isCloud()) {
if (Server::find(0) == null) {
$server_details = [
'id' => 0,
'name' => 'localhost',
'description' => "This is the server where Coolify is running on. Don't delete this!",
'user' => 'root',
'ip' => 'host.docker.internal',
'team_id' => 0,
'private_key_id' => 0,
];
$server_details['proxy'] = ServerMetadata::from([
'type' => ProxyTypes::TRAEFIK->value,
'status' => ProxyStatus::EXITED->value,
]);
$server = Server::create($server_details);
$server->settings->is_reachable = true;
$server->settings->is_usable = true;
$server->settings->save();
} else {
$server = Server::find(0);
$server->settings->is_reachable = true;
$server->settings->is_usable = true;
$server->settings->save();
}
if (StandaloneDocker::find(0) == null) {
StandaloneDocker::create([
'id' => 0,
'name' => 'localhost-coolify',
'network' => 'coolify',
'server_id' => 0,
]);
}
}
if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) {
echo "Checking localhost key.\n";
$coolify_key_name = '@host.docker.internal';
$ssh_keys_directory = Storage::disk('ssh-keys')->files();
$coolify_key = collect($ssh_keys_directory)->firstWhere(fn ($item) => str($item)->contains($coolify_key_name));
$found = PrivateKey::find(0);
if ($found) {
echo 'Private Key found in database.\n';
$server = Server::find(0);
$found = $server->privateKey;
if (! $found) {
if ($coolify_key) {
echo "SSH key found for the Coolify host machine (localhost).\n";
}
} else {
if ($coolify_key) {
$coolify_key = Storage::disk('ssh-keys')->get($coolify_key);
$user = str($coolify_key)->before('@')->after('id.');
$coolify_key = Storage::disk('ssh-keys')->get($coolify_key);
PrivateKey::create([
'id' => 0,
'team_id' => 0,
@@ -123,17 +120,7 @@ class ProductionSeeder extends Seeder
]);
$server->update(['user' => $user]);
echo "SSH key found for the Coolify host machine (localhost).\n";
} else {
PrivateKey::create(
[
'id' => 0,
'team_id' => 0,
'name' => 'localhost\'s key',
'description' => 'The private key for the Coolify host machine (localhost).',
'private_key' => 'Paste here you private key!!',
]
);
echo "No SSH key found for the Coolify host machine (localhost).\n";
echo "Please read the following documentation (point 3) to fix it: https://coolify.io/docs/knowledge-base/server/openssh/\n";
echo "Your localhost connection won't work until then.";

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

@@ -19,7 +19,7 @@ services:
PUSHER_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
volumes:
- .:/var/www/html/:cached
- /data/coolify/backups/:/var/www/html/storage/app/backups
- dev_backups_data:/var/www/html/storage/app/backups
postgres:
pull_policy: always
ports:
@@ -32,8 +32,7 @@ services:
POSTGRES_DB: "${DB_DATABASE:-coolify}"
POSTGRES_HOST_AUTH_METHOD: "trust"
volumes:
- /data/coolify/_volumes/database/:/var/lib/postgresql/data
# - coolify-pg-data-dev:/var/lib/postgresql/data
- dev_postgres_data:/var/lib/postgresql/data
redis:
pull_policy: always
ports:
@@ -41,8 +40,7 @@ services:
env_file:
- .env
volumes:
- /data/coolify/_volumes/redis/:/data
# - coolify-redis-data-dev:/data
- dev_redis_data:/data
soketi:
build:
context: .
@@ -64,12 +62,13 @@ services:
image: node:20
pull_policy: always
working_dir: /var/www/html
# environment:
# VITE_PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
environment:
VITE_HOST: "${VITE_HOST:-localhost}"
VITE_PORT: "${VITE_PORT:-5173}"
ports:
- "${VITE_PORT:-5173}:${VITE_PORT:-5173}"
volumes:
- .:/var/www/html:cached
- .:/var/www/html/:cached
command: sh -c "npm install && npm run dev"
networks:
- coolify
@@ -79,9 +78,12 @@ services:
init: true
container_name: coolify-testing-host
volumes:
- /:/host
- /var/run/docker.sock:/var/run/docker.sock
- /data/coolify/:/data/coolify
- dev_coolify_data:/data/coolify
- dev_backups_data:/data/coolify/backups
- dev_postgres_data:/data/coolify/_volumes/database
- dev_redis_data:/data/coolify/_volumes/redis
- dev_minio_data:/data/coolify/_volumes/minio
networks:
- coolify
mailpit:
@@ -105,17 +107,16 @@ services:
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
volumes:
- /data/coolify/_volumes/minio/:/data
# - coolify-minio-data-dev:/data
- dev_minio_data:/data
networks:
- coolify
volumes:
coolify-data-dev:
coolify-pg-data-dev:
coolify-redis-data-dev:
coolify-minio-data-dev:
dev_backups_data:
dev_postgres_data:
dev_redis_data:
dev_coolify_data:
dev_minio_data:
networks:
coolify:

View File

@@ -68,7 +68,11 @@ wss.on('connection', (ws) => {
const messageHandlers = {
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(),
resume: (session) => session.ptyProcess.resume(),
checkActive: (session, data) => {
@@ -140,6 +144,7 @@ async function handleCommand(ws, command, userId) {
ptyProcess.onData((data) => ws.send(data));
// when parent closes
ptyProcess.onExit(({ exitCode, signal }) => {
console.error(`Process exited with code ${exitCode} and signal ${signal}`);
userSession.isActive = false;

View File

@@ -32,5 +32,6 @@
"resource.non_persistent": "All non-persistent data will be deleted.",
"resource.delete_volumes": "Permanently delete all volumes associated with this resource.",
"resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.",
"resource.delete_configurations": "Permanently delete all configuration files from the server."
"resource.delete_configurations": "Permanently delete all configuration files from the server.",
"database.delete_backups_locally": "All backups will be permanently deleted from local storage."
}

View File

@@ -1,34 +0,0 @@
# Coolify Configuration
APP_ENV=local
APP_NAME="Coolify Development"
APP_ID=development
APP_KEY=
APP_URL=http://localhost
APP_PORT=8000
APP_DEBUG=true
SSH_MUX_ENABLED=false
# PostgreSQL Database Configuration
DB_DATABASE=coolify
DB_USERNAME=coolify
DB_PASSWORD=password
DB_HOST=host.docker.internal
DB_PORT=5432
# Ray Configuration
# Set to true to enable Ray
RAY_ENABLED=false
# Set custom ray port
# RAY_PORT=
# Enable Laravel Telescope for debugging
TELESCOPE_ENABLED=false
# Selenium Driver URL for Dusk
DUSK_DRIVER_URL=http://selenium:4444
# Special Keys for Andras
# For cache purging
BUNNY_API_KEY=
# For asset uploads
BUNNY_STORAGE_API_KEY=

View File

@@ -8,7 +8,7 @@ set -o pipefail # Cause a pipeline to return the status of the last command that
CDN="https://cdn.coollabs.io/coolify-nightly"
DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.5"
VERSION="1.6"
DOCKER_VERSION="26.0"
# TODO: Ask for a user
CURRENT_USER=$USER
@@ -39,6 +39,11 @@ if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
OS_TYPE="arch"
fi
# Check if the OS is Asahi Linux, if so, change it to fedora
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
OS_TYPE="fedora"
fi
# Check if the OS is popOS, if so, change it to ubuntu
if [ "$OS_TYPE" = "pop" ]; then
OS_TYPE="ubuntu"
@@ -399,10 +404,10 @@ if [ ! -f ~/.ssh/authorized_keys ]; then
fi
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
if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
if [ "$IF_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
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
chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal

View File

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

6
package-lock.json generated
View File

@@ -1860,9 +1860,9 @@
}
},
"node_modules/rollup": {
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"version": "3.29.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"

View File

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

@@ -6,22 +6,26 @@
'instantSave' => false,
'value' => null,
'hideLabel' => false,
'fullWidth' => false,
])
<div class="flex flex-row items-center gap-4 px-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100">
@if(!$hideLabel)
<label class="flex gap-4 px-0 min-w-fit label">
<span class="flex gap-2">
@if ($label)
{!! $label !!}
@else
{{ $id }}
@endif
@if ($helper)
<x-helper :helper="$helper" />
@endif
</span>
</label>
<div @class([
'flex flex-row items-center gap-4 px-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100',
'w-full' => $fullWidth,
])>
@if (!$hideLabel)
<label class="flex gap-4 px-0 min-w-fit label">
<span class="flex gap-2">
@if ($label)
{!! $label !!}
@else
{{ $id }}
@endif
@if ($helper)
<x-helper :helper="$helper" />
@endif
</span>
</label>
@endif
<span class="flex-grow"></span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}

View File

@@ -3,7 +3,7 @@
'w-full' => !$isMultiline,
])>
@if ($label)
<label class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
<label class="flex gap-1 items-center mb-1 text-sm font-medium">{{ $label }}
@if ($required)
<x-highlighted text="*" />
@endif
@@ -16,7 +16,7 @@
<div class="relative" x-data="{ type: 'password' }">
@if ($allowToPeak)
<div x-on:click="changePasswordFieldType"
class="absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer hover:dark:text-white">
class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer hover:dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
@@ -25,7 +25,8 @@
</svg>
</div>
@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
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"
@@ -35,7 +36,7 @@
</div>
@else
<input @if ($value) value="{{ $value }}" @endif
<input autocomplete="{{ $autocomplete }}" @if ($value) value="{{ $value }}" @endif
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly)
@if ($id !== 'null') wire:model={{ $id }} @endif
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'

View File

@@ -139,7 +139,7 @@
@endif
@else
@if ($buttonFullWidth)
<x-forms.button @click="modalOpen=true" class="flex w-full gap-2" wire:target>
<x-forms.button @click="modalOpen=true" class="flex gap-2 w-full" wire:target>
{{ $buttonTitle }}
</x-forms.button>
@else
@@ -162,17 +162,17 @@
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
<div class="flex items-center justify-between pb-3">
<h3 class="text-2xl font-bold pr-8">{{ $title }}</h3>
<div class="flex justify-between items-center pb-3">
<h3 class="pr-8 text-2xl font-bold">{{ $title }}</h3>
<button @click="modalOpen = false; resetModal()"
class="absolute top-2 right-2 flex items-center justify-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="relative w-auto pb-8">
<div class="relative pb-8 w-auto">
@if (!empty($checkboxes))
<!-- Step 1: Select actions -->
<div x-show="step === 1">
@@ -180,15 +180,11 @@
<h4>Actions</h4>
</div>
@foreach ($checkboxes as $index => $checkbox)
<div class="flex items-center justify-between mb-2">
<label for="{{ $checkbox['id'] }}"
class="text-sm leading-5 text-gray-700 dark:text-gray-300 flex-grow pr-4">
{{ $checkbox['label'] }}
</label>
<x-forms.checkbox :id="$checkbox['id']" :wire:model="$checkbox['id']"
<div class="flex justify-between items-center mb-2">
<x-forms.checkbox fullWidth :label="$checkbox['label']" :id="$checkbox['id']"
:wire:model="$checkbox['id']"
x-on:change="toggleAction('{{ $checkbox['id'] }}')" :checked="$this->{$checkbox['id']}"
x-bind:checked="selectedActions.includes('{{ $checkbox['id'] }}')"
class="flex-shrink-0" :hideLabel="true" />
x-bind:checked="selectedActions.includes('{{ $checkbox['id'] }}')" />
</div>
@endforeach
</div>
@@ -196,7 +192,7 @@
<!-- Step 2: Confirm deletion -->
<div x-show="step === 2">
<div class="bg-error border-l-4 border-red-500 text-white p-4 mb-4" role="alert">
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error" role="alert">
<p class="font-bold">Warning</p>
<p>This operation is permanent and cannot be undone. Please think again before proceeding!
</p>
@@ -205,7 +201,7 @@
<ul class="mb-4 space-y-2">
@foreach ($actions as $action)
<li class="flex items-center text-red-500">
<svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor"
<svg class="flex-shrink-0 mr-2 w-5 h-5" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
@@ -216,7 +212,7 @@
@foreach ($checkboxes as $checkbox)
<template x-if="selectedActions.includes('{{ $checkbox['id'] }}')">
<li class="flex items-center text-red-500">
<svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor"
<svg class="flex-shrink-0 mr-2 w-5 h-5" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
@@ -228,16 +224,16 @@
</ul>
@if ($confirmWithText)
<div class="mb-4">
<h4 class="text-lg font-semibold mb-2">Confirm Actions</h4>
<p class="text-sm mb-2">{{ $confirmationLabel }}</p>
<h4 class="mb-2 text-lg font-semibold">Confirm Actions</h4>
<p class="mb-2 text-sm">{{ $confirmationLabel }}</p>
<div class="relative mb-2">
<input type="text" x-model="confirmationText"
class="w-full p-2 pr-10 rounded text-black input cursor-text" readonly>
class="p-2 pr-10 w-full text-black rounded cursor-text input" readonly>
<button @click="copyConfirmationText()"
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
class="absolute right-2 top-1/2 text-gray-500 transform -translate-y-1/2 hover:text-gray-700"
title="Copy confirmation text" x-ref="copyButton">
<template x-if="!copied">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20"
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 20 20"
fill="currentColor">
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
<path
@@ -245,7 +241,7 @@
</svg>
</template>
<template x-if="copied">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500"
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-green-500"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
@@ -256,18 +252,18 @@
</div>
<label for="userConfirmationText"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mt-4">
class="block mt-4 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ $shortConfirmationLabel }}
</label>
<input type="text" x-model="userConfirmationText"
class="w-full p-2 rounded text-black input mt-1">
class="p-2 mt-1 w-full text-black rounded input">
</div>
@endif
</div>
<!-- Step 3: Password confirmation -->
<div x-show="step === 3 && confirmWithPassword">
<div class="bg-error border-l-4 border-red-500 text-white p-4 mb-4" role="alert">
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error" role="alert">
<p class="font-bold">Final Confirmation</p>
<p>Please enter your password to confirm this destructive action.</p>
</div>
@@ -276,11 +272,11 @@
class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Your Password
</label>
<input type="password" id="password-confirm" x-model="password" class="input w-full"
<input type="password" id="password-confirm" x-model="password" class="w-full input"
placeholder="Enter your password">
<p x-show="passwordError" x-text="passwordError" class="text-red-500 text-sm mt-1"></p>
<p x-show="passwordError" x-text="passwordError" class="mt-1 text-sm text-red-500"></p>
@error('password')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
<p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
</div>

View File

@@ -1,7 +1,7 @@
<nav wire:poll.5000ms="check_status">
<x-resources.breadcrumbs :resource="$database" :parameters="$parameters" />
<x-slide-over @startdatabase.window="slideOverOpen = true" closeWithX fullScreen>
<x-slot:title>Database Startup Logs</x-slot:title>
<x-slot:title>Database Startup</x-slot:title>
<x-slot:content>
<livewire:activity-monitor header="Logs" showWaiting />
</x-slot:content>

View File

@@ -76,7 +76,7 @@
@endif
@if ($application->fqdn)
<span class="flex gap-1 text-xs">{{ Str::limit($application->fqdn, 60) }}
<x-modal-input title="Edit Domains">
<x-modal-input title="Edit Domains" :closeOutside="false">
<x-slot:content>
<span class="cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg"
@@ -107,15 +107,14 @@
Settings
</a>
@if (str($application->status)->contains('running'))
<x-modal-confirmation
title="Confirm Service Application Restart?"
buttonTitle="Restart"
submitAction="restartApplication({{ $application->id }})"
:actions="['The selected service application will be unavailable during the restart.', 'If the service application is currently in use data could be lost.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Restart Service Container"
/>
<x-modal-confirmation title="Confirm Service Application Restart?"
buttonTitle="Restart"
submitAction="restartApplication({{ $application->id }})" :actions="[
'The selected service application will be unavailable during the restart.',
'If the service application is currently in use data could be lost.',
]"
:confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Restart Service Container" />
@endif
</div>
</div>
@@ -155,15 +154,13 @@
Settings
</a>
@if (str($database->status)->contains('running'))
<x-modal-confirmation
title="Confirm Service Database Restart?"
buttonTitle="Restart"
submitAction="restartDatabase({{ $database->id }})"
:actions="['This service database will be unavailable during the restart.', 'If the service database is currently in use data could be lost.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Restart Database"
/>
<x-modal-confirmation title="Confirm Service Database Restart?"
buttonTitle="Restart" submitAction="restartDatabase({{ $database->id }})"
:actions="[
'This service database will be unavailable during the restart.',
'If the service database is currently in use data could be lost.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Restart Database" />
@endif
</div>
</div>
@@ -183,7 +180,8 @@
lazy />
@endforeach
@foreach ($databases as $database)
<livewire:project.service.storage wire:key="database-{{ $database->id }}" :resource="$database" lazy />
<livewire:project.service.storage wire:key="database-{{ $database->id }}" :resource="$database"
lazy />
@endforeach
</div>
<div x-cloak x-show="activeTab === 'scheduled-tasks'">

View File

@@ -6,6 +6,6 @@
</div>
<x-modal-confirmation title="Confirm Resource Deletion?" buttonTitle="Delete" isErrorButton submitAction="delete"
buttonTitle="Delete" :checkboxes="$checkboxes" :actions="['Permanently delete all containers of this resource.']" confirmationText="{{ $resourceName }}"
confirmationLabel="Please confirm the execution of the actions by entering the NAME of the resource below"
confirmationLabel="Please confirm the execution of the actions by entering the Resource Name below"
shortConfirmationLabel="Resource Name" step3ButtonText="Permanently Delete" />
</div>

View File

@@ -32,7 +32,7 @@
<x-forms.button type="submit">Connect</x-forms.button>
</form>
@else
<div class="pt-4">No containers are not running.</div>
<div class="pt-4">No containers are running.</div>
@endif
</div>
</div>

View File

@@ -11,7 +11,7 @@
<form wire:submit="submit" class="w-full">
<div class="flex flex-col gap-2 pb-2">
<div class="flex items-end gap-2 pt-4">
<div class="flex gap-2 items-end pt-4">
<h2>Scheduled Task</h2>
<x-forms.button type="submit">
Save
@@ -23,7 +23,10 @@
step2ButtonText="Permanently Delete" />
</div>
<div class="flex w-full gap-2">
<div class="w-48">
<x-forms.checkbox instantSave id="task.enabled" label="Enabled" />
</div>
<div class="flex gap-2 w-full">
<x-forms.input placeholder="Name" id="task.name" label="Name" required />
<x-forms.input placeholder="php artisan schedule:run" id="task.command" label="Command" required />
<x-forms.input placeholder="0 0 * * * or daily" id="task.frequency" label="Frequency" required />

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 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>
@@ -22,219 +22,14 @@
</g>
</svg></button>
</div>
@script
<script>
const MAX_PENDING_WRITES = 5;
let pendingWrites = 0;
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') }}",
host: "{{ env('TERMINAL_HOST') }}",
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>
// expose terminal config to the terminal.js file
window.terminalConfig = {
protocol: "{{ env('TERMINAL_PROTOCOL') }}",
host: "{{ env('TERMINAL_HOST') }}",
port: "{{ env('TERMINAL_PORT') }}"
}
</script>
@endscript
</div>

View File

@@ -3,19 +3,23 @@
API Tokens | Coolify
</x-slot>
<x-security.navbar />
<div class="pb-4 ">
<div class="pb-4">
<h2>API Tokens</h2>
<div>Tokens are created with the current team as scope. You will only have access to this team's resources.
</div>
@if (!$isApiEnabled)
<div>API is disabled. If you want to use the API, please enable it in the <a href="{{ route('settings.index') }}" class="underline dark:text-white">Settings</a> menu.</div>
@else
<div>Tokens are created with the current team as scope. You will only have access to this team's resources.
</div>
</div>
<h3>New Token</h3>
<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.button type="submit">Create New Token</x-forms.button>
</div>
<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>
<div class="flex gap-1 font-bold dark:text-white">
@if ($permissions)
@@ -56,18 +60,15 @@
@endif
</div>
<x-modal-confirmation
title="Confirm API Token Revocation?"
isErrorButton
buttonTitle="Revoke token"
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.']"
<x-modal-confirmation title="Confirm API Token Revocation?" isErrorButton buttonTitle="Revoke token"
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 }}"
confirmationLabel="Please confirm the execution of the actions by entering the API Token Description below"
shortConfirmationLabel="API Token Description"
:confirmWithPassword="false"
step2ButtonText="Revoke API Token"
/>
shortConfirmationLabel="API Token Description" :confirmWithPassword="false"
step2ButtonText="Revoke API Token" />
</div>
@empty
<div>
@@ -75,5 +76,5 @@
</div>
@endforelse
</div>
@endif
</div>

View File

@@ -16,7 +16,7 @@
<x-modal-confirmation title="Confirm Server Deletion?" isErrorButton buttonTitle="Delete"
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"
shortConfirmationLabel="Server Name" step2ButtonText="Permanently Delete" />
shortConfirmationLabel="Server Name" step2ButtonText="Continue" step3ButtonText="Permanently Delete" />
@endif
@endif
</div>

View File

@@ -133,16 +133,7 @@
</div>
@elseif (!$server->isFunctional())
<div class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded dark:bg-yellow-900 dark:text-yellow-300">
<x-slide-over closeWithX fullScreen>
<x-slot:title>Validate & configure</x-slot:title>
<x-slot:content>
<livewire:server.validate-and-install :server="$server" />
</x-slot:content>
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnels, please click
<span @click="slideOverOpen=true"
wire:click.prevent='validateServer' class="underline cursor-pointer">
here.</span> You will need a Cloudflare token and domain.
</x-slide-over>
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnels, please validate your server first.</span> Then you will need a Cloudflare token and an SSH domain configured.
<br/>
To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please click <span wire:click="manualCloudflareConfig" class="underline cursor-pointer">here</span>, then you should validate the server.
<br/><br/>
@@ -194,26 +185,78 @@
@if ($server->isFunctional())
<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 flex-wrap gap-2 sm:flex-nowrap">
<div class="flex flex-wrap items-center gap-4">
<div class="w-64">
<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" />
</div>
@if ($server->settings->force_docker_cleanup)
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
label="Docker cleanup frequency" required
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." />
@else
<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)
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
label="Docker cleanup frequency" required
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." />
@else
<x-forms.input id="server.settings.docker_cleanup_threshold"
label="Docker cleanup threshold (%)" required
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 class="flex flex-wrap gap-2 sm:flex-nowrap">
<div class="flex flex-wrap gap-4 sm:flex-nowrap">
<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." />
<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" />
@elseif (data_get($server, 'proxy.status') === 'restarting')
<x-status.restarting status="Proxy Restarting" />
@else
@elseif (data_get($server, 'proxy.force_stop'))
<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
@if (data_get($server, 'proxy.status') === 'running')
<x-forms.button wire:click='checkProxy(true)'>Refresh</x-forms.button>
@endif
<x-forms.button wire:click='checkProxy(true)'>Refresh</x-forms.button>
</div>

View File

@@ -269,11 +269,12 @@ Route::middleware(['auth'])->group(function () {
} else {
$server = $execution->scheduledDatabaseBackup->database->destination->server;
}
$privateKeyLocation = $server->privateKey->getKeyLocation();
$disk = Storage::build([
'driver' => 'sftp',
'host' => $server->ip,
'port' => $server->port,
'port' => (int) $server->port,
'username' => $server->user,
'privateKey' => $privateKeyLocation,
'root' => '/',

View File

@@ -10,6 +10,8 @@ DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.6"
DOCKER_VERSION="26.0"
# TODO: Ask for a user
CURRENT_USER=$USER
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs}
mkdir -p /data/coolify/ssh/{keys,mux}
@@ -401,88 +403,18 @@ if [ ! -f ~/.ssh/authorized_keys ]; then
chmod 600 ~/.ssh/authorized_keys
fi
checkSshKeyInAuthorizedKeys() {
grep -qw "root@coolify" ~/.ssh/authorized_keys
return $?
}
set +e
IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l)
set -e
checkSshKeyInCoolifyData() {
[ -s /data/coolify/ssh/keys/id.root@host.docker.internal ]
return $?
}
generateAuthorizedKeys() {
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
rm -f /data/coolify/ssh/keys/id.root@host.docker.internal.pub
}
generateSshKey() {
if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
echo " - Generating SSH key."
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.root@host.docker.internal -q -N "" -C root@coolify
chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal
generateAuthorizedKeys
}
syncSshKeys() {
DB_RUNNING=$(docker inspect coolify-db --format '{{ .State.Status }}' 2>/dev/null)
# Check if SSH key exists in Coolify data but not in authorized_keys
if checkSshKeyInCoolifyData && ! checkSshKeyInAuthorizedKeys; then
# Add the existing Coolify SSH key to authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
# Check if SSH key exists in authorized_keys but not in Coolify data
elif checkSshKeyInAuthorizedKeys && ! checkSshKeyInCoolifyData; then
# Ensure Coolify DB is running before proceeding
if [ "$DB_RUNNING" = "running" ]; then
# Retrieve DB user and SSH key from Coolify database
DB_USER=$(docker inspect coolify-db --format '{{ .Config.Env }}' | grep -oP 'POSTGRES_USER=\K[^ ]+')
DB_SSH_KEY=$(docker exec coolify-db psql -U $DB_USER -d coolify -t -c "SELECT \"private_key\" FROM \"private_keys\" WHERE id = 0 AND team_id = 0 LIMIT 1;" -A -t)
if [ -z "$DB_SSH_KEY" ]; then
# If no key found in DB, generate a new one
echo " - SSH key not found in database. Generating new key."
generateSshKey
else
# If key found in DB, save it and update authorized_keys
echo " - SSH key found in database. Saving to file."
echo "$DB_SSH_KEY" > /data/coolify/ssh/keys/id.root@host.docker.internal
chmod 600 /data/coolify/ssh/keys/id.root@host.docker.internal
chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal
# Generate public key from private key and update authorized_keys
ssh-keygen -y -f /data/coolify/ssh/keys/id.root@host.docker.internal -C root@coolify > /data/coolify/ssh/keys/id.root@host.docker.internal.pub
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
rm -f /data/coolify/ssh/keys/id.root@host.docker.internal.pub
chmod 600 ~/.ssh/authorized_keys
fi
fi
# If SSH key doesn't exist in either location
elif ! checkSshKeyInAuthorizedKeys && ! checkSshKeyInCoolifyData; then
# Ensure Coolify DB is running before proceeding
if [ "$DB_RUNNING" = "running" ]; then
# Retrieve DB user and SSH key from Coolify database
DB_USER=$(docker inspect coolify-db --format '{{ .Config.Env }}' | grep -oP 'POSTGRES_USER=\K[^ ]+')
DB_SSH_KEY=$(docker exec coolify-db psql -U $DB_USER -d coolify -t -c "SELECT \"private_key\" FROM \"private_keys\" WHERE id = 0 AND team_id = 0 LIMIT 1;" -A -t)
if [ -z "$DB_SSH_KEY" ]; then
# If no key found in DB, generate a new one
echo " - SSH key not found in database. Generating new key."
generateSshKey
else
# If key found in DB, save it and update authorized_keys
echo " - SSH key found in database. Saving to file."
echo "$DB_SSH_KEY" > /data/coolify/ssh/keys/id.root@host.docker.internal
chmod 600 /data/coolify/ssh/keys/id.root@host.docker.internal
ssh-keygen -y -f /data/coolify/ssh/keys/id.root@host.docker.internal -C root@coolify > /data/coolify/ssh/keys/id.root@host.docker.internal.pub
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
fi
else
generateSshKey
fi
fi
}
syncSshKeys || true
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
sed -i "/coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >> ~/.ssh/authorized_keys
rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub
fi
chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify
@@ -492,7 +424,7 @@ echo -e " - It could take a while based on your server's performance, network sp
echo -e " - Please wait."
getAJoke
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" >/dev/null 2>&1
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}"
echo " - Coolify installed successfully."
rm -f $ENV_FILE-$DATE

View File

@@ -9,7 +9,7 @@ services:
depends_on:
postgres:
condition: service_healthy
entrypoint: ["sh", "-c", "npm run alteration deploy latest && npm run cli db seed -- --swe && npm start'"]
entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"]
environment:
- SERVICE_FQDN_LOGTO
- TRUST_PROXY_HEADER=1

View File

@@ -22,7 +22,7 @@ services:
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_SES_CONFIGURATION_SET=${AWS_SES_CONFIGURATION_SET}
- NEXT_PUBLIC_API_URI=${API_URI}
- NEXT_PUBLIC_API_URI=${SERVICE_FQDN_PLUNK}/api
- APP_URI=${SERVICE_FQDN_PLUNK}
- API_URI=${SERVICE_FQDN_PLUNK}/api
- DISABLE_SIGNUPS=False

View File

@@ -3,21 +3,30 @@
# slogan:
# tags:
# logo:
# port: 5000
# port: 4200
services:
postiz:
image: "ghcr.io/gitroomhq/postiz-app:latest"
environment:
- SERVICE_FQDN_POSTIZ_5000
- SERVICE_FQDN_POSTIZ_4200
- MAIN_URL=${SERVICE_FQDN_POSTIZ}
- FRONTEND_URL=${SERVICE_FQDN_POSTIZ}
- NEXT_PUBLIC_BACKEND_URL=${SERVICE_FQDN_POSTIZ_3000}
- NEXT_PUBLIC_BACKEND_URL=${SERVICE_FQDN_POSTIZAPI_3000}
- JWT_SECRET=${SERVICE_REALBASE64_JWTSECRET}
- DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${POSTGRES_DB:-postiz}?schema=public
- REDIS_URL=redis://redis:6379
- BACKEND_INTERNAL_URL=${SERVICE_FQDN_POSTIZ}
- BACKEND_INTERNAL_URL=http://localhost:3000/
- IS_GENERAL=true
- CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID}
- CLOUDFLARE_ACCESS_KEY=${CLOUDFLARE_ACCESS_KEY}
- CLOUDFLARE_SECRET_ACCESS_KEY=${CLOUDFLARE_SECRET_ACCESS_KEY}
- CLOUDFLARE_BUCKETNAME=${CLOUDFLARE_BUCKETNAME}
- CLOUDFLARE_BUCKET_URL=${CLOUDFLARE_BUCKET_URL}
- CLOUDFLARE_REGION=${CLOUDFLARE_REGION}
- RESEND_API_KEY=${RESEND_API_KEY}
- EMAIL_FROM_ADDRESS=${EMAIL_FROM_ADDRESS}
- EMAIL_FROM_NAME=${EMAIL_FROM_NAME}
depends_on:
postgres:
condition: service_healthy

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -21,7 +21,7 @@ export default defineConfig({
clientPort: 443,
}
: {
host: "localhost",
host: process.env.VITE_HOST,
},
},
plugins: [