diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml index f3d52b1b4..42df4785e 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -1,46 +1,65 @@ -name: Bug report -description: "Create a new bug report." +name: 🐞 Bug Report +description: "File a new bug report." title: "[Bug]: " +labels: ["🐛 Bug", "🔍 Triage"] body: - type: markdown attributes: - value: >- - # 💎 Bounty program (with - [algora.io](https://console.algora.io/org/coollabsio/bounties/new)) + value: | + > [!IMPORTANT] + > **Please ensure you are using the latest version of Coolify before submitting an issue, as the bug may have already been fixed in a recent update.** (Of course, if you're experiencing an issue on the latest version that wasn't present in a previous version, please let us know.) + # 💎 Bounty Program (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new)) + - If you would like to prioritize the issue resolution, consider adding a bounty to this issue through our [Bounty Program](https://console.algora.io/org/coollabsio/bounties/new). - If you would like to prioritize the issue resolution, you can add bounty - to this issue. - - - Click [here](https://console.algora.io/org/coollabsio/bounties/new) to - get started. - type: textarea attributes: - label: Description - description: A clear and concise description of the problem - - type: textarea - attributes: - label: Minimal Reproduction (if possible, example repository) - description: Please provide a step by step guide to reproduce the issue. + label: Error Message and Logs + description: Provide a detailed description of the error or exception you encountered, along with any relevant log output. validations: required: true + - type: textarea attributes: - label: Exception or Error - description: Please provide error logs if possible. + label: Steps to Reproduce + description: Please provide a step-by-step guide to reproduce the issue. Be as detailed as possible, otherwise we may not be able to assist you. + value: | + 1. + 2. + 3. + 4. + validations: + required: true + - type: input attributes: - label: Version - description: Coolify's version (see top of your screen). + label: Example Repository URL + description: If applicable, provide a URL to a repository demonstrating the issue. + + - type: input + attributes: + label: Coolify Version + description: Please provide the Coolify version you are using. This can be found in the top left corner of your Coolify dashboard. + placeholder: "v4.0.0-beta.335" validations: required: true - - type: checkboxes + + - type: dropdown attributes: - label: Cloud? - description: "Are you using the cloud version of Coolify?" + label: Are you using Coolify Cloud? options: - - label: 'Yes' - required: false - - label: 'No' - required: false + - "No (self-hosted)" + - "Yes (Coolify Cloud)" + validations: + required: true + + - type: input + attributes: + label: Operating System and Version (self-hosted) + description: Run `cat /etc/os-release` or `lsb_release -a` in your terminal and provide the operating system and version. + placeholder: "Ubuntu 22.04" + + - type: textarea + attributes: + label: Additional Information + description: Any other relevant details about the issue. diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml b/.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml new file mode 100644 index 000000000..ef26125e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml @@ -0,0 +1,31 @@ +name: 💎 Enhancement Bounty +description: "Propose a new feature, service, or improvement with an attached bounty." +title: "[Enhancement]: " +labels: ["✨ Enhancement", "🔍 Triage"] +body: + - type: markdown + attributes: + value: | + > [!IMPORTANT] + > **This issue template is exclusively for proposing new features, services, or improvements with an attached bounty.** Enhancements without a bounty can be discussed in the appropriate category of [Github Discussions](https://github.com/coollabsio/coolify/discussions). + + # 💎 Add a Bounty (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new)) + - [Click here to add the required bounty](https://console.algora.io/org/coollabsio/bounties/new) + + - type: dropdown + attributes: + label: Request Type + description: Select the type of request you are making. + options: + - New Feature + - New Service + - Improvement + validations: + required: true + + - type: textarea + attributes: + label: Description + description: Provide a detailed description of the feature, improvement, or service you are proposing. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4f12f436c..92c48e2d6 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,18 @@ blank_issues_enabled: false + contact_links: - - name: 🤔 Community Support (Chat) + - name: 🤔 Questions and Community Support url: https://coollabs.io/discord - about: Reach out to us on Discord. - - name: 🙋‍♂️ Feature Requests - url: https://github.com/coollabsio/coolify/discussions/categories/new-features - about: All feature requests will be discussed here. + about: If you have any questions, reach out to us on Discord inside the "#support" channel. + + - name: 💡 Feature Request + url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests + about: Suggest a new feature for Coolify. + + - name: ⚙️ Service Request + url: https://github.com/coollabsio/coolify/discussions/categories/service-requests + about: Request a new service integration for Coolify. + + - name: 🔧 Improvements + url: https://github.com/coollabsio/coolify/discussions/categories/improvements + about: Suggest improvements to existing features for Coolify. diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index 830b36d28..fd4be2f11 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -2,7 +2,7 @@ name: Coolify Helper Image (v4) on: push: - branches: [ "main", "next" ] + branches: [ "main" ] paths: - .github/workflows/coolify-helper.yml - docker/coolify-helper/Dockerfile diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml new file mode 100644 index 000000000..75e3f1681 --- /dev/null +++ b/.github/workflows/coolify-realtime.yml @@ -0,0 +1,103 @@ +name: Coolify Realtime (v4) + +on: + push: + branches: [ "main", "next" ] + paths: + - .github/workflows/coolify-realtime.yml + - docker/coolify-realtime/Dockerfile + - docker/coolify-realtime/terminal-server.js + - docker/coolify-realtime/package.json + - docker/coolify-realtime/soketi-entrypoint.sh + +env: + REGISTRY: ghcr.io + IMAGE_NAME: "coollabsio/coolify-realtime" + +jobs: + amd64: + runs-on: ubuntu-latest + permissions: + contents: read + packages: 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: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + - name: Build image and push to registry + uses: docker/build-push-action@v5 + with: + no-cache: true + context: . + file: docker/coolify-realtime/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + labels: | + coolify.managed=true + aarch64: + runs-on: [ self-hosted, arm64 ] + permissions: + contents: read + packages: 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: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + - name: Build image and push to registry + uses: docker/build-push-action@v5 + with: + no-cache: true + context: . + file: docker/coolify-realtime/Dockerfile + platforms: linux/aarch64 + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + labels: | + coolify.managed=true + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: 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: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT + - name: Create & publish manifest + run: | + docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 017399e73..d7a680170 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -21,6 +21,7 @@ jobs: packages: write attestations: write id-token: write + actions: write steps: - uses: actions/checkout@v4 - name: Login to ghcr.io @@ -44,6 +45,7 @@ jobs: packages: write attestations: write id-token: write + actions: write steps: - uses: actions/checkout@v4 - name: Login to ghcr.io @@ -67,6 +69,7 @@ jobs: packages: write attestations: write id-token: write + actions: write needs: [amd64, aarch64] steps: - name: Checkout diff --git a/.github/workflows/remove-labels-and-assignees-on-close.yml b/.github/workflows/remove-labels-and-assignees-on-close.yml new file mode 100644 index 000000000..04d62623c --- /dev/null +++ b/.github/workflows/remove-labels-and-assignees-on-close.yml @@ -0,0 +1,75 @@ +name: Remove Labels and Assignees on Issue Close + +on: + issues: + types: [closed] + pull_request: + types: [closed] + pull_request_target: + types: [closed] + +jobs: + remove-labels-and-assignees: + runs-on: ubuntu-latest + steps: + - name: Remove labels and assignees + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo } = context.repo; + + async function processIssue(issueNumber) { + try { + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: issueNumber + }); + + const labelsToKeep = currentLabels + .filter(label => label.name === '⏱︎ Stale') + .map(label => label.name); + + await github.rest.issues.setLabels({ + owner, + repo, + issue_number: issueNumber, + labels: labelsToKeep + }); + + const { data: issue } = await github.rest.issues.get({ + owner, + repo, + issue_number: issueNumber + }); + + if (issue.assignees && issue.assignees.length > 0) { + await github.rest.issues.removeAssignees({ + owner, + repo, + issue_number: issueNumber, + assignees: issue.assignees.map(assignee => assignee.login) + }); + } + } catch (error) { + if (error.status !== 404) { + console.error(`Error processing issue ${issueNumber}:`, error); + } + } + } + + if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') { + const issue = context.payload.issue || context.payload.pull_request; + await processIssue(issue.number); + } + + if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') { + const { data: closedIssues } = await github.rest.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} is:issue is:closed linked:${context.payload.pull_request.number}`, + per_page: 100 + }); + for (const issue of closedIssues.items) { + await processIssue(issue.number); + } + } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9618bfae5..590360ddb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,27 @@ -# Contributing +# Contributing to Coolify > "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai) You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel. +## Table of Contents -## Code Contribution +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. [Development Notes](#7-development-notes) +8. [Create a Pull Request](#8-create-a-pull-request) +9. [Additional Contribution Guidelines](#additional-contribution-guidelines) -## 1. Setup your development environment +## 1. Setup Development Environment Follow the steps below for your operating system: -### Windows +
+Windows 1. Install `docker-ce`, Docker Desktop (or similar): - Docker CE (recommended): @@ -25,7 +35,10 @@ Follow the steps below for your operating system: 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) -### MacOS +
+ +
+MacOS 1. Install Orbstack, Docker Desktop (or similar): - Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop): @@ -36,7 +49,10 @@ Follow the steps below for your operating system: 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) -### Linux +
+ +
+Linux 1. Install Docker Engine, Docker Desktop (or similar): - Docker Engine (recommended, as there is no VM overhead): @@ -47,8 +63,9 @@ Follow the steps below for your operating system: 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) +
-## 2. Verify installation (optional) +## 2. Verify Installation (Optional) After installing Docker (or Orbstack) and Spin, verify the installation: @@ -60,25 +77,20 @@ After installing Docker (or Orbstack) and Spin, verify the installation: ``` You should see version information for both Docker and Spin. - -## 3. Fork the Coolify repository and setup your local repository +## 3. Fork and Setup Local Repository 1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account. -2. Install a code editor on your machine (below are some popular choices, choose one): +2. Install a code editor on your machine (choose one): - - Visual Studio Code (recommended free): - - Windows/macOS/Linux: Download and install from [https://code.visualstudio.com/download](https://code.visualstudio.com/download) - - - Cursor (recommended but paid for getting the full benefits): - - Windows/macOS/Linux: Download and install from [https://www.cursor.com/](https://www.cursor.com/) - - - Zed (very fast code editor): - - macOS/Linux: Download and install from [https://zed.dev/download](https://zed.dev/download) - - Windows: Not available yet + | 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) | 3. Clone the Coolify Repository from your fork to your local machine - - Use `git clone` in the command line + - Use `git clone` in the command line, or - Use GitHub Desktop (recommended): - Download and install from [https://desktop.github.com/](https://desktop.github.com/) - Open GitHub Desktop and login with your GitHub account @@ -86,37 +98,32 @@ After installing Docker (or Orbstack) and Spin, verify the installation: 4. Open the cloned Coolify Repository in your chosen code editor. - ## 4. Set up Environment Variables 1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository. - 2. Duplicate the `.env.development.example` file and rename the copy to `.env`. - 3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup. - 4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`. - 5. Save the changes to your `.env` file. - ## 5. Start Coolify 1. Open a terminal in the local Coolify directory. - 2. Run the following command in the terminal (leave that terminal open): - ``` + ```bash spin up ``` - Note: You may see some errors, but don't worry; this is expected. + +> [!NOTE] +> You may see some errors, but don't worry; this is expected. 3. If you encounter permission errors, especially on macOS, use: - ``` + ```bash sudo spin up ``` -Note: If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again. - +> [!NOTE] +> If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again. ## 6. Start Development @@ -126,15 +133,17 @@ Note: If you change environment variables afterwards or anything seems broken, p - Password: `password` 2. Additional development tools: - - Laravel Horizon (scheduler): `http://localhost:8000/horizon` - Note: Only accessible when logged in as root user - - Mailpit (email catcher): `http://localhost:8025` - - Telescope (debugging tool): `http://localhost:8000/telescope` - Note: Disabled by default (so the database is not overloaded), enable by adding the following environment variable to your `.env` file: - ```env - TELESCOPE_ENABLED=true - ``` + | Tool | URL | Note | + |------|-----|------| + | Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user | + | Mailpit (email catcher) | `http://localhost:8025` | | + | Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default | +> [!NOTE] +> To enable Telescope, add the following to your `.env` file: +> ```env +> TELESCOPE_ENABLED=true +> ``` ## 7. Development Notes @@ -150,18 +159,12 @@ When working on Coolify, keep the following in mind: docker exec -it coolify php artisan migrate:fresh --seed ``` -3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any envrionement specific issues. +3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any environment-specific issues. -Remember, forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches. +> [!IMPORTANT] +> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches. - -## 8. Contributing a New Service - -To add a new service to Coolify, please refer to our documentation: -[Adding a New Service](https://coolify.io/docs/knowledge-base/add-a-service) - - -## 9. Create a Pull Request +## 8. Create a Pull Request 1. After making changes or adding a new service: - Commit your changes to your forked repository. @@ -179,11 +182,26 @@ To add a new service to Coolify, please refer to our documentation: - In the description, explain the changes you've made. - Reference any related issues by using keywords like "Fixes #123" or "Closes #456". -4. Important note: - Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch. +> [!IMPORTANT] +> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch. -5. Submit your PR: +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. + +## Additional Contribution Guidelines + +### Contributing a New Service + +To add a new service to Coolify, please refer to our documentation: +[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service) + +### Contributing to Documentation + +To contribute to the Coolify documentation, please refer to this guide: +[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md) diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index c4af6bb21..901f2cf77 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -2,6 +2,7 @@ namespace App\Actions\Server; +use App\Jobs\PullHelperImageJob; use App\Models\InstanceSettings; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; @@ -55,6 +56,13 @@ class UpdateCoolify return; } + + $all_servers = Server::all(); + $servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); + foreach ($servers as $server) { + PullHelperImageJob::dispatch($server); + } + instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false); remote_process([ diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 7aef457a1..06d2e0efb 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -16,7 +16,7 @@ class StartService $service->saveComposeConfigs(); $commands[] = 'cd '.$service->workdir(); $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; - if($service->networks()->count() > 0){ + if ($service->networks()->count() > 0) { $commands[] = "echo 'Creating Docker network.'"; $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid"; } @@ -31,7 +31,7 @@ class StartService $network = $service->destination->network; $serviceNames = data_get(Yaml::parse($compose), 'services', []); foreach ($serviceNames as $serviceName => $serviceConfig) { - $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true"; + $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true"; } } $activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged'); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 718cea639..32db4ac32 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -514,7 +514,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue 'hidden' => true, 'ignore_errors' => true, ], [ - "docker network connect {$networkId} coolify-proxy || true", + "docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true", 'hidden' => true, 'ignore_errors' => true, ]); @@ -919,10 +919,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH={$local_branch}"); + $envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); } if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + $envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\""); } } @@ -978,10 +978,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $envs->push("COOLIFY_BRANCH={$local_branch}"); + $envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); } if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + $envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\""); } } @@ -2049,6 +2049,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], [ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, @@ -2068,6 +2072,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], [ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, @@ -2110,6 +2118,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], [ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, @@ -2129,6 +2141,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], [ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, @@ -2157,6 +2173,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], [ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, @@ -2176,6 +2196,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, ], + [ + executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'), + 'hidden' => true, + ], [ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index e6fa05b55..3bd13564b 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -4,6 +4,7 @@ namespace App\Jobs; use App\Actions\Database\StopDatabase; use App\Events\BackupCreated; +use App\Models\InstanceSettings; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackupExecution; @@ -478,10 +479,37 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } } + // private function upload_to_s3(): void + // { + // try { + // if (is_null($this->s3)) { + // return; + // } + // $key = $this->s3->key; + // $secret = $this->s3->secret; + // // $region = $this->s3->region; + // $bucket = $this->s3->bucket; + // $endpoint = $this->s3->endpoint; + // $this->s3->testConnection(shouldSave: true); + // $configName = new Cuid2; + + // $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/'); + // $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'"; + // $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'"; + // instant_remote_process($commands, $this->server); + // $this->add_to_backup_output('Uploaded to S3.'); + // } catch (\Throwable $e) { + // $this->add_to_backup_output($e->getMessage()); + // throw $e; + // } finally { + // $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'"; + // $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'"; + // instant_remote_process($removeConfigCommands, $this->server, false); + // } + // } private function upload_to_s3(): void { try { - ray($this->backup_location); if (is_null($this->s3)) { return; } @@ -491,20 +519,64 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $bucket = $this->s3->bucket; $endpoint = $this->s3->endpoint; $this->s3->testConnection(shouldSave: true); - $configName = new Cuid2; + if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + $network = $this->database->service->destination->network; + } else { + $network = $this->database->destination->network; + } - $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/'); - $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'"; - $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'"; + $this->ensureHelperImageAvailable(); + + $fullImageName = $this->getFullImageName(); + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}"; + $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; + $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; instant_remote_process($commands, $this->server); $this->add_to_backup_output('Uploaded to S3.'); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); throw $e; } finally { - $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'"; - $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'"; - instant_remote_process($removeConfigCommands, $this->server, false); + $command = "docker rm -f backup-of-{$this->backup->uuid}"; + instant_remote_process([$command], $this->server); } } + + private function ensureHelperImageAvailable(): void + { + $fullImageName = $this->getFullImageName(); + + $imageExists = $this->checkImageExists($fullImageName); + + if (! $imageExists) { + $this->pullHelperImage($fullImageName); + } + } + + private function checkImageExists(string $fullImageName): bool + { + $result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false); + + return trim($result) === 'exists'; + } + + private function pullHelperImage(string $fullImageName): void + { + try { + instant_remote_process(["docker pull {$fullImageName}"], $this->server); + } catch (\Exception $e) { + $errorMessage = 'Failed to pull helper image: '.$e->getMessage(); + $this->add_to_backup_output($errorMessage); + throw new \RuntimeException($errorMessage); + } + } + + private function getFullImageName(): string + { + $settings = InstanceSettings::get(); + $helperImage = config('coolify.helper_image'); + $latestVersion = $settings->helper_version; + + return "{$helperImage}:{$latestVersion}"; + } } diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index 420119069..63b7fa920 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -42,8 +42,8 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue $current_version = $settings->helper_version; if (version_compare($latest_version, $current_version, '>')) { // New version available - $helperImage = config('coolify.helper_image'); - instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server); + // $helperImage = config('coolify.helper_image'); + // instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server); $settings->update(['helper_version' => $latest_version]); } } diff --git a/app/Livewire/CommandCenter/Index.php b/app/Livewire/CommandCenter/Index.php deleted file mode 100644 index 0a05e811f..000000000 --- a/app/Livewire/CommandCenter/Index.php +++ /dev/null @@ -1,21 +0,0 @@ -servers = Server::isReachable()->get(); - } - - public function render() - { - return view('livewire.command-center.index'); - } -} diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 674182df5..e6bb6d9bf 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -20,6 +20,8 @@ class Navbar extends Component public $isDeploymentProgress = false; + public $title = 'Configuration'; + public function mount() { if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) { diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 343915d9c..d95443621 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -2,18 +2,16 @@ namespace App\Livewire\Project\Shared; -use App\Actions\Server\RunCommand; use App\Models\Application; use App\Models\Server; use App\Models\Service; use Illuminate\Support\Collection; +use Livewire\Attributes\On; use Livewire\Component; class ExecuteContainerCommand extends Component { - public string $command; - - public string $container; + public $container; public Collection $containers; @@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component public string $type; - public string $workDir = ''; - public Server $server; public Collection $servers; @@ -33,11 +29,13 @@ class ExecuteContainerCommand extends Component 'server' => 'required', 'container' => 'required', 'command' => 'required', - 'workDir' => 'nullable', ]; public function mount() { + if (! auth()->user()->isAdmin()) { + abort(403); + } $this->parameters = get_route_parameters(); $this->containers = collect(); $this->servers = collect(); @@ -62,24 +60,13 @@ class ExecuteContainerCommand extends Component if ($this->resource->destination->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->destination->server); } - $this->container = $this->resource->uuid; - $this->containers->push($this->container); } elseif (data_get($this->parameters, 'service_uuid')) { $this->type = 'service'; $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); - $this->resource->applications()->get()->each(function ($application) { - $this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid')); - }); - $this->resource->databases()->get()->each(function ($database) { - $this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid')); - }); if ($this->resource->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->server); } } - if ($this->containers->count() > 0) { - $this->container = $this->containers->first(); - } } public function loadContainers() @@ -102,44 +89,65 @@ class ExecuteContainerCommand extends Component ]; $this->containers = $this->containers->push($payload); } + } elseif (data_get($this->parameters, 'database_uuid')) { + if ($this->resource->isRunning()) { + $this->containers = $this->containers->push([ + 'server' => $server, + 'container' => [ + 'Names' => $this->resource->uuid, + ], + ]); + } + } elseif (data_get($this->parameters, 'service_uuid')) { + $this->resource->applications()->get()->each(function ($application) { + ray($application); + if ($application->isRunning()) { + $this->containers->push([ + 'server' => $this->resource->server, + 'container' => [ + 'Names' => data_get($application, 'name').'-'.data_get($this->resource, 'uuid'), + ], + ]); + } + }); + $this->resource->databases()->get()->each(function ($database) { + if ($database->isRunning()) { + $this->containers->push([ + 'server' => $this->resource->server, + 'container' => [ + 'Names' => data_get($database, 'name').'-'.data_get($this->resource, 'uuid'), + ], + ]); + } + }); } + } if ($this->containers->count() > 0) { - if (data_get($this->parameters, 'application_uuid')) { - $this->container = data_get($this->containers->first(), 'container.Names'); - } elseif (data_get($this->parameters, 'database_uuid')) { - $this->container = $this->containers->first(); - } elseif (data_get($this->parameters, 'service_uuid')) { - $this->container = $this->containers->first(); - } + $this->container = $this->containers->first(); } } - public function runCommand() + #[On('connectToContainer')] + public function connectToContainer() { try { - if (data_get($this->parameters, 'application_uuid')) { - $container = $this->containers->where('container.Names', $this->container)->first(); - $container_name = data_get($container, 'container.Names'); - if (is_null($container)) { - throw new \RuntimeException('Container not found.'); - } - $server = data_get($container, 'server'); - } else { - $container_name = $this->container; - $server = $this->servers->first(); + $container_name = data_get($this->container, 'container.Names'); + if (is_null($container_name)) { + throw new \RuntimeException('Container not found.'); } + $server = data_get($this->container, 'server'); + if ($server->isForceDisabled()) { throw new \RuntimeException('Server is disabled.'); } - $cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'"; - if (! empty($this->workDir)) { - $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}"; - } else { - $exec = "docker exec {$container_name} {$cmd}"; - } - $activity = RunCommand::run(server: $server, command: $exec); - $this->dispatch('activityMonitor', $activity->id); + + $this->dispatch('send-terminal-command', + true, + $container_name, + $server->uuid, + ); + } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php new file mode 100644 index 000000000..802e65a30 --- /dev/null +++ b/app/Livewire/Project/Shared/Terminal.php @@ -0,0 +1,43 @@ +whereUuid($serverUuid)->firstOrFail(); + + if ($isContainer) { + $status = getContainerStatus($server, $identifier); + if ($status !== 'running') { + return; + } + $command = generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + } else { + $command = generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); + } + + // ssh command is sent back to frontend then to websocket + // this is done because the websocket connection is not available here + // a better solution would be to remove websocket on NodeJS and work with something like + // 1. Laravel Pusher/Echo connection (not possible without a sdk) + // 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies) + // 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used + // 4. Follow-up discussions here: + // - https://github.com/coollabsio/coolify/issues/2298 + // - https://github.com/coollabsio/coolify/discussions/3362 + $this->dispatch('send-back-command', $command); + } + + public function render() + { + return view('livewire.project.shared.terminal'); + } +} diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php deleted file mode 100644 index c2d3adeea..000000000 --- a/app/Livewire/RunCommand.php +++ /dev/null @@ -1,43 +0,0 @@ - 'required', - 'command' => 'required', - ]; - - protected $validationAttributes = [ - 'server' => 'server', - 'command' => 'command', - ]; - - public function mount($servers) - { - $this->servers = $servers; - $this->server = $servers[0]->uuid; - } - - public function runCommand() - { - $this->validate(); - try { - $activity = ServerRunCommand::run(server: Server::where('uuid', $this->server)->first(), command: $this->command); - $this->dispatch('activityMonitor', $activity->id); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Terminal/Index.php b/app/Livewire/Terminal/Index.php new file mode 100644 index 000000000..945b25714 --- /dev/null +++ b/app/Livewire/Terminal/Index.php @@ -0,0 +1,76 @@ +user()->isAdmin()) { + abort(403); + } + $this->servers = Server::isReachable()->get(); + $this->containers = $this->getAllActiveContainers(); + } + + private function getAllActiveContainers() + { + return collect($this->servers)->flatMap(function ($server) { + if (! $server->isFunctional()) { + return []; + } + + return $server->loadAllContainers()->map(function ($container) use ($server) { + $state = data_get_str($container, 'State')->lower(); + if ($state->contains('running')) { + return [ + 'name' => data_get($container, 'Names'), + 'connection_name' => data_get($container, 'Names'), + 'uuid' => data_get($container, 'Names'), + 'status' => data_get_str($container, 'State')->lower(), + 'server' => $server, + 'server_uuid' => $server->uuid, + ]; + } + + return null; + })->filter(); + }); + } + + public function updatedSelectedUuid() + { + $this->connectToContainer(); + } + + #[On('connectToContainer')] + public function connectToContainer() + { + if ($this->selected_uuid === 'default') { + $this->dispatch('error', 'Please select a server or a container.'); + + return; + } + $container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid); + $this->dispatch('send-terminal-command', + isset($container), + $container['connection_name'] ?? $this->selected_uuid, + $container['server_uuid'] ?? $this->selected_uuid + ); + } + + public function render() + { + return view('livewire.terminal.index'); + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index 65d70083f..90e6ade14 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -305,6 +305,13 @@ respond 404 'service' => 'coolify-realtime', 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", ], + 'coolify-terminal-ws' => [ + 'entryPoints' => [ + 0 => 'http', + ], + 'service' => 'coolify-terminal', + 'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)", + ], ], 'services' => [ 'coolify' => [ @@ -325,6 +332,15 @@ respond 404 ], ], ], + 'coolify-terminal' => [ + 'loadBalancer' => [ + 'servers' => [ + 0 => [ + 'url' => 'http://coolify-realtime:6002', + ], + ], + ], + ], ], ], ]; @@ -354,6 +370,16 @@ respond 404 'certresolver' => 'letsencrypt', ], ]; + $traefik_dynamic_conf['http']['routers']['coolify-terminal-wss'] = [ + 'entryPoints' => [ + 0 => 'https', + ], + 'service' => 'coolify-terminal', + 'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)", + 'tls' => [ + 'certresolver' => 'letsencrypt', + ], + ]; } $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); $yaml = @@ -387,6 +413,9 @@ $schema://$host { handle /app/* { reverse_proxy coolify-realtime:6001 } + handle /terminal/ws { + reverse_proxy coolify-realtime:6002 + } reverse_proxy coolify:80 }"; $base64 = base64_encode($caddy_file); @@ -746,6 +775,18 @@ $schema://$host { } } + public function loadAllContainers(): Collection + { + if ($this->isFunctional()) { + $containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this); + $containers = format_docker_command_output_to_json($containers); + + return collect($containers); + } + + return collect([]); + } + public function loadUnmanagedContainers(): Collection { if ($this->isFunctional()) { diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index 6690f254e..d312fab96 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -32,6 +32,16 @@ class ServiceApplication extends BaseModel return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); } + public function isRunning() + { + return str($this->status)->contains('running'); + } + + public function isExited() + { + return str($this->status)->contains('exited'); + } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 4a749913e..6b96738e8 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -25,6 +25,16 @@ class ServiceDatabase extends BaseModel remote_process(["docker restart {$container_id}"], $this->service->server); } + public function isRunning() + { + return str($this->status)->contains('running'); + } + + public function isExited() + { + return str($this->status)->contains('exited'); + } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 4cd194cd8..ee5c3becc 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -75,6 +75,11 @@ class StandaloneClickhouse extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 8726b2546..361abf110 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -75,6 +75,11 @@ class StandaloneDragonfly extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 7ecb00348..e05879371 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -75,6 +75,11 @@ class StandaloneKeydb extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index d88653e41..c1e6c85d7 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -75,6 +75,11 @@ class StandaloneMariadb extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index f09e932bf..e5ed0a5f4 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -79,6 +79,11 @@ class StandaloneMongodb extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index f4e56fab2..bd4a7abb7 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -76,6 +76,11 @@ class StandaloneMysql extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 311c09c36..db771c7cd 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -102,6 +102,11 @@ class StandalonePostgresql extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 8a202ea9e..c524d4d03 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -71,6 +71,11 @@ class StandaloneRedis extends BaseModel } } + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 90093deb8..8dce52f15 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -40,6 +40,20 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul return $containers; } +function getCurrentServiceContainerStatus(Server $server, int $id): Collection +{ + $containers = collect([]); + if (! $server->isSwarm()) { + $containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --format '{{json .}}' "], $server); + $containers = format_docker_command_output_to_json($containers); + $containers = $containers->filter(); + + return $containers; + } + + return $containers; +} + function format_docker_command_output_to_json($rawOutput): Collection { $outputLines = explode(PHP_EOL, $rawOutput); @@ -215,12 +229,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource) } if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) { $MINIO_BROWSER_REDIRECT_URL?->update([ - 'value' => generateFqdn($server, 'console-'.$uuid), + 'value' => generateFqdn($server, 'console-'.$uuid, true), ]); } if (is_null($MINIO_SERVER_URL?->value)) { $MINIO_SERVER_URL?->update([ - 'value' => generateFqdn($server, 'minio-'.$uuid), + 'value' => generateFqdn($server, 'minio-'.$uuid, true), ]); } $payload = collect([ diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 028d20f33..cd2779466 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -478,7 +478,7 @@ function data_get_str($data, $key, $default = null): Stringable return str($str); } -function generateFqdn(Server $server, string $random): string +function generateFqdn(Server $server, string $random, bool $forceHttps = false): string { $wildcard = data_get($server, 'settings.wildcard_domain'); if (is_null($wildcard) || $wildcard === '') { @@ -488,6 +488,9 @@ function generateFqdn(Server $server, string $random): string $host = $url->getHost(); $path = $url->getPath() === '/' ? '' : $url->getPath(); $scheme = $url->getScheme(); + if ($forceHttps) { + $scheme = 'https'; + } $finalFqdn = "$scheme://{$random}.$host$path"; return $finalFqdn; @@ -786,7 +789,7 @@ function replaceLocalSource(Stringable $source, Stringable $replacedWith) if ($source->startsWith('..')) { $source = $source->replaceFirst('..', $replacedWith->value()); } - if ($source->endsWith('/')) { + if ($source->endsWith('/') && $source->value() !== '/') { $source = $source->replaceLast('/', ''); } @@ -2100,16 +2103,16 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // TODO: move this in a shared function if (! $parsedServiceVariables->has('COOLIFY_APP_NAME')) { - $parsedServiceVariables->put('COOLIFY_APP_NAME', $resource->name); + $parsedServiceVariables->put('COOLIFY_APP_NAME', "\"{$resource->name}\""); } if (! $parsedServiceVariables->has('COOLIFY_SERVER_IP')) { - $parsedServiceVariables->put('COOLIFY_SERVER_IP', $resource->destination->server->ip); + $parsedServiceVariables->put('COOLIFY_SERVER_IP', "\"{$resource->destination->server->ip}\""); } if (! $parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) { - $parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', $resource->environment->name); + $parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\""); } if (! $parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) { - $parsedServiceVariables->put('COOLIFY_PROJECT_NAME', $resource->project()->name); + $parsedServiceVariables->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\""); } $parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) { @@ -2982,7 +2985,11 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int // Get magic environments where we need to preset the FQDN if ($key->startsWith('SERVICE_FQDN_')) { // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 - $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + if (substr_count(str($key)->value(), '_') === 3) { + $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value(); + } else { + $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); + } if ($isApplication) { $fqdn = generateFqdn($server, "{$resource->name}-$uuid"); } elseif ($isService) { @@ -3229,7 +3236,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($isApplication && $isPullRequest) { $source = $source."-pr-$pullRequestId"; } - LocalFileVolume::updateOrCreate( [ 'mount_path' => $target, @@ -3469,13 +3475,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $branch = "pull/{$pullRequestId}/head"; } if ($originalResource->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { - $coolifyEnvironments->put('COOLIFY_BRANCH', $branch); + $coolifyEnvironments->put('COOLIFY_BRANCH', "\"{$branch}\""); } } // Add COOLIFY_CONTAINER_NAME to environment if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { - $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', $containerName); + $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', "\"{$containerName}\""); } if ($isApplication) { @@ -3548,7 +3554,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int if ($isApplication) { $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; $uuid = $resource->uuid; - $network = $resource->destination->network; + $network = data_get($resource, 'destination.network'); if ($isPullRequest) { $uuid = "{$resource->uuid}-{$pullRequestId}"; } @@ -3558,7 +3564,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } else { $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; $uuid = $resource->uuid; - $network = $resource->destination->network; + $network = data_get($resource, 'destination.network'); } if ($shouldGenerateLabelsExactly) { switch ($server->proxyType()) { @@ -3723,30 +3729,30 @@ function add_coolify_default_environment_variables(StandaloneRedis|StandalonePos } if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_APP_NAME')->isEmpty()) { if ($isAssociativeArray) { - $where_to_add->put('COOLIFY_APP_NAME', $resource->name); + $where_to_add->put('COOLIFY_APP_NAME', "\"{$resource->name}\""); } else { - $where_to_add->push("COOLIFY_APP_NAME={$resource->name}"); + $where_to_add->push("COOLIFY_APP_NAME=\"{$resource->name}\""); } } if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_SERVER_IP')->isEmpty()) { if ($isAssociativeArray) { - $where_to_add->put('COOLIFY_SERVER_IP', $ip); + $where_to_add->put('COOLIFY_SERVER_IP', "\"{$ip}\""); } else { - $where_to_add->push("COOLIFY_SERVER_IP={$ip}"); + $where_to_add->push("COOLIFY_SERVER_IP=\"{$ip}\""); } } if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_ENVIRONMENT_NAME')->isEmpty()) { if ($isAssociativeArray) { - $where_to_add->put('COOLIFY_ENVIRONMENT_NAME', $resource->environment->name); + $where_to_add->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\""); } else { - $where_to_add->push("COOLIFY_ENVIRONMENT_NAME={$resource->environment->name}"); + $where_to_add->push("COOLIFY_ENVIRONMENT_NAME=\"{$resource->environment->name}\""); } } if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_PROJECT_NAME')->isEmpty()) { if ($isAssociativeArray) { - $where_to_add->put('COOLIFY_PROJECT_NAME', $resource->project()->name); + $where_to_add->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\""); } else { - $where_to_add->push("COOLIFY_PROJECT_NAME={$resource->project()->name}"); + $where_to_add->push("COOLIFY_PROJECT_NAME=\"{$resource->project()->name}\""); } } } diff --git a/config/sentry.php b/config/sentry.php index 253202507..471a1e0fc 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -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.335', + 'release' => '4.0.0-beta.341', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index c41d57f66..32eb01cd0 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 & +TERMINAL_PID=$! + +# Start the Soketi process in the background with logging +node /app/bin/server.js start > >(while read line; do echo "$(timestamp) [SOKETI] $line"; done) 2>&1 & +SOKETI_PID=$! + +# Function to forward signals to child processes +forward_signal() { + kill -$1 $TERMINAL_PID $SOKETI_PID +} + +# Forward SIGTERM to child processes +trap 'forward_signal TERM' TERM + +# Wait for any process to exit +wait -n + +# Exit with status of process that exited first +exit $? diff --git a/docker/coolify-realtime/terminal-server.js b/docker/coolify-realtime/terminal-server.js new file mode 100755 index 000000000..6dfbe3531 --- /dev/null +++ b/docker/coolify-realtime/terminal-server.js @@ -0,0 +1,229 @@ +import { WebSocketServer } from 'ws'; +import http from 'http'; +import pty from 'node-pty'; +import axios from 'axios'; +import cookie from 'cookie'; +import 'dotenv/config' + +const server = http.createServer((req, res) => { + if (req.url === '/ready') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('OK'); + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } +}); + +const verifyClient = async (info, callback) => { + const cookies = cookie.parse(info.req.headers.cookie || ''); + // const origin = new URL(info.origin); + // const protocol = origin.protocol; + const xsrfToken = cookies['XSRF-TOKEN']; + + // Generate session cookie name based on APP_NAME + const appName = process.env.APP_NAME || 'laravel'; + const sessionCookieName = `${appName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_session`; + const laravelSession = cookies[sessionCookieName]; + + // Verify presence of required tokens + if (!laravelSession || !xsrfToken) { + return callback(false, 401, 'Unauthorized: Missing required tokens'); + } + + try { + // Authenticate with Laravel backend + const response = await axios.post(`http://coolify/terminal/auth`, null, { + headers: { + 'Cookie': `${sessionCookieName}=${laravelSession}`, + 'X-XSRF-TOKEN': xsrfToken + }, + }); + + if (response.status === 200) { + // Authentication successful + callback(true); + } else { + callback(false, 401, 'Unauthorized: Invalid credentials'); + } + } catch (error) { + console.error('Authentication error:', error.message); + callback(false, 500, 'Internal Server Error'); + } +}; + + +const wss = new WebSocketServer({ server, path: '/terminal/ws', verifyClient: verifyClient }); +const userSessions = new Map(); + +wss.on('connection', (ws) => { + const userId = generateUserId(); + const userSession = { ws, userId, ptyProcess: null, isActive: false }; + userSessions.set(userId, userSession); + + ws.on('message', (message) => handleMessage(userSession, message)); + ws.on('error', (err) => handleError(err, userId)); + ws.on('close', () => handleClose(userId)); +}); + +const messageHandlers = { + message: (session, data) => session.ptyProcess.write(data), + resize: (session, { cols, rows }) => session.ptyProcess.resize(cols, rows), + pause: (session) => session.ptyProcess.pause(), + resume: (session) => session.ptyProcess.resume(), + checkActive: (session, data) => { + if (data === 'force' && session.isActive) { + killPtyProcess(session.userId); + } else { + session.ws.send(session.isActive); + } + }, + command: (session, data) => handleCommand(session.ws, data, session.userId) +}; + +function handleMessage(userSession, message) { + const parsed = parseMessage(message); + if (!parsed) return; + + Object.entries(parsed).forEach(([key, value]) => { + const handler = messageHandlers[key]; + if (handler && (userSession.isActive || key === 'checkActive' || key === 'command')) { + handler(userSession, value); + } + }); +} + +function parseMessage(message) { + try { + return JSON.parse(message); + } catch (e) { + console.error('Failed to parse message:', e); + return null; + } +} + +async function handleCommand(ws, command, userId) { + const userSession = userSessions.get(userId); + + if (userSession && userSession.isActive) { + const result = await killPtyProcess(userId); + if (!result) { + // if terminal is still active, even after we tried to kill it, dont continue and show error + ws.send('unprocessable'); + return; + } + } + + const commandString = command[0].split('\n').join(' '); + const timeout = extractTimeout(commandString); + const sshArgs = extractSshArgs(commandString); + const hereDocContent = extractHereDocContent(commandString); + const options = { + name: 'xterm-color', + cols: 80, + rows: 30, + cwd: process.env.HOME, + }; + + // NOTE: - Initiates a process within the Terminal container + // Establishes an SSH connection to root@coolify with RequestTTY enabled + // Executes the 'docker exec' command to connect to a specific container + // If the user types 'exit', it terminates the container connection and reverts to the server. + const ptyProcess = pty.spawn('ssh', sshArgs.concat(['bash']), options); + userSession.ptyProcess = ptyProcess; + userSession.isActive = true; + ptyProcess.write(hereDocContent + '\n'); + // clear the terminal if the user has clear command + ptyProcess.write('command -v clear >/dev/null 2>&1 && clear\n'); + + ws.send('pty-ready'); + + ptyProcess.onData((data) => ws.send(data)); + + ptyProcess.onExit(({ exitCode, signal }) => { + console.error(`Process exited with code ${exitCode} and signal ${signal}`); + userSession.isActive = false; + }); + + if (timeout) { + setTimeout(async () => { + await killPtyProcess(userId); + }, timeout * 1000); + } +} + +async function handleError(err, userId) { + console.error('WebSocket error:', err); + await killPtyProcess(userId); +} + +async function handleClose(userId) { + await killPtyProcess(userId); + userSessions.delete(userId); +} + +async function killPtyProcess(userId) { + const session = userSessions.get(userId); + if (!session?.ptyProcess) return false; + + return new Promise((resolve) => { + // Loop to ensure terminal is killed before continuing + let killAttempts = 0; + const maxAttempts = 5; + + const attemptKill = () => { + killAttempts++; + + // session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098 + // patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947 + session.ptyProcess.write('kill -TERM -$$ && exit\n'); + + setTimeout(() => { + if (!session.isActive || !session.ptyProcess) { + resolve(true); + return; + } + + if (killAttempts < maxAttempts) { + attemptKill(); + } else { + resolve(false); + } + }, 500); + }; + + attemptKill(); + }); +} + +function generateUserId() { + return Math.random().toString(36).substring(2, 11); +} + +function extractTimeout(commandString) { + const timeoutMatch = commandString.match(/timeout (\d+)/); + return timeoutMatch ? parseInt(timeoutMatch[1], 10) : null; +} + +function extractSshArgs(commandString) { + const sshCommandMatch = commandString.match(/ssh (.+?) 'bash -se'/); + let sshArgs = sshCommandMatch ? sshCommandMatch[1].split(' ') : []; + sshArgs = sshArgs.map(arg => arg === 'RequestTTY=no' ? 'RequestTTY=yes' : arg); + if (!sshArgs.includes('RequestTTY=yes')) { + sshArgs.push('-o', 'RequestTTY=yes'); + } + return sshArgs; +} + +function extractHereDocContent(commandString) { + const delimiterMatch = commandString.match(/<< (\S+)/); + const delimiter = delimiterMatch ? delimiterMatch[1] : null; + const escapedDelimiter = delimiter.slice(1).trim().replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); + const hereDocRegex = new RegExp(`<< \\\\${escapedDelimiter}([\\s\\S\\.]*?)${escapedDelimiter}`); + const hereDocMatch = commandString.match(hereDocRegex); + return hereDocMatch ? hereDocMatch[1] : ''; +} + +server.listen(6002, () => { + console.log('Server listening on port 6002'); +}); diff --git a/other/nightly/.env.development.example b/other/nightly/.env.development.example index f9bcd361a..3023a21a6 100644 --- a/other/nightly/.env.development.example +++ b/other/nightly/.env.development.example @@ -6,13 +6,7 @@ APP_KEY= APP_URL=http://localhost APP_PORT=8000 APP_DEBUG=true -MUX_ENABLED=false - -# Enable Laravel Telescope for debugging -TELESCOPE_ENABLED=false - -# Selenium Driver URL for Dusk -DUSK_DRIVER_URL=http://selenium:4444 +SSH_MUX_ENABLED=false # PostgreSQL Database Configuration DB_DATABASE=coolify @@ -21,9 +15,22 @@ DB_PASSWORD=password DB_HOST=host.docker.internal DB_PORT=5432 -#Set custom ray port +# Ray Configuration +# Set to true to enable Ray +RAY_ENABLED=false +# Set custom ray port RAY_PORT= +# Clockwork Configuration +CLOCKWORK_ENABLED=false +CLOCKWORK_QUEUE_COLLECT=true + +# 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= diff --git a/other/nightly/docker-compose.prod.yml b/other/nightly/docker-compose.prod.yml index b26cd5746..3eb270a2a 100644 --- a/other/nightly/docker-compose.prod.yml +++ b/other/nightly/docker-compose.prod.yml @@ -48,6 +48,7 @@ services: - PUSHER_APP_SECRET - AUTOUPDATE - SELF_HOSTED + - SSH_MUX_ENABLED - SSH_MUX_PERSIST_TIME - FEEDBACK_DISCORD_WEBHOOK - WAITLIST @@ -109,18 +110,24 @@ services: retries: 10 timeout: 2s soketi: + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.1' ports: - "${SOKETI_PORT:-6001}:6001" + - "6002:6002" + volumes: + - /data/coolify/ssh:/var/www/html/storage/app/ssh environment: + APP_NAME: "${APP_NAME:-Coolify}" SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" healthcheck: - test: wget -qO- http://127.0.0.1:6001/ready || exit 1 + test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ] interval: 5s retries: 10 timeout: 2s + volumes: coolify-db: name: coolify-db diff --git a/other/nightly/docker-compose.windows.yml b/other/nightly/docker-compose.windows.yml index a1ee1aeea..ef2de82e9 100644 --- a/other/nightly/docker-compose.windows.yml +++ b/other/nightly/docker-compose.windows.yml @@ -45,7 +45,7 @@ services: - PUSHER_APP_SECRET - AUTOUPDATE=true - SELF_HOSTED=true - - MUX_ENABLED=false + - SSH_MUX_ENABLED=false - IS_WINDOWS_DOCKER_DESKTOP=true ports: - "${APP_PORT:-8000}:80" @@ -103,7 +103,7 @@ services: retries: 10 timeout: 2s soketi: - image: 'quay.io/soketi/soketi:1.6-16-alpine' + image: 'ghcr.io/coollabsio/coolify-realtime:1.0.0' pull_policy: always container_name: coolify-realtime restart: always @@ -111,16 +111,21 @@ services: - .env ports: - "${SOKETI_PORT:-6001}:6001" + - "6002:6002" + volumes: + - ./ssh:/var/www/html/storage/app/ssh environment: + APP_NAME: "${APP_NAME:-Coolify}" SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" healthcheck: - test: wget -qO- http://localhost:6001/ready || exit 1 + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"] interval: 5s retries: 10 timeout: 2s + volumes: coolify-db: name: coolify-db diff --git a/other/nightly/docker-compose.yml b/other/nightly/docker-compose.yml index 930c0a6b9..68d0f0744 100644 --- a/other/nightly/docker-compose.yml +++ b/other/nightly/docker-compose.yml @@ -24,8 +24,9 @@ services: networks: - coolify soketi: - image: 'quay.io/soketi/soketi:1.6-16-alpine' container_name: coolify-realtime + extra_hosts: + - 'host.docker.internal:host-gateway' restart: always networks: - coolify diff --git a/other/nightly/install.sh b/other/nightly/install.sh index 23c2efc6f..09613e12b 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -5,11 +5,30 @@ set -e # Exit immediately if a command exits with a non-zero status ## $1 could be empty, so we need to disable this check #set -u # Treat unset variables as an error and exit set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status +CDN="https://cdn.coollabs.io/coolify-nightly" +DATE=$(date +"%Y%m%d-%H%M%S") -VERSION="1.4" +VERSION="1.5" DOCKER_VERSION="26.0" -CDN="https://cdn.coollabs.io/coolify-nightly" +mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs} +mkdir -p /data/coolify/ssh/{keys,mux} +mkdir -p /data/coolify/proxy/dynamic + +chown -R 9999:root /data/coolify +chmod -R 700 /data/coolify + +INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log" + +exec > >(tee -a $INSTALLATION_LOG_WITH_DATE) 2>&1 + +getAJoke() { + JOKES=$(curl -s --max-time 2 https://v2.jokeapi.dev/joke/Programming?format=txt&type=single&amount=1 || true) + if [ "$JOKES" != "" ]; then + echo -e " - Until then, here's a joke for you:\n" + echo -e "$JOKES\n" + fi +} OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') ENV_FILE="/data/coolify/source/.env" @@ -46,12 +65,16 @@ fi LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',') LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',') +LATEST_REALTIME_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',') if [ -z "$LATEST_HELPER_VERSION" ]; then LATEST_HELPER_VERSION=latest fi -DATE=$(date +"%Y%m%d-%H%M%S") +if [ -z "$LATEST_REALTIME_VERSION" ]; then + LATEST_REALTIME_VERSION=latest +fi + if [ $EUID != 0 ]; then echo "Please run as root" @@ -73,18 +96,29 @@ if [ "$1" != "" ]; then LATEST_VERSION="${LATEST_VERSION#v}" fi -echo -e "-------------" -echo -e "Welcome to Coolify v4 beta installer!" -echo -e "This script will install everything for you." +echo -e "\033[0;35m" +cat << "EOF" + _____ _ _ __ + / ____| | (_)/ _| + | | ___ ___ | |_| |_ _ _ + | | / _ \ / _ \| | | _| | | | + | |___| (_) | (_) | | | | | |_| | + \_____\___/ \___/|_|_|_| \__, | + __/ | + |___/ +EOF +echo -e "\033[0m" +echo -e "Welcome to Coolify Installer!" +echo -e "This script will install everything for you. Sit back and relax." echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n" -echo -e "-------------" - -echo "OS: $OS_TYPE $OS_VERSION" -echo "Coolify version: $LATEST_VERSION" -echo "Helper version: $LATEST_HELPER_VERSION" - -echo -e "-------------" -echo "Installing required packages..." +echo -e "---------------------------------------------" +echo "| Operating System | $OS_TYPE $OS_VERSION" +echo "| Docker | $DOCKER_VERSION" +echo "| Coolify | $LATEST_VERSION" +echo "| Helper | $LATEST_HELPER_VERSION" +echo "| Realtime | $LATEST_REALTIME_VERSION" +echo -e "---------------------------------------------\n" +echo -e "1. Installing required packages (curl, wget, git, jq). " case "$OS_TYPE" in arch) @@ -122,24 +156,26 @@ sles | opensuse-leap | opensuse-tumbleweed) ;; esac + + +echo -e "2. Check OpenSSH server configuration. " + # Detect OpenSSH server SSH_DETECTED=false if [ -x "$(command -v systemctl)" ]; then if systemctl status sshd >/dev/null 2>&1; then - echo "OpenSSH server is installed." + echo " - OpenSSH server is installed." SSH_DETECTED=true - fi - if systemctl status ssh >/dev/null 2>&1; then - echo "OpenSSH server is installed." + elif systemctl status ssh >/dev/null 2>&1; then + echo " - OpenSSH server is installed." SSH_DETECTED=true fi elif [ -x "$(command -v service)" ]; then if service sshd status >/dev/null 2>&1; then - echo "OpenSSH server is installed." + echo " - OpenSSH server is installed." SSH_DETECTED=true - fi - if service ssh status >/dev/null 2>&1; then - echo "OpenSSH server is installed." + elif service ssh status >/dev/null 2>&1; then + echo " - OpenSSH server is installed." SSH_DETECTED=true fi fi @@ -151,104 +187,91 @@ if [ "$SSH_DETECTED" = "false" ]; then fi # Detect SSH PermitRootLogin -SSH_PERMIT_ROOT_LOGIN=false -SSH_PERMIT_ROOT_LOGIN_CONFIG=$(grep "^PermitRootLogin" /etc/ssh/sshd_config | awk '{print $2}') || SSH_PERMIT_ROOT_LOGIN_CONFIG="N/A (commented out or not found at all)" -if [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "prohibit-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "without-password" ]; then - echo "PermitRootLogin is enabled." - SSH_PERMIT_ROOT_LOGIN=true -fi - -if [ "$SSH_PERMIT_ROOT_LOGIN" != "true" ]; then - echo "###############################################################################" - echo "WARNING: PermitRootLogin is not enabled in /etc/ssh/sshd_config." - echo -e "It is set to $SSH_PERMIT_ROOT_LOGIN_CONFIG. Should be prohibit-password, yes or without-password.\n" - echo -e "Please make sure it is set, otherwise Coolify cannot connect to the host system. \n" - echo "###############################################################################" +SSH_PERMIT_ROOT_LOGIN=$(sshd -T | grep -i "permitrootlogin" | awk '{print $2}') || true +if [ "$SSH_PERMIT_ROOT_LOGIN" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "without-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "prohibit-password" ]; then + echo " - SSH PermitRootLogin is enabled." +else + echo " - SSH PermitRootLogin is disabled." + echo " If you have problems with SSH, please read this: https://coolify.io/docs/knowledge-base/server/openssh" fi # Detect if docker is installed via snap if [ -x "$(command -v snap)" ]; then - if snap list | grep -q docker; then - echo "Docker is installed via snap." - echo "Please note that Coolify does not support Docker installed via snap." - echo "Please remove Docker with snap (snap remove docker) and reexecute this script." + SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false") + if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then + echo " - Docker is installed via snap." + echo " Please note that Coolify does not support Docker installed via snap." + echo " Please remove Docker with snap (snap remove docker) and reexecute this script." exit 1 fi fi +echo -e "3. Check Docker Installation. " if ! [ -x "$(command -v docker)" ]; then + echo " - Docker is not installed. Installing Docker. It may take a while." + getAJoke case "$OS_TYPE" in "almalinux") - dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo - dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin + dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1 + dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1 if ! [ -x "$(command -v docker)" ]; then - echo "Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." + echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." exit 1 fi - systemctl start docker - systemctl enable docker + systemctl start docker >/dev/null 2>&1 + systemctl enable docker >/dev/null 2>&1 ;; "alpine") - apk add docker docker-cli-compose - rc-update add docker default - service docker start - if [ -x "$(command -v docker)" ]; then - echo "Docker installed successfully." - else - echo "Failed to install Docker with apk. Try to install it manually." - echo "Please visit https://wiki.alpinelinux.org/wiki/Docker for more information." - exit + apk add docker docker-cli-compose >/dev/null 2>&1 + rc-update add docker default >/dev/null 2>&1 + service docker start >/dev/null 2>&1 + if ! [ -x "$(command -v docker)" ]; then + echo " - Failed to install Docker with apk. Try to install it manually." + echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information." + exit 1 fi ;; "arch") - pacman -Sy docker docker-compose --noconfirm - systemctl enable docker.service - if [ -x "$(command -v docker)" ]; then - echo "Docker installed successfully." - else - echo "Failed to install Docker with pacman. Try to install it manually." - echo "Please visit https://wiki.archlinux.org/title/docker for more information." - exit + pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1 + systemctl enable docker.service >/dev/null 2>&1 + if ! [ -x "$(command -v docker)" ]; then + echo " - Failed to install Docker with pacman. Try to install it manually." + echo " Please visit https://wiki.archlinux.org/title/docker for more information." + exit 1 fi ;; "amzn") - dnf install docker -y + dnf install docker -y >/dev/null 2>&1 DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker} - mkdir -p $DOCKER_CONFIG/cli-plugins - curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose - chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose - systemctl start docker - systemctl enable docker - if [ -x "$(command -v docker)" ]; then - echo "Docker installed successfully." - else - echo "Failed to install Docker with dnf. Try to install it manually." - echo "Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information." - exit + mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1 + curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1 + chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1 + systemctl start docker >/dev/null 2>&1 + systemctl enable docker >/dev/null 2>&1 + if ! [ -x "$(command -v docker)" ]; then + echo " - Failed to install Docker with dnf. Try to install it manually." + echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information." + exit 1 fi ;; *) - # Automated Docker installation - curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh - if [ -x "$(command -v docker)" ]; then - echo "Docker installed successfully." - else - echo "Docker installation failed with Rancher script. Trying with official script." - curl https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} - if [ -x "$(command -v docker)" ]; then - echo "Docker installed successfully." - else - echo "Docker installation failed with official script." - echo "Maybe your OS is not supported?" - echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." + curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh >/dev/null 2>&1 + if ! [ -x "$(command -v docker)" ]; then + curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} >/dev/null 2>&1 + if ! [ -x "$(command -v docker)" ]; then + echo " - Docker installation failed." + echo " Maybe your OS is not supported?" + echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." exit 1 fi fi esac + echo " - Docker installed successfully." +else + echo " - Docker is installed." fi -echo -e "-------------" -echo -e "Check Docker Configuration..." +echo -e "4. Check Docker Configuration. " mkdir -p /etc/docker # shellcheck disable=SC2015 test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" || cat >/etc/docker/daemon.json </dev/null 2>&1; then - echo "Using systemctl to restart Docker..." + echo " - Using systemctl to restart Docker." systemctl restart docker if [ $? -eq 0 ]; then - echo "Docker restarted successfully using systemctl." + echo " - Docker restarted successfully using systemctl." else - echo "Failed to restart Docker using systemctl." + echo " - Failed to restart Docker using systemctl." return 1 fi # Check if service command is available elif command -v service >/dev/null 2>&1; then - echo "Using service command to restart Docker..." + echo " - Using service command to restart Docker." service docker restart if [ $? -eq 0 ]; then - echo "Docker restarted successfully using service." + echo " - Docker restarted successfully using service." else - echo "Failed to restart Docker using service." + echo " - Failed to restart Docker using service." return 1 fi # If neither systemctl nor service is available else - echo "Neither systemctl nor service command is available on this system." + echo " - Neither systemctl nor service command is available on this system." return 1 fi } @@ -312,40 +334,30 @@ restart_docker_service() { if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE")) if [ "$DIFF" != "" ]; then - echo "Docker configuration updated, restart docker daemon..." + echo " - Docker configuration updated, restart docker daemon..." restart_docker_service else - echo "Docker configuration is up to date." + echo " - Docker configuration is up to date." fi else - echo "Docker configuration updated, restart docker daemon..." + echo " - Docker configuration updated, restart docker daemon..." restart_docker_service fi -echo -e "-------------" - -mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs} -mkdir -p /data/coolify/ssh/{keys,mux} -mkdir -p /data/coolify/proxy/dynamic - -chown -R 9999:root /data/coolify -chmod -R 700 /data/coolify - -echo "Downloading required files from CDN..." +echo -e "5. Download required files from CDN. " curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh +echo -e "6. Make backup of .env to .env-$DATE" + # Copy .env.example if .env does not exist if [ -f $ENV_FILE ]; then - echo "File exists: $ENV_FILE" - cat $ENV_FILE - echo "Copying .env to .env-$DATE" cp $ENV_FILE $ENV_FILE-$DATE else - echo "File does not exist: $ENV_FILE" - echo "Copying .env.production to .env-$DATE" + echo " - File does not exist: $ENV_FILE" + echo " - Copying .env.production to .env-$DATE" cp /data/coolify/source/.env.production $ENV_FILE-$DATE # Generate a secure APP_ID and APP_KEY sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE" @@ -366,6 +378,7 @@ else fi # Merge .env and .env.production. New values will be added to .env +echo -e "7. Propagating .env with new values - if necessary." awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production > $ENV_FILE if [ "$AUTOUPDATE" = "false" ]; then @@ -375,33 +388,122 @@ if [ "$AUTOUPDATE" = "false" ]; then sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env fi fi - -# Generate an ssh key (ed25519) at /data/coolify/ssh/keys/id.root@host.docker.internal -if [ ! -f /data/coolify/ssh/keys/id.root@host.docker.internal ]; then - 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 -fi - -addSshKey() { - cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >>~/.ssh/authorized_keys - chmod 600 ~/.ssh/authorized_keys -} - +echo -e "8. Checking for SSH key for localhost access." if [ ! -f ~/.ssh/authorized_keys ]; then mkdir -p ~/.ssh chmod 700 ~/.ssh touch ~/.ssh/authorized_keys - addSshKey + chmod 600 ~/.ssh/authorized_keys fi -if ! grep -qw "root@coolify" ~/.ssh/authorized_keys; then - addSshKey -fi +checkSshKeyInAuthorizedKeys() { + grep -qw "root@coolify" ~/.ssh/authorized_keys + return $? +} -bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" +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() { + 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 + +chown -R 9999:root /data/coolify +chmod -R 700 /data/coolify + +echo -e "9. Installing Coolify ($LATEST_VERSION)" +echo -e " - It could take a while based on your server's performance, network speed, stars, etc." +echo -e " - Please wait." +getAJoke + +bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" >/dev/null 2>&1 +echo " - Coolify installed successfully." rm -f $ENV_FILE-$DATE -echo "Waiting for 20 seconds for Coolify to be ready..." + +echo " - Waiting for 20 seconds for Coolify (database migrations) to be ready." +getAJoke sleep 20 -echo "Please visit http://$(curl -4s https://ifconfig.io):8000 to get started." -echo -e "\nCongratulations! Your Coolify instance is ready to use.\n" +echo -e "\033[0;35m + ____ _ _ _ _ _ + / ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| | + | | / _ \| '_ \ / _\` | '__/ _\` | __| | | | |/ _\` | __| |/ _ \| '_ \/ __| | + | |__| (_) | | | | (_| | | | (_| | |_| |_| | | (_| | |_| | (_) | | | \__ \_| + \____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_) + |___/ +\033[0m" +echo -e "\nYour instance is ready to use." +echo -e "Please visit http://$(curl -4s https://ifconfig.io):8000 to get started.\n" +echo -e "WARNING: We recommend you to backup your /data/coolify/source/.env file to a safe location, outside of this server." +cp /data/coolify/source/.env /data/coolify/source/.env.backup diff --git a/other/nightly/upgrade.sh b/other/nightly/upgrade.sh index 45295a510..9aa3a5f9a 100644 --- a/other/nightly/upgrade.sh +++ b/other/nightly/upgrade.sh @@ -12,7 +12,6 @@ curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production # Merge .env and .env.production. New values will be added to .env awk -F '=' '!seen[$1]++' /data/coolify/source/.env /data/coolify/source/.env.production > /data/coolify/source/.env.tmp && mv /data/coolify/source/.env.tmp /data/coolify/source/.env - # Check if PUSHER_APP_ID or PUSHER_APP_KEY or PUSHER_APP_SECRET is empty in /data/coolify/source/.env if grep -q "PUSHER_APP_ID=$" /data/coolify/source/.env; then sed -i "s|PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|g" /data/coolify/source/.env diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 7bb400bfd..b7e48c698 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,13 +1,16 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.330" + "version": "4.0.0-beta.339" }, "nightly": { - "version": "4.0.0-beta.331" + "version": "4.0.0-beta.340" }, "helper": { - "version": "1.0.0" + "version": "1.0.1" + }, + "realtime": { + "version": "1.0.1" } } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ff77563b0..ad1a3cc31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,15 @@ "dependencies": { "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "0.5.13", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", + "cookie": "^0.6.0", + "dotenv": "^16.4.5", "ioredis": "5.4.1", - "tailwindcss-scrollbar": "0.1.0" + "node-pty": "^1.0.0", + "tailwindcss-scrollbar": "0.1.0", + "ws": "^8.17.0" }, "devDependencies": { "@vitejs/plugin-vue": "4.5.1", @@ -20,7 +26,7 @@ "postcss": "8.4.38", "pusher-js": "8.4.0-rc2", "tailwindcss": "3.4.4", - "vite": "4.5.3", + "vite": "4.5.5", "vue": "3.4.29" } }, @@ -692,6 +698,19 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" + }, "node_modules/alpinejs": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.0.tgz", @@ -940,6 +959,15 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1000,6 +1028,18 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.692", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz", @@ -1475,6 +1515,11 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -1492,6 +1537,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "dependencies": { + "nan": "^2.17.0" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -2028,9 +2082,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", + "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -2124,6 +2178,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yaml": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", diff --git a/package.json b/package.json index cacef3e06..3633a7ed1 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,20 @@ "postcss": "8.4.38", "pusher-js": "8.4.0-rc2", "tailwindcss": "3.4.4", - "vite": "4.5.3", + "vite": "4.5.5", "vue": "3.4.29" }, "dependencies": { "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "0.5.13", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", "alpinejs": "3.14.0", + "cookie": "^0.6.0", + "dotenv": "^16.4.5", "ioredis": "5.4.1", - "tailwindcss-scrollbar": "0.1.0" + "node-pty": "^1.0.0", + "tailwindcss-scrollbar": "0.1.0", + "ws": "^8.17.0" } } \ No newline at end of file diff --git a/public/svgs/budge.png b/public/svgs/budge.png new file mode 100644 index 000000000..b44606c64 Binary files /dev/null and b/public/svgs/budge.png differ diff --git a/resources/js/app.js b/resources/js/app.js index befec919e..bbf8104c6 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -4,3 +4,18 @@ // const app = createApp({}); // 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'; + +if (!window.term) { + window.term = new Terminal({ + cols: 80, + rows: 30, + fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', + cursorBlink: true, + }); + window.fitAddon = new FitAddon(); + window.term.loadAddon(window.fitAddon); +} diff --git a/resources/js/components/MagicBar.vue b/resources/js/components/MagicBar.vue index 611ec0891..22af9dff4 100644 --- a/resources/js/components/MagicBar.vue +++ b/resources/js/components/MagicBar.vue @@ -390,7 +390,7 @@ const magicActions = [{ }, { id: 19, - name: 'Goto: Command Center', + name: 'Goto: Terminal', icon: 'goto', sequence: ['main', 'redirect'] }, @@ -653,7 +653,7 @@ async function redirect() { targetUrl.pathname = `/settings` break; case 19: - targetUrl.pathname = `/command-center` + targetUrl.pathname = `/terminal` break; case 20: targetUrl.pathname = `/team/notifications` diff --git a/resources/views/components/forms/select.blade.php b/resources/views/components/forms/select.blade.php index 02308ceb5..4da9eca1b 100644 --- a/resources/views/components/forms/select.blade.php +++ b/resources/views/components/forms/select.blade.php @@ -1,6 +1,6 @@
@if ($label) -
@@ -226,9 +228,9 @@
  • - + @@ -236,7 +238,7 @@ - Command Center + Terminal
  • diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 2481d9bd2..e0f044353 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -9,6 +9,10 @@ open: false, init() { this.pageWidth = localStorage.getItem('pageWidth'); + if (!this.pageWidth) { + this.pageWidth = 'full'; + localStorage.setItem('pageWidth', 'full'); + } } }" x-cloak class="mx-auto" :class="pageWidth === 'full' ? '' : 'max-w-7xl'">