diff --git a/.env.development.example b/.env.development.example
index 3023a21a6..d4daed4f7 100644
--- a/.env.development.example
+++ b/.env.development.example
@@ -6,7 +6,7 @@ APP_KEY=
APP_URL=http://localhost
APP_PORT=8000
APP_DEBUG=true
-SSH_MUX_ENABLED=false
+SSH_MUX_ENABLED=true
# PostgreSQL Database Configuration
DB_DATABASE=coolify
@@ -19,11 +19,7 @@ DB_PORT=5432
# Set to true to enable Ray
RAY_ENABLED=false
# Set custom ray port
-RAY_PORT=
-
-# Clockwork Configuration
-CLOCKWORK_ENABLED=false
-CLOCKWORK_QUEUE_COLLECT=true
+# RAY_PORT=
# Enable Laravel Telescope for debugging
TELESCOPE_ENABLED=false
diff --git a/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml
new file mode 100644
index 000000000..42df4785e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml
@@ -0,0 +1,65 @@
+name: 🐞 Bug Report
+description: "File a new bug report."
+title: "[Bug]: "
+labels: ["🐛 Bug", "🔍 Triage"]
+body:
+ - type: markdown
+ attributes:
+ 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).
+
+ - type: textarea
+ attributes:
+ 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: 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: 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: dropdown
+ attributes:
+ label: Are you using Coolify Cloud?
+ options:
+ - "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/02_ENHANCEMENT_BOUNTY.yml b/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml
new file mode 100644
index 000000000..ef26125e0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/02_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/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml
deleted file mode 100644
index f3d52b1b4..000000000
--- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: Bug report
-description: "Create a new bug report."
-title: "[Bug]: "
-body:
- - type: markdown
- attributes:
- value: >-
- # 💎 Bounty program (with
- [algora.io](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.
- validations:
- required: true
- - type: textarea
- attributes:
- label: Exception or Error
- description: Please provide error logs if possible.
- - type: input
- attributes:
- label: Version
- description: Coolify's version (see top of your screen).
- validations:
- required: true
- - type: checkboxes
- attributes:
- label: Cloud?
- description: "Are you using the cloud version of Coolify?"
- options:
- - label: 'Yes'
- required: false
- - label: 'No'
- required: false
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/pull_request_template.md b/.github/pull_request_template.md
index 3ded74ce3..5afe00a30 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1 +1,13 @@
-> Always use `next` branch as destination branch for PRs, not `main`
+## Submit Checklist (REMOVE THIS SECTION BEFORE SUBMITTING)
+- [ ] I have selected the `next` branch as the destination for my PR, not `main`.
+- [ ] I have listed all changes in the `Changes` section.
+- [ ] I have filled out the `Issues` section with the issue/discussion link(s) (if applicable).
+- [ ] I have tested my changes.
+- [ ] I have considered backwards compatibility.
+- [ ] I have removed this checklist and any unused sections.
+
+## Changes
+-
+
+## Issues
+- fix #
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/pr-build.yml b/.github/workflows/coolify-realtime-next.yml
similarity index 50%
rename from .github/workflows/pr-build.yml
rename to .github/workflows/coolify-realtime-next.yml
index 017399e73..33e048627 100644
--- a/.github/workflows/pr-build.yml
+++ b/.github/workflows/coolify-realtime-next.yml
@@ -1,17 +1,18 @@
-name: PR Build (v4)
+name: Coolify Realtime Development (v4)
on:
- pull_request:
- types:
- - opened
- branches-ignore: ["main", "v3"]
- paths-ignore:
- - .github/workflows/coolify-helper.yml
- - docker/coolify-helper/Dockerfile
+ push:
+ branches: [ "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"
+ IMAGE_NAME: "coollabsio/coolify-realtime"
jobs:
amd64:
@@ -19,8 +20,6 @@ jobs:
permissions:
contents: read
packages: write
- attestations: write
- id-token: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
@@ -29,21 +28,26 @@ jobs:
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/prod/Dockerfile
+ file: docker/coolify-realtime/Dockerfile
platforms: linux/amd64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ labels: |
+ coolify.managed=true
aarch64:
- runs-on: [self-hosted, arm64]
+ runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
- attestations: write
- id-token: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
@@ -52,22 +56,27 @@ jobs:
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/prod/Dockerfile
+ file: docker/coolify-realtime/Dockerfile
platforms: linux/aarch64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}-aarch64
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
+ labels: |
+ coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
- attestations: write
- id-token: write
- needs: [amd64, aarch64]
+ needs: [ amd64, aarch64 ]
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -81,10 +90,14 @@ jobs:
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 }}:${{ github.event.number }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}
+ docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
- uses: sarisia/actions-status-discord@v1
if: always()
with:
- webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
+ webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml
new file mode 100644
index 000000000..30910ae0b
--- /dev/null
+++ b/.github/workflows/coolify-realtime.yml
@@ -0,0 +1,103 @@
+name: Coolify Realtime (v4)
+
+on:
+ push:
+ branches: [ "main" ]
+ 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/docker-image.yml b/.github/workflows/docker-image.yml
deleted file mode 100644
index 0edaa4f1c..000000000
--- a/.github/workflows/docker-image.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-name: Docker Image CI
-
-on:
- # push:
- # branches: [ "main" ]
- # pull_request:
- # branches: [ "*" ]
- push:
- branches: ["this-does-not-exist"]
- pull_request:
- branches: ["this-does-not-exist"]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Cache Docker layers
- uses: actions/cache@v2
- with:
- path: |
- /usr/local/share/ca-certificates
- /var/cache/apt/archives
- /var/lib/apt/lists
- ~/.cache
- key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
- restore-keys: |
- ${{ runner.os }}-docker-
- - name: Build the Docker image
- run: |
- cp .env.example .env
- docker run --rm -u "$(id -u):$(id -g)" \
- -v "$(pwd):/app" \
- -w /app composer:2 \
- composer install --ignore-platform-reqs
- ./vendor/bin/spin build
- - name: Start the stack
- run: |
- ./vendor/bin/spin up -d
- ./vendor/bin/spin exec coolify php artisan key:generate
- ./vendor/bin/spin exec coolify php artisan migrate:fresh --seed
- - name: Test (missing E2E tests)
- run: |
- ./vendor/bin/spin exec coolify php artisan test
diff --git a/.github/workflows/fix-php-code-style-issues b/.github/workflows/fix-php-code-style-issues
deleted file mode 100644
index aebce91bc..000000000
--- a/.github/workflows/fix-php-code-style-issues
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Fix PHP code style issues
-
-on: [push]
-
-permissions:
- contents: write
-
-jobs:
- php-code-styling:
- runs-on: ubuntu-latest
- timeout-minutes: 5
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- ref: ${{ github.head_ref }}
-
- - name: Fix PHP code style issues
- uses: aglipanci/laravel-pint-action@2.4
-
- - name: Commit changes
- uses: stefanzweifel/git-auto-commit-action@v5
- with:
- commit_message: Fix styling
diff --git a/.github/workflows/lock-closed-issues-discussions-and-prs.yml b/.github/workflows/lock-closed-issues-discussions-and-prs.yml
new file mode 100644
index 000000000..d00853964
--- /dev/null
+++ b/.github/workflows/lock-closed-issues-discussions-and-prs.yml
@@ -0,0 +1,17 @@
+name: Lock closed Issues, Discussions, and PRs
+
+on:
+ schedule:
+ - cron: '0 1 * * *'
+
+jobs:
+ lock-threads:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Lock threads after 30 days of inactivity
+ uses: dessant/lock-threads@v5
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ issue-inactive-days: '30'
+ pr-inactive-days: '30'
+ discussion-inactive-days: '30'
diff --git a/.github/workflows/manage-stale-issues-and-prs.yml b/.github/workflows/manage-stale-issues-and-prs.yml
new file mode 100644
index 000000000..2afc996cb
--- /dev/null
+++ b/.github/workflows/manage-stale-issues-and-prs.yml
@@ -0,0 +1,28 @@
+name: Manage Stale Issues and PRs
+
+on:
+ schedule:
+ - cron: '0 2 * * *'
+
+jobs:
+ manage-stale:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Manage stale issues and PRs
+ uses: actions/stale@v9
+ id: stale
+ with:
+ stale-issue-message: 'This issue will be automatically closed in a few days if no response is received. Please provide an update with the requested information.'
+ stale-pr-message: 'This pull request will be automatically closed in a few days if no response is received. Please update your PR or comment if you would like to continue working on it.'
+ close-issue-message: 'This issue has been automatically closed due to inactivity.'
+ close-pr-message: 'This pull request has been automatically closed due to inactivity.'
+ days-before-stale: 14
+ days-before-close: 7
+ stale-issue-label: '⏱︎ Stale'
+ stale-pr-label: '⏱︎ Stale'
+ only-labels: '💤 Waiting for feedback'
+ remove-stale-when-updated: true
+ operations-per-run: 100
+ labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback'
+ close-issue-reason: 'not_planned'
+ exempt-all-milestones: false
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..ea097e328
--- /dev/null
+++ b/.github/workflows/remove-labels-and-assignees-on-close.yml
@@ -0,0 +1,78 @@
+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 pr = context.payload.pull_request;
+ if (pr.body) {
+ const issueReferences = pr.body.match(/#(\d+)/g);
+ if (issueReferences) {
+ for (const reference of issueReferences) {
+ const issueNumber = parseInt(reference.substring(1));
+ await processIssue(issueNumber);
+ }
+ }
+ }
+ }
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9618bfae5..80ec0614e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,54 +1,72 @@
-# 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. [Create a Pull Request](#7-create-a-pull-request)
+8. [Development Notes](#development-notes)
+9. [Resetting Development Environment](#resetting-development-environment)
+10. [Additional Contribution Guidelines](#additional-contribution-guidelines)
-## 1. Setup 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):
- - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install)
- - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/)
+ - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify)
+ - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify)
- Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide
- Install Docker Desktop (easier):
- - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/)
+ - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify)
- Ensure WSL2 backend is enabled in Docker Desktop settings
2. Install Spin:
- - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2)
+ - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2?ref=coolify)
-### MacOS
+
+
+
+MacOS
1. Install Orbstack, Docker Desktop (or similar):
- Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
- - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation)
+ - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify)
- Docker Desktop:
- - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/)
+ - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify)
2. Install Spin:
- - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin)
+ - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin?ref=coolify)
-### Linux
+
+
+
+Linux
1. Install Docker Engine, Docker Desktop (or similar):
- Docker Engine (recommended, as there is no VM overhead):
- - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/) for your Linux distribution
+ - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution
- Docker Desktop:
- - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/)
+ - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify)
2. Install Spin:
- - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions)
+ - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions?ref=coolify)
+
-## 2. Verify installation (optional)
+## 2. Verify Installation (Optional)
After installing Docker (or Orbstack) and Spin, verify the installation:
@@ -60,63 +78,53 @@ ## 2. Verify installation (optional)
```
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?ref=coolify) |
+ | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/?ref=coolify) |
+ | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download?ref=coolify) |
3. Clone the Coolify Repository from your fork to your local machine
- - Use `git clone` in the command line
+ - Use `git clone` in the command line, or
- Use GitHub Desktop (recommended):
- - Download and install from [https://desktop.github.com/](https://desktop.github.com/)
+ - Download and install from [https://desktop.github.com/](https://desktop.github.com/?ref=coolify)
- Open GitHub Desktop and login with your GitHub account
- Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone`
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,42 +134,19 @@ ## 6. Start Development
- 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
-
-When working on Coolify, keep the following in mind:
-
-1. **Database Migrations**: After switching branches or making changes to the database structure, always run migrations:
- ```bash
- docker exec -it coolify php artisan migrate
- ```
-
-2. **Resetting Development Setup**: To reset your development setup to a clean database with default values:
- ```bash
- 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.
-
-Remember, 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
+## 7. Create a Pull Request
1. After making changes or adding a new service:
- Commit your changes to your forked repository.
@@ -176,14 +161,83 @@ ## 9. Create a Pull Request
3. Filling out the PR details:
- Give your PR a descriptive title.
- - In the description, explain the changes you've made.
- - Reference any related issues by using keywords like "Fixes #123" or "Closes #456".
+ - Use the Pull Request Template provided and fill in the details.
-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.
+
+## Development Notes
+
+When working on Coolify, keep the following in mind:
+
+1. **Database Migrations**: After switching branches or making changes to the database structure, always run migrations:
+ ```bash
+ docker exec -it coolify php artisan migrate
+ ```
+
+2. **Resetting Development Setup**: To reset your development setup to a clean database with default values:
+ ```bash
+ 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 environment-specific issues.
+
+> [!IMPORTANT]
+> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
+
+## Resetting Development Environment
+
+If you encounter issues or break your database or something else, follow these steps to start from a clean slate (works since `v4.0.0-beta.342`):
+
+1. Stop all running containers `ctrl + c`.
+
+2. Remove all Coolify containers:
+ ```bash
+ docker rm coolify coolify-db coolify-redis coolify-realtime coolify-testing-host coolify-minio coolify-vite-1 coolify-mail
+ ```
+
+3. Remove Coolify volumes (it is possible that the volumes have no `coolify` prefix on your machine, in that case remove the prefix from the command):
+ ```bash
+ docker volume rm coolify_dev_backups_data coolify_dev_postgres_data coolify_dev_redis_data coolify_dev_coolify_data coolify_dev_minio_data
+ ```
+
+4. Remove unused images:
+ ```bash
+ docker image prune -a
+ ```
+
+5. Start Coolify again:
+ ```bash
+ spin up
+ ```
+
+6. Run database migrations and seeders:
+ ```bash
+ docker exec -it coolify php artisan migrate:fresh --seed
+ ```
+
+After completing these steps, you'll have a fresh development setup.
+
+> [!IMPORTANT]
+> Always run database migrations and seeders after switching branches or pulling updates to ensure your local database structure matches the current codebase and includes necessary seed data.
+
+## Additional Contribution Guidelines
+
+### 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/RELEASE.md b/RELEASE.md
index 2cb96b72b..d9f05f17d 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -2,35 +2,120 @@ # Coolify Release Guide
This guide outlines the release process for Coolify, intended for developers and those interested in understanding how releases are managed and deployed.
+## Table of Contents
+- [Release Process](#release-process)
+- [Version Types](#version-types)
+ - [Stable](#stable)
+ - [Nightly](#nightly)
+ - [Beta](#beta)
+- [Version Availability](#version-availability)
+ - [Self-Hosted](#self-hosted)
+ - [Cloud](#cloud)
+- [Manually Update to Specific Versions](#manually-update-to-specific-versions)
+
## Release Process
-1. **Development on `next` or separate branches**
- - Changes, fixes and new features are developed on the `next` or even separate branches.
+1. **Development on `next` or Feature Branches**
+ - Improvements, fixes, and new features are developed on the `next` branch or separate feature branches.
2. **Merging to `main`**
- - Once changes are ready, they are merged from `next` into the `main` branch.
+ - Once ready, changes are merged from the `next` branch into the `main` branch.
-3. **Building the release**
- - After merging to `main`, a new release is built.
- - Note: A push to `main` does not automatically mean a new version is released.
+3. **Building the Release**
+ - After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry with the version tag and the `latest` tag.
-4. **Creating a GitHub release**
- - A new release is created on GitHub with the new version details.
+4. **Creating a GitHub Release**
+ - A new GitHub release is manually created with details of the changes made in the version.
5. **Updating the CDN**
- - The final step is updating the version information on the CDN:
- [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json)
+ - To make a new version publicly available, the version information on the CDN needs to be updated: [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json)
> [!NOTE]
-> The CDN update may not occur immediately after the GitHub release. It can happen hours or even days later due to additional testing, stability checks, or potential hotfixes.
+> The CDN update may not occur immediately after the GitHub release. It can take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated.**
+## Version Types
+
+
+ Stable (coming soon)
+
+- **Stable**
+ - The production version suitable for stable, production environments (generally recommended).
+ - **Update Frequency:** Every 2 to 4 weeks, with more frequent possible hotfixes.
+ - **Release Size:** Larger but less frequent releases. Multiple nightly versions are consolidated into a single stable release.
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`).
+ - **Installation Command:**
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
+ ```
+
+
+
+
+ Nightly
+
+- **Nightly**
+ - The latest development version, suitable for testing the latest changes and experimenting with new features.
+ - **Update Frequency:** Daily or bi-weekly updates.
+ - **Release Size:** Smaller, more frequent releases.
+ - **Versioning Scheme:** TO BE DETERMINED
+ - **Installation Command:**
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify-nightly/install.sh | bash -s next
+ ```
+
+
+
+
+ Beta
+
+- **Beta**
+ - Test releases for the upcoming stable version.
+ - **Purpose:** Allows users to test and provide feedback on new features and changes before they become stable.
+ - **Update Frequency:** Available if we think beta testing is necessary.
+ - **Release Size:** Same size as stable release as it will become the next stabe release after some time.
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`).
+ - **Installation Command:**
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
+ ```
+
+
+
+> [!WARNING]
+> Do not use nightly/beta builds in production as there is no guarantee of stability.
## Version Availability
-It's important to understand that a new version released on GitHub may not immediately become available for users to update (through manual or auto-update).
+When a new version is released and a new GitHub release is created, it doesn't immediately become available for your instance. Here's how version availability works for different instance types.
+
+### Self-Hosted
+
+- **Update Frequency:** More frequent updates, especially on the nightly release channel.
+- **Update Availability:** New versions are available once the CDN has been updated.
+- **Update Methods:**
+ 1. **Manual Update in Instance Settings:**
+ - Go to `Settings > Update Check Frequency` and click the `Check Manually` button.
+ - If an update is available, an upgrade button will appear on the sidebar.
+ 2. **Automatic Update:**
+ - If enabled, the instance will update automatically at the time set in the settings.
+ 3. **Re-run Installation Script:**
+ - Run the installation script again to upgrade to the latest version available on the CDN:
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
+ ```
> [!IMPORTANT]
-> If you see a new release on GitHub but haven't received the update, it's likely because the CDN hasn't been updated yet. This is intentional and ensures stability and allows for hotfixes before the new version is officially released.
+> If a new release is available on GitHub but your instance hasn't updated yet or no upgrade button is shown in the UI, the CDN might not have been updated yet. This intentional delay ensures stability and allows for hotfixes before official release.
+
+### Cloud
+
+- **Update Frequency:** Less frequent as it's a managed service.
+- **Update Availability:** New versions are available once Andras has updated the cloud version manually.
+- **Update Method:**
+ - Updates are managed by Andras, who ensures each cloud version is thoroughly tested and stable before releasing it.
+
+> [!IMPORTANT]
+> The cloud version of Coolify may be several versions behind the latest GitHub releases even if the CDN is updated. This is intentional to ensure stability and reliability for cloud users and Andras will manully update the cloud version when the update is ready.
## Manually Update to Specific Versions
@@ -42,4 +127,4 @@ ## Manually Update to Specific Versions
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s
```
--> Replace `` with the version you want to update to (for example `4.0.0-beta.332`).
+Replace `` with the version you want to update to (for example `4.0.0-beta.332`).
diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php
index 7155f9a0a..61005845b 100644
--- a/app/Actions/Application/StopApplication.php
+++ b/app/Actions/Application/StopApplication.php
@@ -2,6 +2,7 @@
namespace App\Actions\Application;
+use App\Actions\Server\CleanupDocker;
use App\Models\Application;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,44 +10,35 @@ class StopApplication
{
use AsAction;
- public function handle(Application $application, bool $previewDeployments = false)
+ public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{
- if ($application->destination->server->isSwarm()) {
- instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
-
- return;
- }
-
- $servers = collect([]);
- $servers->push($application->destination->server);
- $application->additional_servers->map(function ($server) use ($servers) {
- $servers->push($server);
- });
- foreach ($servers as $server) {
+ try {
+ $server = $application->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
- if ($previewDeployments) {
- $containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
- } else {
- $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
- }
- if ($containers->count() > 0) {
- foreach ($containers as $container) {
- $containerName = data_get($container, 'Names');
- if ($containerName) {
- instant_remote_process(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false);
- }
- }
+ ray('Stopping application: '.$application->name);
+
+ if ($server->isSwarm()) {
+ instant_remote_process(["docker stack rm {$application->uuid}"], $server);
+
+ return;
}
+
+ $containersToStop = $application->getContainersToStop($previewDeployments);
+ $application->stopContainers($containersToStop, $server);
+
if ($application->build_pack === 'dockercompose') {
- // remove network
- $uuid = $application->uuid;
- instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
- instant_remote_process(["docker network rm {$uuid}"], $server, false);
+ $application->delete_connected_networks($application->uuid);
}
+
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
+ } catch (\Exception $e) {
+ ray($e->getMessage());
+
+ return $e->getMessage();
}
}
}
diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php
index 63e3afe2f..c691f52c0 100644
--- a/app/Actions/CoolifyTask/RunRemoteProcess.php
+++ b/app/Actions/CoolifyTask/RunRemoteProcess.php
@@ -4,6 +4,7 @@
use App\Enums\ActivityTypes;
use App\Enums\ProcessStatus;
+use App\Helpers\SshMultiplexingHelper;
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Server;
use Illuminate\Process\ProcessResult;
@@ -137,7 +138,7 @@ protected function getCommand(): string
$command = $this->activity->getExtraProperty('command');
$server = Server::whereUuid($server_uuid)->firstOrFail();
- return generateSshCommand($server, $command);
+ return SshMultiplexingHelper::generateSshCommand($server, $command);
}
protected function handleOutput(string $type, string $output)
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index 621834df0..3ee46a2e1 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -23,7 +23,7 @@ public function handle(StandaloneDragonfly $database)
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
+ $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -46,9 +46,6 @@ public function handle(StandaloneDragonfly $database)
'networks' => [
$this->database->destination->network,
],
- 'ulimits' => [
- 'memlock' => '-1',
- ],
'labels' => [
'coolify.managed' => 'true',
],
@@ -75,7 +72,7 @@ public function handle(StandaloneDragonfly $database)
],
],
];
- if (!is_null($this->database->limits_cpuset)) {
+ if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -118,10 +115,10 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -152,7 +149,7 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}");
}
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index 9290efc7c..a11452a68 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -24,7 +24,7 @@ public function handle(StandaloneKeydb $database)
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
+ $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -74,7 +74,7 @@ public function handle(StandaloneKeydb $database)
],
],
];
- if (!is_null($this->database->limits_cpuset)) {
+ if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -94,10 +94,10 @@ public function handle(StandaloneKeydb $database)
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- if (!is_null($this->database->keydb_conf) || !empty($this->database->keydb_conf)) {
+ if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir . '/keydb.conf',
+ 'source' => $this->configuration_dir.'/keydb.conf',
'target' => '/etc/keydb/keydb.conf',
'read_only' => true,
];
@@ -125,10 +125,10 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -159,7 +159,7 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
}
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index f37a5e361..a5630f734 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -21,7 +21,7 @@ public function handle(StandaloneMariadb $database)
$this->database = $database;
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
+ $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -69,7 +69,7 @@ public function handle(StandaloneMariadb $database)
],
],
];
- if (!is_null($this->database->limits_cpuset)) {
+ if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -89,10 +89,10 @@ public function handle(StandaloneMariadb $database)
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- if (!is_null($this->database->mariadb_conf) || !empty($this->database->mariadb_conf)) {
+ if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir . '/custom-config.cnf',
+ 'source' => $this->configuration_dir.'/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
@@ -120,10 +120,10 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -154,18 +154,18 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
$environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
$environment_variables->push("MARIADB_USER={$this->database->mariadb_user}");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
}
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 42fc8f348..5bff194d5 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -23,7 +23,7 @@ public function handle(StandaloneMongodb $database)
$startCommand = 'mongod';
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
+ $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -77,7 +77,7 @@ public function handle(StandaloneMongodb $database)
],
],
];
- if (!is_null($this->database->limits_cpuset)) {
+ if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -97,19 +97,19 @@ public function handle(StandaloneMongodb $database)
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- if (!is_null($this->database->mongo_conf) || !empty($this->database->mongo_conf)) {
+ if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir . '/mongod.conf',
+ 'source' => $this->configuration_dir.'/mongod.conf',
'target' => '/etc/mongo/mongod.conf',
'read_only' => true,
];
- $docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf';
+ $docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf';
}
$this->add_default_database();
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d',
+ 'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
'target' => '/docker-entrypoint-initdb.d',
'read_only' => true,
];
@@ -136,10 +136,10 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -170,15 +170,15 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
}
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index 2043342fe..cc4203580 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -21,7 +21,7 @@ public function handle(StandaloneMysql $database)
$this->database = $database;
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
+ $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -69,7 +69,7 @@ public function handle(StandaloneMysql $database)
],
],
];
- if (!is_null($this->database->limits_cpuset)) {
+ if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -89,10 +89,10 @@ public function handle(StandaloneMysql $database)
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- if (!is_null($this->database->mysql_conf) || !empty($this->database->mysql_conf)) {
+ if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir . '/custom-config.cnf',
+ 'source' => $this->configuration_dir.'/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
@@ -120,10 +120,10 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -154,18 +154,18 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
$environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
$environment_variables->push("MYSQL_USER={$this->database->mysql_user}");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
}
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index bc37fd5cf..2a8e5476c 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -37,7 +37,6 @@ public function handle(StandalonePostgresql $database)
$this->generate_init_scripts();
$this->add_custom_conf();
-
$docker_compose = [
'services' => [
$container_name => [
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index b837414d6..eeddab924 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -24,7 +24,7 @@ public function handle(StandaloneRedis $database)
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
+ $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -78,7 +78,7 @@ public function handle(StandaloneRedis $database)
],
],
];
- if (!is_null($this->database->limits_cpuset)) {
+ if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -98,10 +98,10 @@ public function handle(StandaloneRedis $database)
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- if (!is_null($this->database->redis_conf) || !empty($this->database->redis_conf)) {
+ if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir . '/redis.conf',
+ 'source' => $this->configuration_dir.'/redis.conf',
'target' => '/usr/local/etc/redis/redis.conf',
'read_only' => true,
];
@@ -130,10 +130,10 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -164,7 +164,7 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
}
diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php
index d562ec56f..e4cea7cee 100644
--- a/app/Actions/Database/StopDatabase.php
+++ b/app/Actions/Database/StopDatabase.php
@@ -2,6 +2,7 @@
namespace App\Actions\Database;
+use App\Actions\Server\CleanupDocker;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
@@ -10,25 +11,65 @@
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
+use Illuminate\Support\Facades\Process;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase
{
use AsAction;
- public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
+ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
$server = $database->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
- instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false);
+ $this->stopContainer($database, $database->uuid, 300);
+ if (! $isDeleteOperation) {
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
+ }
if ($database->is_public) {
StopDatabaseProxy::run($database);
}
+
+ return 'Database stopped successfully';
+ }
+
+ private function stopContainer($database, string $containerName, int $timeout = 300): void
+ {
+ $server = $database->destination->server;
+
+ $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+
+ $startTime = time();
+ while ($process->running()) {
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopContainer($containerName, $server);
+ break;
+ }
+ usleep(100000);
+ }
+
+ $this->removeContainer($containerName, $server);
+ }
+
+ private function forceStopContainer(string $containerName, $server): void
+ {
+ instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
+ }
+
+ private function removeContainer(string $containerName, $server): void
+ {
+ instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
+ }
+
+ private function deleteConnectedNetworks($uuid, $server)
+ {
+ instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
+ instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
}
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index fdaa88ebf..ed563eaae 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -543,7 +543,7 @@ private function old_way()
}
}
}
- $exitedServices = $exitedServices->unique('id');
+ $exitedServices = $exitedServices->unique('uuid');
foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) {
continue;
@@ -651,8 +651,9 @@ private function old_way()
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
- // Check if proxy is running
- $this->server->proxyType();
+ if (! $this->server->proxySet() || $this->server->proxy->force_stop) {
+ return;
+ }
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php
index f8882d12a..481757162 100644
--- a/app/Actions/Fortify/CreateNewUser.php
+++ b/app/Actions/Fortify/CreateNewUser.php
@@ -2,7 +2,6 @@
namespace App\Actions\Fortify;
-use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
@@ -20,7 +19,7 @@ class CreateNewUser implements CreatesNewUsers
*/
public function create(array $input): User
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->is_registration_enabled) {
abort(403);
}
@@ -48,7 +47,7 @@ public function create(array $input): User
$team = $user->teams()->first();
// Disable registration after first user is created
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$settings->is_registration_enabled = false;
$settings->save();
} else {
diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php
index dcb4058c0..55af1a8c0 100644
--- a/app/Actions/License/CheckResaleLicense.php
+++ b/app/Actions/License/CheckResaleLicense.php
@@ -2,7 +2,6 @@
namespace App\Actions\License;
-use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -13,7 +12,7 @@ class CheckResaleLicense
public function handle()
{
try {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (isDev()) {
$settings->update([
'is_resale_license_active' => true,
diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php
index 735b972af..cf0f6015c 100644
--- a/app/Actions/Proxy/CheckProxy.php
+++ b/app/Actions/Proxy/CheckProxy.php
@@ -26,7 +26,7 @@ public function handle(Server $server, $fromUI = false)
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
return false;
}
- ['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
+ ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
if (! $uptime) {
throw new \Exception($error);
}
diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php
index 991c94b11..4ef9618d0 100644
--- a/app/Actions/Proxy/StartProxy.php
+++ b/app/Actions/Proxy/StartProxy.php
@@ -35,7 +35,7 @@ public function handle(Server $server, bool $async = true, bool $force = false):
"echo 'Creating required Docker Compose file.'",
"echo 'Starting coolify-proxy.'",
'docker stack deploy -c docker-compose.yml coolify-proxy',
- "echo 'Proxy started successfully.'",
+ "echo 'Successfully started coolify-proxy.'",
]);
} else {
$caddfile = 'import /dynamic/*.caddy';
@@ -46,11 +46,14 @@ public function handle(Server $server, bool $async = true, bool $force = false):
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
- "echo 'Stopping existing coolify-proxy.'",
- 'docker compose down -v --remove-orphans > /dev/null 2>&1',
+ 'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then',
+ " echo 'Stopping and removing existing coolify-proxy.'",
+ ' docker rm -f coolify-proxy || true',
+ " echo 'Successfully stopped and removed existing coolify-proxy.'",
+ 'fi',
"echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
- "echo 'Proxy started successfully.'",
+ "echo 'Successfully started coolify-proxy.'",
]);
$commands = $commands->merge(connectProxyToNetworks($server));
}
diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php
index 1034c13d6..dc6ac12bf 100644
--- a/app/Actions/Server/CleanupDocker.php
+++ b/app/Actions/Server/CleanupDocker.php
@@ -2,7 +2,6 @@
namespace App\Actions\Server;
-use App\Models\InstanceSettings;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -12,28 +11,29 @@ class CleanupDocker
public function handle(Server $server)
{
+ $settings = instanceSettings();
+ $helperImageVersion = data_get($settings, 'helper_version');
+ $helperImage = config('coolify.helper_image');
+ $helperImageWithVersion = "$helperImage:$helperImageVersion";
- $commands = $this->getCommands();
+ $commands = [
+ 'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"',
+ 'docker image prune -af --filter "label!=coolify.managed=true"',
+ 'docker builder prune -af',
+ "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
+ ];
+
+ $serverSettings = $server->settings;
+ if ($serverSettings->delete_unused_volumes) {
+ $commands[] = 'docker volume prune -af';
+ }
+
+ if ($serverSettings->delete_unused_networks) {
+ $commands[] = 'docker network prune -f';
+ }
foreach ($commands as $command) {
instant_remote_process([$command], $server, false);
}
}
-
- private function getCommands(): array
- {
- $settings = InstanceSettings::get();
- $helperImageVersion = data_get($settings, 'helper_version');
- $helperImage = config('coolify.helper_image');
- $helperImageWithVersion = config('coolify.helper_image').':'.$helperImageVersion;
-
- $commonCommands = [
- 'docker container prune -f --filter "label=coolify.managed=true"',
- 'docker image prune -af --filter "label!=coolify.managed=true"',
- 'docker builder prune -af',
- "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi",
- ];
-
- return $commonCommands;
- }
}
diff --git a/app/Actions/Server/ConfigureCloudflared.php b/app/Actions/Server/ConfigureCloudflared.php
index 3946afe95..0d36e8863 100644
--- a/app/Actions/Server/ConfigureCloudflared.php
+++ b/app/Actions/Server/ConfigureCloudflared.php
@@ -2,6 +2,7 @@
namespace App\Actions\Server;
+use App\Events\CloudflareTunnelConfigured;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -40,12 +41,17 @@ public function handle(Server $server, string $cloudflare_token)
instant_remote_process($commands, $server);
} catch (\Throwable $e) {
ray($e);
+ $server->settings->is_cloudflare_tunnel = false;
+ $server->settings->save();
throw $e;
} finally {
+ CloudflareTunnelConfigured::dispatch($server->team_id);
+
$commands = collect([
'rm -fr /tmp/cloudflared',
]);
instant_remote_process($commands, $server);
+
}
}
}
diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php
index c4af6bb21..30664df26 100644
--- a/app/Actions/Server/UpdateCoolify.php
+++ b/app/Actions/Server/UpdateCoolify.php
@@ -2,7 +2,7 @@
namespace App\Actions\Server;
-use App\Models\InstanceSettings;
+use App\Jobs\PullHelperImageJob;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -19,7 +19,7 @@ class UpdateCoolify
public function handle($manual_update = false)
{
try {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$this->server = Server::find(0);
if (! $this->server) {
return;
@@ -55,6 +55,13 @@ private function update()
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/DeleteService.php b/app/Actions/Service/DeleteService.php
index 194cf4db9..f28e5490e 100644
--- a/app/Actions/Service/DeleteService.php
+++ b/app/Actions/Service/DeleteService.php
@@ -2,6 +2,7 @@
namespace App\Actions\Service;
+use App\Actions\Server\CleanupDocker;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,11 +10,11 @@ class DeleteService
{
use AsAction;
- public function handle(Service $service)
+ public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
{
try {
$server = data_get($service, 'server');
- if ($server->isFunctional()) {
+ if ($deleteVolumes && $server->isFunctional()) {
$storagesToDelete = collect([]);
$service->environment_variables()->delete();
@@ -33,13 +34,29 @@ public function handle(Service $service)
foreach ($storagesToDelete as $storage) {
$commands[] = "docker volume rm -f $storage->name";
}
- $commands[] = "docker rm -f $service->uuid";
- instant_remote_process($commands, $server, false);
+ // Execute volume deletion first, this must be done first otherwise volumes will not be deleted.
+ if (! empty($commands)) {
+ foreach ($commands as $command) {
+ $result = instant_remote_process([$command], $server, false);
+ if ($result !== 0) {
+ ray("Failed to execute: $command");
+ }
+ }
+ }
}
+
+ if ($deleteConnectedNetworks) {
+ $service->delete_connected_networks($service->uuid);
+ }
+
+ instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
} finally {
+ if ($deleteConfigurations) {
+ $service->delete_configurations();
+ }
foreach ($service->applications()->get() as $application) {
$application->forceDelete();
}
@@ -50,6 +67,11 @@ public function handle(Service $service)
$task->delete();
}
$service->tags()->detach();
+ $service->forceDelete();
+
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
}
}
}
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 @@ public function handle(Service $service)
$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 @@ public function handle(Service $service)
$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/Actions/Service/StopService.php b/app/Actions/Service/StopService.php
index 82b0b3ece..5c7bbc2aa 100644
--- a/app/Actions/Service/StopService.php
+++ b/app/Actions/Service/StopService.php
@@ -2,6 +2,7 @@
namespace App\Actions\Service;
+use App\Actions\Server\CleanupDocker;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,40 +10,27 @@ class StopService
{
use AsAction;
- public function handle(Service $service)
+ public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
try {
$server = $service->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
- ray('Stopping service: '.$service->name);
- $applications = $service->applications()->get();
- foreach ($applications as $application) {
- if ($applications->count() < 6) {
- instant_remote_process(command: ["docker stop --time=10 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
- }
- instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
- $application->update(['status' => 'exited']);
- }
- $dbs = $service->databases()->get();
- foreach ($dbs as $db) {
- if ($dbs->count() < 6) {
- instant_remote_process(command: ["docker stop --time=10 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
+ $containersToStop = $service->getContainersToStop();
+ $service->stopContainers($containersToStop, $server);
+
+ if (! $isDeleteOperation) {
+ $service->delete_connected_networks($service->uuid);
+ if ($dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
}
- instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
- instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
- $db->update(['status' => 'exited']);
}
- instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
- instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
} catch (\Exception $e) {
ray($e->getMessage());
return $e->getMessage();
}
-
}
}
diff --git a/app/Console/Commands/CleanupQueue.php b/app/Console/Commands/CleanupQueue.php
deleted file mode 100644
index fd2b637ac..000000000
--- a/app/Console/Commands/CleanupQueue.php
+++ /dev/null
@@ -1,24 +0,0 @@
-keys('*:laravel*');
- foreach ($keys as $key) {
- $keyWithoutPrefix = str_replace($prefix, '', $key);
- Redis::connection()->del($keyWithoutPrefix);
- }
- }
-}
diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php
new file mode 100644
index 000000000..ed0740d34
--- /dev/null
+++ b/app/Console/Commands/CleanupRedis.php
@@ -0,0 +1,31 @@
+keys('*:laravel*');
+ collect($keys)->each(function ($key) use ($prefix) {
+ $keyWithoutPrefix = str_replace($prefix, '', $key);
+ Redis::connection()->del($keyWithoutPrefix);
+ });
+
+ $queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*');
+ collect($queueOverlaps)->each(function ($key) {
+ Redis::connection()->del($key);
+ });
+
+ }
+}
diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php
index 68beb448a..dfd09d4b7 100644
--- a/app/Console/Commands/CleanupStuckedResources.php
+++ b/app/Console/Commands/CleanupStuckedResources.php
@@ -2,10 +2,12 @@
namespace App\Console\Commands;
+use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
+use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
@@ -35,6 +37,16 @@ public function handle()
private function cleanup_stucked_resources()
{
+ try {
+ $servers = Server::all()->filter(function ($server) {
+ return $server->isFunctional();
+ });
+ foreach ($servers as $server) {
+ CleanupHelperContainersJob::dispatch($server);
+ }
+ } catch (\Throwable $e) {
+ echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
+ }
try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) {
diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php
index 964b8e46e..20a2667c3 100644
--- a/app/Console/Commands/Dev.php
+++ b/app/Console/Commands/Dev.php
@@ -48,6 +48,13 @@ public function init()
echo "Generating APP_KEY.\n";
Artisan::call('key:generate');
}
+
+ // Generate STORAGE link if not exists
+ if (! file_exists(public_path('storage'))) {
+ echo "Generating STORAGE link.\n";
+ Artisan::call('storage:link');
+ }
+
// Seed database if it's empty
$settings = InstanceSettings::find(0);
if (! $settings) {
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index 7bfd1a14f..ad7bff86d 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -5,10 +5,8 @@
use App\Actions\Server\StopSentinel;
use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus;
-use App\Jobs\CleanupHelperContainersJob;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
-use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
@@ -18,7 +16,7 @@
class Init extends Command
{
- protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments} {--cleanup-proxy-networks}';
+ protected $signature = 'app:init {--force-cloud}';
protected $description = 'Cleanup instance related stuffs';
@@ -26,9 +24,63 @@ class Init extends Command
public function handle()
{
+ if (isCloud() && ! $this->option('force-cloud')) {
+ echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
+
+ return;
+ }
+
$this->servers = Server::all();
- $this->alive();
- get_public_ips();
+ if (isCloud()) {
+
+ } else {
+ $this->send_alive_signal();
+ get_public_ips();
+ }
+
+ // Backward compatibility
+ $this->disable_metrics();
+ $this->replace_slash_in_environment_name();
+ $this->restore_coolify_db_backup();
+ //
+ $this->update_traefik_labels();
+ if (! isCloud() || $this->option('force-cloud')) {
+ $this->cleanup_unused_network_from_coolify_proxy();
+ }
+ if (isCloud()) {
+ $this->cleanup_unnecessary_dynamic_proxy_configuration();
+ } else {
+ $this->cleanup_in_progress_application_deployments();
+ }
+ $this->call('cleanup:redis');
+ $this->call('cleanup:stucked-resources');
+
+ if (isCloud()) {
+ $response = Http::retry(3, 1000)->get(config('constants.services.official'));
+ if ($response->successful()) {
+ $services = $response->json();
+ File::put(base_path('templates/service-templates.json'), json_encode($services));
+ }
+ } else {
+ try {
+ $localhost = $this->servers->where('id', 0)->first();
+ $localhost->setupDynamicProxyConfiguration();
+ } catch (\Throwable $e) {
+ echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
+ }
+ $settings = instanceSettings();
+ if (! is_null(env('AUTOUPDATE', null))) {
+ if (env('AUTOUPDATE') == true) {
+ $settings->update(['is_auto_update_enabled' => true]);
+ } else {
+ $settings->update(['is_auto_update_enabled' => false]);
+ }
+ }
+ }
+ }
+
+ private function disable_metrics()
+ {
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
foreach ($this->servers as $server) {
if ($server->settings->is_metrics_enabled === true) {
@@ -39,62 +91,6 @@ public function handle()
}
}
}
-
- $full_cleanup = $this->option('full-cleanup');
- $cleanup_deployments = $this->option('cleanup-deployments');
- $cleanup_proxy_networks = $this->option('cleanup-proxy-networks');
- $this->replace_slash_in_environment_name();
- if ($cleanup_deployments) {
- echo "Running cleanup deployments.\n";
- $this->cleanup_in_progress_application_deployments();
-
- return;
- }
- if ($cleanup_proxy_networks) {
- echo "Running cleanup proxy networks.\n";
- $this->cleanup_unused_network_from_coolify_proxy();
-
- return;
- }
- if ($full_cleanup) {
- // Required for falsely deleted coolify db
- $this->restore_coolify_db_backup();
- $this->update_traefik_labels();
- $this->cleanup_unused_network_from_coolify_proxy();
- $this->cleanup_unnecessary_dynamic_proxy_configuration();
- $this->cleanup_in_progress_application_deployments();
- $this->cleanup_stucked_helper_containers();
- $this->call('cleanup:queue');
- $this->call('cleanup:stucked-resources');
- if (! isCloud()) {
- try {
- $localhost = $this->servers->where('id', 0)->first();
- $localhost->setupDynamicProxyConfiguration();
- } catch (\Throwable $e) {
- echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
- }
- }
-
- $settings = InstanceSettings::get();
- if (! is_null(env('AUTOUPDATE', null))) {
- if (env('AUTOUPDATE') == true) {
- $settings->update(['is_auto_update_enabled' => true]);
- } else {
- $settings->update(['is_auto_update_enabled' => false]);
- }
- }
- if (isCloud()) {
- $response = Http::retry(3, 1000)->get(config('constants.services.official'));
- if ($response->successful()) {
- $services = $response->json();
- File::put(base_path('templates/service-templates.json'), json_encode($services));
- }
- }
-
- return;
- }
- $this->cleanup_stucked_helper_containers();
- $this->call('cleanup:stucked-resources');
}
private function update_traefik_labels()
@@ -108,33 +104,28 @@ private function update_traefik_labels()
private function cleanup_unnecessary_dynamic_proxy_configuration()
{
- if (isCloud()) {
- foreach ($this->servers as $server) {
- try {
- if (! $server->isFunctional()) {
- continue;
- }
- if ($server->id === 0) {
- continue;
- }
- $file = $server->proxyPath().'/dynamic/coolify.yaml';
-
- return instant_remote_process([
- "rm -f $file",
- ], $server, false);
- } catch (\Throwable $e) {
- echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
+ foreach ($this->servers as $server) {
+ try {
+ if (! $server->isFunctional()) {
+ continue;
}
+ if ($server->id === 0) {
+ continue;
+ }
+ $file = $server->proxyPath().'/dynamic/coolify.yaml';
+ return instant_remote_process([
+ "rm -f $file",
+ ], $server, false);
+ } catch (\Throwable $e) {
+ echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
}
+
}
}
private function cleanup_unused_network_from_coolify_proxy()
{
- if (isCloud()) {
- return;
- }
foreach ($this->servers as $server) {
if (! $server->isFunctional()) {
continue;
@@ -175,43 +166,36 @@ private function cleanup_unused_network_from_coolify_proxy()
private function restore_coolify_db_backup()
{
- try {
- $database = StandalonePostgresql::withTrashed()->find(0);
- if ($database && $database->trashed()) {
- echo "Restoring coolify db backup\n";
- $database->restore();
- $scheduledBackup = ScheduledDatabaseBackup::find(0);
- if (! $scheduledBackup) {
- ScheduledDatabaseBackup::create([
- 'id' => 0,
- 'enabled' => true,
- 'save_s3' => false,
- 'frequency' => '0 0 * * *',
- 'database_id' => $database->id,
- 'database_type' => 'App\Models\StandalonePostgresql',
- 'team_id' => 0,
- ]);
+ if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
+ try {
+ $database = StandalonePostgresql::withTrashed()->find(0);
+ if ($database && $database->trashed()) {
+ echo "Restoring coolify db backup\n";
+ $database->restore();
+ $scheduledBackup = ScheduledDatabaseBackup::find(0);
+ if (! $scheduledBackup) {
+ ScheduledDatabaseBackup::create([
+ 'id' => 0,
+ 'enabled' => true,
+ 'save_s3' => false,
+ 'frequency' => '0 0 * * *',
+ 'database_id' => $database->id,
+ 'database_type' => 'App\Models\StandalonePostgresql',
+ 'team_id' => 0,
+ ]);
+ }
}
- }
- } catch (\Throwable $e) {
- echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
- }
- }
-
- private function cleanup_stucked_helper_containers()
- {
- foreach ($this->servers as $server) {
- if ($server->isFunctional()) {
- CleanupHelperContainersJob::dispatch($server);
+ } catch (\Throwable $e) {
+ echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
}
}
}
- private function alive()
+ private function send_alive_signal()
{
$id = config('app.id');
$version = config('version');
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
echo "Skipping alive as do_not_track is enabled\n";
@@ -225,23 +209,7 @@ private function alive()
echo "Error in alive: {$e->getMessage()}\n";
}
}
- // private function cleanup_ssh()
- // {
- // TODO: it will cleanup id.root@host.docker.internal
- // try {
- // $files = Storage::allFiles('ssh/keys');
- // foreach ($files as $file) {
- // Storage::delete($file);
- // }
- // $files = Storage::allFiles('ssh/mux');
- // foreach ($files as $file) {
- // Storage::delete($file);
- // }
- // } catch (\Throwable $e) {
- // echo "Error in cleaning ssh: {$e->getMessage()}\n";
- // }
- // }
private function cleanup_in_progress_application_deployments()
{
// Cleanup any failed deployments
@@ -263,11 +231,13 @@ private function cleanup_in_progress_application_deployments()
private function replace_slash_in_environment_name()
{
- $environments = Environment::all();
- foreach ($environments as $environment) {
- if (str_contains($environment->name, '/')) {
- $environment->name = str_replace('/', '-', $environment->name);
- $environment->save();
+ if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
+ $environments = Environment::all();
+ foreach ($environments as $environment) {
+ if (str_contains($environment->name, '/')) {
+ $environment->name = str_replace('/', '-', $environment->name);
+ $environment->save();
+ }
}
}
}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index b960a4a8b..1430fcdd1 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -12,8 +12,8 @@
use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob;
+use App\Jobs\ServerStorageCheckJob;
use App\Jobs\UpdateCoolifyJob;
-use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Server;
@@ -28,7 +28,7 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
$this->all_servers = Server::all();
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
@@ -43,6 +43,8 @@ protected function schedule(Schedule $schedule): void
$schedule->command('uploads:clear')->everyTwoMinutes();
$schedule->command('telescope:prune')->daily();
+
+ $schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
@@ -64,7 +66,7 @@ protected function schedule(Schedule $schedule): void
private function pull_images($schedule)
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
if ($server->isSentinelEnabled()) {
@@ -77,16 +79,16 @@ private function pull_images($schedule)
}
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
}
- $schedule->job(new PullHelperImageJob($server))
- ->cron($settings->update_check_frequency)
- ->timezone($settings->instance_timezone)
- ->onOneServer();
}
+ $schedule->job(new PullHelperImageJob)
+ ->cron($settings->update_check_frequency)
+ ->timezone($settings->instance_timezone)
+ ->onOneServer();
}
private function schedule_updates($schedule)
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$updateCheckFrequency = $settings->update_check_frequency;
$schedule->job(new CheckForUpdatesJob)
@@ -114,6 +116,7 @@ private function check_resources($schedule)
}
foreach ($servers as $server) {
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
+ // $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer();
$serverTimezone = $server->settings->server_timezone;
if ($server->settings->force_docker_cleanup) {
$schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
diff --git a/app/Enums/ContainerStatusTypes.php b/app/Enums/ContainerStatusTypes.php
new file mode 100644
index 000000000..ffcb6d5b5
--- /dev/null
+++ b/app/Enums/ContainerStatusTypes.php
@@ -0,0 +1,14 @@
+user()->currentTeam()->id ?? null;
+ }
+ if (is_null($teamId)) {
+ throw new \Exception('Team id is null');
+ }
+ $this->teamId = $teamId;
+ }
+
+ public function broadcastOn(): array
+ {
+ return [
+ new PrivateChannel("team.{$this->teamId}"),
+ ];
+ }
+}
diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
index 6b69350fe..63fbfc862 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -65,7 +65,7 @@ public function register(): void
if ($e instanceof RuntimeException) {
return;
}
- $this->settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
if ($this->settings->do_not_track) {
return;
}
diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php
new file mode 100644
index 000000000..b0a832605
--- /dev/null
+++ b/app/Helpers/SshMultiplexingHelper.php
@@ -0,0 +1,184 @@
+private_key_id);
+ $sshKeyLocation = $privateKey->getKeyLocation();
+ $muxFilename = '/var/www/html/storage/app/ssh/mux/mux_'.$server->uuid;
+
+ return [
+ 'sshKeyLocation' => $sshKeyLocation,
+ 'muxFilename' => $muxFilename,
+ ];
+ }
+
+ public static function ensureMultiplexedConnection(Server $server)
+ {
+ if (! self::isMultiplexingEnabled()) {
+ return;
+ }
+
+ $sshConfig = self::serverSshConfiguration($server);
+ $muxSocket = $sshConfig['muxFilename'];
+ $sshKeyLocation = $sshConfig['sshKeyLocation'];
+
+ self::validateSshKey($sshKeyLocation);
+
+ $checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
+ if (data_get($server, 'settings.is_cloudflare_tunnel')) {
+ $checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
+ }
+ $checkCommand .= "{$server->user}@{$server->ip}";
+ $process = Process::run($checkCommand);
+
+ if ($process->exitCode() !== 0) {
+ self::establishNewMultiplexedConnection($server);
+ }
+ }
+
+ public static function establishNewMultiplexedConnection(Server $server)
+ {
+ $sshConfig = self::serverSshConfiguration($server);
+ $sshKeyLocation = $sshConfig['sshKeyLocation'];
+ $muxSocket = $sshConfig['muxFilename'];
+
+ $connectionTimeout = config('constants.ssh.connection_timeout');
+ $serverInterval = config('constants.ssh.server_interval');
+ $muxPersistTime = config('constants.ssh.mux_persist_time');
+
+ $establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
+
+ if (data_get($server, 'settings.is_cloudflare_tunnel')) {
+ $establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
+ }
+
+ $establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
+ $establishCommand .= "{$server->user}@{$server->ip}";
+
+ $establishProcess = Process::run($establishCommand);
+
+ if ($establishProcess->exitCode() !== 0) {
+ throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
+ }
+ }
+
+ public static function removeMuxFile(Server $server)
+ {
+ $sshConfig = self::serverSshConfiguration($server);
+ $muxSocket = $sshConfig['muxFilename'];
+
+ $closeCommand = "ssh -O exit -o ControlPath=$muxSocket ";
+ if (data_get($server, 'settings.is_cloudflare_tunnel')) {
+ $closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
+ }
+ $closeCommand .= "{$server->user}@{$server->ip}";
+ Process::run($closeCommand);
+ }
+
+ public static function generateScpCommand(Server $server, string $source, string $dest)
+ {
+ $sshConfig = self::serverSshConfiguration($server);
+ $sshKeyLocation = $sshConfig['sshKeyLocation'];
+ $muxSocket = $sshConfig['muxFilename'];
+
+ $timeout = config('constants.ssh.command_timeout');
+ $muxPersistTime = config('constants.ssh.mux_persist_time');
+
+ $scp_command = "timeout $timeout scp ";
+
+ if (self::isMultiplexingEnabled()) {
+ $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
+ self::ensureMultiplexedConnection($server);
+ }
+
+ if (data_get($server, 'settings.is_cloudflare_tunnel')) {
+ $scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
+ }
+
+ $scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
+ $scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
+
+ return $scp_command;
+ }
+
+ public static function generateSshCommand(Server $server, string $command)
+ {
+ if ($server->settings->force_disabled) {
+ throw new \RuntimeException('Server is disabled.');
+ }
+
+ $sshConfig = self::serverSshConfiguration($server);
+ $sshKeyLocation = $sshConfig['sshKeyLocation'];
+ $muxSocket = $sshConfig['muxFilename'];
+
+ $timeout = config('constants.ssh.command_timeout');
+ $muxPersistTime = config('constants.ssh.mux_persist_time');
+
+ $ssh_command = "timeout $timeout ssh ";
+
+ if (self::isMultiplexingEnabled()) {
+ $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
+ self::ensureMultiplexedConnection($server);
+ }
+
+ if (data_get($server, 'settings.is_cloudflare_tunnel')) {
+ $ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
+ }
+
+ $ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
+
+ $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
+ $delimiter = Hash::make($command);
+ $command = str_replace($delimiter, '', $command);
+
+ $ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
+ .$command.PHP_EOL
+ .$delimiter;
+
+ return $ssh_command;
+ }
+
+ private static function isMultiplexingEnabled(): bool
+ {
+ return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
+ }
+
+ private static function validateSshKey(string $sshKeyLocation): void
+ {
+ $checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
+ $keyCheckProcess = Process::run($checkKeyCommand);
+
+ if ($keyCheckProcess->exitCode() !== 0) {
+ throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
+ }
+ }
+
+ private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
+ {
+ $options = "-i {$sshKeyLocation} "
+ .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
+ .'-o PasswordAuthentication=no '
+ ."-o ConnectTimeout=$connectionTimeout "
+ ."-o ServerAliveInterval=$serverInterval "
+ .'-o RequestTTY=no '
+ .'-o LogLevel=ERROR ';
+
+ // Bruh
+ if ($isScp) {
+ $options .= "-P {$server->port} ";
+ } else {
+ $options .= "-p {$server->port} ";
+ }
+
+ return $options;
+ }
+}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index 81b173011..85d8c0c85 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -177,6 +177,7 @@ public function applications(Request $request)
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -279,6 +280,7 @@ public function create_public_application(Request $request)
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -381,6 +383,7 @@ public function create_private_gh_app_application(Request $request)
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -468,6 +471,7 @@ public function create_private_deploy_key_application(Request $request)
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'],
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -552,6 +556,7 @@ public function create_dockerfile_application(Request $request)
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'],
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -602,6 +607,7 @@ public function create_dockerimage_application(Request $request)
'name' => ['type' => 'string', 'description' => 'The application name.'],
'description' => ['type' => 'string', 'description' => 'The application description.'],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -627,7 +633,7 @@ public function create_dockercompose_application(Request $request)
private function create_application(Request $request, $type)
{
- $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths'];
+ $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -665,6 +671,7 @@ private function create_application(Request $request, $type)
$fqdn = $request->domains;
$instantDeploy = $request->instant_deploy;
$githubAppUuid = $request->github_app_uuid;
+ $useBuildServer = $request->use_build_server;
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
@@ -737,6 +744,8 @@ private function create_application(Request $request, $type)
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
@@ -833,6 +842,8 @@ private function create_application(Request $request, $type)
$application->environment_id = $environment->id;
$application->source_type = $githubApp->getMorphClass();
$application->source_id = $githubApp->id;
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
@@ -925,6 +936,8 @@ private function create_application(Request $request, $type)
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
@@ -1004,6 +1017,8 @@ private function create_application(Request $request, $type)
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
@@ -1062,6 +1077,8 @@ private function create_application(Request $request, $type)
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
@@ -1259,16 +1276,10 @@ public function application_by_uuid(Request $request)
format: 'uuid',
)
),
- new OA\Parameter(
- name: 'cleanup',
- in: 'query',
- description: 'Delete configurations and volumes.',
- required: false,
- schema: new OA\Schema(
- type: 'boolean',
- default: true,
- )
- ),
+ new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
],
responses: [
new OA\Response(
@@ -1316,10 +1327,14 @@ public function delete_by_uuid(Request $request)
'message' => 'Application not found',
], 404);
}
+
DeleteResourceJob::dispatch(
resource: $application,
- deleteConfigurations: $cleanup,
- deleteVolumes: $cleanup);
+ deleteConfigurations: $request->query->get('delete_configurations', true),
+ deleteVolumes: $request->query->get('delete_volumes', true),
+ dockerCleanup: $request->query->get('docker_cleanup', true),
+ deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
+ );
return response()->json([
'message' => 'Application deletion request queued.',
@@ -1404,6 +1419,7 @@ public function delete_by_uuid(Request $request)
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -1460,7 +1476,7 @@ public function update_by_uuid(Request $request)
], 404);
}
$server = $application->destination->server;
- $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy'];
+ $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server'];
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
@@ -1538,6 +1554,10 @@ public function update_by_uuid(Request $request)
}
$instantDeploy = $request->instant_deploy;
+ $use_build_server = $request->use_build_server;
+ $application->settings->is_build_server_enabled = $use_build_server;
+ $application->settings->save();
+
removeUnnecessaryFieldsFromRequest($request);
$data = $request->all();
@@ -2529,6 +2549,131 @@ public function action_restart(Request $request)
}
+ #[OA\Post(
+ summary: 'Execute Command',
+ description: "Execute a command on the application's current container.",
+ path: '/applications/{uuid}/execute',
+ operationId: 'execute-command-application',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Applications'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the application.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ requestBody: new OA\RequestBody(
+ required: true,
+ description: 'Command to execute.',
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'command' => ['type' => 'string', 'description' => 'Command to execute.'],
+ ],
+ ),
+ ),
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: "Execute a command on the application's current container.",
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Command executed.'],
+ 'response' => ['type' => 'string'],
+ ]
+ )
+ ),
+ ]
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ new OA\Response(
+ response: 404,
+ ref: '#/components/responses/404',
+ ),
+ ]
+ )]
+ public function execute_command_by_uuid(Request $request)
+ {
+ // TODO: Need to review this from security perspective, to not allow arbitrary command execution
+ $allowedFields = ['command'];
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
+ if (! $application) {
+ return response()->json(['message' => 'Application not found.'], 404);
+ }
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+ $validator = customApiValidator($request->all(), [
+ 'command' => 'string|required',
+ ]);
+
+ $extraFields = array_diff(array_keys($request->all()), $allowedFields);
+ if ($validator->fails() || ! empty($extraFields)) {
+ $errors = $validator->errors();
+ if (! empty($extraFields)) {
+ foreach ($extraFields as $field) {
+ $errors->add($field, 'This field is not allowed.');
+ }
+ }
+
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+
+ $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
+ $status = getContainerStatus($application->destination->server, $container['Names']);
+
+ if ($status !== 'running') {
+ return response()->json([
+ 'message' => 'Application is not running.',
+ ], 400);
+ }
+
+ $commands = collect([
+ executeInDocker($container['Names'], $request->command),
+ ]);
+
+ $res = instant_remote_process(command: $commands, server: $application->destination->server);
+
+ return response()->json([
+ 'message' => 'Command executed.',
+ 'response' => $res,
+ ]);
+ }
+
private function validateDataApplications(Request $request, Server $server)
{
$teamId = getTeamIdFromToken();
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index a205704cc..65873f818 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -1541,16 +1541,10 @@ public function create_database(Request $request, NewDatabaseTypes $type)
format: 'uuid',
)
),
- new OA\Parameter(
- name: 'cleanup',
- in: 'query',
- description: 'Delete configurations and volumes.',
- required: false,
- schema: new OA\Schema(
- type: 'boolean',
- default: true,
- )
- ),
+ new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
],
responses: [
new OA\Response(
@@ -1595,10 +1589,14 @@ public function delete_by_uuid(Request $request)
if (! $database) {
return response()->json(['message' => 'Database not found.'], 404);
}
+
DeleteResourceJob::dispatch(
resource: $database,
- deleteConfigurations: $cleanup,
- deleteVolumes: $cleanup);
+ deleteConfigurations: $request->query->get('delete_configurations', true),
+ deleteVolumes: $request->query->get('delete_volumes', true),
+ dockerCleanup: $request->query->get('docker_cleanup', true),
+ deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
+ );
return response()->json([
'message' => 'Database deletion request queued.',
diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php
index c085b88a5..2414b7a42 100644
--- a/app/Http/Controllers/Api/OtherController.php
+++ b/app/Http/Controllers/Api/OtherController.php
@@ -86,7 +86,7 @@ public function enable_api(Request $request)
if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to enable the API.'], 403);
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$settings->update(['is_api_enabled' => true]);
return response()->json(['message' => 'API enabled.'], 200);
@@ -138,7 +138,7 @@ public function disable_api(Request $request)
if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to disable the API.'], 403);
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$settings->update(['is_api_enabled' => false]);
return response()->json(['message' => 'API disabled.'], 200);
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index 5f0d6bb12..a49515579 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -308,7 +308,7 @@ public function domains_by_server(Request $request)
$projects = Project::where('team_id', $teamId)->get();
$domains = collect();
$applications = $projects->pluck('applications')->flatten();
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($applications->count() > 0) {
foreach ($applications as $application) {
$ip = $application->destination->server->ip;
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 0a6154410..89418517b 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -432,6 +432,10 @@ public function service_by_uuid(Request $request)
tags: ['Services'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
],
responses: [
new OA\Response(
@@ -476,7 +480,14 @@ public function delete_by_uuid(Request $request)
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
- DeleteResourceJob::dispatch($service);
+
+ DeleteResourceJob::dispatch(
+ resource: $service,
+ deleteConfigurations: $request->query->get('delete_configurations', true),
+ deleteVolumes: $request->query->get('delete_volumes', true),
+ dockerCleanup: $request->query->get('docker_cleanup', true),
+ deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
+ );
return response()->json([
'message' => 'Service deletion request queued.',
@@ -516,7 +527,8 @@ public function delete_by_uuid(Request $request)
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -619,7 +631,8 @@ public function envs(Request $request)
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -738,7 +751,8 @@ public function update_env_by_uuid(Request $request)
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -853,7 +867,8 @@ public function create_bulk_envs(Request $request)
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -953,7 +968,8 @@ public function create_env(Request $request)
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1025,9 +1041,11 @@ public function delete_env_by_uuid(Request $request)
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service starting request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1101,9 +1119,11 @@ public function action_deploy(Request $request)
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1177,9 +1197,11 @@ public function action_stop(Request $request)
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php
index 9569e8cfa..630d01045 100644
--- a/app/Http/Controllers/OauthController.php
+++ b/app/Http/Controllers/OauthController.php
@@ -2,7 +2,6 @@
namespace App\Http\Controllers;
-use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -22,7 +21,7 @@ public function callback(string $provider)
$oauthUser = get_socialite_provider($provider)->user();
$user = User::whereEmail($oauthUser->email)->first();
if (! $user) {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->is_registration_enabled) {
abort(403, 'Registration is disabled');
}
diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php
index 648720ba4..471e6d602 100644
--- a/app/Http/Middleware/ApiAllowed.php
+++ b/app/Http/Middleware/ApiAllowed.php
@@ -14,7 +14,7 @@ public function handle(Request $request, Closure $next): Response
if (isCloud()) {
return $next($request);
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->is_api_enabled === false) {
return response()->json(['success' => true, 'message' => 'API is disabled.'], 403);
}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 718cea639..9ae383a9f 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -12,7 +12,6 @@
use App\Models\EnvironmentVariable;
use App\Models\GithubApp;
use App\Models\GitlabApp;
-use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
@@ -27,6 +26,7 @@
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
use RuntimeException;
@@ -210,7 +210,6 @@ public function __construct(int $application_deployment_queue_id)
}
ray('New container name: ', $this->container_name)->green();
- savePrivateKeyToFs($this->server);
$this->saved_outputs = collect();
// Set preview fqdn
@@ -514,7 +513,7 @@ private function deploy_docker_compose_buildpack()
'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 +918,10 @@ private function save_environment_variables()
}
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}\"");
}
}
@@ -962,7 +961,7 @@ private function save_environment_variables()
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
- if ($this->application->compose_parsing_version === '3') {
+ if ((int) $this->application->compose_parsing_version >= 3) {
$envs->push("COOLIFY_URL={$this->application->fqdn}");
} else {
$envs->push("COOLIFY_FQDN={$this->application->fqdn}");
@@ -970,7 +969,7 @@ private function save_environment_variables()
}
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
- if ($this->application->compose_parsing_version === '3') {
+ if ((int) $this->application->compose_parsing_version >= 3) {
$envs->push("COOLIFY_FQDN={$url}");
} else {
$envs->push("COOLIFY_URL={$url}");
@@ -978,10 +977,10 @@ private function save_environment_variables()
}
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}\"");
}
}
@@ -1334,7 +1333,7 @@ private function create_workdir()
private function prepare_builder_image()
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$helperImage = config('coolify.helper_image');
$helperImage = "{$helperImage}:{$settings->helper_version}";
// Get user home directory
@@ -1456,10 +1455,10 @@ private function check_git_if_build_needed()
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
],
[
- executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
+ executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
'hidden' => true,
'save' => 'git_commit_sha',
- ],
+ ]
);
} else {
$this->execute_remote_command(
@@ -2049,6 +2048,10 @@ private function build_image()
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 +2071,10 @@ private function build_image()
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 +2117,10 @@ private function build_image()
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 +2140,10 @@ private function build_image()
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 +2172,10 @@ private function build_image()
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 +2195,10 @@ private function build_image()
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,
@@ -2187,20 +2210,40 @@ private function build_image()
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
}
- /**
- * @param int $timeout in seconds
- */
- private function graceful_shutdown_container(string $containerName, int $timeout = 30)
+ private function graceful_shutdown_container(string $containerName, int $timeout = 300)
{
try {
- $this->execute_remote_command(
- ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
- ["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
- );
+ $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+
+ $startTime = time();
+ while ($process->running()) {
+ if (time() - $startTime >= $timeout) {
+ $this->execute_remote_command(
+ ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
+ );
+ break;
+ }
+ usleep(100000);
+ }
+
+ $isRunning = $this->execute_remote_command(
+ ["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
+ ) === 'true';
+
+ if ($isRunning) {
+ $this->execute_remote_command(
+ ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
+ );
+ }
} catch (\Exception $error) {
- // report error if needed
+ $this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
}
+ $this->remove_container($containerName);
+ }
+
+ private function remove_container(string $containerName)
+ {
$this->execute_remote_command(
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);
diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php
index ddc264839..f2348118a 100644
--- a/app/Jobs/CheckForUpdatesJob.php
+++ b/app/Jobs/CheckForUpdatesJob.php
@@ -2,15 +2,14 @@
namespace App\Jobs;
-use App\Models\InstanceSettings;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
-use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\File;
+use Illuminate\Support\Facades\Http;
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -22,7 +21,7 @@ public function handle(): void
if (isDev() || isCloud()) {
return;
}
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php
index 7b064a464..b8ca8b7ed 100644
--- a/app/Jobs/CleanupHelperContainersJob.php
+++ b/app/Jobs/CleanupHelperContainersJob.php
@@ -21,11 +21,10 @@ public function handle(): void
{
try {
ray('Cleaning up helper containers on '.$this->server->name);
- $containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false);
- $containers = format_docker_command_output_to_json($containers);
- if ($containers->count() > 0) {
- foreach ($containers as $container) {
- $containerId = data_get($container, 'ID');
+ $containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
+ $containerIds = collect(json_decode($containers))->pluck('ID');
+ if ($containerIds->count() > 0) {
+ foreach ($containerIds as $containerId) {
ray('Removing container '.$containerId);
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
}
diff --git a/app/Jobs/CleanupStaleMultiplexedConnections.php b/app/Jobs/CleanupStaleMultiplexedConnections.php
index bcca77c18..6d49bee4b 100644
--- a/app/Jobs/CleanupStaleMultiplexedConnections.php
+++ b/app/Jobs/CleanupStaleMultiplexedConnections.php
@@ -3,12 +3,14 @@
namespace App\Jobs;
use App\Models\Server;
+use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Process;
+use Illuminate\Support\Facades\Storage;
class CleanupStaleMultiplexedConnections implements ShouldQueue
{
@@ -16,22 +18,65 @@ class CleanupStaleMultiplexedConnections implements ShouldQueue
public function handle()
{
- Server::chunk(100, function ($servers) {
- foreach ($servers as $server) {
- $this->cleanupStaleConnection($server);
- }
- });
+ $this->cleanupStaleConnections();
+ $this->cleanupNonExistentServerConnections();
}
- private function cleanupStaleConnection(Server $server)
+ private function cleanupStaleConnections()
{
- $muxSocket = "/tmp/mux_{$server->id}";
- $checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
- $checkProcess = Process::run($checkCommand);
+ $muxFiles = Storage::disk('ssh-mux')->files();
- if ($checkProcess->exitCode() !== 0) {
- $closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
- Process::run($closeCommand);
+ foreach ($muxFiles as $muxFile) {
+ $serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
+ $server = Server::where('uuid', $serverUuid)->first();
+
+ if (! $server) {
+ $this->removeMultiplexFile($muxFile);
+
+ continue;
+ }
+
+ $muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
+ $checkCommand = "ssh -O check -o ControlPath={$muxSocket} {$server->user}@{$server->ip} 2>/dev/null";
+ $checkProcess = Process::run($checkCommand);
+
+ if ($checkProcess->exitCode() !== 0) {
+ $this->removeMultiplexFile($muxFile);
+ } else {
+ $muxContent = Storage::disk('ssh-mux')->get($muxFile);
+ $establishedAt = Carbon::parse(substr($muxContent, 37));
+ $expirationTime = $establishedAt->addSeconds(config('constants.ssh.mux_persist_time'));
+
+ if (Carbon::now()->isAfter($expirationTime)) {
+ $this->removeMultiplexFile($muxFile);
+ }
+ }
}
}
+
+ private function cleanupNonExistentServerConnections()
+ {
+ $muxFiles = Storage::disk('ssh-mux')->files();
+ $existingServerUuids = Server::pluck('uuid')->toArray();
+
+ foreach ($muxFiles as $muxFile) {
+ $serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
+ if (! in_array($serverUuid, $existingServerUuids)) {
+ $this->removeMultiplexFile($muxFile);
+ }
+ }
+ }
+
+ private function extractServerUuidFromMuxFile($muxFile)
+ {
+ return substr($muxFile, 4);
+ }
+
+ private function removeMultiplexFile($muxFile)
+ {
+ $muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
+ $closeCommand = "ssh -O exit -o ControlPath={$muxSocket} localhost 2>/dev/null";
+ Process::run($closeCommand);
+ Storage::disk('ssh-mux')->delete($muxFile);
+ }
}
diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php
index e919855d5..22ae06ebd 100644
--- a/app/Jobs/ContainerStatusJob.php
+++ b/app/Jobs/ContainerStatusJob.php
@@ -9,7 +9,6 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -25,16 +24,6 @@ public function backoff(): int
public function __construct(public Server $server) {}
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->server->uuid))];
- }
-
- public function uniqueId(): int
- {
- return $this->server->uuid;
- }
-
public function handle()
{
GetContainersStatus::run($this->server);
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index e6fa05b55..7109fda3b 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -22,7 +22,6 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
@@ -79,22 +78,11 @@ public function __construct($backup)
}
}
- public function middleware(): array
- {
- return [new WithoutOverlapping($this->backup->id)];
- }
-
- public function uniqueId(): int
- {
- return $this->backup->id;
- }
-
public function handle(): void
{
try {
// Check if team is exists
if (is_null($this->team)) {
- $this->backup->update(['status' => 'failed']);
StopDatabase::run($this->database);
$this->database->delete();
@@ -478,10 +466,37 @@ private function remove_old_backups(): void
}
}
+ // 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 +506,64 @@ private function upload_to_s3(): void
$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();
+ $helperImage = config('coolify.helper_image');
+ $latestVersion = $settings->helper_version;
+
+ return "{$helperImage}:{$latestVersion}";
+ }
}
diff --git a/app/Jobs/DatabaseBackupStatusJob.php b/app/Jobs/DatabaseBackupStatusJob.php
deleted file mode 100644
index d3b0e99cf..000000000
--- a/app/Jobs/DatabaseBackupStatusJob.php
+++ /dev/null
@@ -1,62 +0,0 @@
-scheduledDatabaseBackups()->get();
- // if ($scheduled_backups->isEmpty()) {
- // continue;
- // }
- // foreach ($scheduled_backups as $scheduled_backup) {
- // $last_days_backups = $scheduled_backup->get_last_days_backup_status();
- // if ($last_days_backups->isEmpty()) {
- // continue;
- // }
- // $failed = $last_days_backups->where('status', 'failed');
- // }
- // }
-
- // $scheduled_backups = ScheduledDatabaseBackup::all();
- // $databases = collect();
- // $teams = collect();
- // foreach ($scheduled_backups as $scheduled_backup) {
- // $last_days_backups = $scheduled_backup->get_last_days_backup_status();
- // if ($last_days_backups->isEmpty()) {
- // continue;
- // }
- // $failed = $last_days_backups->where('status', 'failed');
- // $database = $scheduled_backup->database;
- // $team = $database->team();
- // $teams->put($team->id, $team);
- // $databases->put("{$team->id}:{$database->name}", [
- // 'failed_count' => $failed->count(),
- // ]);
- // }
- // foreach ($databases as $name => $database) {
- // [$team_id, $name] = explode(':', $name);
- // $team = $teams->get($team_id);
- // $team?->notify(new DailyBackup($databases));
- // }
- }
-}
diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php
index dbf44dd5d..2442d5b06 100644
--- a/app/Jobs/DeleteResourceJob.php
+++ b/app/Jobs/DeleteResourceJob.php
@@ -4,6 +4,7 @@
use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase;
+use App\Actions\Server\CleanupDocker;
use App\Actions\Service\DeleteService;
use App\Actions\Service\StopService;
use App\Models\Application;
@@ -30,8 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
- public bool $deleteConfigurations = false,
- public bool $deleteVolumes = false) {}
+ public bool $deleteConfigurations = true,
+ public bool $deleteVolumes = true,
+ public bool $dockerCleanup = true,
+ public bool $deleteConnectedNetworks = true
+ ) {}
public function handle()
{
@@ -51,11 +55,11 @@ public function handle()
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get();
- StopDatabase::run($this->resource);
+ StopDatabase::run($this->resource, true);
break;
case 'service':
- StopService::run($this->resource);
- DeleteService::run($this->resource);
+ StopService::run($this->resource, true);
+ DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
break;
}
@@ -65,12 +69,31 @@ public function handle()
if ($this->deleteConfigurations) {
$this->resource?->delete_configurations();
}
+
+ $isDatabase = $this->resource instanceof StandalonePostgresql
+ || $this->resource instanceof StandaloneRedis
+ || $this->resource instanceof StandaloneMongodb
+ || $this->resource instanceof StandaloneMysql
+ || $this->resource instanceof StandaloneMariadb
+ || $this->resource instanceof StandaloneKeydb
+ || $this->resource instanceof StandaloneDragonfly
+ || $this->resource instanceof StandaloneClickhouse;
+ $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
+ if (($this->dockerCleanup || $isDatabase) && $server) {
+ CleanupDocker::dispatch($server, true);
+ }
+
+ if ($this->deleteConnectedNetworks && ! $isDatabase) {
+ $this->resource?->delete_connected_networks($this->resource->uuid);
+ }
} catch (\Throwable $e) {
- ray($e->getMessage());
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();
+ if ($this->dockerCleanup) {
+ CleanupDocker::dispatch($server, true);
+ }
Artisan::queue('cleanup:stucked-resources');
}
}
diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php
index f95cd2920..900bae99c 100644
--- a/app/Jobs/DockerCleanupJob.php
+++ b/app/Jobs/DockerCleanupJob.php
@@ -10,7 +10,6 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
@@ -24,17 +23,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $usageBefore = null;
- public function __construct(public Server $server) {}
-
- public function middleware(): array
- {
- return [new WithoutOverlapping($this->server->id)];
- }
-
- public function uniqueId(): int
- {
- return $this->server->id;
- }
+ public function __construct(public Server $server, public bool $manualCleanup = false) {}
public function handle(): void
{
@@ -42,8 +31,9 @@ public function handle(): void
if (! $this->server->isFunctional()) {
return;
}
- if ($this->server->settings->force_docker_cleanup) {
- Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
+
+ if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
+ Log::info('DockerCleanupJob '.($this->manualCleanup ? 'manual' : 'force').' cleanup on '.$this->server->name);
CleanupDocker::run(server: $this->server);
return;
diff --git a/app/Jobs/GithubAppPermissionJob.php b/app/Jobs/GithubAppPermissionJob.php
index 3188d35d6..9c0a2b55b 100644
--- a/app/Jobs/GithubAppPermissionJob.php
+++ b/app/Jobs/GithubAppPermissionJob.php
@@ -8,7 +8,6 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
@@ -25,16 +24,6 @@ public function backoff(): int
public function __construct(public GithubApp $github_app) {}
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->github_app->uuid))];
- }
-
- public function uniqueId(): int
- {
- return $this->github_app->uuid;
- }
-
public function handle()
{
try {
diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php
index 420119069..4b208fc31 100644
--- a/app/Jobs/PullHelperImageJob.php
+++ b/app/Jobs/PullHelperImageJob.php
@@ -2,14 +2,12 @@
namespace App\Jobs;
-use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
@@ -19,17 +17,7 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->server->uuid))];
- }
-
- public function uniqueId(): string
- {
- return $this->server->uuid;
- }
-
- public function __construct(public Server $server) {}
+ public function __construct() {}
public function handle(): void
{
@@ -37,13 +25,13 @@ public function handle(): void
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$latest_version = data_get($versions, 'coolify.helper.version');
$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/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php
index f8c769382..32f84e6d5 100644
--- a/app/Jobs/PullSentinelImageJob.php
+++ b/app/Jobs/PullSentinelImageJob.php
@@ -9,7 +9,6 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
@@ -18,16 +17,6 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->server->uuid))];
- }
-
- public function uniqueId(): string
- {
- return $this->server->uuid;
- }
-
public function __construct(public Server $server) {}
public function handle(): void
diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php
index 93d5fca70..6850ae98a 100644
--- a/app/Jobs/ScheduledTaskJob.php
+++ b/app/Jobs/ScheduledTaskJob.php
@@ -13,7 +13,6 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ScheduledTaskJob implements ShouldQueue
@@ -56,24 +55,17 @@ private function getServerTimezone(): string
{
if ($this->resource instanceof Application) {
$timezone = $this->resource->destination->server->settings->server_timezone;
+
return $timezone;
} elseif ($this->resource instanceof Service) {
$timezone = $this->resource->server->settings->server_timezone;
+
return $timezone;
}
+
return 'UTC';
}
- public function middleware(): array
- {
- return [new WithoutOverlapping($this->task->id)];
- }
-
- public function uniqueId(): int
- {
- return $this->task->id;
- }
-
public function handle(): void
{
@@ -94,12 +86,12 @@ public function handle(): void
} elseif ($this->resource->type() == 'service') {
$this->resource->applications()->get()->each(function ($application) {
if (str(data_get($application, 'status'))->contains('running')) {
- $this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid');
+ $this->containers[] = data_get($application, 'name').'-'.data_get($this->resource, 'uuid');
}
});
$this->resource->databases()->get()->each(function ($database) {
if (str(data_get($database, 'status'))->contains('running')) {
- $this->containers[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid');
+ $this->containers[] = data_get($database, 'name').'-'.data_get($this->resource, 'uuid');
}
});
}
@@ -112,8 +104,8 @@ public function handle(): void
}
foreach ($this->containers as $containerName) {
- if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) {
- $cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'";
+ if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) {
+ $cmd = "sh -c '".str_replace("'", "'\''", $this->task->command)."'";
$exec = "docker exec {$containerName} {$cmd}";
$this->task_output = instant_remote_process([$exec], $this->server, true);
$this->task_log->update([
diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php
index 540085385..39d4aa0c0 100644
--- a/app/Jobs/ServerCheckJob.php
+++ b/app/Jobs/ServerCheckJob.php
@@ -16,7 +16,6 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
@@ -24,7 +23,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public $tries = 3;
+ public $tries = 1;
public $timeout = 60;
@@ -45,16 +44,6 @@ public function backoff(): int
public function __construct(public Server $server) {}
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->server->id))];
- }
-
- public function uniqueId(): int
- {
- return $this->server->id;
- }
-
public function handle()
{
try {
@@ -80,7 +69,9 @@ public function handle()
return 'No containers found.';
}
GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
- $this->checkLogDrainContainer();
+ if ($this->server->isLogDrainEnabled()) {
+ $this->checkLogDrainContainer();
+ }
}
} catch (\Throwable $e) {
@@ -93,7 +84,7 @@ public function handle()
private function serverStatus()
{
- ['uptime' => $uptime] = $this->server->validateConnection();
+ ['uptime' => $uptime] = $this->server->validateConnection(false);
if ($uptime) {
if ($this->server->unreachable_notification_sent === true) {
$this->server->update(['unreachable_notification_sent' => false]);
@@ -126,9 +117,6 @@ private function serverStatus()
private function checkLogDrainContainer()
{
- if (! $this->server->isLogDrainEnabled()) {
- return;
- }
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain';
})->first();
diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php
index 24292025b..1f09d5a3b 100644
--- a/app/Jobs/ServerLimitCheckJob.php
+++ b/app/Jobs/ServerLimitCheckJob.php
@@ -10,7 +10,6 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +25,6 @@ public function backoff(): int
public function __construct(public Team $team) {}
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->team->uuid))];
- }
-
- public function uniqueId(): int
- {
- return $this->team->uuid;
- }
-
public function handle()
{
try {
diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php
index ac9182eca..fcc33c859 100644
--- a/app/Jobs/ServerStatusJob.php
+++ b/app/Jobs/ServerStatusJob.php
@@ -8,7 +8,6 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +25,6 @@ public function backoff(): int
public function __construct(public Server $server) {}
- public function middleware(): array
- {
- return [(new WithoutOverlapping($this->server->uuid))];
- }
-
- public function uniqueId(): int
- {
- return $this->server->uuid;
- }
-
public function handle()
{
if (! $this->server->isServerReady($this->tries)) {
diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php
new file mode 100644
index 000000000..376cb8532
--- /dev/null
+++ b/app/Jobs/ServerStorageCheckJob.php
@@ -0,0 +1,59 @@
+server->isFunctional()) {
+ ray('Server is not ready.');
+
+ return 'Server is not ready.';
+ }
+ $team = $this->server->team;
+ $percentage = $this->server->storageCheck();
+ if ($percentage > 1) {
+ ray('Server storage is at '.$percentage.'%');
+ }
+
+ } catch (\Throwable $e) {
+ ray($e->getMessage());
+
+ return handleError($e);
+ }
+
+ }
+}
diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php
index 4c65a711f..2cc705e4a 100644
--- a/app/Jobs/UpdateCoolifyJob.php
+++ b/app/Jobs/UpdateCoolifyJob.php
@@ -3,7 +3,6 @@
namespace App\Jobs;
use App\Actions\Server\UpdateCoolify;
-use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -23,7 +22,7 @@ public function handle(): void
{
try {
CheckForUpdatesJob::dispatchSync();
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->new_version_available) {
Log::info('No new version available. Skipping update.');
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index af05ad767..52d4674ee 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -141,7 +141,7 @@ public function setServerType(string $type)
if (! $this->createdServer) {
return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
}
- $this->serverPublicKey = $this->createdServer->privateKey->publicKey();
+ $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') {
@@ -175,7 +175,7 @@ public function selectExistingServer()
return;
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
- $this->serverPublicKey = $this->createdServer->privateKey->publicKey();
+ $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
$this->updateServerDetails();
$this->currentState = 'validate-server';
}
@@ -231,17 +231,24 @@ public function setPrivateKey(string $type)
public function savePrivateKey()
{
$this->validate([
- 'privateKeyName' => 'required',
- 'privateKey' => 'required',
+ 'privateKeyName' => 'required|string|max:255',
+ 'privateKeyDescription' => 'nullable|string|max:255',
+ 'privateKey' => 'required|string',
]);
- $this->createdPrivateKey = PrivateKey::create([
- 'name' => $this->privateKeyName,
- 'description' => $this->privateKeyDescription,
- 'private_key' => $this->privateKey,
- 'team_id' => currentTeam()->id,
- ]);
- $this->createdPrivateKey->save();
- $this->currentState = 'create-server';
+
+ try {
+ $privateKey = PrivateKey::createAndStore([
+ 'name' => $this->privateKeyName,
+ 'description' => $this->privateKeyDescription,
+ 'private_key' => $this->privateKey,
+ 'team_id' => currentTeam()->id,
+ ]);
+
+ $this->createdPrivateKey = $privateKey;
+ $this->currentState = 'create-server';
+ } catch (\Exception $e) {
+ $this->addError('privateKey', 'Failed to save private key: '.$e->getMessage());
+ }
}
public function saveServer()
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/Dashboard.php b/app/Livewire/Dashboard.php
index 68555d26c..1f0b68dd3 100644
--- a/app/Livewire/Dashboard.php
+++ b/app/Livewire/Dashboard.php
@@ -30,7 +30,6 @@ public function mount()
public function cleanup_queue()
{
- $this->dispatch('success', 'Cleanup started.');
Artisan::queue('cleanup:application-deployment-queue', [
'--team-id' => currentTeam()->id,
]);
diff --git a/app/Livewire/Destination/Form.php b/app/Livewire/Destination/Form.php
index 7125f2120..87ae83931 100644
--- a/app/Livewire/Destination/Form.php
+++ b/app/Livewire/Destination/Form.php
@@ -38,7 +38,7 @@ public function delete()
}
$this->destination->delete();
- return redirect()->route('dashboard');
+ return redirect()->route('destination.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php
index d6dc0d521..68691c1cd 100644
--- a/app/Livewire/Help.php
+++ b/app/Livewire/Help.php
@@ -47,7 +47,7 @@ public function submit()
]
);
$mail->subject("[HELP]: {$this->subject}");
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$type = set_transanctional_email_settings($settings);
if (! $type) {
$url = 'https://app.coolify.io/api/feedback';
diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php
index ec196c154..988add7c8 100644
--- a/app/Livewire/NavbarDeleteTeam.php
+++ b/app/Livewire/NavbarDeleteTeam.php
@@ -2,13 +2,28 @@
namespace App\Livewire;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class NavbarDeleteTeam extends Component
{
- public function delete()
+ public $team;
+
+ public function mount()
{
+ $this->team = currentTeam()->name;
+ }
+
+ public function delete($password)
+ {
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
$currentTeam = currentTeam();
$currentTeam->delete();
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index 2960ed226..53673292e 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -172,7 +172,7 @@ public function submitResend()
public function copyFromInstanceSettings()
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->smtp_enabled) {
$team = currentTeam();
$team->update([
diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php
index f2968f6d9..3de895f8c 100644
--- a/app/Livewire/Project/Application/Deployment/Show.php
+++ b/app/Livewire/Project/Application/Deployment/Show.php
@@ -4,7 +4,6 @@
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
-use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index c02949e17..1082b48cd 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -21,6 +21,8 @@ class Heading extends Component
protected string $deploymentUuid;
+ public bool $docker_cleanup = true;
+
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@@ -102,7 +104,7 @@ protected function setDeploymentUuid()
public function stop()
{
- StopApplication::run($this->application);
+ StopApplication::run($this->application, false, $this->docker_cleanup);
$this->application->status = 'exited';
$this->application->save();
if ($this->application->additional_servers->count() > 0) {
@@ -135,4 +137,13 @@ public function restart()
'environment_name' => $this->parameters['environment_name'],
]);
}
+
+ public function render()
+ {
+ return view('livewire.project.application.heading', [
+ 'checkboxes' => [
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php
index 317a2ae51..b1ba035dc 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -5,7 +5,9 @@
use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
+use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Process;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
@@ -184,17 +186,20 @@ protected function setDeploymentUuid()
public function stop(int $pull_request_id)
{
try {
+ $server = $this->application->destination->server;
+ $timeout = 300;
+
if ($this->application->destination->server->isSwarm()) {
- instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
+ instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
- $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
- foreach ($containers as $container) {
- $name = str_replace('/', '', $container['Names']);
- instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
- }
+ $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
+ $this->stopContainers($containers, $server, $timeout);
}
- GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high');
- $this->dispatch('reloadWindow');
+
+ GetContainersStatus::run($server);
+ $this->application->refresh();
+ $this->dispatch('containerStatusUpdated');
+ $this->dispatch('success', 'Preview Deployment stopped.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -203,16 +208,21 @@ public function stop(int $pull_request_id)
public function delete(int $pull_request_id)
{
try {
+ $server = $this->application->destination->server;
+ $timeout = 300;
+
if ($this->application->destination->server->isSwarm()) {
- instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
+ instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
- $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
- foreach ($containers as $container) {
- $name = str_replace('/', '', $container['Names']);
- instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
- }
+ $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
+ $this->stopContainers($containers, $server, $timeout);
}
- ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
+
+ ApplicationPreview::where('application_id', $this->application->id)
+ ->where('pull_request_id', $pull_request_id)
+ ->first()
+ ->delete();
+
$this->application->refresh();
$this->dispatch('update_links');
$this->dispatch('success', 'Preview deleted.');
@@ -220,4 +230,49 @@ public function delete(int $pull_request_id)
return handleError($e, $this);
}
}
+
+ private function stopContainers(array $containers, $server, int $timeout)
+ {
+ $processes = [];
+ foreach ($containers as $container) {
+ $containerName = str_replace('/', '', $container['Names']);
+ $processes[$containerName] = $this->stopContainer($containerName, $timeout);
+ }
+
+ $startTime = time();
+ while (count($processes) > 0) {
+ $finishedProcesses = array_filter($processes, function ($process) {
+ return ! $process->running();
+ });
+ foreach (array_keys($finishedProcesses) as $containerName) {
+ unset($processes[$containerName]);
+ $this->removeContainer($containerName, $server);
+ }
+
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopRemainingContainers(array_keys($processes), $server);
+ break;
+ }
+
+ usleep(100000);
+ }
+ }
+
+ private function stopContainer(string $containerName, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ private function removeContainer(string $containerName, $server)
+ {
+ instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
+ }
+
+ private function forceStopRemainingContainers(array $containerNames, $server)
+ {
+ foreach ($containerNames as $containerName) {
+ instant_remote_process(["docker kill $containerName"], $server, throwError: false);
+ $this->removeContainer($containerName, $server);
+ }
+ }
}
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index 59f2f9a39..98b2b4263 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Spatie\Url\Url;
@@ -12,6 +14,12 @@ class BackupEdit extends Component
public $s3s;
+ public bool $delete_associated_backups_locally = false;
+
+ public bool $delete_associated_backups_s3 = false;
+
+ public bool $delete_associated_backups_sftp = false;
+
public ?string $status = null;
public array $parameters;
@@ -46,10 +54,24 @@ public function mount()
}
}
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
try {
+ if ($this->delete_associated_backups_locally) {
+ $this->deleteAssociatedBackupsLocally();
+ }
+ if ($this->delete_associated_backups_s3) {
+ $this->deleteAssociatedBackupsS3();
+ }
+
$this->backup->delete();
+
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$previousUrl = url()->previous();
$url = Url::fromString($previousUrl);
@@ -104,4 +126,66 @@ public function submit()
$this->dispatch('error', $e->getMessage());
}
}
+
+ public function deleteAssociatedBackupsLocally()
+ {
+ $executions = $this->backup->executions;
+ $backupFolder = null;
+
+ foreach ($executions as $execution) {
+ if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
+ $server = $this->backup->database->service->destination->server;
+ } else {
+ $server = $this->backup->database->destination->server;
+ }
+
+ if (! $backupFolder) {
+ $backupFolder = dirname($execution->filename);
+ }
+
+ delete_backup_locally($execution->filename, $server);
+ $execution->delete();
+ }
+
+ if ($backupFolder) {
+ $this->deleteEmptyBackupFolder($backupFolder, $server);
+ }
+ }
+
+ public function deleteAssociatedBackupsS3()
+ {
+ //Add function to delete backups from S3
+ }
+
+ public function deleteAssociatedBackupsSftp()
+ {
+ //Add function to delete backups from SFTP
+ }
+
+ private function deleteEmptyBackupFolder($folderPath, $server)
+ {
+ $checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
+
+ if (trim($checkEmpty) === 'empty') {
+ instant_remote_process(["rmdir '$folderPath'"], $server);
+
+ $parentFolder = dirname($folderPath);
+ $checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
+
+ if (trim($checkParentEmpty) === 'empty') {
+ instant_remote_process(["rmdir '$parentFolder'"], $server);
+ }
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.project.database.backup-edit', [
+ 'checkboxes' => [
+ ['id' => 'delete_associated_backups_locally', 'label' => __('database.delete_backups_locally')],
+ // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
+ // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php
index 5d56ea53d..c8c33a022 100644
--- a/app/Livewire/Project/Database/BackupExecutions.php
+++ b/app/Livewire/Project/Database/BackupExecutions.php
@@ -3,18 +3,28 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+use Livewire\Attributes\On;
use Livewire\Component;
class BackupExecutions extends Component
{
public ?ScheduledDatabaseBackup $backup = null;
+
public $database;
+
public $executions = [];
+
public $setDeletableBackup;
+ public $delete_backup_s3 = true;
+
+ public $delete_backup_sftp = true;
+
public function getListeners()
{
- $userId = auth()->user()->id;
+ $userId = Auth::id();
return [
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
@@ -31,19 +41,36 @@ public function cleanupFailed()
}
}
- public function deleteBackup($exeuctionId)
+ #[On('deleteBackup')]
+ public function deleteBackup($executionId, $password)
{
- $execution = $this->backup->executions()->where('id', $exeuctionId)->first();
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
+ $execution = $this->backup->executions()->where('id', $executionId)->first();
if (is_null($execution)) {
$this->dispatch('error', 'Backup execution not found.');
return;
}
+
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
} else {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
}
+
+ if ($this->delete_backup_s3) {
+ // Add logic to delete from S3
+ }
+
+ if ($this->delete_backup_sftp) {
+ // Add logic to delete from SFTP
+ }
+
$execution->delete();
$this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions();
@@ -82,16 +109,18 @@ public function server()
return $server;
}
}
+
return null;
}
public function getServerTimezone()
{
$server = $this->server();
- if (!$server) {
+ if (! $server) {
return 'UTC';
}
$serverTimezone = $server->settings->server_timezone;
+
return $serverTimezone;
}
@@ -104,6 +133,17 @@ public function formatDateInServerTimezone($date)
} catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
+
return $dateObj->format('Y-m-d H:i:s T');
}
+
+ public function render()
+ {
+ return view('livewire.project.database.backup-executions', [
+ 'checkboxes' => [
+ ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
+ ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php
index a6e2a1320..7a6446815 100644
--- a/app/Livewire/Project/Database/Clickhouse/General.php
+++ b/app/Livewire/Project/Database/Clickhouse/General.php
@@ -56,7 +56,7 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (!$this->server->isLogDrainEnabled()) {
+ if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -73,14 +73,14 @@ public function instantSaveAdvanced()
public function instantSave()
{
try {
- if ($this->database->is_public && !$this->database->public_port) {
+ if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (!str($this->database->status)->startsWith('running')) {
+ if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -95,7 +95,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = !$this->database->is_public;
+ $this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index 00e0ff09f..394ba6c9a 100644
--- a/app/Livewire/Project/Database/Dragonfly/General.php
+++ b/app/Livewire/Project/Database/Dragonfly/General.php
@@ -54,7 +54,7 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (!$this->server->isLogDrainEnabled()) {
+ if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -88,14 +88,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && !$this->database->public_port) {
+ if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (!str($this->database->status)->startsWith('running')) {
+ if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -110,7 +110,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = !$this->database->is_public;
+ $this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index 6435f6781..49884ff9a 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -14,6 +14,8 @@ class Heading extends Component
public array $parameters;
+ public $docker_cleanup = true;
+
public function getListeners()
{
$userId = auth()->user()->id;
@@ -54,7 +56,7 @@ public function mount()
public function stop()
{
- StopDatabase::run($this->database);
+ StopDatabase::run($this->database, false, $this->docker_cleanup);
$this->database->status = 'exited';
$this->database->save();
$this->check_status();
@@ -71,4 +73,13 @@ public function start()
$activity = StartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
}
+
+ public function render()
+ {
+ return view('livewire.project.database.heading', [
+ 'checkboxes' => [
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index 320feeac7..f976e1edd 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -57,7 +57,7 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (!$this->server->isLogDrainEnabled()) {
+ if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -94,14 +94,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && !$this->database->public_port) {
+ if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (!str($this->database->status)->startsWith('running')) {
+ if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -116,7 +116,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = !$this->database->is_public;
+ $this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index 70545910c..12d4882f3 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -63,7 +63,7 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (!$this->server->isLogDrainEnabled()) {
+ if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -100,14 +100,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && !$this->database->public_port) {
+ if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (!str($this->database->status)->startsWith('running')) {
+ if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -122,7 +122,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = !$this->database->is_public;
+ $this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index d23b66c00..ac40e7dfa 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -61,7 +61,7 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (!$this->server->isLogDrainEnabled()) {
+ if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -101,14 +101,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && !$this->database->public_port) {
+ if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (!str($this->database->status)->startsWith('running')) {
+ if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -123,7 +123,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = !$this->database->is_public;
+ $this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php
index 29a9cbae2..7d5270ddf 100644
--- a/app/Livewire/Project/Database/Mysql/General.php
+++ b/app/Livewire/Project/Database/Mysql/General.php
@@ -62,7 +62,7 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (!$this->server->isLogDrainEnabled()) {
+ if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -99,14 +99,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && !$this->database->public_port) {
+ if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (!str($this->database->status)->startsWith('running')) {
+ if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -121,7 +121,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = !$this->database->is_public;
+ $this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index fd2f9834f..72fd95de8 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -57,7 +57,7 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (!$this->server->isLogDrainEnabled()) {
+ if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -88,14 +88,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && !$this->database->public_port) {
+ if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (!str($this->database->status)->startsWith('running')) {
+ if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -110,7 +110,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = !$this->database->is_public;
+ $this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php
index 22478916f..e01741770 100644
--- a/app/Livewire/Project/DeleteEnvironment.php
+++ b/app/Livewire/Project/DeleteEnvironment.php
@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
public bool $disabled = false;
+ public string $environmentName = '';
+
public function mount()
{
$this->parameters = get_route_parameters();
+ $this->environmentName = Environment::findOrFail($this->environment_id)->name;
}
public function delete()
diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php
index 499b86e3e..360fad10a 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -13,9 +13,12 @@ class DeleteProject extends Component
public bool $disabled = false;
+ public string $projectName = '';
+
public function mount()
{
$this->parameters = get_route_parameters();
+ $this->projectName = Project::findOrFail($this->project_id)->name;
}
public function delete()
diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php
index fa44fdfbf..a2e48fee7 100644
--- a/app/Livewire/Project/Service/Configuration.php
+++ b/app/Livewire/Project/Service/Configuration.php
@@ -52,7 +52,7 @@ public function restartApplication($id)
$application = $this->service->applications->find($id);
if ($application) {
$application->restart();
- $this->dispatch('success', 'Application restarted successfully.');
+ $this->dispatch('success', 'Service application restarted successfully.');
}
} catch (\Exception $e) {
return handleError($e, $this);
@@ -65,7 +65,7 @@ public function restartDatabase($id)
$database = $this->service->databases->find($id);
if ($database) {
$database->restart();
- $this->dispatch('success', 'Database restarted successfully.');
+ $this->dispatch('success', 'Service database restarted successfully.');
}
} catch (\Exception $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 6cd54883e..215019112 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -14,6 +14,8 @@
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class FileStorage extends Component
@@ -83,8 +85,14 @@ public function convertToFile()
}
}
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
try {
$message = 'File deleted.';
if ($this->fileStorage->is_directory) {
@@ -129,6 +137,13 @@ public function instantSave()
public function render()
{
- return view('livewire.project.service.file-storage');
+ return view('livewire.project.service.file-storage', [
+ 'directoryDeletionCheckboxes' => [
+ ['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
+ ],
+ 'fileDeletionCheckboxes' => [
+ ['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
+ ],
+ ]);
}
}
diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php
index 674182df5..9cd309885 100644
--- a/app/Livewire/Project/Service/Navbar.php
+++ b/app/Livewire/Project/Service/Navbar.php
@@ -20,6 +20,10 @@ class Navbar extends Component
public $isDeploymentProgress = false;
+ public $docker_cleanup = true;
+
+ public $title = 'Configuration';
+
public function mount()
{
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
@@ -40,7 +44,7 @@ public function getListeners()
public function serviceStarted()
{
- $this->dispatch('success', 'Service status changed.');
+ // $this->dispatch('success', 'Service status changed.');
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
@@ -60,11 +64,6 @@ public function check_status()
$this->dispatch('success', 'Service status updated.');
}
- public function render()
- {
- return view('livewire.project.service.navbar');
- }
-
public function checkDeployments()
{
try {
@@ -95,14 +94,9 @@ public function start()
$this->dispatch('activityMonitor', $activity->id);
}
- public function stop(bool $forceCleanup = false)
+ public function stop()
{
- StopService::run($this->service);
- if ($forceCleanup) {
- $this->dispatch('success', 'Containers cleaned up.');
- } else {
- $this->dispatch('success', 'Service stopped.');
- }
+ StopService::run($this->service, false, $this->docker_cleanup);
ServiceStatusChanged::dispatch();
}
@@ -115,10 +109,19 @@ public function restart()
return;
}
PullImage::run($this->service);
- StopService::run($this->service);
+ StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse();
$this->dispatch('imagePulled');
$activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id);
}
+
+ public function render()
+ {
+ return view('livewire.project.service.navbar', [
+ 'checkboxes' => [
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php
index e7d00c3dd..56b506043 100644
--- a/app/Livewire/Project/Service/ServiceApplicationView.php
+++ b/app/Livewire/Project/Service/ServiceApplicationView.php
@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class ServiceApplicationView extends Component
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
public $parameters;
+ public $docker_cleanup = true;
+
+ public $delete_volumes = true;
+
protected $rules = [
'application.human_name' => 'nullable',
'application.description' => 'nullable',
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
'application.is_stripprefix_enabled' => 'nullable|boolean',
];
- public function render()
- {
- return view('livewire.project.service.service-application-view');
- }
-
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
@@ -56,8 +57,14 @@ public function instantSaveAdvanced()
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
}
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
try {
$this->application->delete();
$this->dispatch('success', 'Application deleted.');
@@ -91,4 +98,17 @@ public function submit()
$this->dispatch('generateDockerCompose');
}
}
+
+ public function render()
+ {
+ return view('livewire.project.service.service-application-view', [
+ 'checkboxes' => [
+ ['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ // ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
+ // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
+ // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php
index 04bb136db..7f2416e3e 100644
--- a/app/Livewire/Project/Service/StackForm.php
+++ b/app/Livewire/Project/Service/StackForm.php
@@ -33,7 +33,7 @@ public function mount()
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$rules = data_get($field, 'rules', 'nullable');
- $isPassword = data_get($field, 'isPassword');
+ $isPassword = data_get($field, 'isPassword', false);
$this->fields->put($key, [
'serviceName' => $serviceName,
'key' => $key,
@@ -47,7 +47,15 @@ public function mount()
$this->validationAttributes["fields.$key.value"] = $fieldKey;
}
}
- $this->fields = $this->fields->sortBy('name');
+ $this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
+ return $group->sortBy(function ($field) {
+ return data_get($field, 'isPassword') ? 1 : 0;
+ })->mapWithKeys(function ($field) {
+ return [$field['key'] => $field];
+ });
+ })->flatMap(function ($group) {
+ return $group;
+ });
}
public function saveCompose($raw)
diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php
index 5f0178be4..c05260899 100644
--- a/app/Livewire/Project/Shared/Danger.php
+++ b/app/Livewire/Project/Shared/Danger.php
@@ -3,6 +3,11 @@
namespace App\Livewire\Project\Shared;
use App\Jobs\DeleteResourceJob;
+use App\Models\Service;
+use App\Models\ServiceApplication;
+use App\Models\ServiceDatabase;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -10,6 +15,8 @@ class Danger extends Component
{
public $resource;
+ public $resourceName;
+
public $projectUuid;
public $environmentName;
@@ -18,22 +25,95 @@ class Danger extends Component
public bool $delete_volumes = true;
+ public bool $docker_cleanup = true;
+
+ public bool $delete_connected_networks = true;
+
public ?string $modalId = null;
+ public string $resourceDomain = '';
+
public function mount()
{
- $this->modalId = new Cuid2;
$parameters = get_route_parameters();
+ $this->modalId = new Cuid2;
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name');
+
+ if ($this->resource === null) {
+ if (isset($parameters['service_uuid'])) {
+ $this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
+ } elseif (isset($parameters['stack_service_uuid'])) {
+ $this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
+ ?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
+ }
+ }
+
+ if ($this->resource === null) {
+ $this->resourceName = 'Unknown Resource';
+
+ return;
+ }
+
+ if (! method_exists($this->resource, 'type')) {
+ $this->resourceName = 'Unknown Resource';
+
+ return;
+ }
+
+ switch ($this->resource->type()) {
+ case 'application':
+ $this->resourceName = $this->resource->name ?? 'Application';
+ break;
+ case 'standalone-postgresql':
+ case 'standalone-redis':
+ case 'standalone-mongodb':
+ case 'standalone-mysql':
+ case 'standalone-mariadb':
+ case 'standalone-keydb':
+ case 'standalone-dragonfly':
+ case 'standalone-clickhouse':
+ $this->resourceName = $this->resource->name ?? 'Database';
+ break;
+ case 'service':
+ $this->resourceName = $this->resource->name ?? 'Service';
+ break;
+ case 'service-application':
+ $this->resourceName = $this->resource->name ?? 'Service Application';
+ break;
+ case 'service-database':
+ $this->resourceName = $this->resource->name ?? 'Service Database';
+ break;
+ default:
+ $this->resourceName = 'Unknown Resource';
+ }
}
- public function delete()
+ public function delete($password)
{
+ if (isProduction()) {
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+ }
+
+ if (! $this->resource) {
+ $this->addError('resource', 'Resource not found.');
+
+ return;
+ }
+
try {
- // $this->authorize('delete', $this->resource);
$this->resource->delete();
- DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
+ DeleteResourceJob::dispatch(
+ $this->resource,
+ $this->delete_configurations,
+ $this->delete_volumes,
+ $this->docker_cleanup,
+ $this->delete_connected_networks
+ );
return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid,
@@ -43,4 +123,19 @@ public function delete()
return handleError($e, $this);
}
}
+
+ public function render()
+ {
+ return view('livewire.project.shared.danger', [
+ 'checkboxes' => [
+ ['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
+ ['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
+ ['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
+ // ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
+ // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
+ // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
+ ],
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php
index a2c018beb..7fb5c45db 100644
--- a/app/Livewire/Project/Shared/Destination.php
+++ b/app/Livewire/Project/Shared/Destination.php
@@ -8,6 +8,8 @@
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use App\Models\StandaloneDocker;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -115,8 +117,14 @@ public function addServer(int $network_id, int $server_id)
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
}
- public function removeServer(int $network_id, int $server_id)
+ public function removeServer(int $network_id, int $server_id, $password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
index 343915d9c..90419caed 100644
--- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php
+++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
@@ -2,18 +2,18 @@
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 $selected_container = 'default';
- public string $container;
+ public $container;
public Collection $containers;
@@ -23,8 +23,6 @@ class ExecuteContainerCommand extends Component
public string $type;
- public string $workDir = '';
-
public Server $server;
public Collection $servers;
@@ -33,11 +31,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 +62,13 @@ public function mount()
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()
@@ -96,50 +85,78 @@ public function loadContainers()
$containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true);
}
foreach ($containers as $container) {
- $payload = [
- 'server' => $server,
- 'container' => $container,
- ];
- $this->containers = $this->containers->push($payload);
+ // if container state is running
+ if (data_get($container, 'State') === 'running') {
+ $payload = [
+ 'server' => $server,
+ 'container' => $container,
+ ];
+ $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) {
+ 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()
{
+ if ($this->selected_container === 'default') {
+ $this->dispatch('error', 'Please select a container.');
+
+ return;
+ }
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 = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
+ if (is_null($container)) {
+ 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',
+ isset($container),
+ data_get($container, 'container.Names'),
+ data_get($container, 'server.uuid')
+ );
+
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php
index deccc875c..0e140b8c1 100644
--- a/app/Livewire/Project/Shared/GetLogs.php
+++ b/app/Livewire/Project/Shared/GetLogs.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Shared;
+use App\Helpers\SshMultiplexingHelper;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
@@ -108,14 +109,14 @@ public function getLogs($refresh = false)
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
- $sshCommand = generateSshCommand($this->server, $command);
+ $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} else {
$command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
if ($this->server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
- $sshCommand = generateSshCommand($this->server, $command);
+ $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
}
} else {
if ($this->server->isSwarm()) {
@@ -124,14 +125,14 @@ public function getLogs($refresh = false)
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
- $sshCommand = generateSshCommand($this->server, $command);
+ $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} else {
$command = "docker logs -n {$this->numberOfLines} {$this->container}";
if ($this->server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
- $sshCommand = generateSshCommand($this->server, $command);
+ $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
}
}
if ($refresh) {
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php
index 5bd6b4b9b..017cc9fd7 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php
@@ -7,7 +7,9 @@
class Executions extends Component
{
public $executions = [];
+
public $selectedKey;
+
public $task;
public function getListeners()
@@ -29,7 +31,7 @@ public function selectTask($key): void
public function server()
{
- if (!$this->task) {
+ if (! $this->task) {
return null;
}
@@ -42,16 +44,18 @@ public function server()
return $this->task->service->destination->server;
}
}
+
return null;
}
public function getServerTimezone()
{
$server = $this->server();
- if (!$server) {
+ if (! $server) {
return 'UTC';
}
$serverTimezone = $server->settings->server_timezone;
+
return $serverTimezone;
}
@@ -64,6 +68,7 @@ public function formatDateInServerTimezone($date)
} catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
+
return $dateObj->format('Y-m-d H:i:s T');
}
}
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php
index 8be4ff643..37f50dd32 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Show.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php
@@ -20,6 +20,8 @@ class Show extends Component
public string $type;
+ public string $scheduledTaskName;
+
protected $rules = [
'task.enabled' => 'required|boolean',
'task.name' => 'required|string',
@@ -49,6 +51,7 @@ public function mount()
$this->modalId = new Cuid2;
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
+ $this->scheduledTaskName = $this->task->name;
}
public function instantSave()
@@ -75,9 +78,9 @@ public function delete()
$this->task->delete();
if ($this->type == 'application') {
- return redirect()->route('project.application.configuration', $this->parameters);
+ return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName);
} else {
- return redirect()->route('project.service.configuration', $this->parameters);
+ return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName);
}
} catch (\Exception $e) {
return handleError($e);
diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php
index 08f51ce08..e4b5c9b89 100644
--- a/app/Livewire/Project/Shared/Storages/Show.php
+++ b/app/Livewire/Project/Shared/Storages/Show.php
@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Show extends Component
@@ -36,8 +38,14 @@ public function submit()
$this->dispatch('success', 'Storage updated successfully');
}
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
+
$this->storage->delete();
$this->dispatch('refreshStorages');
}
diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php
new file mode 100644
index 000000000..27be46227
--- /dev/null
+++ b/app/Livewire/Project/Shared/Terminal.php
@@ -0,0 +1,58 @@
+user()->currentTeam()->id;
+
+ return [
+ "echo-private:team.{$teamId},ApplicationStatusChanged" => 'closeTerminal',
+ ];
+ }
+
+ public function closeTerminal()
+ {
+ $this->dispatch('reloadWindow');
+ }
+
+ #[On('send-terminal-command')]
+ public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
+ {
+
+ $server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
+
+ if ($isContainer) {
+ $status = getContainerStatus($server, $identifier);
+ if ($status !== 'running') {
+ return;
+ }
+ $command = SshMultiplexingHelper::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 = SshMultiplexingHelper::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/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php
index ff8679d21..40752630e 100644
--- a/app/Livewire/Security/ApiTokens.php
+++ b/app/Livewire/Security/ApiTokens.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Security;
+use App\Models\InstanceSettings;
use Livewire\Component;
class ApiTokens extends Component
@@ -16,6 +17,8 @@ class ApiTokens extends Component
public array $permissions = ['read-only'];
+ public $isApiEnabled;
+
public function render()
{
return view('livewire.security.api-tokens');
@@ -23,6 +26,7 @@ public function render()
public function mount()
{
+ $this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
$this->tokens = auth()->user()->tokens->sortByDesc('created_at');
}
diff --git a/app/Livewire/Security/PrivateKey/Create.php b/app/Livewire/Security/PrivateKey/Create.php
index 32a67bbea..319cec192 100644
--- a/app/Livewire/Security/PrivateKey/Create.php
+++ b/app/Livewire/Security/PrivateKey/Create.php
@@ -3,17 +3,13 @@
namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey;
-use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component;
-use phpseclib3\Crypt\PublicKeyLoader;
class Create extends Component
{
- use WithRateLimiting;
+ public string $name = '';
- public string $name;
-
- public string $value;
+ public string $value = '';
public ?string $from = null;
@@ -26,72 +22,69 @@ class Create extends Component
'value' => 'required|string',
];
- protected $validationAttributes = [
- 'name' => 'name',
- 'value' => 'private Key',
- ];
-
public function generateNewRSAKey()
{
- try {
- $this->rateLimit(10);
- $this->name = generate_random_name();
- $this->description = 'Created by Coolify';
- ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey();
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
+ $this->generateNewKey('rsa');
}
public function generateNewEDKey()
{
- try {
- $this->rateLimit(10);
- $this->name = generate_random_name();
- $this->description = 'Created by Coolify';
- ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519');
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
+ $this->generateNewKey('ed25519');
}
- public function updated($updateProperty)
+ private function generateNewKey($type)
{
- if ($updateProperty === 'value') {
- try {
- $this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
- } catch (\Throwable $e) {
- if ($this->$updateProperty === '') {
- $this->publicKey = '';
- } else {
- $this->publicKey = 'Invalid private key';
- }
- }
+ $keyData = PrivateKey::generateNewKeyPair($type);
+ $this->setKeyData($keyData);
+ }
+
+ public function updated($property)
+ {
+ if ($property === 'value') {
+ $this->validatePrivateKey();
}
- $this->validateOnly($updateProperty);
}
public function createPrivateKey()
{
$this->validate();
+
try {
- $this->value = trim($this->value);
- if (! str_ends_with($this->value, "\n")) {
- $this->value .= "\n";
- }
- $private_key = PrivateKey::create([
+ $privateKey = PrivateKey::createAndStore([
'name' => $this->name,
'description' => $this->description,
- 'private_key' => $this->value,
+ 'private_key' => trim($this->value)."\n",
'team_id' => currentTeam()->id,
]);
- if ($this->from === 'server') {
- return redirect()->route('dashboard');
- }
- return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
+ return $this->redirectAfterCreation($privateKey);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
+
+ private function setKeyData(array $keyData)
+ {
+ $this->name = $keyData['name'];
+ $this->description = $keyData['description'];
+ $this->value = $keyData['private_key'];
+ $this->publicKey = $keyData['public_key'];
+ }
+
+ private function validatePrivateKey()
+ {
+ $validationResult = PrivateKey::validateAndExtractPublicKey($this->value);
+ $this->publicKey = $validationResult['publicKey'];
+
+ if (! $validationResult['isValid']) {
+ $this->addError('value', 'Invalid private key');
+ }
+ }
+
+ private function redirectAfterCreation(PrivateKey $privateKey)
+ {
+ return $this->from === 'server'
+ ? redirect()->route('dashboard')
+ : redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]);
+ }
}
diff --git a/app/Livewire/Security/PrivateKey/Index.php b/app/Livewire/Security/PrivateKey/Index.php
new file mode 100644
index 000000000..76441a67e
--- /dev/null
+++ b/app/Livewire/Security/PrivateKey/Index.php
@@ -0,0 +1,24 @@
+get();
+
+ return view('livewire.security.private-key.index', [
+ 'privateKeys' => $privateKeys,
+ ])->layout('components.layout');
+ }
+
+ public function cleanupUnusedKeys()
+ {
+ PrivateKey::cleanupUnusedKeys();
+ $this->dispatch('success', 'Unused keys have been cleaned up.');
+ }
+}
diff --git a/app/Livewire/Security/PrivateKey/Show.php b/app/Livewire/Security/PrivateKey/Show.php
index d86bd5d1e..249c84f14 100644
--- a/app/Livewire/Security/PrivateKey/Show.php
+++ b/app/Livewire/Security/PrivateKey/Show.php
@@ -29,25 +29,27 @@ public function mount()
try {
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
} catch (\Throwable $e) {
- return handleError($e, $this);
+ abort(404);
}
}
public function loadPublicKey()
{
- $this->public_key = $this->private_key->publicKey();
+ $this->public_key = $this->private_key->getPublicKey();
+ if ($this->public_key === 'Error loading private key') {
+ $this->dispatch('error', 'Failed to load public key. The private key may be invalid.');
+ }
}
public function delete()
{
try {
- if ($this->private_key->isEmpty()) {
- $this->private_key->delete();
- currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
+ $this->private_key->safeDelete();
+ currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
- return redirect()->route('security.private-key.index');
- }
- $this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
+ return redirect()->route('security.private-key.index');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -56,8 +58,9 @@ public function delete()
public function changePrivateKey()
{
try {
- $this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
- $this->private_key->save();
+ $this->private_key->updatePrivateKey([
+ 'private_key' => formatPrivateKey($this->private_key->private_key),
+ ]);
refresh_server_connection($this->private_key);
$this->dispatch('success', 'Private key updated.');
} catch (\Throwable $e) {
diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php
index f7306a5b5..a69a5e15d 100644
--- a/app/Livewire/Server/ConfigureCloudflareTunnels.php
+++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php
@@ -31,13 +31,12 @@ public function submit()
{
try {
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
- ConfigureCloudflared::run($server, $this->cloudflare_token);
+ ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true;
$server->ip = $this->ssh_domain;
$server->save();
$server->settings->save();
- $this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
- $this->dispatch('refreshServerShow');
+ $this->dispatch('warning', 'Cloudflare Tunnels configuration started.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php
index 3beec0c91..ed2345b2a 100644
--- a/app/Livewire/Server/Delete.php
+++ b/app/Livewire/Server/Delete.php
@@ -3,6 +3,8 @@
namespace App\Livewire\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Delete extends Component
@@ -11,8 +13,13 @@ class Delete extends Component
public $server;
- public function delete()
+ public function delete($password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
try {
$this->authorize('delete', $this->server);
if ($this->server->hasDefinedResources()) {
diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php
index 3b3747a81..c4f25c79d 100644
--- a/app/Livewire/Server/Form.php
+++ b/app/Livewire/Server/Form.php
@@ -4,6 +4,7 @@
use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel;
+use App\Jobs\DockerCleanupJob;
use App\Jobs\PullSentinelImageJob;
use App\Models\Server;
use Livewire\Component;
@@ -24,11 +25,20 @@ class Form extends Component
public $timezones;
- protected $listeners = [
- 'serverInstalled',
- 'refreshServerShow' => 'serverInstalled',
- 'revalidate' => '$refresh',
- ];
+ public $delete_unused_volumes = false;
+
+ public $delete_unused_networks = false;
+
+ public function getListeners()
+ {
+ $teamId = auth()->user()->currentTeam()->id;
+
+ return [
+ "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured',
+ 'refreshServerShow' => 'serverInstalled',
+ 'revalidate' => '$refresh',
+ ];
+ }
protected $rules = [
'server.name' => 'required',
@@ -53,6 +63,8 @@ class Form extends Component
'server.settings.force_docker_cleanup' => 'required|boolean',
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
+ 'server.settings.delete_unused_volumes' => 'boolean',
+ 'server.settings.delete_unused_networks' => 'boolean',
];
protected $validationAttributes = [
@@ -74,6 +86,8 @@ class Form extends Component
'server.settings.metrics_history_days' => 'Metrics History',
'server.settings.is_server_api_enabled' => 'Server API',
'server.settings.server_timezone' => 'Server Timezone',
+ 'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
+ 'server.settings.delete_unused_networks' => 'Delete Unused Networks',
];
public function mount(Server $server)
@@ -83,6 +97,8 @@ public function mount(Server $server)
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
+ $this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes;
+ $this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks;
}
public function updated($field)
@@ -96,6 +112,12 @@ public function updated($field)
}
}
+ public function cloudflareTunnelConfigured()
+ {
+ $this->serverInstalled();
+ $this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
+ }
+
public function serverInstalled()
{
$this->server->refresh();
@@ -126,6 +148,7 @@ public function instantSave()
try {
refresh_server_connection($this->server->privateKey);
$this->validateServer(false);
+
$this->server->settings->save();
$this->server->save();
$this->dispatch('success', 'Server updated.');
@@ -143,6 +166,7 @@ public function instantSave()
ray('Sentinel is not enabled');
StopSentinel::dispatch($this->server);
}
+ $this->server->settings->save();
// $this->checkPortForServerApi();
} catch (\Throwable $e) {
@@ -223,9 +247,9 @@ public function submit()
$this->server->settings->server_timezone = $newTimezone;
$this->server->settings->save();
}
-
$this->server->settings->save();
$this->server->save();
+
$this->dispatch('success', 'Server updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -238,4 +262,22 @@ public function updatedServerSettingsServerTimezone($value)
$this->server->settings->save();
$this->dispatch('success', 'Server timezone updated.');
}
+
+ public function manualCleanup()
+ {
+ try {
+ DockerCleanupJob::dispatch($this->server, true);
+ $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function manualCloudflareConfig()
+ {
+ $this->server->settings->is_cloudflare_tunnel = true;
+ $this->server->settings->save();
+ $this->server->refresh();
+ $this->dispatch('success', 'Cloudflare Tunnels enabled.');
+ }
}
diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php
index 123b29d70..55d0c4966 100644
--- a/app/Livewire/Server/Proxy.php
+++ b/app/Livewire/Server/Proxy.php
@@ -39,6 +39,7 @@ public function changeProxy()
{
$this->server->proxy = null;
$this->server->save();
+ $this->dispatch('proxyChanged');
}
public function selectProxy($proxy_type)
@@ -47,7 +48,7 @@ public function selectProxy($proxy_type)
$this->server->proxy->set('type', $proxy_type);
$this->server->save();
$this->selectedProxy = $this->server->proxy->type;
- if ($this->selectedProxy !== 'NONE') {
+ if ($this->server->proxySet()) {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated');
diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php
index 2279951ee..eaa312663 100644
--- a/app/Livewire/Server/Proxy/Deploy.php
+++ b/app/Livewire/Server/Proxy/Deploy.php
@@ -6,6 +6,8 @@
use App\Actions\Proxy\StartProxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
+use Illuminate\Process\InvokedProcess;
+use Illuminate\Support\Facades\Process;
use Livewire\Component;
class Deploy extends Component
@@ -29,6 +31,7 @@ public function getListeners()
'serverRefresh' => 'proxyStatusUpdated',
'checkProxy',
'startProxy',
+ 'proxyChanged' => 'proxyStatusUpdated',
];
}
@@ -94,21 +97,43 @@ public function startProxy()
public function stop(bool $forceStop = true)
{
try {
- if ($this->server->isSwarm()) {
- instant_remote_process([
- 'docker service rm coolify-proxy_traefik',
- ], $this->server);
- } else {
- instant_remote_process([
- 'docker rm -f coolify-proxy',
- ], $this->server);
+ $containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
+ $timeout = 30;
+
+ $process = $this->stopContainer($containerName, $timeout);
+
+ $startTime = time();
+ while ($process->running()) {
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopContainer($containerName);
+ break;
+ }
+ usleep(100000);
}
- $this->server->proxy->status = 'exited';
- $this->server->proxy->force_stop = $forceStop;
- $this->server->save();
- $this->dispatch('proxyStatusUpdated');
+
+ $this->removeContainer($containerName);
} catch (\Throwable $e) {
return handleError($e, $this);
+ } finally {
+ $this->server->proxy->force_stop = $forceStop;
+ $this->server->proxy->status = 'exited';
+ $this->server->save();
+ $this->dispatch('proxyStatusUpdated');
}
}
+
+ private function stopContainer(string $containerName, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ private function forceStopContainer(string $containerName)
+ {
+ instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
+ }
+
+ private function removeContainer(string $containerName)
+ {
+ instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
+ }
}
diff --git a/app/Livewire/Server/Proxy/Show.php b/app/Livewire/Server/Proxy/Show.php
index cef909a45..d70e44e55 100644
--- a/app/Livewire/Server/Proxy/Show.php
+++ b/app/Livewire/Server/Proxy/Show.php
@@ -11,7 +11,7 @@ class Show extends Component
public $parameters = [];
- protected $listeners = ['proxyStatusUpdated'];
+ protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated'];
public function proxyStatusUpdated()
{
diff --git a/app/Livewire/Server/Proxy/Status.php b/app/Livewire/Server/Proxy/Status.php
index d23d7fc20..20db4dad4 100644
--- a/app/Livewire/Server/Proxy/Status.php
+++ b/app/Livewire/Server/Proxy/Status.php
@@ -49,6 +49,10 @@ public function checkProxy(bool $notification = false)
if ($this->server->proxy->status === 'running') {
$this->polling = false;
$notification && $this->dispatch('success', 'Proxy is running.');
+ } elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) {
+ $notification && $this->dispatch('error', 'Proxy has exited.');
+ } elseif ($this->server->proxy->force_stop) {
+ $notification && $this->dispatch('error', 'Proxy is stopped manually.');
} else {
$notification && $this->dispatch('error', 'Proxy is not running.');
}
diff --git a/app/Livewire/Server/ShowPrivateKey.php b/app/Livewire/Server/ShowPrivateKey.php
index 578a08967..92869c44b 100644
--- a/app/Livewire/Server/ShowPrivateKey.php
+++ b/app/Livewire/Server/ShowPrivateKey.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Server;
+use App\Models\PrivateKey;
use App\Models\Server;
use Livewire\Component;
@@ -13,25 +14,15 @@ class ShowPrivateKey extends Component
public $parameters;
- public function setPrivateKey($newPrivateKeyId)
+ public function setPrivateKey($privateKeyId)
{
try {
- $oldPrivateKeyId = $this->server->private_key_id;
- refresh_server_connection($this->server->privateKey);
- $this->server->update([
- 'private_key_id' => $newPrivateKeyId,
- ]);
+ $privateKey = PrivateKey::findOrFail($privateKeyId);
+ $this->server->update(['private_key_id' => $privateKey->id]);
$this->server->refresh();
- refresh_server_connection($this->server->privateKey);
- $this->checkConnection();
- } catch (\Throwable $e) {
- $this->server->update([
- 'private_key_id' => $oldPrivateKeyId,
- ]);
- $this->server->refresh();
- refresh_server_connection($this->server->privateKey);
-
- return handleError($e, $this);
+ $this->dispatch('success', 'Private key updated successfully.');
+ } catch (\Exception $e) {
+ $this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
}
}
@@ -43,7 +34,7 @@ public function checkConnection()
$this->dispatch('success', 'Server is reachable.');
} else {
ray($error);
- $this->dispatch('error', 'Server is not reachable.
Please validate your configuration and connection.
Check this documentation for further help.');
+ $this->dispatch('error', 'Server is not reachable.
Check this documentation for further help.
Error: '.$error);
return;
}
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index c52970258..754f0929b 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -60,7 +60,7 @@ class Index extends Component
public function mount()
{
if (isInstanceAdmin()) {
- $this->settings = InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled;
@@ -162,7 +162,7 @@ public function checkManually()
{
CheckForUpdatesJob::dispatchSync();
$this->dispatch('updateAvailable');
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->new_version_available) {
$this->dispatch('success', 'New version available!');
} else {
diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php
index f9402fd7b..ca0c9c1ae 100644
--- a/app/Livewire/Settings/License.php
+++ b/app/Livewire/Settings/License.php
@@ -29,7 +29,7 @@ public function mount()
abort(404);
}
$this->instance_id = config('app.id');
- $this->settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
}
public function render()
diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php
index 99b8f8d49..9240aa96d 100644
--- a/app/Livewire/SettingsBackup.php
+++ b/app/Livewire/SettingsBackup.php
@@ -42,7 +42,7 @@ class SettingsBackup extends Component
public function mount()
{
if (isInstanceAdmin()) {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
if ($this->database) {
diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php
index 3eb8ea646..4515df9a7 100644
--- a/app/Livewire/SettingsEmail.php
+++ b/app/Livewire/SettingsEmail.php
@@ -43,7 +43,7 @@ class SettingsEmail extends Component
public function mount()
{
if (isInstanceAdmin()) {
- $this->settings = InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->emails = auth()->user()->email;
} else {
return redirect()->route('dashboard');
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index 75d7fd04a..193b650ff 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -99,7 +99,7 @@ public function mount()
return redirect()->route('source.all');
}
$this->applications = $this->github_app->applications;
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->name = str($this->github_app->name)->kebab();
diff --git a/app/Livewire/Subscription/Index.php b/app/Livewire/Subscription/Index.php
index c278bf58e..df450cf7e 100644
--- a/app/Livewire/Subscription/Index.php
+++ b/app/Livewire/Subscription/Index.php
@@ -23,7 +23,7 @@ public function mount()
if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) {
return redirect()->route('subscription.show');
}
- $this->settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->alreadySubscribed = currentTeam()->subscription()->exists();
}
diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php
index 97d4fcdbf..3026cb297 100644
--- a/app/Livewire/Team/AdminView.php
+++ b/app/Livewire/Team/AdminView.php
@@ -4,6 +4,8 @@
use App\Models\Team;
use App\Models\User;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class AdminView extends Component
@@ -73,8 +75,13 @@ private function finalizeDeletion(User $user, Team $team)
$team->delete();
}
- public function delete($id)
+ public function delete($id, $password)
{
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
+
+ return;
+ }
if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users');
}
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/Application.php b/app/Models/Application.php
index d0cc34a06..dfa875a5a 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -6,7 +6,9 @@
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
use RuntimeException;
@@ -102,7 +104,7 @@ class Application extends BaseModel
{
use SoftDeletes;
- private static $parserVersion = '3';
+ private static $parserVersion = '4';
protected $guarded = [];
@@ -149,12 +151,64 @@ public static function ownedByCurrentTeamAPI(int $teamId)
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
}
+ public function getContainersToStop(bool $previewDeployments = false): array
+ {
+ $containers = $previewDeployments
+ ? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true)
+ : getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0);
+
+ return $containers->pluck('Names')->toArray();
+ }
+
+ public function stopContainers(array $containerNames, $server, int $timeout = 600)
+ {
+ $processes = [];
+ foreach ($containerNames as $containerName) {
+ $processes[$containerName] = $this->stopContainer($containerName, $server, $timeout);
+ }
+
+ $startTime = time();
+ while (count($processes) > 0) {
+ $finishedProcesses = array_filter($processes, function ($process) {
+ return ! $process->running();
+ });
+ foreach ($finishedProcesses as $containerName => $process) {
+ unset($processes[$containerName]);
+ $this->removeContainer($containerName, $server);
+ }
+
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopRemainingContainers(array_keys($processes), $server);
+ break;
+ }
+
+ usleep(100000);
+ }
+ }
+
+ public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ public function removeContainer(string $containerName, $server)
+ {
+ instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
+ }
+
+ public function forceStopRemainingContainers(array $containerNames, $server)
+ {
+ foreach ($containerNames as $containerName) {
+ instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
+ $this->removeContainer($containerName, $server);
+ }
+ }
+
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
- ray('Deleting workdir');
instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
}
}
@@ -176,6 +230,13 @@ public function delete_volumes(?Collection $persistentStorages)
}
}
+ public function delete_connected_networks($uuid)
+ {
+ $server = data_get($this, 'destination.server');
+ instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
+ instant_remote_process(["docker network rm {$uuid}"], $server, false);
+ }
+
public function additional_servers()
{
return $this->belongsToMany(Server::class, 'additional_destinations')
@@ -243,7 +304,7 @@ public function failedTaskLink($task_uuid)
'application_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid,
]);
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (data_get($settings, 'fqdn')) {
$url = Url::fromString($route);
$url = $url->withPort(null);
@@ -1034,6 +1095,7 @@ public function oldRawParser()
throw new \Exception($e->getMessage());
}
$services = data_get($yaml, 'services');
+
$commands = collect([]);
$services = collect($services)->map(function ($service) use ($commands) {
$serviceVolumes = collect(data_get($service, 'volumes', []));
@@ -1088,7 +1150,7 @@ public function oldRawParser()
public function parse(int $pull_request_id = 0, ?int $preview_id = null)
{
- if ($this->compose_parsing_version === '3') {
+ if ((int) $this->compose_parsing_version >= 3) {
return newParser($this, $pull_request_id, $preview_id);
} elseif ($this->docker_compose_raw) {
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
@@ -1166,7 +1228,6 @@ public function loadComposeFile($isInit = false)
} else {
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile
Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
-
}
public function parseContainerLabels(?ApplicationPreview $preview = null)
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 138775aba..9f8e4b342 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -126,11 +126,6 @@ public function realValue(): Attribute
$env = $this->get_real_environment_variables($this->value, $resource);
return data_get($env, 'value', $env);
- if (is_string($env)) {
- return $env;
- }
-
- return $env->value;
}
);
}
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index 27a181ee4..bb3d1478b 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -85,4 +85,17 @@ public function getTitleDisplayName(): string
return "[{$instanceName}]";
}
+
+ public function helperVersion(): Attribute
+ {
+ return Attribute::make(
+ get: function ($value) {
+ if (isDev()) {
+ return 'latest';
+ }
+
+ return $value;
+ }
+ );
+ }
}
diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php
index 45bc6bc84..065746ede 100644
--- a/app/Models/PrivateKey.php
+++ b/app/Models/PrivateKey.php
@@ -2,6 +2,9 @@
namespace App\Models;
+use DanHarrin\LivewireRateLimiting\WithRateLimiting;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Validation\ValidationException;
use OpenApi\Attributes as OA;
use phpseclib3\Crypt\PublicKeyLoader;
@@ -22,48 +25,144 @@
)]
class PrivateKey extends BaseModel
{
+ use WithRateLimiting;
+
protected $fillable = [
'name',
'description',
'private_key',
'is_git_related',
'team_id',
+ 'fingerprint',
+ ];
+
+ protected $casts = [
+ 'private_key' => 'encrypted',
];
protected static function booted()
{
static::saving(function ($key) {
- $privateKey = data_get($key, 'private_key');
- if (substr($privateKey, -1) !== "\n") {
- $key->private_key = $privateKey."\n";
+ $key->private_key = formatPrivateKey($key->private_key);
+
+ if (! self::validatePrivateKey($key->private_key)) {
+ throw ValidationException::withMessages([
+ 'private_key' => ['The private key is invalid.'],
+ ]);
+ }
+
+ $key->fingerprint = self::generateFingerprint($key->private_key);
+ if (self::fingerprintExists($key->fingerprint, $key->id)) {
+ throw ValidationException::withMessages([
+ 'private_key' => ['This private key already exists.'],
+ ]);
}
});
+ static::deleted(function ($key) {
+ self::deleteFromStorage($key);
+ });
+ }
+
+ public function getPublicKey()
+ {
+ return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
}
public static function ownedByCurrentTeam(array $select = ['*'])
{
$selectArray = collect($select)->concat(['id']);
- return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
+ return self::whereTeamId(currentTeam()->id)->select($selectArray->all());
}
- public function publicKey()
+ public static function validatePrivateKey($privateKey)
{
try {
- return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
+ PublicKeyLoader::load($privateKey);
+
+ return true;
} catch (\Throwable $e) {
- return 'Error loading private key';
+ return false;
}
}
- public function isEmpty()
+ public static function createAndStore(array $data)
{
- if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) {
- return true;
- }
+ $privateKey = new self($data);
+ $privateKey->save();
+ $privateKey->storeInFileSystem();
- return false;
+ return $privateKey;
+ }
+
+ public static function generateNewKeyPair($type = 'rsa')
+ {
+ try {
+ $instance = new self;
+ $instance->rateLimit(10);
+ $name = generate_random_name();
+ $description = 'Created by Coolify';
+ $keyPair = generateSSHKey($type === 'ed25519' ? 'ed25519' : 'rsa');
+
+ return [
+ 'name' => $name,
+ 'description' => $description,
+ 'private_key' => $keyPair['private'],
+ 'public_key' => $keyPair['public'],
+ ];
+ } catch (\Throwable $e) {
+ throw new \Exception("Failed to generate new {$type} key: ".$e->getMessage());
+ }
+ }
+
+ public static function extractPublicKeyFromPrivate($privateKey)
+ {
+ try {
+ $key = PublicKeyLoader::load($privateKey);
+
+ return $key->getPublicKey()->toString('OpenSSH', ['comment' => '']);
+ } catch (\Throwable $e) {
+ return null;
+ }
+ }
+
+ public static function validateAndExtractPublicKey($privateKey)
+ {
+ $isValid = self::validatePrivateKey($privateKey);
+ $publicKey = $isValid ? self::extractPublicKeyFromPrivate($privateKey) : '';
+
+ return [
+ 'isValid' => $isValid,
+ 'publicKey' => $publicKey,
+ ];
+ }
+
+ public function storeInFileSystem()
+ {
+ $filename = "ssh_key@{$this->uuid}";
+ Storage::disk('ssh-keys')->put($filename, $this->private_key);
+
+ return "/var/www/html/storage/app/ssh/keys/{$filename}";
+ }
+
+ public static function deleteFromStorage(self $privateKey)
+ {
+ $filename = "ssh_key@{$privateKey->uuid}";
+ Storage::disk('ssh-keys')->delete($filename);
+ }
+
+ public function getKeyLocation()
+ {
+ return "/var/www/html/storage/app/ssh/keys/ssh_key@{$this->uuid}";
+ }
+
+ public function updatePrivateKey(array $data)
+ {
+ $this->update($data);
+ $this->storeInFileSystem();
+
+ return $this;
}
public function servers()
@@ -85,4 +184,53 @@ public function gitlabApps()
{
return $this->hasMany(GitlabApp::class);
}
+
+ public function isInUse()
+ {
+ return $this->servers()->exists()
+ || $this->applications()->exists()
+ || $this->githubApps()->exists()
+ || $this->gitlabApps()->exists();
+ }
+
+ public function safeDelete()
+ {
+ if (! $this->isInUse()) {
+ $this->delete();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function generateFingerprint($privateKey)
+ {
+ try {
+ $key = PublicKeyLoader::load($privateKey);
+ $publicKey = $key->getPublicKey();
+
+ return $publicKey->getFingerprint('sha256');
+ } catch (\Throwable $e) {
+ return null;
+ }
+ }
+
+ private static function fingerprintExists($fingerprint, $excludeId = null)
+ {
+ $query = self::where('fingerprint', $fingerprint);
+
+ if (! is_null($excludeId)) {
+ $query->where('id', '!=', $excludeId);
+ }
+
+ return $query->exists();
+ }
+
+ public static function cleanupUnusedKeys()
+ {
+ self::ownedByCurrentTeam()->each(function ($privateKey) {
+ $privateKey->safeDelete();
+ });
+ }
}
diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php
index 50a0c8173..ce5d3a87f 100644
--- a/app/Models/ScheduledDatabaseBackup.php
+++ b/app/Models/ScheduledDatabaseBackup.php
@@ -35,14 +35,17 @@ public function get_last_days_backup_status($days = 7)
{
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
}
+
public function server()
{
if ($this->database) {
if ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server;
+
return $server;
}
}
+
return null;
}
}
diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php
index 82f0036a5..3cee5a875 100644
--- a/app/Models/ScheduledTask.php
+++ b/app/Models/ScheduledTask.php
@@ -4,8 +4,6 @@
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
-use App\Models\Service;
-use App\Models\Application;
class ScheduledTask extends BaseModel
{
@@ -37,19 +35,23 @@ public function server()
if ($this->application) {
if ($this->application->destination && $this->application->destination->server) {
$server = $this->application->destination->server;
+
return $server;
}
} elseif ($this->service) {
if ($this->service->destination && $this->service->destination->server) {
$server = $this->service->destination->server;
+
return $server;
}
} elseif ($this->database) {
if ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server;
+
return $server;
}
}
+
return null;
}
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 65d70083f..f896541ad 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -5,7 +5,6 @@
use App\Actions\Server\InstallDocker;
use App\Enums\ProxyTypes;
use App\Jobs\PullSentinelImageJob;
-use App\Notifications\Server\Revived;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Collection;
@@ -37,6 +36,8 @@
'validation_logs' => ['type' => 'string'],
'log_drain_notification_sent' => ['type' => 'boolean'],
'swarm_cluster' => ['type' => 'string'],
+ 'delete_unused_volumes' => ['type' => 'boolean'],
+ 'delete_unused_networks' => ['type' => 'boolean'],
]
)]
@@ -106,6 +107,8 @@ protected static function booted()
'proxy' => SchemalessAttributes::class,
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
+ 'delete_unused_volumes' => 'boolean',
+ 'delete_unused_networks' => 'boolean',
];
protected $schemalessAttributes = [
@@ -156,6 +159,11 @@ public function settings()
return $this->hasOne(ServerSetting::class);
}
+ public function proxySet()
+ {
+ return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
+ }
+
public function setupDefault404Redirect()
{
$dynamic_conf_path = $this->proxyPath().'/dynamic';
@@ -163,11 +171,11 @@ public function setupDefault404Redirect()
$redirect_url = $this->proxy->redirect_url;
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
- } elseif ($proxy_type === 'CADDY') {
+ } elseif ($proxy_type === ProxyTypes::CADDY->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy";
}
if (empty($redirect_url)) {
- if ($proxy_type === 'CADDY') {
+ if ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ':80, :443 {
respond 404
}';
@@ -237,7 +245,7 @@ public function setupDefault404Redirect()
$conf;
$base64 = base64_encode($conf);
- } elseif ($proxy_type === 'CADDY') {
+ } elseif ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ":80, :443 {
redir $redirect_url
}";
@@ -253,9 +261,6 @@ public function setupDefault404Redirect()
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
- if (config('app.env') == 'local') {
- ray($conf);
- }
if ($proxy_type === 'CADDY') {
$this->reloadCaddy();
}
@@ -263,7 +268,7 @@ public function setupDefault404Redirect()
public function setupDynamicProxyConfiguration()
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$dynamic_config_path = $this->proxyPath().'/dynamic';
if ($this->proxyType() === ProxyTypes::TRAEFIK->value) {
$file = "$dynamic_config_path/coolify.yaml";
@@ -305,6 +310,13 @@ public function setupDynamicProxyConfiguration()
'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 +337,15 @@ public function setupDynamicProxyConfiguration()
],
],
],
+ 'coolify-terminal' => [
+ 'loadBalancer' => [
+ 'servers' => [
+ 0 => [
+ 'url' => 'http://coolify-realtime:6002',
+ ],
+ ],
+ ],
+ ],
],
],
];
@@ -354,6 +375,16 @@ public function setupDynamicProxyConfiguration()
'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 +418,9 @@ public function setupDynamicProxyConfiguration()
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 +780,18 @@ public function getContainersWithSentinel(): Collection
}
}
+ 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()) {
@@ -792,9 +838,9 @@ public function databases()
$clickhouses = data_get($standaloneDocker, 'clickhouses', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
- })->filter(function ($item) {
+ })->flatten()->filter(function ($item) {
return data_get($item, 'name') !== 'coolify-db';
- })->flatten();
+ });
}
public function applications()
@@ -838,6 +884,35 @@ public function services()
return $this->hasMany(Service::class);
}
+ public function port(): Attribute
+ {
+ return Attribute::make(
+ get: function ($value) {
+ return preg_replace('/[^0-9]/', '', $value);
+ }
+ );
+ }
+
+ public function user(): Attribute
+ {
+ return Attribute::make(
+ get: function ($value) {
+ $sanitizedValue = preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
+
+ return $sanitizedValue;
+ }
+ );
+ }
+
+ public function ip(): Attribute
+ {
+ return Attribute::make(
+ get: function ($value) {
+ return preg_replace('/[^0-9a-zA-Z.:%-]/', '', $value);
+ }
+ );
+ }
+
public function getIp(): Attribute
{
return Attribute::make(
@@ -910,10 +985,9 @@ public function isProxyShouldRun()
public function isFunctional()
{
$isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled;
- ['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this);
+
if (! $isFunctional) {
- Storage::disk('ssh-keys')->delete($private_key_filename);
- Storage::disk('ssh-mux')->delete($mux_filename);
+ Storage::disk('ssh-mux')->delete($this->muxFilename());
}
return $isFunctional;
@@ -965,9 +1039,10 @@ public function isSwarmWorker()
return data_get($this, 'settings.is_swarm_worker');
}
- public function validateConnection()
+ public function validateConnection($isManualCheck = true)
{
- config()->set('constants.ssh.mux_enabled', false);
+ config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
+ // ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false'));
$server = Server::find($this->id);
if (! $server) {
@@ -977,7 +1052,10 @@ public function validateConnection()
return ['uptime' => false, 'error' => 'Server skipped.'];
}
try {
- // EC2 does not have `uptime` command, lol
+ // Make sure the private key is stored
+ if ($server->privateKey) {
+ $server->privateKey->storeInFileSystem();
+ }
instant_remote_process(['ls /'], $server);
$server->settings()->update([
'is_reachable' => true,
@@ -986,7 +1064,6 @@ public function validateConnection()
'unreachable_count' => 0,
]);
if (data_get($server, 'unreachable_notification_sent') === true) {
- // $server->team?->notify(new Revived($server));
$server->update(['unreachable_notification_sent' => false]);
}
@@ -1115,4 +1192,33 @@ public function isBuildServer()
{
return $this->settings->is_build_server;
}
+
+ public static function createWithPrivateKey(array $data, PrivateKey $privateKey)
+ {
+ $server = new self($data);
+ $server->privateKey()->associate($privateKey);
+ $server->save();
+
+ return $server;
+ }
+
+ public function updateWithPrivateKey(array $data, ?PrivateKey $privateKey = null)
+ {
+ $this->update($data);
+ if ($privateKey) {
+ $this->privateKey()->associate($privateKey);
+ $this->save();
+ }
+
+ return $this;
+ }
+
+ public function storageCheck(): ?string
+ {
+ $commands = [
+ 'df / --output=pcent | tr -cd 0-9',
+ ];
+
+ return instant_remote_process($commands, $this, false);
+ }
}
diff --git a/app/Models/Service.php b/app/Models/Service.php
index d8def6663..c9c086622 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -6,7 +6,9 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use OpenApi\Attributes as OA;
use Spatie\Url\Url;
@@ -40,7 +42,7 @@ class Service extends BaseModel
{
use HasFactory, SoftDeletes;
- private static $parserVersion = '3';
+ private static $parserVersion = '4';
protected $guarded = [];
@@ -131,15 +133,81 @@ public function tags()
return $this->morphToMany(Tag::class, 'taggable');
}
+ public function getContainersToStop(): array
+ {
+ $containersToStop = [];
+ $applications = $this->applications()->get();
+ foreach ($applications as $application) {
+ $containersToStop[] = "{$application->name}-{$this->uuid}";
+ }
+ $dbs = $this->databases()->get();
+ foreach ($dbs as $db) {
+ $containersToStop[] = "{$db->name}-{$this->uuid}";
+ }
+
+ return $containersToStop;
+ }
+
+ public function stopContainers(array $containerNames, $server, int $timeout = 300)
+ {
+ $processes = [];
+ foreach ($containerNames as $containerName) {
+ $processes[$containerName] = $this->stopContainer($containerName, $timeout);
+ }
+
+ $startTime = time();
+ while (count($processes) > 0) {
+ $finishedProcesses = array_filter($processes, function ($process) {
+ return ! $process->running();
+ });
+ foreach (array_keys($finishedProcesses) as $containerName) {
+ unset($processes[$containerName]);
+ $this->removeContainer($containerName, $server);
+ }
+
+ if (time() - $startTime >= $timeout) {
+ $this->forceStopRemainingContainers(array_keys($processes), $server);
+ break;
+ }
+
+ usleep(100000);
+ }
+ }
+
+ public function stopContainer(string $containerName, int $timeout): InvokedProcess
+ {
+ return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
+ }
+
+ public function removeContainer(string $containerName, $server)
+ {
+ instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
+ }
+
+ public function forceStopRemainingContainers(array $containerNames, $server)
+ {
+ foreach ($containerNames as $containerName) {
+ instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
+ $this->removeContainer($containerName, $server);
+ }
+ }
+
public function delete_configurations()
{
- $server = data_get($this, 'server');
+ $server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
}
}
+ public function delete_connected_networks($uuid)
+ {
+ $server = data_get($this, 'destination.server');
+ instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
+ instant_remote_process(["docker network rm {$uuid}"], $server, false);
+ }
+
public function status()
{
$applications = $this->applications;
@@ -1027,7 +1095,22 @@ public function saveComposeConfigs()
return 3;
});
foreach ($sorted as $env) {
- $commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
+ if (version_compare($env->version, '4.0.0-beta.347', '<=')) {
+ $commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
+ } else {
+ $real_value = $env->real_value;
+ if ($env->version === '4.0.0-beta.239') {
+ $real_value = $env->real_value;
+ } else {
+ if ($env->is_literal || $env->is_multiline) {
+ $real_value = '\''.$real_value.'\'';
+ } else {
+ $real_value = escapeEnvVariables($env->real_value);
+ }
+ }
+ ray("echo \"{$env->key}={$real_value}\" >> .env");
+ $commands[] = "echo \"{$env->key}={$real_value}\" >> .env";
+ }
}
if ($sorted->count() === 0) {
$commands[] = 'touch .env';
@@ -1037,7 +1120,7 @@ public function saveComposeConfigs()
public function parse(bool $isNew = false): Collection
{
- if ($this->compose_parsing_version === '3') {
+ if ((int) $this->compose_parsing_version >= 3) {
return newParser($this);
} elseif ($this->docker_compose_raw) {
return parseDockerComposeFile($this, $isNew);
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 @@ public static function ownedByCurrentTeamAPI(int $teamId)
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 @@ public function restart()
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 @@ public function isConfigurationChanged(bool $save = false)
}
}
+ 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 @@ public function isConfigurationChanged(bool $save = false)
}
}
+ 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 @@ public function isConfigurationChanged(bool $save = false)
}
}
+ 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 @@ public function isConfigurationChanged(bool $save = false)
}
}
+ 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 @@ public function isConfigurationChanged(bool $save = false)
}
}
+ 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 @@ public function isConfigurationChanged(bool $save = false)
}
}
+ 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 @@ public function isConfigurationChanged(bool $save = false)
}
}
+ 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 @@ public function isConfigurationChanged(bool $save = false)
}
}
+ public function isRunning()
+ {
+ return (bool) str($this->status)->contains('running');
+ }
+
public function isExited()
{
return (bool) str($this->status)->startsWith('exited');
diff --git a/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php
index 549fc6cd3..cc7d76ebf 100644
--- a/app/Notifications/Channels/TransactionalEmailChannel.php
+++ b/app/Notifications/Channels/TransactionalEmailChannel.php
@@ -13,7 +13,7 @@ class TransactionalEmailChannel
{
public function send(User $notifiable, Notification $notification): void
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) {
Log::info('SMTP/Resend not enabled');
diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php
index c0e2a3c31..6377f2f15 100644
--- a/app/Notifications/Server/ForceDisabled.php
+++ b/app/Notifications/Server/ForceDisabled.php
@@ -52,7 +52,7 @@ public function toMail(): MailMessage
public function toDiscord(): string
{
- $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).";
+ $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).";
return $message;
}
@@ -60,7 +60,7 @@ public function toDiscord(): string
public function toTelegram(): array
{
return [
- 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).",
+ 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).",
];
}
}
diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php
index 8b1c02d39..3938a8da7 100644
--- a/app/Notifications/TransactionalEmails/ResetPassword.php
+++ b/app/Notifications/TransactionalEmails/ResetPassword.php
@@ -18,7 +18,7 @@ class ResetPassword extends Notification
public function __construct($token)
{
- $this->settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->token = $token;
}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index cd90918ad..8b4c2eef2 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -2,10 +2,8 @@
namespace App\Providers;
-use App\Models\InstanceSettings;
use App\Models\PersonalAccessToken;
use Illuminate\Support\Facades\Http;
-use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Laravel\Sanctum\Sanctum;
@@ -30,9 +28,5 @@ public function boot(): void
])->baseUrl($api_url);
}
});
- // if (! env('CI')) {
- // View::share('instanceSettings', InstanceSettings::get());
- // }
-
}
}
diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php
index 53a2e9281..b916b6234 100644
--- a/app/Providers/FortifyServiceProvider.php
+++ b/app/Providers/FortifyServiceProvider.php
@@ -46,7 +46,7 @@ public function boot(): void
Fortify::registerView(function () {
$isFirstUser = User::count() === 0;
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->is_registration_enabled) {
return redirect()->route('login');
}
@@ -60,7 +60,7 @@ public function boot(): void
});
Fortify::loginView(function () {
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$enabled_oauth_providers = OauthSetting::where('enabled', true)->get();
$users = User::count();
if ($users == 0) {
diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php
index 9b58882eb..f8ccee9db 100644
--- a/app/Traits/ExecuteRemoteCommand.php
+++ b/app/Traits/ExecuteRemoteCommand.php
@@ -3,6 +3,7 @@
namespace App\Traits;
use App\Enums\ApplicationDeploymentStatus;
+use App\Helpers\SshMultiplexingHelper;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Support\Collection;
@@ -42,7 +43,7 @@ public function execute_remote_command(...$commands)
$command = parseLineForSudo($command, $this->server);
}
}
- $remote_command = generateSshCommand($this->server, $command);
+ $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php
index 6c9378cac..fbd7b0b15 100644
--- a/app/View/Components/Forms/Input.php
+++ b/app/View/Components/Forms/Input.php
@@ -22,6 +22,7 @@ public function __construct(
public bool $allowToPeak = true,
public bool $isMultiline = false,
public string $defaultClass = 'input',
+ public string $autocomplete = 'off',
) {}
public function render(): View|Closure|string
diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php
index 8e14ef9ee..006b095cf 100644
--- a/bootstrap/helpers/api.php
+++ b/bootstrap/helpers/api.php
@@ -175,4 +175,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('instant_deploy');
$request->offsetUnset('github_app_uuid');
$request->offsetUnset('private_key_uuid');
+ $request->offsetUnset('use_build_server');
}
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 90093deb8..e252bda10 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);
@@ -120,6 +134,9 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
return 'exited';
}
$container = format_docker_command_output_to_json($container);
+ if ($container->isEmpty()) {
+ return 'exited';
+ }
if ($all_data) {
return $container[0];
}
@@ -215,12 +232,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/proxy.php b/bootstrap/helpers/proxy.php
index c4c15b8fe..5d1ad5390 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -96,6 +96,8 @@ function connectProxyToNetworks(Server $server)
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
+ "echo 'Successfully connected coolify-proxy to $network network.'",
+ "echo 'Proxy started and configured successfully!'",
];
});
} else {
@@ -104,6 +106,8 @@ function connectProxyToNetworks(Server $server)
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
+ "echo 'Successfully connected coolify-proxy to $network network.'",
+ "echo 'Proxy started and configured successfully!'",
];
});
}
@@ -144,6 +148,7 @@ function generate_default_proxy_configuration(Server $server)
'traefik.http.routers.traefik.service=api@internal',
'traefik.http.services.traefik.loadbalancer.server.port=8080',
'coolify.managed=true',
+ 'coolify.proxy=true',
];
$config = [
'networks' => $array_of_networks->toArray(),
@@ -217,7 +222,6 @@ function generate_default_proxy_configuration(Server $server)
}
} elseif ($proxy_type === 'CADDY') {
$config = [
- 'version' => '3.8',
'networks' => $array_of_networks->toArray(),
'services' => [
'caddy' => [
@@ -236,12 +240,9 @@ function generate_default_proxy_configuration(Server $server)
'80:80',
'443:443',
],
- // "healthcheck" => [
- // "test" => "wget -qO- http://localhost:80|| exit 1",
- // "interval" => "4s",
- // "timeout" => "2s",
- // "retries" => 5,
- // ],
+ 'labels' => [
+ 'coolify.managed=true',
+ ],
'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro',
"{$proxy_path}/dynamic:/dynamic",
diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php
index 4ba378e67..67b60d6b7 100644
--- a/bootstrap/helpers/remoteProcess.php
+++ b/bootstrap/helpers/remoteProcess.php
@@ -3,6 +3,7 @@
use App\Actions\CoolifyTask\PrepareCoolifyTask;
use App\Data\CoolifyTaskArgs;
use App\Enums\ActivityTypes;
+use App\Helpers\SshMultiplexingHelper;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\PrivateKey;
@@ -10,9 +11,8 @@
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Process;
-use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Spatie\Activitylog\Contracts\Activity;
@@ -26,29 +26,28 @@ function remote_process(
$callEventOnFinish = null,
$callEventData = null
): Activity {
- if (is_null($type)) {
- $type = ActivityTypes::INLINE->value;
- }
- if ($command instanceof Collection) {
- $command = $command->toArray();
- }
+ $type = $type ?? ActivityTypes::INLINE->value;
+ $command = $command instanceof Collection ? $command->toArray() : $command;
+
if ($server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $server);
}
+
$command_string = implode("\n", $command);
- if (auth()->user()) {
- $teams = auth()->user()->teams->pluck('id');
+
+ if (Auth::check()) {
+ $teams = Auth::user()->teams->pluck('id');
if (! $teams->contains($server->team_id) && ! $teams->contains(0)) {
throw new \Exception('User is not part of the team that owns this server');
}
}
+ SshMultiplexingHelper::ensureMultiplexedConnection($server);
+
return resolve(PrepareCoolifyTask::class, [
'remoteProcessArgs' => new CoolifyTaskArgs(
server_uuid: $server->uuid,
- command: <<uuid}";
- $location = '/var/www/html/storage/app/ssh/keys/'.$private_key_filename;
- $mux_filename = '/var/www/html/storage/app/ssh/mux/'.$server->muxFilename();
- return [
- 'location' => $location,
- 'mux_filename' => $mux_filename,
- 'private_key_filename' => $private_key_filename,
- ];
-}
-function savePrivateKeyToFs(Server $server)
-{
- if (data_get($server, 'privateKey.private_key') === null) {
- throw new \Exception("Server {$server->name} does not have a private key");
- }
- ['location' => $location, 'private_key_filename' => $private_key_filename] = server_ssh_configuration($server);
- Storage::disk('ssh-keys')->makeDirectory('.');
- Storage::disk('ssh-mux')->makeDirectory('.');
- Storage::disk('ssh-keys')->put($private_key_filename, $server->privateKey->private_key);
-
- return $location;
-}
-
-function generateScpCommand(Server $server, string $source, string $dest)
-{
- $user = $server->user;
- $port = $server->port;
- $privateKeyLocation = savePrivateKeyToFs($server);
- $timeout = config('constants.ssh.command_timeout');
- $connectionTimeout = config('constants.ssh.connection_timeout');
- $serverInterval = config('constants.ssh.server_interval');
- $muxPersistTime = config('constants.ssh.mux_persist_time');
-
- $scp_command = "timeout $timeout scp ";
- $muxEnabled = config('constants.ssh.mux_enabled', true) && config('coolify.is_windows_docker_desktop') == false;
- // ray('SSH Multiplexing Enabled:', $muxEnabled)->blue();
-
- if ($muxEnabled) {
- $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
- $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
- ensureMultiplexedConnection($server);
- // ray('Using SSH Multiplexing')->green();
- } else {
- // ray('Not using SSH Multiplexing')->red();
- }
-
- if (data_get($server, 'settings.is_cloudflare_tunnel')) {
- $scp_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
- }
- $scp_command .= "-i {$privateKeyLocation} "
- .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
- .'-o PasswordAuthentication=no '
- ."-o ConnectTimeout=$connectionTimeout "
- ."-o ServerAliveInterval=$serverInterval "
- .'-o RequestTTY=no '
- .'-o LogLevel=ERROR '
- ."-P {$port} "
- ."{$source} "
- ."{$user}@{$server->ip}:{$dest}";
-
- return $scp_command;
-}
function instant_scp(string $source, string $dest, Server $server, $throwError = true)
{
- $timeout = config('constants.ssh.command_timeout');
- $scp_command = generateScpCommand($server, $source, $dest);
- $process = Process::timeout($timeout)->run($scp_command);
+ $scp_command = SshMultiplexingHelper::generateScpCommand($server, $source, $dest);
+ $process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command);
$output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
- if (! $throwError) {
- return null;
- }
-
- return excludeCertainErrors($process->errorOutput(), $exitCode);
- }
- if ($output === 'null') {
- $output = null;
+ return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null;
}
- return $output;
-}
-function generateSshCommand(Server $server, string $command)
-{
- if ($server->settings->force_disabled) {
- throw new \RuntimeException('Server is disabled.');
- }
- $user = $server->user;
- $port = $server->port;
- $privateKeyLocation = savePrivateKeyToFs($server);
- $timeout = config('constants.ssh.command_timeout');
- $connectionTimeout = config('constants.ssh.connection_timeout');
- $serverInterval = config('constants.ssh.server_interval');
- $muxPersistTime = config('constants.ssh.mux_persist_time');
-
- $ssh_command = "timeout $timeout ssh ";
-
- $muxEnabled = config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false;
- // ray('SSH Multiplexing Enabled:', $muxEnabled)->blue();
- if ($muxEnabled) {
- // Always use multiplexing when enabled
- $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
- $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
- ensureMultiplexedConnection($server);
- // ray('Using SSH Multiplexing')->green();
- } else {
- // ray('Not using SSH Multiplexing')->red();
- }
-
- if (data_get($server, 'settings.is_cloudflare_tunnel')) {
- $ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
- }
- $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
- $delimiter = Hash::make($command);
- $command = str_replace($delimiter, '', $command);
- $ssh_command .= "-i {$privateKeyLocation} "
- .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
- .'-o PasswordAuthentication=no '
- ."-o ConnectTimeout=$connectionTimeout "
- ."-o ServerAliveInterval=$serverInterval "
- .'-o RequestTTY=no '
- .'-o LogLevel=ERROR '
- ."-p {$port} "
- ."{$user}@{$server->ip} "
- ." 'bash -se' << \\$delimiter".PHP_EOL
- .$command.PHP_EOL
- .$delimiter;
-
- return $ssh_command;
-}
-
-function ensureMultiplexedConnection(Server $server)
-{
- if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) {
- return;
- }
-
- static $ensuredConnections = [];
-
- if (isset($ensuredConnections[$server->id])) {
- if (! shouldResetMultiplexedConnection($server)) {
- // ray('Using Existing Multiplexed Connection')->green();
-
- return;
- }
- }
-
- $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
- $checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
- if (data_get($server, 'settings.is_cloudflare_tunnel')) {
- $checkCommand .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
- }
- $checkCommand .= " {$server->user}@{$server->ip}";
-
- $process = Process::run($checkCommand);
-
- if ($process->exitCode() === 0) {
- // ray('Existing Multiplexed Connection is Valid')->green();
- $ensuredConnections[$server->id] = [
- 'timestamp' => now(),
- 'muxSocket' => $muxSocket,
- ];
-
- return;
- }
-
- // ray('Establishing New Multiplexed Connection')->orange();
-
- $privateKeyLocation = savePrivateKeyToFs($server);
- $connectionTimeout = config('constants.ssh.connection_timeout');
- $serverInterval = config('constants.ssh.server_interval');
- $muxPersistTime = config('constants.ssh.mux_persist_time');
-
- $establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
-
- if (data_get($server, 'settings.is_cloudflare_tunnel')) {
- $establishCommand .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
- }
- $establishCommand .= "-i {$privateKeyLocation} "
- .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
- .'-o PasswordAuthentication=no '
- ."-o ConnectTimeout=$connectionTimeout "
- ."-o ServerAliveInterval=$serverInterval "
- .'-o RequestTTY=no '
- .'-o LogLevel=ERROR '
- ."-p {$server->port} "
- ."{$server->user}@{$server->ip}";
-
- $establishProcess = Process::run($establishCommand);
-
- if ($establishProcess->exitCode() !== 0) {
- throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
- }
-
- $ensuredConnections[$server->id] = [
- 'timestamp' => now(),
- 'muxSocket' => $muxSocket,
- ];
-
- // ray('Established New Multiplexed Connection')->green();
-}
-
-function shouldResetMultiplexedConnection(Server $server)
-{
- if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) {
- return false;
- }
-
- static $ensuredConnections = [];
-
- if (! isset($ensuredConnections[$server->id])) {
- return true;
- }
-
- $lastEnsured = $ensuredConnections[$server->id]['timestamp'];
- $muxPersistTime = config('constants.ssh.mux_persist_time');
- $resetInterval = strtotime($muxPersistTime) - time();
-
- return $lastEnsured->addSeconds($resetInterval)->isPast();
-}
-
-function resetMultiplexedConnection(Server $server)
-{
- if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) {
- return;
- }
-
- static $ensuredConnections = [];
-
- if (isset($ensuredConnections[$server->id])) {
- $muxSocket = $ensuredConnections[$server->id]['muxSocket'];
- $closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip}";
- Process::run($closeCommand);
- unset($ensuredConnections[$server->id]);
- }
+ return $output === 'null' ? null : $output;
}
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
{
- static $processCount = 0;
- $processCount++;
-
- $timeout = config('constants.ssh.command_timeout');
- if ($command instanceof Collection) {
- $command = $command->toArray();
- }
+ $command = $command instanceof Collection ? $command->toArray() : $command;
if ($server->isNonRoot() && ! $no_sudo) {
$command = parseCommandsByLineForSudo(collect($command), $server);
}
$command_string = implode("\n", $command);
- $start_time = microtime(true);
- $sshCommand = generateSshCommand($server, $command_string);
- $process = Process::timeout($timeout)->run($sshCommand);
- $end_time = microtime(true);
+ // $start_time = microtime(true);
+ $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string);
+ $process = Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand);
+ // $end_time = microtime(true);
- $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds
+ // $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds
// ray('SSH command execution time:', $execution_time.' ms')->orange();
$output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
- if (! $throwError) {
- return null;
- }
-
- return excludeCertainErrors($process->errorOutput(), $exitCode);
- }
- if ($output === 'null') {
- $output = null;
+ return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null;
}
- return $output;
+ return $output === 'null' ? null : $output;
}
+
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
{
$ignoredErrors = collect([
'Permission denied (publickey',
'Could not resolve hostname',
]);
- $ignored = false;
- foreach ($ignoredErrors as $ignoredError) {
- if (Str::contains($errorOutput, $ignoredError)) {
- $ignored = true;
- break;
- }
- }
+ $ignored = $ignoredErrors->contains(fn ($error) => Str::contains($errorOutput, $error));
if ($ignored) {
// TODO: Create new exception and disable in sentry
throw new \RuntimeException($errorOutput, $exitCode);
}
throw new \RuntimeException($errorOutput, $exitCode);
}
+
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection
{
- $application = Application::find(data_get($application_deployment_queue, 'application_id'));
- $is_debug_enabled = data_get($application, 'settings.is_debug_enabled');
if (is_null($application_deployment_queue)) {
return collect([]);
}
+ $application = Application::find(data_get($application_deployment_queue, 'application_id'));
+ $is_debug_enabled = data_get($application, 'settings.is_debug_enabled');
try {
$decoded = json_decode(
data_get($application_deployment_queue, 'logs'),
@@ -379,7 +132,8 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
if (! $is_debug_enabled) {
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
}
- $formatted = $formatted
+
+ return $formatted
->sortBy(fn ($i) => data_get($i, 'order'))
->map(function ($i) {
data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u'));
@@ -421,36 +175,22 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
return $deploymentLogLines;
}, collect());
-
- return $formatted;
}
+
function remove_iip($text)
{
$text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text);
return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
}
-function remove_mux_and_private_key(Server $server)
-{
- $muxFilename = $server->muxFilename();
- $privateKeyLocation = savePrivateKeyToFs($server);
- $closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}";
- Process::run($closeCommand);
-
- Storage::disk('ssh-mux')->delete($muxFilename);
- Storage::disk('ssh-keys')->delete($privateKeyLocation);
-}
function refresh_server_connection(?PrivateKey $private_key = null)
{
if (is_null($private_key)) {
return;
}
foreach ($private_key->servers as $server) {
- $muxFilename = $server->muxFilename();
- $closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}";
- Process::run($closeCommand);
- Storage::disk('ssh-mux')->delete($muxFilename);
+ SshMultiplexingHelper::removeMuxFile($server);
}
}
@@ -468,9 +208,8 @@ function checkRequiredCommands(Server $server)
break;
}
$commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
- if ($commandFound) {
- continue;
+ if (! $commandFound) {
+ break;
}
- break;
}
}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 028d20f33..ffd53a99a 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -247,7 +247,7 @@ function is_transactional_emails_active(): bool
function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string
{
if (! $settings) {
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
}
config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
@@ -281,7 +281,7 @@ function base_ip(): string
if (isDev()) {
return 'localhost';
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->public_ipv4) {
return "$settings->public_ipv4";
}
@@ -309,7 +309,7 @@ function getFqdnWithoutPort(string $fqdn)
*/
function base_url(bool $withPort = true): string
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->fqdn) {
return $settings->fqdn;
}
@@ -343,6 +343,11 @@ function isSubscribed()
{
return isSubscriptionActive() || auth()->user()->isInstanceAdmin();
}
+
+function isProduction(): bool
+{
+ return ! isDev();
+}
function isDev(): bool
{
return config('app.env') === 'local';
@@ -384,7 +389,7 @@ function send_internal_notification(string $message): void
}
function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null): void
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$type = set_transanctional_email_settings($settings);
if (! $type) {
throw new Exception('No email settings found.');
@@ -478,7 +483,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 +493,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;
@@ -502,6 +510,12 @@ function sslip(Server $server)
return "http://$baseIp.sslip.io";
}
+ // ipv6
+ if (str($server->ip)->contains(':')) {
+ $ipv6 = str($server->ip)->replace(':', '-');
+
+ return "http://{$ipv6}.sslip.io";
+ }
return "http://{$server->ip}.sslip.io";
}
@@ -786,7 +800,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('/', '');
}
@@ -961,7 +975,7 @@ function validate_dns_entry(string $fqdn, Server $server)
if (str($host)->contains('sslip.io')) {
return true;
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled');
if (! $is_dns_validation_enabled) {
return true;
@@ -1081,7 +1095,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId =
if ($domainFound) {
return true;
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if (data_get($settings, 'fqdn')) {
$domain = data_get($settings, 'fqdn');
if (str($domain)->endsWith('/')) {
@@ -1153,7 +1167,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
}
}
if ($resource) {
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if (data_get($settings, 'fqdn')) {
$domain = data_get($settings, 'fqdn');
if (str($domain)->endsWith('/')) {
@@ -1170,12 +1184,24 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
function parseCommandsByLineForSudo(Collection $commands, Server $server): array
{
$commands = $commands->map(function ($line) {
- if (! str($line)->startsWith('cd') && ! str($line)->startsWith('command') && ! str($line)->startsWith('echo') && ! str($line)->startsWith('true')) {
+ if (! str(trim($line))->startsWith([
+ 'cd',
+ 'command',
+ 'echo',
+ 'true',
+ 'if',
+ 'fi',
+ ])) {
return "sudo $line";
}
+ if (str(trim($line))->startsWith('if')) {
+ return str_replace('if', 'if sudo', $line);
+ }
+
return $line;
});
+
$commands = $commands->map(function ($line) use ($server) {
if (Str::startsWith($line, 'sudo mkdir -p')) {
return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p');
@@ -1183,6 +1209,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array
return $line;
});
+
$commands = $commands->map(function ($line) {
$line = str($line);
if (str($line)->contains('$(')) {
@@ -1227,8 +1254,6 @@ function parseLineForSudo(string $command, Server $server): string
function get_public_ips()
{
try {
- echo "Refreshing public ips!\n";
- $settings = \App\Models\InstanceSettings::get();
[$first, $second] = Process::concurrently(function (Pool $pool) {
$pool->path(__DIR__)->command('curl -4s https://ifconfig.io');
$pool->path(__DIR__)->command('curl -6s https://ifconfig.io');
@@ -1242,8 +1267,12 @@ function get_public_ips()
return;
}
- $settings->update(['public_ipv4' => $ipv4]);
+ InstanceSettings::get()->update(['public_ipv4' => $ipv4]);
}
+ } catch (\Exception $e) {
+ echo "Error: {$e->getMessage()}\n";
+ }
+ try {
$ipv6 = $second->output();
if ($ipv6) {
$ipv6 = trim($ipv6);
@@ -1253,7 +1282,7 @@ function get_public_ips()
return;
}
- $settings->update(['public_ipv6' => $ipv6]);
+ InstanceSettings::get()->update(['public_ipv6' => $ipv6]);
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
@@ -2100,16 +2129,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) {
@@ -2921,10 +2950,11 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}
$parsedServices = collect([]);
- ray()->clearAll();
+ // ray()->clearAll();
$allMagicEnvironments = collect([]);
foreach ($services as $serviceName => $service) {
+ $predefinedPort = null;
$magicEnvironments = collect([]);
$image = data_get_str($service, 'image');
$environment = collect(data_get($service, 'environment', []));
@@ -2933,6 +2963,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$isDatabase = isDatabaseImage(data_get_str($service, 'image'));
if ($isService) {
+ $containerName = "$serviceName-{$resource->uuid}";
+
+ if ($serviceName === 'registry') {
+ $tempServiceName = 'docker-registry';
+ } else {
+ $tempServiceName = $serviceName;
+ }
+ if (str(data_get($service, 'image'))->contains('glitchtip')) {
+ $tempServiceName = 'glitchtip';
+ }
+ if ($serviceName === 'supabase-kong') {
+ $tempServiceName = 'supabase';
+ }
+ $serviceDefinition = data_get($allServices, $tempServiceName);
+ $predefinedPort = data_get($serviceDefinition, 'port');
+ if ($serviceName === 'plausible') {
+ $predefinedPort = '8000';
+ }
if ($isDatabase) {
$savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName,
@@ -2982,7 +3030,13 @@ 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();
+ $port = $key->afterLast('_')->value();
+ } else {
+ $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
+ $port = null;
+ }
if ($isApplication) {
$fqdn = generateFqdn($server, "{$resource->name}-$uuid");
} elseif ($isService) {
@@ -2992,19 +3046,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$fqdn = generateFqdn($server, "{$savedService->name}-$uuid");
}
}
+
if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) {
$path = $value->value();
if ($path !== '/') {
$fqdn = "$fqdn$path";
}
}
+ $fqdnWithPort = $fqdn;
+ if ($port) {
+ $fqdnWithPort = "$fqdn:$port";
+ }
if ($isApplication && is_null($resource->fqdn)) {
data_forget($resource, 'environment_variables');
data_forget($resource, 'environment_variables_preview');
- $resource->fqdn = $fqdn;
+ $resource->fqdn = $fqdnWithPort;
$resource->save();
} elseif ($isService && is_null($savedService->fqdn)) {
- $savedService->fqdn = $fqdn;
+ $savedService->fqdn = $fqdnWithPort;
$savedService->save();
}
@@ -3033,7 +3092,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}
$allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments);
-
if ($magicEnvironments->count() > 0) {
foreach ($magicEnvironments as $key => $value) {
$key = str($key);
@@ -3224,12 +3282,19 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') {
$volume = $source->value().':'.$target->value();
} else {
- $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid);
+ if ((int) $resource->compose_parsing_version >= 4) {
+ if ($isApplication) {
+ $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid);
+ } elseif ($isService) {
+ $mainDirectory = str(base_configuration_dir().'/services/'.$uuid);
+ }
+ } else {
+ $mainDirectory = str(base_configuration_dir().'/applications/'.$uuid);
+ }
$source = replaceLocalSource($source, $mainDirectory);
if ($isApplication && $isPullRequest) {
$source = $source."-pr-$pullRequestId";
}
-
LocalFileVolume::updateOrCreate(
[
'mount_path' => $target,
@@ -3245,6 +3310,17 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'resource_type' => get_class($originalResource),
]
);
+ if (isDev()) {
+ if ((int) $resource->compose_parsing_version >= 4) {
+ if ($isApplication) {
+ $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid);
+ } elseif ($isService) {
+ $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid);
+ }
+ } else {
+ $source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid);
+ }
+ }
$volume = "$source:$target";
}
} elseif ($type->value() === 'volume') {
@@ -3449,6 +3525,18 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$value = $value->after('?');
}
if ($originalValue->value() === $value->value()) {
+ // This means the variable does not have a default value, so it needs to be created in Coolify
+ $parsedKeyValue = replaceVariables($value);
+ $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->firstOrCreate([
+ 'key' => $parsedKeyValue,
+ $nameOfId => $resource->id,
+ ], [
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
+ // Add the variable to the environment so it will be shown in the deployable compose file
+ $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value;
+
continue;
}
$resource->environment_variables()->where('key', $key)->where($nameOfId, $resource->id)->firstOrCreate([
@@ -3469,13 +3557,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) {
@@ -3541,6 +3629,17 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($environment->count() > 0) {
$environment = $environment->filter(function ($value, $key) {
return ! str($key)->startsWith('SERVICE_FQDN_');
+ })->map(function ($value, $key) use ($resource) {
+ // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used
+ if (str($value)->isEmpty()) {
+ if ($resource->environment_variables()->where('key', $key)->exists()) {
+ $value = $resource->environment_variables()->where('key', $key)->first()->value;
+ } else {
+ $value = null;
+ }
+ }
+
+ return $value;
});
}
$serviceLabels = $labels->merge($defaultLabels);
@@ -3548,7 +3647,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 +3657,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()) {
@@ -3625,6 +3724,14 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
data_forget($service, 'volumes.*.is_directory');
data_forget($service, 'exclude_from_hc');
+ $volumesParsed = $volumesParsed->map(function ($volume) {
+ data_forget($volume, 'content');
+ data_forget($volume, 'is_directory');
+ data_forget($volume, 'isDirectory');
+
+ return $volume;
+ });
+
$payload = collect($service)->merge([
'container_name' => $containerName,
'restart' => $restart->value(),
@@ -3655,6 +3762,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$parsedServices->put($serviceName, $payload);
}
$topLevel->put('services', $parsedServices);
+
$customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets'];
$topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) {
@@ -3723,30 +3831,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}\"");
}
}
}
@@ -3755,6 +3863,21 @@ function convertComposeEnvironmentToArray($environment)
{
$convertedServiceVariables = collect([]);
if (isAssociativeArray($environment)) {
+ if ($environment instanceof Collection) {
+ $changedEnvironment = collect([]);
+ $environment->each(function ($value, $key) use ($changedEnvironment) {
+ $parts = explode('=', $value, 2);
+ if (count($parts) === 2) {
+ $key = $parts[0];
+ $realValue = $parts[1] ?? '';
+ $changedEnvironment->put($key, $realValue);
+ } else {
+ $changedEnvironment->put($key, $value);
+ }
+ });
+
+ return $changedEnvironment;
+ }
$convertedServiceVariables = $environment;
} else {
foreach ($environment as $value) {
@@ -3770,3 +3893,7 @@ function convertComposeEnvironmentToArray($environment)
return $convertedServiceVariables;
}
+function instanceSettings()
+{
+ return InstanceSettings::get();
+}
diff --git a/composer.json b/composer.json
index e8b46105d..03adf9823 100644
--- a/composer.json
+++ b/composer.json
@@ -48,6 +48,7 @@
"zircote/swagger-php": "^4.10"
},
"require-dev": {
+ "barryvdh/laravel-debugbar": "^3.13",
"fakerphp/faker": "^v1.21.0",
"laravel/dusk": "^v8.0",
"laravel/pint": "^1.16",
@@ -84,7 +85,11 @@
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"Illuminate\\Foundation\\ComposerScripts::postUpdate"
],
- "post-install-cmd": [],
+ "post-install-cmd": [
+ "cp -r 'hooks/' '.git/hooks/'",
+ "php -r \"copy('hooks/pre-commit', '.git/hooks/pre-commit');\"",
+ "php -r \"chmod('.git/hooks/pre-commit', 0777);\""
+ ],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
diff --git a/composer.lock b/composer.lock
index fffb320d3..420d87ec0 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "96f8146407d0e6e897ff097c5eccd3a4",
+ "content-hash": "42c28ab141b70fcabf75b51afa96c670",
"packages": [
{
"name": "amphp/amp",
@@ -11823,6 +11823,90 @@
}
],
"packages-dev": [
+ {
+ "name": "barryvdh/laravel-debugbar",
+ "version": "v3.13.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/barryvdh/laravel-debugbar.git",
+ "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/92d86be45ee54edff735e46856f64f14b6a8bb07",
+ "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/routing": "^9|^10|^11",
+ "illuminate/session": "^9|^10|^11",
+ "illuminate/support": "^9|^10|^11",
+ "maximebf/debugbar": "~1.22.0",
+ "php": "^8.0",
+ "symfony/finder": "^6|^7"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.3.3",
+ "orchestra/testbench-dusk": "^5|^6|^7|^8|^9",
+ "phpunit/phpunit": "^9.6|^10.5",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.13-dev"
+ },
+ "laravel": {
+ "providers": [
+ "Barryvdh\\Debugbar\\ServiceProvider"
+ ],
+ "aliases": {
+ "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar"
+ }
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "Barryvdh\\Debugbar\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Barry vd. Heuvel",
+ "email": "barryvdh@gmail.com"
+ }
+ ],
+ "description": "PHP Debugbar integration for Laravel",
+ "keywords": [
+ "debug",
+ "debugbar",
+ "laravel",
+ "profiler",
+ "webprofiler"
+ ],
+ "support": {
+ "issues": "https://github.com/barryvdh/laravel-debugbar/issues",
+ "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.13.5"
+ },
+ "funding": [
+ {
+ "url": "https://fruitcake.nl",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/barryvdh",
+ "type": "github"
+ }
+ ],
+ "time": "2024-04-12T11:20:37+00:00"
+ },
{
"name": "brianium/paratest",
"version": "v7.4.3",
@@ -12301,6 +12385,74 @@
},
"time": "2024-09-03T15:00:28+00:00"
},
+ {
+ "name": "maximebf/debugbar",
+ "version": "v1.22.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/maximebf/php-debugbar.git",
+ "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/1b5cabe0ce013134cf595bfa427bbf2f6abcd989",
+ "reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2|^8",
+ "psr/log": "^1|^2|^3",
+ "symfony/var-dumper": "^4|^5|^6|^7"
+ },
+ "require-dev": {
+ "dbrekelmans/bdi": "^1",
+ "phpunit/phpunit": "^8|^9",
+ "symfony/panther": "^1|^2.1",
+ "twig/twig": "^1.38|^2.7|^3.0"
+ },
+ "suggest": {
+ "kriswallsmith/assetic": "The best way to manage assets",
+ "monolog/monolog": "Log using Monolog",
+ "predis/predis": "Redis storage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.22-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "DebugBar\\": "src/DebugBar/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Maxime Bouroumeau-Fuseau",
+ "email": "maxime.bouroumeau@gmail.com",
+ "homepage": "http://maximebf.com"
+ },
+ {
+ "name": "Barry vd. Heuvel",
+ "email": "barryvdh@gmail.com"
+ }
+ ],
+ "description": "Debug bar in the browser for php application",
+ "homepage": "https://github.com/maximebf/php-debugbar",
+ "keywords": [
+ "debug",
+ "debugbar"
+ ],
+ "support": {
+ "issues": "https://github.com/maximebf/php-debugbar/issues",
+ "source": "https://github.com/maximebf/php-debugbar/tree/v1.22.5"
+ },
+ "time": "2024-09-09T08:05:55+00:00"
+ },
{
"name": "mockery/mockery",
"version": "1.6.12",
diff --git a/config/clockwork.php b/config/clockwork.php
deleted file mode 100644
index ce880464a..000000000
--- a/config/clockwork.php
+++ /dev/null
@@ -1,424 +0,0 @@
- env('CLOCKWORK_ENABLE', null),
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Features
- |------------------------------------------------------------------------------------------------------------------
- |
- | You can enable or disable various Clockwork features here. Some features have additional settings (eg. slow query
- | threshold for database queries).
- |
- */
-
- 'features' => [
-
- // Cache usage stats and cache queries including results
- 'cache' => [
- 'enabled' => env('CLOCKWORK_CACHE_ENABLED', true),
-
- // Collect cache queries
- 'collect_queries' => env('CLOCKWORK_CACHE_QUERIES', true),
-
- // Collect values from cache queries (high performance impact with a very high number of queries)
- 'collect_values' => env('CLOCKWORK_CACHE_COLLECT_VALUES', false)
- ],
-
- // Database usage stats and queries
- 'database' => [
- 'enabled' => env('CLOCKWORK_DATABASE_ENABLED', true),
-
- // Collect database queries (high performance impact with a very high number of queries)
- 'collect_queries' => env('CLOCKWORK_DATABASE_COLLECT_QUERIES', true),
-
- // Collect details of models updates (high performance impact with a lot of model updates)
- 'collect_models_actions' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_ACTIONS', true),
-
- // Collect details of retrieved models (very high performance impact with a lot of models retrieved)
- 'collect_models_retrieved' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_RETRIEVED', false),
-
- // Query execution time threshold in milliseconds after which the query will be marked as slow
- 'slow_threshold' => env('CLOCKWORK_DATABASE_SLOW_THRESHOLD'),
-
- // Collect only slow database queries
- 'slow_only' => env('CLOCKWORK_DATABASE_SLOW_ONLY', false),
-
- // Detect and report duplicate queries
- 'detect_duplicate_queries' => env('CLOCKWORK_DATABASE_DETECT_DUPLICATE_QUERIES', false)
- ],
-
- // Dispatched events
- 'events' => [
- 'enabled' => env('CLOCKWORK_EVENTS_ENABLED', true),
-
- // Ignored events (framework events are ignored by default)
- 'ignored_events' => [
- // App\Events\UserRegistered::class,
- // 'user.registered'
- ],
- ],
-
- // Laravel log (you can still log directly to Clockwork with laravel log disabled)
- 'log' => [
- 'enabled' => env('CLOCKWORK_LOG_ENABLED', true)
- ],
-
- // Sent notifications
- 'notifications' => [
- 'enabled' => env('CLOCKWORK_NOTIFICATIONS_ENABLED', true),
- ],
-
- // Performance metrics
- 'performance' => [
- // Allow collecting of client metrics. Requires separate clockwork-browser npm package.
- 'client_metrics' => env('CLOCKWORK_PERFORMANCE_CLIENT_METRICS', true)
- ],
-
- // Dispatched queue jobs
- 'queue' => [
- 'enabled' => env('CLOCKWORK_QUEUE_ENABLED', true)
- ],
-
- // Redis commands
- 'redis' => [
- 'enabled' => env('CLOCKWORK_REDIS_ENABLED', true)
- ],
-
- // Routes list
- 'routes' => [
- 'enabled' => env('CLOCKWORK_ROUTES_ENABLED', false),
-
- // Collect only routes from particular namespaces (only application routes by default)
- 'only_namespaces' => [ 'App' ]
- ],
-
- // Rendered views
- 'views' => [
- 'enabled' => env('CLOCKWORK_VIEWS_ENABLED', true),
-
- // Collect views including view data (high performance impact with a high number of views)
- 'collect_data' => env('CLOCKWORK_VIEWS_COLLECT_DATA', false),
-
- // Use Twig profiler instead of Laravel events for apps using laravel-twigbridge (more precise, but does
- // not support collecting view data)
- 'use_twig_profiler' => env('CLOCKWORK_VIEWS_USE_TWIG_PROFILER', false)
- ]
-
- ],
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Enable web UI
- |------------------------------------------------------------------------------------------------------------------
- |
- | Clockwork comes with a web UI accessible via http://your.app/clockwork. Here you can enable or disable this
- | feature. You can also set a custom path for the web UI.
- |
- */
-
- 'web' => env('CLOCKWORK_WEB', true),
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Enable toolbar
- |------------------------------------------------------------------------------------------------------------------
- |
- | Clockwork can show a toolbar with basic metrics on all responses. Here you can enable or disable this feature.
- | Requires a separate clockwork-browser npm library.
- | For installation instructions see https://underground.works/clockwork/#docs-viewing-data
- |
- */
-
- 'toolbar' => env('CLOCKWORK_TOOLBAR', true),
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | HTTP requests collection
- |------------------------------------------------------------------------------------------------------------------
- |
- | Clockwork collects data about HTTP requests to your app. Here you can choose which requests should be collected.
- |
- */
-
- 'requests' => [
- // With on-demand mode enabled, Clockwork will only profile requests when the browser extension is open or you
- // manually pass a "clockwork-profile" cookie or get/post data key.
- // Optionally you can specify a "secret" that has to be passed as the value to enable profiling.
- 'on_demand' => env('CLOCKWORK_REQUESTS_ON_DEMAND', false),
-
- // Collect only errors (requests with HTTP 4xx and 5xx responses)
- 'errors_only' => env('CLOCKWORK_REQUESTS_ERRORS_ONLY', false),
-
- // Response time threshold in milliseconds after which the request will be marked as slow
- 'slow_threshold' => env('CLOCKWORK_REQUESTS_SLOW_THRESHOLD'),
-
- // Collect only slow requests
- 'slow_only' => env('CLOCKWORK_REQUESTS_SLOW_ONLY', false),
-
- // Sample the collected requests (e.g. set to 100 to collect only 1 in 100 requests)
- 'sample' => env('CLOCKWORK_REQUESTS_SAMPLE', false),
-
- // List of URIs that should not be collected
- 'except' => [
- '/horizon/.*', // Laravel Horizon requests
- '/telescope/.*', // Laravel Telescope requests
- '/_tt/.*', // Laravel Telescope toolbar
- '/_debugbar/.*', // Laravel DebugBar requests
- ],
-
- // List of URIs that should be collected, any other URI will not be collected if not empty
- 'only' => [
- // '/api/.*'
- ],
-
- // Don't collect OPTIONS requests, mostly used in the CSRF pre-flight requests and are rarely of interest
- 'except_preflight' => env('CLOCKWORK_REQUESTS_EXCEPT_PREFLIGHT', true)
- ],
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Artisan commands collection
- |------------------------------------------------------------------------------------------------------------------
- |
- | Clockwork can collect data about executed artisan commands. Here you can enable and configure which commands
- | should be collected.
- |
- */
-
- 'artisan' => [
- // Enable or disable collection of executed Artisan commands
- 'collect' => env('CLOCKWORK_ARTISAN_COLLECT', false),
-
- // List of commands that should not be collected (built-in commands are not collected by default)
- 'except' => [
- // 'inspire'
- ],
-
- // List of commands that should be collected, any other command will not be collected if not empty
- 'only' => [
- // 'inspire'
- ],
-
- // Enable or disable collection of command output
- 'collect_output' => env('CLOCKWORK_ARTISAN_COLLECT_OUTPUT', false),
-
- // Enable or disable collection of built-in Laravel commands
- 'except_laravel_commands' => env('CLOCKWORK_ARTISAN_EXCEPT_LARAVEL_COMMANDS', true)
- ],
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Queue jobs collection
- |------------------------------------------------------------------------------------------------------------------
- |
- | Clockwork can collect data about executed queue jobs. Here you can enable and configure which queue jobs should
- | be collected.
- |
- */
-
- 'queue' => [
- // Enable or disable collection of executed queue jobs
- 'collect' => env('CLOCKWORK_QUEUE_COLLECT', false),
-
- // List of queue jobs that should not be collected
- 'except' => [
- // App\Jobs\ExpensiveJob::class
- ],
-
- // List of queue jobs that should be collected, any other queue job will not be collected if not empty
- 'only' => [
- // App\Jobs\BuggyJob::class
- ]
- ],
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Tests collection
- |------------------------------------------------------------------------------------------------------------------
- |
- | Clockwork can collect data about executed tests. Here you can enable and configure which tests should be
- | collected.
- |
- */
-
- 'tests' => [
- // Enable or disable collection of ran tests
- 'collect' => env('CLOCKWORK_TESTS_COLLECT', false),
-
- // List of tests that should not be collected
- 'except' => [
- // Tests\Unit\ExampleTest::class
- ]
- ],
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Enable data collection when Clockwork is disabled
- |------------------------------------------------------------------------------------------------------------------
- |
- | You can enable this setting to collect data even when Clockwork is disabled, e.g. for future analysis.
- |
- */
-
- 'collect_data_always' => env('CLOCKWORK_COLLECT_DATA_ALWAYS', false),
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Metadata storage
- |------------------------------------------------------------------------------------------------------------------
- |
- | Configure how is the metadata collected by Clockwork stored. Three options are available:
- | - files - A simple fast storage implementation storing data in one-per-request files.
- | - sql - Stores requests in a sql database. Supports MySQL, PostgreSQL and SQLite. Requires PDO.
- | - redis - Stores requests in redis. Requires phpredis.
- */
-
- 'storage' => env('CLOCKWORK_STORAGE', 'files'),
-
- // Path where the Clockwork metadata is stored
- 'storage_files_path' => env('CLOCKWORK_STORAGE_FILES_PATH', storage_path('clockwork')),
-
- // Compress the metadata files using gzip, trading a little bit of performance for lower disk usage
- 'storage_files_compress' => env('CLOCKWORK_STORAGE_FILES_COMPRESS', false),
-
- // SQL database to use, can be a name of database configured in database.php or a path to a SQLite file
- 'storage_sql_database' => env('CLOCKWORK_STORAGE_SQL_DATABASE', storage_path('clockwork.sqlite')),
-
- // SQL table name to use, the table is automatically created and updated when needed
- 'storage_sql_table' => env('CLOCKWORK_STORAGE_SQL_TABLE', 'clockwork'),
-
- // Redis connection, name of redis connection or cluster configured in database.php
- 'storage_redis' => env('CLOCKWORK_STORAGE_REDIS', 'default'),
-
- // Redis prefix for Clockwork keys ("clockwork" if not set)
- 'storage_redis_prefix' => env('CLOCKWORK_STORAGE_REDIS_PREFIX', 'clockwork'),
-
- // Maximum lifetime of collected metadata in minutes, older requests will automatically be deleted, false to disable
- 'storage_expiration' => env('CLOCKWORK_STORAGE_EXPIRATION', 60 * 24 * 7),
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Authentication
- |------------------------------------------------------------------------------------------------------------------
- |
- | Clockwork can be configured to require authentication before allowing access to the collected data. This might be
- | useful when the application is publicly accessible. Setting to true will enable a simple authentication with a
- | pre-configured password. You can also pass a class name of a custom implementation.
- |
- */
-
- 'authentication' => env('CLOCKWORK_AUTHENTICATION', false),
-
- // Password for the simple authentication
- 'authentication_password' => env('CLOCKWORK_AUTHENTICATION_PASSWORD', 'VerySecretPassword'),
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Stack traces collection
- |------------------------------------------------------------------------------------------------------------------
- |
- | Clockwork can collect stack traces for log messages and certain data like database queries. Here you can set
- | whether to collect stack traces, limit the number of collected frames and set further configuration. Collecting
- | long stack traces considerably increases metadata size.
- |
- */
-
- 'stack_traces' => [
- // Enable or disable collecting of stack traces
- 'enabled' => env('CLOCKWORK_STACK_TRACES_ENABLED', true),
-
- // Limit the number of frames to be collected
- 'limit' => env('CLOCKWORK_STACK_TRACES_LIMIT', 10),
-
- // List of vendor names to skip when determining caller, common vendors are automatically added
- 'skip_vendors' => [
- // 'phpunit'
- ],
-
- // List of namespaces to skip when determining caller
- 'skip_namespaces' => [
- // 'Laravel'
- ],
-
- // List of class names to skip when determining caller
- 'skip_classes' => [
- // App\CustomLog::class
- ]
-
- ],
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Serialization
- |------------------------------------------------------------------------------------------------------------------
- |
- | Clockwork serializes the collected data to json for storage and transfer. Here you can configure certain aspects
- | of serialization. Serialization has a large effect on the cpu time and memory usage.
- |
- */
-
- // Maximum depth of serialized multi-level arrays and objects
- 'serialization_depth' => env('CLOCKWORK_SERIALIZATION_DEPTH', 10),
-
- // A list of classes that will never be serialized (e.g. a common service container class)
- 'serialization_blackbox' => [
- \Illuminate\Container\Container::class,
- \Illuminate\Foundation\Application::class,
- \Laravel\Lumen\Application::class
- ],
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Register helpers
- |------------------------------------------------------------------------------------------------------------------
- |
- | Clockwork comes with a "clock" global helper function. You can use this helper to quickly log something and to
- | access the Clockwork instance.
- |
- */
-
- 'register_helpers' => env('CLOCKWORK_REGISTER_HELPERS', true),
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Send headers for AJAX request
- |------------------------------------------------------------------------------------------------------------------
- |
- | When trying to collect data, the AJAX method can sometimes fail if it is missing required headers. For example, an
- | API might require a version number using Accept headers to route the HTTP request to the correct codebase.
- |
- */
-
- 'headers' => [
- // 'Accept' => 'application/vnd.com.whatever.v1+json',
- ],
-
- /*
- |------------------------------------------------------------------------------------------------------------------
- | Server timing
- |------------------------------------------------------------------------------------------------------------------
- |
- | Clockwork supports the W3C Server Timing specification, which allows for collecting a simple performance metrics
- | in a cross-browser way. E.g. in Chrome, your app, database and timeline event timings will be shown in the Dev
- | Tools network tab. This setting specifies the max number of timeline events that will be sent. Setting to false
- | will disable the feature.
- |
- */
-
- 'server_timing' => env('CLOCKWORK_SERVER_TIMING', 10)
-
-];
diff --git a/config/constants.php b/config/constants.php
index 906ef3ba2..5792b358c 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -6,9 +6,8 @@
'contact' => 'https://coolify.io/docs/contact',
],
'ssh' => [
- // Using MUX
- 'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true), true),
- 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', '1h'),
+ 'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true)),
+ 'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', 3600),
'connection_timeout' => 10,
'server_interval' => 20,
'command_timeout' => 7200,
diff --git a/config/debugbar.php b/config/debugbar.php
new file mode 100644
index 000000000..eae406ba7
--- /dev/null
+++ b/config/debugbar.php
@@ -0,0 +1,325 @@
+ env('DEBUGBAR_ENABLED', null),
+ 'except' => [
+ 'telescope*',
+ 'horizon*',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Storage settings
+ |--------------------------------------------------------------------------
+ |
+ | DebugBar stores data for session/ajax requests.
+ | You can disable this, so the debugbar stores data in headers/session,
+ | but this can cause problems with large data collectors.
+ | By default, file storage (in the storage folder) is used. Redis and PDO
+ | can also be used. For PDO, run the package migrations first.
+ |
+ | Warning: Enabling storage.open will allow everyone to access previous
+ | request, do not enable open storage in publicly available environments!
+ | Specify a callback if you want to limit based on IP or authentication.
+ | Leaving it to null will allow localhost only.
+ */
+ 'storage' => [
+ 'enabled' => true,
+ 'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback.
+ 'driver' => 'file', // redis, file, pdo, socket, custom
+ 'path' => storage_path('debugbar'), // For file driver
+ 'connection' => null, // Leave null for default connection (Redis/PDO)
+ 'provider' => '', // Instance of StorageInterface for custom driver
+ 'hostname' => '127.0.0.1', // Hostname to use with the "socket" driver
+ 'port' => 2304, // Port to use with the "socket" driver
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Editor
+ |--------------------------------------------------------------------------
+ |
+ | Choose your preferred editor to use when clicking file name.
+ |
+ | Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote",
+ | "vscode-insiders-remote", "vscodium", "textmate", "emacs",
+ | "sublime", "atom", "nova", "macvim", "idea", "netbeans",
+ | "xdebug", "espresso"
+ |
+ */
+
+ 'editor' => env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'phpstorm'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Remote Path Mapping
+ |--------------------------------------------------------------------------
+ |
+ | If you are using a remote dev server, like Laravel Homestead, Docker, or
+ | even a remote VPS, it will be necessary to specify your path mapping.
+ |
+ | Leaving one, or both of these, empty or null will not trigger the remote
+ | URL changes and Debugbar will treat your editor links as local files.
+ |
+ | "remote_sites_path" is an absolute base path for your sites or projects
+ | in Homestead, Vagrant, Docker, or another remote development server.
+ |
+ | Example value: "/home/vagrant/Code"
+ |
+ | "local_sites_path" is an absolute base path for your sites or projects
+ | on your local computer where your IDE or code editor is running on.
+ |
+ | Example values: "/Users//Code", "C:\Users\\Documents\Code"
+ |
+ */
+
+ 'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH'),
+ 'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', env('IGNITION_LOCAL_SITES_PATH')),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Vendors
+ |--------------------------------------------------------------------------
+ |
+ | Vendor files are included by default, but can be set to false.
+ | This can also be set to 'js' or 'css', to only include javascript or css vendor files.
+ | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
+ | and for js: jquery and highlight.js
+ | So if you want syntax highlighting, set it to true.
+ | jQuery is set to not conflict with existing jQuery scripts.
+ |
+ */
+
+ 'include_vendors' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Capture Ajax Requests
+ |--------------------------------------------------------------------------
+ |
+ | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
+ | you can use this option to disable sending the data through the headers.
+ |
+ | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
+ |
+ | Note for your request to be identified as ajax requests they must either send the header
+ | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header.
+ |
+ | By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar.
+ | Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading.
+ */
+
+ 'capture_ajax' => true,
+ 'add_ajax_timing' => false,
+ 'ajax_handler_auto_show' => true,
+ 'ajax_handler_enable_tab' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Error Handler for Deprecated warnings
+ |--------------------------------------------------------------------------
+ |
+ | When enabled, the Debugbar shows deprecated warnings for Symfony components
+ | in the Messages tab.
+ |
+ */
+ 'error_handler' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Clockwork integration
+ |--------------------------------------------------------------------------
+ |
+ | The Debugbar can emulate the Clockwork headers, so you can use the Chrome
+ | Extension, without the server-side code. It uses Debugbar collectors instead.
+ |
+ */
+ 'clockwork' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | DataCollectors
+ |--------------------------------------------------------------------------
+ |
+ | Enable/disable DataCollectors
+ |
+ */
+
+ 'collectors' => [
+ 'phpinfo' => true, // Php version
+ 'messages' => true, // Messages
+ 'time' => true, // Time Datalogger
+ 'memory' => true, // Memory usage
+ 'exceptions' => true, // Exception displayer
+ 'log' => true, // Logs from Monolog (merged in messages if enabled)
+ 'db' => true, // Show database (PDO) queries and bindings
+ 'views' => true, // Views with their data
+ 'route' => true, // Current route information
+ 'auth' => false, // Display Laravel authentication status
+ 'gate' => true, // Display Laravel Gate checks
+ 'session' => true, // Display session data
+ 'symfony_request' => true, // Only one can be enabled..
+ 'mail' => true, // Catch mail messages
+ 'laravel' => false, // Laravel version and environment
+ 'events' => false, // All events fired
+ 'default_request' => false, // Regular or special Symfony request logger
+ 'logs' => false, // Add the latest log messages
+ 'files' => false, // Show the included files
+ 'config' => false, // Display config settings
+ 'cache' => false, // Display cache events
+ 'models' => true, // Display models
+ 'livewire' => true, // Display Livewire (when available)
+ 'jobs' => false, // Display dispatched jobs
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Extra options
+ |--------------------------------------------------------------------------
+ |
+ | Configure some DataCollectors
+ |
+ */
+
+ 'options' => [
+ 'time' => [
+ 'memory_usage' => false, // Calculated by subtracting memory start and end, it may be inaccurate
+ ],
+ 'messages' => [
+ 'trace' => true, // Trace the origin of the debug message
+ ],
+ 'memory' => [
+ 'reset_peak' => false, // run memory_reset_peak_usage before collecting
+ 'with_baseline' => false, // Set boot memory usage as memory peak baseline
+ 'precision' => 0, // Memory rounding precision
+ ],
+ 'auth' => [
+ 'show_name' => true, // Also show the users name/email in the debugbar
+ 'show_guards' => true, // Show the guards that are used
+ ],
+ 'db' => [
+ 'with_params' => true, // Render SQL with the parameters substituted
+ 'backtrace' => true, // Use a backtrace to find the origin of the query in your files.
+ 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults)
+ 'timeline' => false, // Add the queries to the timeline
+ 'duration_background' => true, // Show shaded background on each query relative to how long it took to execute.
+ 'explain' => [ // Show EXPLAIN output on queries
+ 'enabled' => false,
+ 'types' => ['SELECT'], // Deprecated setting, is always only SELECT
+ ],
+ 'hints' => false, // Show hints for common mistakes
+ 'show_copy' => false, // Show copy button next to the query,
+ 'slow_threshold' => false, // Only track queries that last longer than this time in ms
+ 'memory_usage' => false, // Show queries memory usage
+ 'soft_limit' => 100, // After the soft limit, no parameters/backtrace are captured
+ 'hard_limit' => 500, // After the hard limit, queries are ignored
+ ],
+ 'mail' => [
+ 'timeline' => false, // Add mails to the timeline
+ 'show_body' => true,
+ ],
+ 'views' => [
+ 'timeline' => false, // Add the views to the timeline (Experimental)
+ 'data' => false, //true for all data, 'keys' for only names, false for no parameters.
+ 'group' => 50, // Group duplicate views. Pass value to auto-group, or true/false to force
+ 'exclude_paths' => [ // Add the paths which you don't want to appear in the views
+ 'vendor/filament', // Exclude Filament components by default
+ ],
+ ],
+ 'route' => [
+ 'label' => true, // show complete route on bar
+ ],
+ 'session' => [
+ 'hiddens' => [], // hides sensitive values using array paths
+ ],
+ 'symfony_request' => [
+ 'hiddens' => [], // hides sensitive values using array paths, example: request_request.password
+ ],
+ 'events' => [
+ 'data' => false, // collect events data, listeners
+ ],
+ 'logs' => [
+ 'file' => null,
+ ],
+ 'cache' => [
+ 'values' => true, // collect cache values
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Inject Debugbar in Response
+ |--------------------------------------------------------------------------
+ |
+ | Usually, the debugbar is added just before