diff --git a/.env.development.example b/.env.development.example
index 920c32d92..3023a21a6 100644
--- a/.env.development.example
+++ b/.env.development.example
@@ -1,16 +1,38 @@
-APP_NAME=Coolify-localhost
-APP_ID=development
+# Coolify Configuration
APP_ENV=local
+APP_NAME="Coolify Development"
+APP_ID=development
APP_KEY=
-APP_DEBUG=true
APP_URL=http://localhost
APP_PORT=8000
-MUX_ENABLED=false
+APP_DEBUG=true
+SSH_MUX_ENABLED=false
+# PostgreSQL Database Configuration
+DB_DATABASE=coolify
+DB_USERNAME=coolify
+DB_PASSWORD=password
+DB_HOST=host.docker.internal
+DB_PORT=5432
+
+# Ray Configuration
+# Set to true to enable Ray
+RAY_ENABLED=false
+# Set custom ray port
+RAY_PORT=
+
+# Clockwork Configuration
+CLOCKWORK_ENABLED=false
+CLOCKWORK_QUEUE_COLLECT=true
+
+# Enable Laravel Telescope for debugging
+TELESCOPE_ENABLED=false
+
+# Selenium Driver URL for Dusk
DUSK_DRIVER_URL=http://selenium:4444
-## For Andras only
-# To purge cache
+# Special Keys for Andras
+# For cache purging
BUNNY_API_KEY=
-# To upload assets
+# For asset uploads
BUNNY_STORAGE_API_KEY=
diff --git a/.env.production b/.env.production
index f15a8b0e9..099ec7c25 100644
--- a/.env.production
+++ b/.env.production
@@ -1,10 +1,16 @@
+# Coolify Configuration
APP_ID=
APP_NAME=Coolify
APP_KEY=
+# PostgreSQL Database Configuration
+DB_USERNAME=coolify
DB_PASSWORD=
+
+# Redis Configuration
REDIS_PASSWORD=
+# Pusher Configuration
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml
index f3d52b1b4..42df4785e 100644
--- a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml
+++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml
@@ -1,46 +1,65 @@
-name: Bug report
-description: "Create a new bug report."
+name: 🐞 Bug Report
+description: "File a new bug report."
title: "[Bug]: "
+labels: ["🐛 Bug", "🔍 Triage"]
body:
- type: markdown
attributes:
- value: >-
- # 💎 Bounty program (with
- [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
+ value: |
+ > [!IMPORTANT]
+ > **Please ensure you are using the latest version of Coolify before submitting an issue, as the bug may have already been fixed in a recent update.** (Of course, if you're experiencing an issue on the latest version that wasn't present in a previous version, please let us know.)
+ # 💎 Bounty Program (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
+ - If you would like to prioritize the issue resolution, consider adding a bounty to this issue through our [Bounty Program](https://console.algora.io/org/coollabsio/bounties/new).
- If you would like to prioritize the issue resolution, you can add bounty
- to this issue.
-
-
- Click [here](https://console.algora.io/org/coollabsio/bounties/new) to
- get started.
- type: textarea
attributes:
- label: Description
- description: A clear and concise description of the problem
- - type: textarea
- attributes:
- label: Minimal Reproduction (if possible, example repository)
- description: Please provide a step by step guide to reproduce the issue.
+ label: Error Message and Logs
+ description: Provide a detailed description of the error or exception you encountered, along with any relevant log output.
validations:
required: true
+
- type: textarea
attributes:
- label: Exception or Error
- description: Please provide error logs if possible.
+ label: Steps to Reproduce
+ description: Please provide a step-by-step guide to reproduce the issue. Be as detailed as possible, otherwise we may not be able to assist you.
+ value: |
+ 1.
+ 2.
+ 3.
+ 4.
+ validations:
+ required: true
+
- type: input
attributes:
- label: Version
- description: Coolify's version (see top of your screen).
+ label: Example Repository URL
+ description: If applicable, provide a URL to a repository demonstrating the issue.
+
+ - type: input
+ attributes:
+ label: Coolify Version
+ description: Please provide the Coolify version you are using. This can be found in the top left corner of your Coolify dashboard.
+ placeholder: "v4.0.0-beta.335"
validations:
required: true
- - type: checkboxes
+
+ - type: dropdown
attributes:
- label: Cloud?
- description: "Are you using the cloud version of Coolify?"
+ label: Are you using Coolify Cloud?
options:
- - label: 'Yes'
- required: false
- - label: 'No'
- required: false
+ - "No (self-hosted)"
+ - "Yes (Coolify Cloud)"
+ validations:
+ required: true
+
+ - type: input
+ attributes:
+ label: Operating System and Version (self-hosted)
+ description: Run `cat /etc/os-release` or `lsb_release -a` in your terminal and provide the operating system and version.
+ placeholder: "Ubuntu 22.04"
+
+ - type: textarea
+ attributes:
+ label: Additional Information
+ description: Any other relevant details about the issue.
diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml b/.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml
new file mode 100644
index 000000000..ef26125e0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml
@@ -0,0 +1,31 @@
+name: 💎 Enhancement Bounty
+description: "Propose a new feature, service, or improvement with an attached bounty."
+title: "[Enhancement]: "
+labels: ["✨ Enhancement", "🔍 Triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ > [!IMPORTANT]
+ > **This issue template is exclusively for proposing new features, services, or improvements with an attached bounty.** Enhancements without a bounty can be discussed in the appropriate category of [Github Discussions](https://github.com/coollabsio/coolify/discussions).
+
+ # 💎 Add a Bounty (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
+ - [Click here to add the required bounty](https://console.algora.io/org/coollabsio/bounties/new)
+
+ - type: dropdown
+ attributes:
+ label: Request Type
+ description: Select the type of request you are making.
+ options:
+ - New Feature
+ - New Service
+ - Improvement
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Description
+ description: Provide a detailed description of the feature, improvement, or service you are proposing.
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 4f12f436c..92c48e2d6 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,18 @@
blank_issues_enabled: false
+
contact_links:
- - name: 🤔 Community Support (Chat)
+ - name: 🤔 Questions and Community Support
url: https://coollabs.io/discord
- about: Reach out to us on Discord.
- - name: 🙋♂️ Feature Requests
- url: https://github.com/coollabsio/coolify/discussions/categories/new-features
- about: All feature requests will be discussed here.
+ about: If you have any questions, reach out to us on Discord inside the "#support" channel.
+
+ - name: 💡 Feature Request
+ url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests
+ about: Suggest a new feature for Coolify.
+
+ - name: ⚙️ Service Request
+ url: https://github.com/coollabsio/coolify/discussions/categories/service-requests
+ about: Request a new service integration for Coolify.
+
+ - name: 🔧 Improvements
+ url: https://github.com/coollabsio/coolify/discussions/categories/improvements
+ about: Suggest improvements to existing features for Coolify.
diff --git a/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml
index d9921b363..4add8516e 100644
--- a/.github/workflows/coolify-helper-next.yml
+++ b/.github/workflows/coolify-helper-next.yml
@@ -25,6 +25,10 @@ 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.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
@@ -33,7 +37,9 @@ jobs:
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ labels: |
+ coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
@@ -47,6 +53,10 @@ 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.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
@@ -55,7 +65,9 @@ jobs:
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
+ labels: |
+ coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -75,10 +87,15 @@ 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.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest
run: |
- docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
+ 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 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
+
diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml
index 7e8132ec6..fd4be2f11 100644
--- a/.github/workflows/coolify-helper.yml
+++ b/.github/workflows/coolify-helper.yml
@@ -25,6 +25,10 @@ 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.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
@@ -33,7 +37,9 @@ jobs:
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ labels: |
+ coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
@@ -47,6 +53,10 @@ 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.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
@@ -55,7 +65,9 @@ jobs:
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ labels: |
+ coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -75,9 +87,13 @@ 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.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest
run: |
- docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ 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:
diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml
new file mode 100644
index 000000000..75e3f1681
--- /dev/null
+++ b/.github/workflows/coolify-realtime.yml
@@ -0,0 +1,103 @@
+name: Coolify Realtime (v4)
+
+on:
+ push:
+ branches: [ "main", "next" ]
+ paths:
+ - .github/workflows/coolify-realtime.yml
+ - docker/coolify-realtime/Dockerfile
+ - docker/coolify-realtime/terminal-server.js
+ - docker/coolify-realtime/package.json
+ - docker/coolify-realtime/soketi-entrypoint.sh
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: "coollabsio/coolify-realtime"
+
+jobs:
+ amd64:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Login to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
+ - name: Build image and push to registry
+ uses: docker/build-push-action@v5
+ with:
+ no-cache: true
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ labels: |
+ coolify.managed=true
+ aarch64:
+ runs-on: [ self-hosted, arm64 ]
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Login to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
+ - name: Build image and push to registry
+ uses: docker/build-push-action@v5
+ with:
+ no-cache: true
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ labels: |
+ coolify.managed=true
+ merge-manifest:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [ amd64, aarch64 ]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Login to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
+ - name: Create & publish manifest
+ run: |
+ docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ - uses: sarisia/actions-status-discord@v1
+ if: always()
+ with:
+ webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml
index cf2fae8f3..d7a680170 100644
--- a/.github/workflows/pr-build.yml
+++ b/.github/workflows/pr-build.yml
@@ -16,6 +16,12 @@ env:
jobs:
amd64:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ attestations: write
+ id-token: write
+ actions: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
@@ -37,6 +43,9 @@ jobs:
permissions:
contents: read
packages: write
+ attestations: write
+ id-token: write
+ actions: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
@@ -58,6 +67,9 @@ jobs:
permissions:
contents: read
packages: write
+ attestations: write
+ id-token: write
+ actions: write
needs: [amd64, aarch64]
steps:
- name: Checkout
diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml
index e4bad6a65..c78c865bf 100644
--- a/.github/workflows/production-build.yml
+++ b/.github/workflows/production-build.yml
@@ -4,6 +4,8 @@ on:
push:
branches: ["main"]
paths-ignore:
+ - .github/workflows/coolify-helper.yml
+ - docker/coolify-helper/Dockerfile
- templates/service-templates.json
env:
diff --git a/.github/workflows/remove-labels-and-assignees-on-close.yml b/.github/workflows/remove-labels-and-assignees-on-close.yml
new file mode 100644
index 000000000..04d62623c
--- /dev/null
+++ b/.github/workflows/remove-labels-and-assignees-on-close.yml
@@ -0,0 +1,75 @@
+name: Remove Labels and Assignees on Issue Close
+
+on:
+ issues:
+ types: [closed]
+ pull_request:
+ types: [closed]
+ pull_request_target:
+ types: [closed]
+
+jobs:
+ remove-labels-and-assignees:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Remove labels and assignees
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { owner, repo } = context.repo;
+
+ async function processIssue(issueNumber) {
+ try {
+ const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
+ owner,
+ repo,
+ issue_number: issueNumber
+ });
+
+ const labelsToKeep = currentLabels
+ .filter(label => label.name === '⏱︎ Stale')
+ .map(label => label.name);
+
+ await github.rest.issues.setLabels({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ labels: labelsToKeep
+ });
+
+ const { data: issue } = await github.rest.issues.get({
+ owner,
+ repo,
+ issue_number: issueNumber
+ });
+
+ if (issue.assignees && issue.assignees.length > 0) {
+ await github.rest.issues.removeAssignees({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ assignees: issue.assignees.map(assignee => assignee.login)
+ });
+ }
+ } catch (error) {
+ if (error.status !== 404) {
+ console.error(`Error processing issue ${issueNumber}:`, error);
+ }
+ }
+ }
+
+ if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
+ const issue = context.payload.issue || context.payload.pull_request;
+ await processIssue(issue.number);
+ }
+
+ if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
+ const { data: closedIssues } = await github.rest.search.issuesAndPullRequests({
+ q: `repo:${owner}/${repo} is:issue is:closed linked:${context.payload.pull_request.number}`,
+ per_page: 100
+ });
+ for (const issue of closedIssues.items) {
+ await processIssue(issue.number);
+ }
+ }
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..590360ddb
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,207 @@
+# 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
+
+1. [Setup Development Environment](#1-setup-development-environment)
+2. [Verify Installation](#2-verify-installation-optional)
+3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
+4. [Set up Environment Variables](#4-set-up-environment-variables)
+5. [Start Coolify](#5-start-coolify)
+6. [Start Development](#6-start-development)
+7. [Development Notes](#7-development-notes)
+8. [Create a Pull Request](#8-create-a-pull-request)
+9. [Additional Contribution Guidelines](#additional-contribution-guidelines)
+
+## 1. Setup Development Environment
+
+Follow the steps below for your operating system:
+
+
+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/)
+ - 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/)
+ - 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)
+
+
+
+
+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)
+ - Docker Desktop:
+ - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/)
+
+2. Install Spin:
+ - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin)
+
+
+
+
+Linux
+
+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
+ - Docker Desktop:
+ - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/)
+
+2. Install Spin:
+ - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions)
+
+
+
+## 2. Verify Installation (Optional)
+
+After installing Docker (or Orbstack) and Spin, verify the installation:
+
+1. Open a terminal or command prompt
+2. Run the following commands:
+ ```bash
+ docker --version
+ spin --version
+ ```
+ You should see version information for both Docker and Spin.
+
+## 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 (choose one):
+
+ | Editor | Platform | Download Link |
+ |--------|----------|---------------|
+ | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download) |
+ | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/) |
+ | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download) |
+
+3. Clone the Coolify Repository from your fork to your local machine
+ - Use `git clone` in the command line, or
+ - Use GitHub Desktop (recommended):
+ - Download and install from [https://desktop.github.com/](https://desktop.github.com/)
+ - Open GitHub Desktop and login with your GitHub account
+ - 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.
+
+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.
+
+## 6. Start Development
+
+1. Access your Coolify instance:
+ - URL: `http://localhost:8000`
+ - Login: `test@example.com`
+ - Password: `password`
+
+2. Additional development tools:
+ | 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 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.
+
+## 8. Create a Pull Request
+
+1. After making changes or adding a new service:
+ - Commit your changes to your forked repository.
+ - Push the changes to your GitHub account.
+
+2. Creating the Pull Request (PR):
+ - Navigate to the main Coolify repository on GitHub.
+ - Click the "Pull requests" tab.
+ - Click the green "New pull request" button.
+ - Choose your fork and branch as the compare branch.
+ - Click "Create pull request".
+
+3. Filling out the PR details:
+ - Give your PR a descriptive title.
+ - In the description, explain the changes you've made.
+ - Reference any related issues by using keywords like "Fixes #123" or "Closes #456".
+
+> [!IMPORTANT]
+> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch.
+
+4. Submit your PR:
+ - Review your changes one last time.
+ - Click "Create pull request" to submit.
+
+> [!NOTE]
+> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers.
+
+After submission, maintainers will review your PR and may request changes or provide feedback.
+
+## 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/CONTRIBUTION.md b/CONTRIBUTION.md
deleted file mode 100644
index 02a21573c..000000000
--- a/CONTRIBUTION.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Contributing
-
-> "First, thanks for considering to contribute 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 `#contribution` channel.
-
-## Code Contribution
-
-### 1) Setup your development environment
-
-- You need to have Docker Engine (or equivalent) [installed](https://docs.docker.com/engine/install/) on your system.
-- For better DX, install [Spin](https://serversideup.net/open-source/spin/).
-
-### 2) Set your environment variables
-
-- Copy [.env.development.example](./.env.development.example) to .env.
-
-## 3) Start & setup Coolify
-
-- Run `spin up` - You can notice that errors will be thrown. Don't worry.
- - If you see weird permission errors, especially on Mac, run `sudo spin up` instead.
-
-### 4) Start development
-You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
-
-Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user.
-
-Mails are caught by Mailpit: `localhost:8025`
-
-## New Service Contribution
-Check out the docs [here](https://coolify.io/docs/knowledge-base/add-a-service).
-
diff --git a/README.md b/README.md
index ce6289d48..14a741088 100644
--- a/README.md
+++ b/README.md
@@ -35,20 +35,32 @@ Thank you so much!
Special thanks to our biggest sponsors!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+### Special Sponsors
+
+
+
+* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry.
+* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions.
+* [Logto](https://logto.io/?ref=coolify) - An open-source authentication and authorization solution for building secure login systems and managing user identities.
+* [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions and online business growth strategies.
+* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
+* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
+* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
+* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
+* [Fractal Networks](https://fractalnetworks.co/?ref=coolify.io) - A decentralized network infrastructure company focusing on secure and private communication solutions.
+* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies.
+* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
+* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers.
+* [Latitude](https://latitude.sh/?ref=coolify.io) - A cloud computing platform offering bare metal servers and cloud instances for developers and businesses.
+* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities.
+* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries.
+* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
+* [Glueops](https://www.glueops.dev/?ref=coolify.io) - A DevOps consulting company providing infrastructure automation and cloud optimization services.
+* [Ubicloud](https://ubicloud.com/?ref=coolify.io) - An open-source alternative to hyperscale cloud providers, offering high-performance cloud computing services.
+* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses.
+* [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly.
+* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
+
## Github Sponsors ($40+)
@@ -71,8 +83,11 @@ Special thanks to our biggest sponsors!
+
+
+
## Organizations
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 000000000..2cb96b72b
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,45 @@
+# 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.
+
+## Release Process
+
+1. **Development on `next` or separate branches**
+ - Changes, fixes and new features are developed on the `next` or even separate branches.
+
+2. **Merging to `main`**
+ - Once changes are ready, they are merged from `next` 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.
+
+4. **Creating a GitHub release**
+ - A new release is created on GitHub with the new version details.
+
+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)
+
+> [!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.
+
+
+## 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).
+
+> [!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.
+
+## Manually Update to Specific Versions
+
+> [!CAUTION]
+> Updating to unreleased versions is not recommended and may cause issues. Use at your own risk!
+
+To update your Coolify instance to a specific (unreleased) version, use the following command:
+
+```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`).
diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php
index e97c55930..6d0063749 100644
--- a/app/Actions/Database/StartClickhouse.php
+++ b/app/Actions/Database/StartClickhouse.php
@@ -79,14 +79,7 @@ class StartClickhouse
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -102,6 +95,11 @@ class StartClickhouse
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -162,6 +160,8 @@ class StartClickhouse
$environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
}
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index 862fc54fc..621834df0 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -23,7 +23,7 @@ class StartDragonfly
$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}.'",
@@ -75,18 +75,11 @@ class StartDragonfly
],
],
];
- 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()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -102,6 +95,11 @@ class StartDragonfly
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -120,10 +118,10 @@ class StartDragonfly
$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,7 +152,7 @@ class StartDragonfly
$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 85cb89c1c..9290efc7c 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -24,7 +24,7 @@ class StartKeydb
$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,18 +74,11 @@ class StartKeydb
],
],
];
- 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()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -101,15 +94,19 @@ class StartKeydb
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,
];
$docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -128,10 +125,10 @@ class StartKeydb
$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;
}
}
@@ -162,10 +159,12 @@ class StartKeydb
$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}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index 33948192b..f37a5e361 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -21,7 +21,7 @@ class StartMariadb
$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,18 +69,11 @@ class StartMariadb
],
],
];
- 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()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -96,14 +89,19 @@ class StartMariadb
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,
];
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -122,10 +120,10 @@ class StartMariadb
$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;
}
}
@@ -156,21 +154,23 @@ class StartMariadb
$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}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 911054f5e..42fc8f348 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -23,7 +23,7 @@ class StartMongodb
$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,18 +77,11 @@ class StartMongodb
],
],
];
- 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()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -104,23 +97,27 @@ class StartMongodb
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,
];
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -139,10 +136,10 @@ class StartMongodb
$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;
}
}
@@ -173,18 +170,20 @@ class StartMongodb
$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}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index b55d9dead..2043342fe 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -21,7 +21,7 @@ class StartMysql
$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,18 +69,11 @@ class StartMysql
],
],
];
- 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()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -96,14 +89,19 @@ class StartMysql
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,
];
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -122,10 +120,10 @@ class StartMysql
$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;
}
}
@@ -156,21 +154,23 @@ class StartMysql
$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}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index 909f4c893..bc37fd5cf 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -37,6 +37,7 @@ class StartPostgresql
$this->generate_init_scripts();
$this->add_custom_conf();
+
$docker_compose = [
'services' => [
$container_name => [
@@ -80,14 +81,7 @@ class StartPostgresql
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -126,6 +120,10 @@ class StartPostgresql
'config_file=/etc/postgresql/postgresql.conf',
];
}
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -193,6 +191,8 @@ class StartPostgresql
$environment_variables->push("POSTGRES_DB={$this->database->postgres_db}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index f10afef5e..b837414d6 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -24,7 +24,7 @@ class StartRedis
$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,18 +78,11 @@ class StartRedis
],
],
];
- 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()) {
- $docker_compose['services'][$container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -105,15 +98,20 @@ class StartRedis
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,
];
$docker_compose['services'][$container_name]['command'] = "redis-server /usr/local/etc/redis/redis.conf --requirepass {$this->database->redis_password} --appendonly yes";
}
+
+ // Add custom docker run options
+ $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
+ $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
+
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -132,10 +130,10 @@ class StartRedis
$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;
}
}
@@ -166,10 +164,12 @@ class StartRedis
$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}");
}
+ add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
+
return $environment_variables->all();
}
diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php
index 0009e001d..1034c13d6 100644
--- a/app/Actions/Server/CleanupDocker.php
+++ b/app/Actions/Server/CleanupDocker.php
@@ -2,6 +2,7 @@
namespace App\Actions\Server;
+use App\Models\InstanceSettings;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,17 +10,30 @@ class CleanupDocker
{
use AsAction;
- public function handle(Server $server, bool $force = true)
+ public function handle(Server $server)
{
- // cleanup docker images, containers, and builder caches
- if ($force) {
- instant_remote_process(['docker image prune -af'], $server, false);
- instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
- instant_remote_process(['docker builder prune -af'], $server, false);
- } else {
- instant_remote_process(['docker image prune -f'], $server, false);
- instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
- instant_remote_process(['docker builder prune -f'], $server, false);
+
+ $commands = $this->getCommands();
+
+ 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/InstallLogDrain.php b/app/Actions/Server/InstallLogDrain.php
index 034d89fe7..9b6741211 100644
--- a/app/Actions/Server/InstallLogDrain.php
+++ b/app/Actions/Server/InstallLogDrain.php
@@ -47,7 +47,11 @@ class InstallLogDrain
[FILTER]
Name modify
Match *
- Set server_name {$server->name}
+ Set coolify.server_name {$server->name}
+ Rename COOLIFY_APP_NAME coolify.app_name
+ Rename COOLIFY_PROJECT_NAME coolify.project_name
+ Rename COOLIFY_SERVER_IP coolify.server_ip
+ Rename COOLIFY_ENVIRONMENT_NAME coolify.environment_name
[OUTPUT]
Name nrlogs
Match *
@@ -98,7 +102,11 @@ class InstallLogDrain
[FILTER]
Name modify
Match *
- Set server_name {$server->name}
+ Set coolify.server_name {$server->name}
+ Rename COOLIFY_APP_NAME coolify.app_name
+ Rename COOLIFY_PROJECT_NAME coolify.project_name
+ Rename COOLIFY_SERVER_IP coolify.server_ip
+ Rename COOLIFY_ENVIRONMENT_NAME coolify.environment_name
[OUTPUT]
Name http
Match *
diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php
index 8910d6e97..901f2cf77 100644
--- a/app/Actions/Server/UpdateCoolify.php
+++ b/app/Actions/Server/UpdateCoolify.php
@@ -2,10 +2,9 @@
namespace App\Actions\Server;
+use App\Jobs\PullHelperImageJob;
use App\Models\InstanceSettings;
use App\Models\Server;
-use Illuminate\Support\Facades\File;
-use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
class UpdateCoolify
@@ -26,12 +25,7 @@ class UpdateCoolify
if (! $this->server) {
return;
}
- CleanupDocker::dispatch($this->server, false)->onQueue('high');
- $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
- if ($response->successful()) {
- $versions = $response->json();
- File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
- }
+ CleanupDocker::dispatch($this->server)->onQueue('high');
$this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version');
if (! $manual_update) {
@@ -62,10 +56,18 @@ class UpdateCoolify
return;
}
+
+ $all_servers = Server::all();
+ $servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
+ foreach ($servers as $server) {
+ PullHelperImageJob::dispatch($server);
+ }
+
+ instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
+
remote_process([
'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',
"bash /data/coolify/source/upgrade.sh $this->latestVersion",
], $this->server);
-
}
}
diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php
index 4b6a25dcc..06d2e0efb 100644
--- a/app/Actions/Service/StartService.php
+++ b/app/Actions/Service/StartService.php
@@ -16,8 +16,10 @@ class StartService
$service->saveComposeConfigs();
$commands[] = 'cd '.$service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
- $commands[] = "echo 'Creating Docker network.'";
- $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
+ 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";
+ }
$commands[] = 'echo Starting service.';
$commands[] = "echo 'Pulling images.'";
$commands[] = 'docker compose pull';
@@ -29,7 +31,7 @@ class StartService
$network = $service->destination->network;
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
foreach ($serviceNames as $serviceName => $serviceConfig) {
- $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
+ $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
}
}
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php
index fbbf2c820..68beb448a 100644
--- a/app/Console/Commands/CleanupStuckedResources.php
+++ b/app/Console/Commands/CleanupStuckedResources.php
@@ -3,6 +3,8 @@
namespace App\Console\Commands;
use App\Models\Application;
+use App\Models\ApplicationPreview;
+use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Service;
use App\Models\ServiceApplication;
@@ -42,6 +44,17 @@ class CleanupStuckedResources extends Command
} catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
}
+ try {
+ $applicationsPreviews = ApplicationPreview::get();
+ foreach ($applicationsPreviews as $applicationPreview) {
+ if (! data_get($applicationPreview, 'application')) {
+ echo "Deleting stuck application preview: {$applicationPreview->uuid}\n";
+ $applicationPreview->delete();
+ }
+ }
+ } catch (\Throwable $e) {
+ echo "Error in cleaning stuck application: {$e->getMessage()}\n";
+ }
try {
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($postgresqls as $postgresql) {
@@ -153,6 +166,18 @@ class CleanupStuckedResources extends Command
echo "Error in cleaning stuck scheduledtasks: {$e->getMessage()}\n";
}
+ try {
+ $scheduled_backups = ScheduledDatabaseBackup::all();
+ foreach ($scheduled_backups as $scheduled_backup) {
+ if (! $scheduled_backup->server()) {
+ echo "Deleting stuck scheduledbackup: {$scheduled_backup->name}\n";
+ $scheduled_backup->delete();
+ }
+ }
+ } catch (\Throwable $e) {
+ echo "Error in cleaning stuck scheduledbackups: {$e->getMessage()}\n";
+ }
+
// Cleanup any resources that are not attached to any environment or destination or server
try {
$applications = Application::all();
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index 30d761a10..7bfd1a14f 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -132,6 +132,9 @@ class Init extends Command
private function cleanup_unused_network_from_coolify_proxy()
{
+ if (isCloud()) {
+ return;
+ }
foreach ($this->servers as $server) {
if (! $server->isFunctional()) {
continue;
diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/OpenApi.php
new file mode 100644
index 000000000..e8d73ef47
--- /dev/null
+++ b/app/Console/Commands/OpenApi.php
@@ -0,0 +1,26 @@
+errorOutput();
+ $error = preg_replace('/^.*an object literal,.*$/m', '', $error);
+ $error = preg_replace('/^\h*\v+/m', '', $error);
+ echo $error;
+ echo $process->output();
+
+ }
+}
diff --git a/app/Console/Commands/SyncBunny.php b/app/Console/Commands/SyncBunny.php
index 7135cfc9c..228467f88 100644
--- a/app/Console/Commands/SyncBunny.php
+++ b/app/Console/Commands/SyncBunny.php
@@ -16,7 +16,7 @@ class SyncBunny extends Command
*
* @var string
*/
- protected $signature = 'sync:bunny {--templates} {--release}';
+ protected $signature = 'sync:bunny {--templates} {--release} {--nightly}';
/**
* The console command description.
@@ -33,6 +33,7 @@ class SyncBunny extends Command
$that = $this;
$only_template = $this->option('templates');
$only_version = $this->option('release');
+ $nightly = $this->option('nightly');
$bunny_cdn = 'https://cdn.coollabs.io';
$bunny_cdn_path = 'coolify';
$bunny_cdn_storage_name = 'coolcdn';
@@ -45,9 +46,15 @@ class SyncBunny extends Command
$upgrade_script = 'upgrade.sh';
$production_env = '.env.production';
$service_template = 'service-templates.json';
-
$versions = 'versions.json';
+ $compose_file_location = "$parent_dir/$compose_file";
+ $compose_file_prod_location = "$parent_dir/$compose_file_prod";
+ $install_script_location = "$parent_dir/scripts/install.sh";
+ $upgrade_script_location = "$parent_dir/scripts/upgrade.sh";
+ $production_env_location = "$parent_dir/.env.production";
+ $versions_location = "$parent_dir/$versions";
+
PendingRequest::macro('storage', function ($fileName) use ($that) {
$headers = [
'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
@@ -73,8 +80,26 @@ class SyncBunny extends Command
]);
});
try {
+ if ($nightly) {
+ $bunny_cdn_path = 'coolify-nightly';
+
+ $compose_file_location = "$parent_dir/other/nightly/$compose_file";
+ $compose_file_prod_location = "$parent_dir/other/nightly/$compose_file_prod";
+ $production_env_location = "$parent_dir/other/nightly/$production_env";
+ $upgrade_script_location = "$parent_dir/other/nightly/$upgrade_script";
+ $install_script_location = "$parent_dir/other/nightly/$install_script";
+ $versions_location = "$parent_dir/other/nightly/$versions";
+ }
if (! $only_template && ! $only_version) {
- $this->info('About to sync files (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
+ if ($nightly) {
+ $this->info('About to sync files NIGHTLY (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
+ } else {
+ $this->info('About to sync files PRODUCTION (docker-compose.yml, docker-compose.prod.yml, upgrade.sh, install.sh, etc) to BunnyCDN.');
+ }
+ $confirmed = confirm('Are you sure you want to sync?');
+ if (! $confirmed) {
+ return;
+ }
}
if ($only_template) {
$this->info('About to sync service-templates.json to BunnyCDN.');
@@ -90,8 +115,12 @@ class SyncBunny extends Command
return;
} elseif ($only_version) {
- $this->info('About to sync versions.json to BunnyCDN.');
- $file = file_get_contents("$parent_dir/$versions");
+ if ($nightly) {
+ $this->info('About to sync NIGHLTY versions.json to BunnyCDN.');
+ } else {
+ $this->info('About to sync PRODUCTION versions.json to BunnyCDN.');
+ }
+ $file = file_get_contents($versions_location);
$json = json_decode($file, true);
$actual_version = data_get($json, 'coolify.v4.version');
@@ -100,7 +129,7 @@ class SyncBunny extends Command
return;
}
Http::pool(fn (Pool $pool) => [
- $pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
+ $pool->storage(fileName: $versions_location)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
]);
$this->info('versions.json uploaded & purged...');
@@ -109,11 +138,11 @@ class SyncBunny extends Command
}
Http::pool(fn (Pool $pool) => [
- $pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
- $pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
- $pool->storage(fileName: "$parent_dir/$production_env")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
- $pool->storage(fileName: "$parent_dir/scripts/$upgrade_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
- $pool->storage(fileName: "$parent_dir/scripts/$install_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
+ $pool->storage(fileName: "$compose_file_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
+ $pool->storage(fileName: "$compose_file_prod_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
+ $pool->storage(fileName: "$production_env_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
+ $pool->storage(fileName: "$upgrade_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
+ $pool->storage(fileName: "$install_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
]);
Http::pool(fn (Pool $pool) => [
$pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file"),
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index e8f213b16..b960a4a8b 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -4,9 +4,9 @@ namespace App\Console;
use App\Jobs\CheckForUpdatesJob;
use App\Jobs\CleanupInstanceStuffsJob;
+use App\Jobs\CleanupStaleMultiplexedConnections;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
-use App\Jobs\PullCoolifyImageJob;
use App\Jobs\PullHelperImageJob;
use App\Jobs\PullSentinelImageJob;
use App\Jobs\PullTemplatesFromCDN;
@@ -30,22 +30,24 @@ class Kernel extends ConsoleKernel
$this->all_servers = Server::all();
$settings = InstanceSettings::get();
+ $schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
+
if (isDev()) {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
-
// Server Jobs
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->check_scheduled_tasks($schedule);
$schedule->command('uploads:clear')->everyTwoMinutes();
+
+ $schedule->command('telescope:prune')->daily();
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
- $schedule->command('cleanup:unreachable-servers')->daily();
- $schedule->job(new PullCoolifyImageJob)->cron($settings->update_check_frequency)->onOneServer();
- $schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->onOneServer();
+ $schedule->command('cleanup:unreachable-servers')->daily()->onOneServer();
+ $schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$this->schedule_updates($schedule);
@@ -66,9 +68,19 @@ class Kernel extends ConsoleKernel
$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()) {
- $schedule->job(new PullSentinelImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
+ $schedule->job(function () use ($server) {
+ $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $server, false);
+ $sentinel_found = json_decode($sentinel_found, true);
+ $status = data_get($sentinel_found, '0.State.Status', 'exited');
+ if ($status !== 'running') {
+ PullSentinelImageJob::dispatch($server);
+ }
+ })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
}
- $schedule->job(new PullHelperImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
+ $schedule->job(new PullHelperImageJob($server))
+ ->cron($settings->update_check_frequency)
+ ->timezone($settings->instance_timezone)
+ ->onOneServer();
}
}
@@ -77,11 +89,17 @@ class Kernel extends ConsoleKernel
$settings = InstanceSettings::get();
$updateCheckFrequency = $settings->update_check_frequency;
- $schedule->job(new CheckForUpdatesJob)->cron($updateCheckFrequency)->onOneServer();
+ $schedule->job(new CheckForUpdatesJob)
+ ->cron($updateCheckFrequency)
+ ->timezone($settings->instance_timezone)
+ ->onOneServer();
if ($settings->is_auto_update_enabled) {
$autoUpdateFrequency = $settings->auto_update_frequency;
- $schedule->job(new UpdateCoolifyJob)->cron($autoUpdateFrequency)->onOneServer();
+ $schedule->job(new UpdateCoolifyJob)
+ ->cron($autoUpdateFrequency)
+ ->timezone($settings->instance_timezone)
+ ->onOneServer();
}
}
@@ -96,7 +114,12 @@ class Kernel extends ConsoleKernel
}
foreach ($servers as $server) {
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
- $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->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();
+ } else {
+ $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
+ }
}
}
@@ -117,12 +140,19 @@ class Kernel extends ConsoleKernel
continue;
}
+ $server = $scheduled_backup->server();
+
+ if (! $server) {
+ continue;
+ }
+ $serverTimezone = $server->settings->server_timezone;
+
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
$schedule->job(new DatabaseBackupJob(
backup: $scheduled_backup
- ))->cron($scheduled_backup->frequency)->onOneServer();
+ ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
}
}
@@ -155,12 +185,19 @@ class Kernel extends ConsoleKernel
continue;
}
}
+
+ $server = $scheduled_task->server();
+ if (! $server) {
+ continue;
+ }
+ $serverTimezone = $server->settings->server_timezone ?: config('app.timezone');
+
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}
$schedule->job(new ScheduledTaskJob(
task: $scheduled_task
- ))->cron($scheduled_task->frequency)->onOneServer();
+ ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
}
}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index cd4d724b4..81b173011 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -53,6 +53,7 @@ class ApplicationsController extends Controller
summary: 'List',
description: 'List all applications.',
path: '/applications',
+ operationId: 'list-applications',
security: [
['bearerAuth' => []],
],
@@ -101,6 +102,7 @@ class ApplicationsController extends Controller
summary: 'Create (Public)',
description: 'Create new application based on a public git repository.',
path: '/applications/public',
+ operationId: 'create-public-application',
security: [
['bearerAuth' => []],
],
@@ -201,7 +203,8 @@ class ApplicationsController extends Controller
#[OA\Post(
summary: 'Create (Private - GH App)',
description: 'Create new application based on a private repository through a Github App.',
- path: '/applications/private-gh-app',
+ path: '/applications/private-github-app',
+ operationId: 'create-private-github-app-application',
security: [
['bearerAuth' => []],
],
@@ -303,6 +306,7 @@ class ApplicationsController extends Controller
summary: 'Create (Private - Deploy Key)',
description: 'Create new application based on a private repository through a Deploy Key.',
path: '/applications/private-deploy-key',
+ operationId: 'create-private-deploy-key-application',
security: [
['bearerAuth' => []],
],
@@ -404,6 +408,7 @@ class ApplicationsController extends Controller
summary: 'Create (Dockerfile)',
description: 'Create new application based on a simple Dockerfile.',
path: '/applications/dockerfile',
+ operationId: 'create-dockerfile-application',
security: [
['bearerAuth' => []],
],
@@ -490,6 +495,7 @@ class ApplicationsController extends Controller
summary: 'Create (Docker Image)',
description: 'Create new application based on a prebuilt docker image',
path: '/applications/dockerimage',
+ operationId: 'create-dockerimage-application',
security: [
['bearerAuth' => []],
],
@@ -573,6 +579,7 @@ class ApplicationsController extends Controller
summary: 'Create (Docker Compose)',
description: 'Create new application based on a docker-compose file.',
path: '/applications/dockercompose',
+ operationId: 'create-dockercompose-application',
security: [
['bearerAuth' => []],
],
@@ -1171,6 +1178,7 @@ class ApplicationsController extends Controller
summary: 'Get',
description: 'Get application by UUID.',
path: '/applications/{uuid}',
+ operationId: 'get-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1235,6 +1243,7 @@ class ApplicationsController extends Controller
summary: 'Delete',
description: 'Delete application by UUID.',
path: '/applications/{uuid}',
+ operationId: 'delete-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1321,6 +1330,7 @@ class ApplicationsController extends Controller
summary: 'Update',
description: 'Update application by UUID.',
path: '/applications/{uuid}',
+ operationId: 'update-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1450,7 +1460,7 @@ class ApplicationsController extends Controller
], 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'];
+ $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'];
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
@@ -1526,6 +1536,10 @@ class ApplicationsController extends Controller
}
$request->offsetUnset('docker_compose_domains');
}
+ $instantDeploy = $request->instant_deploy;
+
+ removeUnnecessaryFieldsFromRequest($request);
+
$data = $request->all();
data_set($data, 'fqdn', $domains);
if ($dockerComposeDomainsJson->count() > 0) {
@@ -1534,6 +1548,16 @@ class ApplicationsController extends Controller
$application->fill($data);
$application->save();
+ if ($instantDeploy) {
+ $deployment_uuid = new Cuid2;
+
+ queue_application_deployment(
+ application: $application,
+ deployment_uuid: $deployment_uuid,
+ is_api: true,
+ );
+ }
+
return response()->json([
'uuid' => $application->uuid,
]);
@@ -1543,6 +1567,7 @@ class ApplicationsController extends Controller
summary: 'List Envs',
description: 'List all envs by application UUID.',
path: '/applications/{uuid}/envs',
+ operationId: 'list-envs-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -1625,6 +1650,7 @@ class ApplicationsController extends Controller
summary: 'Update Env',
description: 'Update env by application UUID.',
path: '/applications/{uuid}/envs',
+ operationId: 'update-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -1807,6 +1833,7 @@ class ApplicationsController extends Controller
summary: 'Update Envs (Bulk)',
description: 'Update multiple envs by application UUID.',
path: '/applications/{uuid}/envs/bulk',
+ operationId: 'update-envs-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -1998,6 +2025,7 @@ class ApplicationsController extends Controller
summary: 'Create Env',
description: 'Create env by application UUID.',
path: '/applications/{uuid}/envs',
+ operationId: 'create-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -2157,6 +2185,7 @@ class ApplicationsController extends Controller
summary: 'Delete Env',
description: 'Delete env by UUID.',
path: '/applications/{uuid}/envs/{env_uuid}',
+ operationId: 'delete-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -2242,6 +2271,7 @@ class ApplicationsController extends Controller
summary: 'Start',
description: 'Start application. `Post` request is also accepted.',
path: '/applications/{uuid}/start',
+ operationId: 'start-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -2345,6 +2375,7 @@ class ApplicationsController extends Controller
summary: 'Stop',
description: 'Stop application. `Post` request is also accepted.',
path: '/applications/{uuid}/stop',
+ operationId: 'stop-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -2417,6 +2448,7 @@ class ApplicationsController extends Controller
summary: 'Restart',
description: 'Restart application. `Post` request is also accepted.',
path: '/applications/{uuid}/restart',
+ operationId: 'restart-application-by-uuid',
security: [
['bearerAuth' => []],
],
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index 0f1ee00d8..a205704cc 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -46,6 +46,7 @@ class DatabasesController extends Controller
summary: 'List',
description: 'List all databases.',
path: '/databases',
+ operationId: 'list-databases',
security: [
['bearerAuth' => []],
],
@@ -91,6 +92,7 @@ class DatabasesController extends Controller
summary: 'Get',
description: 'Get database by UUID.',
path: '/databases/{uuid}',
+ operationId: 'get-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -151,6 +153,7 @@ class DatabasesController extends Controller
summary: 'Update',
description: 'Update database by UUID.',
path: '/databases/{uuid}',
+ operationId: 'update-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -510,6 +513,7 @@ class DatabasesController extends Controller
summary: 'Create (PostgreSQL)',
description: 'Create a new PostgreSQL database.',
path: '/databases/postgresql',
+ operationId: 'create-database-postgresql',
security: [
['bearerAuth' => []],
],
@@ -575,6 +579,7 @@ class DatabasesController extends Controller
summary: 'Create (Clickhouse)',
description: 'Create a new Clickhouse database.',
path: '/databases/clickhouse',
+ operationId: 'create-database-clickhouse',
security: [
['bearerAuth' => []],
],
@@ -636,6 +641,7 @@ class DatabasesController extends Controller
summary: 'Create (DragonFly)',
description: 'Create a new DragonFly database.',
path: '/databases/dragonfly',
+ operationId: 'create-database-dragonfly',
security: [
['bearerAuth' => []],
],
@@ -696,6 +702,7 @@ class DatabasesController extends Controller
summary: 'Create (Redis)',
description: 'Create a new Redis database.',
path: '/databases/redis',
+ operationId: 'create-database-redis',
security: [
['bearerAuth' => []],
],
@@ -757,6 +764,7 @@ class DatabasesController extends Controller
summary: 'Create (KeyDB)',
description: 'Create a new KeyDB database.',
path: '/databases/keydb',
+ operationId: 'create-database-keydb',
security: [
['bearerAuth' => []],
],
@@ -818,6 +826,7 @@ class DatabasesController extends Controller
summary: 'Create (MariaDB)',
description: 'Create a new MariaDB database.',
path: '/databases/mariadb',
+ operationId: 'create-database-mariadb',
security: [
['bearerAuth' => []],
],
@@ -882,6 +891,7 @@ class DatabasesController extends Controller
summary: 'Create (MySQL)',
description: 'Create a new MySQL database.',
path: '/databases/mysql',
+ operationId: 'create-database-mysql',
security: [
['bearerAuth' => []],
],
@@ -945,6 +955,7 @@ class DatabasesController extends Controller
summary: 'Create (MongoDB)',
description: 'Create a new MongoDB database.',
path: '/databases/mongodb',
+ operationId: 'create-database-mongodb',
security: [
['bearerAuth' => []],
],
@@ -1514,6 +1525,7 @@ class DatabasesController extends Controller
summary: 'Delete',
description: 'Delete database by UUID.',
path: '/databases/{uuid}',
+ operationId: 'delete-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1597,6 +1609,7 @@ class DatabasesController extends Controller
summary: 'Start',
description: 'Start database. `Post` request is also accepted.',
path: '/databases/{uuid}/start',
+ operationId: 'start-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1672,6 +1685,7 @@ class DatabasesController extends Controller
summary: 'Stop',
description: 'Stop database. `Post` request is also accepted.',
path: '/databases/{uuid}/stop',
+ operationId: 'stop-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1747,6 +1761,7 @@ class DatabasesController extends Controller
summary: 'Restart',
description: 'Restart database. `Post` request is also accepted.',
path: '/databases/{uuid}/restart',
+ operationId: 'restart-database-by-uuid',
security: [
['bearerAuth' => []],
],
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
index 437162058..d1c8f5ea6 100644
--- a/app/Http/Controllers/Api/DeployController.php
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -32,6 +32,7 @@ class DeployController extends Controller
summary: 'List',
description: 'List currently running deployments',
path: '/deployments',
+ operationId: 'list-deployments',
security: [
['bearerAuth' => []],
],
@@ -79,12 +80,13 @@ class DeployController extends Controller
summary: 'Get',
description: 'Get deployment by UUID.',
path: '/deployments/{uuid}',
+ operationId: 'get-deployment-by-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Deployments'],
parameters: [
- new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
@@ -134,6 +136,7 @@ class DeployController extends Controller
summary: 'Deploy',
description: 'Deploy by tag or uuid. `Post` request also accepted.',
path: '/deploy',
+ operationId: 'deploy-by-tag-or-uuid',
security: [
['bearerAuth' => []],
],
@@ -147,7 +150,7 @@ class DeployController extends Controller
responses: [
new OA\Response(
response: 200,
- description: 'Get deployment(s) Uuid\'s',
+ description: 'Get deployment(s) UUID\'s',
content: [
new OA\MediaType(
mediaType: 'application/json',
diff --git a/app/Http/Controllers/Api/EnvironmentVariablesController.php b/app/Http/Controllers/Api/EnvironmentVariablesController.php
deleted file mode 100644
index d127d0525..000000000
--- a/app/Http/Controllers/Api/EnvironmentVariablesController.php
+++ /dev/null
@@ -1,35 +0,0 @@
-env_uuid)->first();
- if (! $env) {
- return response()->json([
- 'message' => 'Environment variable not found.',
- ], 404);
- }
- $found_app = $env->resource()->whereRelation('environment.project.team', 'id', $teamId)->first();
- if (! $found_app) {
- return response()->json([
- 'message' => 'Environment variable not found.',
- ], 404);
- }
- $env->delete();
-
- return response()->json([
- 'message' => 'Environment variable deleted.',
- ]);
- }
-}
diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php
index 1e48ffdbe..c085b88a5 100644
--- a/app/Http/Controllers/Api/OtherController.php
+++ b/app/Http/Controllers/Api/OtherController.php
@@ -13,6 +13,7 @@ class OtherController extends Controller
summary: 'Version',
description: 'Get Coolify version.',
path: '/version',
+ operationId: 'version',
security: [
['bearerAuth' => []],
],
@@ -43,6 +44,7 @@ class OtherController extends Controller
summary: 'Enable API',
description: 'Enable API (only with root permissions).',
path: '/enable',
+ operationId: 'enable-api',
security: [
['bearerAuth' => []],
],
@@ -94,6 +96,7 @@ class OtherController extends Controller
summary: 'Disable API',
description: 'Disable API (only with root permissions).',
path: '/disable',
+ operationId: 'disable-api',
security: [
['bearerAuth' => []],
],
@@ -158,6 +161,7 @@ class OtherController extends Controller
summary: 'Healthcheck',
description: 'Healthcheck endpoint.',
path: '/healthcheck',
+ operationId: 'healthcheck',
responses: [
new OA\Response(
response: 200,
diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php
index 6aec31e9b..f1958de2c 100644
--- a/app/Http/Controllers/Api/ProjectController.php
+++ b/app/Http/Controllers/Api/ProjectController.php
@@ -11,8 +11,9 @@ class ProjectController extends Controller
{
#[OA\Get(
summary: 'List',
- description: 'list projects.',
+ description: 'List projects.',
path: '/projects',
+ operationId: 'list-projects',
security: [
['bearerAuth' => []],
],
@@ -46,7 +47,7 @@ class ProjectController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
- $projects = Project::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
+ $projects = Project::whereTeamId($teamId)->select('id', 'name', 'description', 'uuid')->get();
return response()->json(serializeApiResponse($projects),
);
@@ -54,8 +55,9 @@ class ProjectController extends Controller
#[OA\Get(
summary: 'Get',
- description: 'Get project by Uuid.',
+ description: 'Get project by UUID.',
path: '/projects/{uuid}',
+ operationId: 'get-project-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -102,6 +104,7 @@ class ProjectController extends Controller
summary: 'Environment',
description: 'Get environment by name.',
path: '/projects/{uuid}/{environment_name}',
+ operationId: 'get-environment-by-name',
security: [
['bearerAuth' => []],
],
@@ -136,12 +139,15 @@ class ProjectController extends Controller
return invalidTokenResponse();
}
if (! $request->uuid) {
- return response()->json(['message' => 'Uuid is required.'], 422);
+ return response()->json(['message' => 'UUID is required.'], 422);
}
if (! $request->environment_name) {
return response()->json(['message' => 'Environment name is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
+ if (! $project) {
+ return response()->json(['message' => 'Project not found.'], 404);
+ }
$environment = $project->environments()->whereName($request->environment_name)->first();
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
@@ -155,6 +161,7 @@ class ProjectController extends Controller
summary: 'Create',
description: 'Create Project.',
path: '/projects',
+ operationId: 'create-project',
security: [
['bearerAuth' => []],
],
@@ -167,7 +174,7 @@ class ProjectController extends Controller
schema: new OA\Schema(
type: 'object',
properties: [
- 'uuid' => ['type' => 'string', 'description' => 'The name of the project.'],
+ 'name' => ['type' => 'string', 'description' => 'The name of the project.'],
'description' => ['type' => 'string', 'description' => 'The description of the project.'],
],
),
@@ -250,6 +257,7 @@ class ProjectController extends Controller
summary: 'Update',
description: 'Update Project.',
path: '/projects/{uuid}',
+ operationId: 'update-project-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -333,7 +341,7 @@ class ProjectController extends Controller
}
$uuid = $request->uuid;
if (! $uuid) {
- return response()->json(['message' => 'Uuid is required.'], 422);
+ return response()->json(['message' => 'UUID is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($uuid)->first();
@@ -355,6 +363,7 @@ class ProjectController extends Controller
summary: 'Delete',
description: 'Delete project by UUID.',
path: '/projects/{uuid}',
+ operationId: 'delete-project-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -408,7 +417,7 @@ class ProjectController extends Controller
}
if (! $request->uuid) {
- return response()->json(['message' => 'Uuid is required.'], 422);
+ return response()->json(['message' => 'UUID is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $project) {
diff --git a/app/Http/Controllers/Api/ResourcesController.php b/app/Http/Controllers/Api/ResourcesController.php
index ae076bb71..1fd5792e0 100644
--- a/app/Http/Controllers/Api/ResourcesController.php
+++ b/app/Http/Controllers/Api/ResourcesController.php
@@ -13,6 +13,7 @@ class ResourcesController extends Controller
summary: 'List',
description: 'Get all resources.',
path: '/resources',
+ operationId: 'list-resources',
security: [
['bearerAuth' => []],
],
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index 67128234e..bb474aed3 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -26,6 +26,7 @@ class SecurityController extends Controller
summary: 'List',
description: 'List all private keys.',
path: '/security/keys',
+ operationId: 'list-private-keys',
security: [
['bearerAuth' => []],
],
@@ -68,12 +69,13 @@ class SecurityController extends Controller
summary: 'Get',
description: 'Get key by UUID.',
path: '/security/keys/{uuid}',
+ operationId: 'get-private-key-by-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
parameters: [
- new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
@@ -124,6 +126,7 @@ class SecurityController extends Controller
summary: 'Create',
description: 'Create a new private key.',
path: '/security/keys',
+ operationId: 'create-private-key',
security: [
['bearerAuth' => []],
],
@@ -217,6 +220,7 @@ class SecurityController extends Controller
summary: 'Update',
description: 'Update a private key.',
path: '/security/keys',
+ operationId: 'update-private-key',
security: [
['bearerAuth' => []],
],
@@ -313,12 +317,13 @@ class SecurityController extends Controller
summary: 'Delete',
description: 'Delete a private key.',
path: '/security/keys/{uuid}',
+ operationId: 'delete-private-key-by-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
parameters: [
- new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index 9044c4a35..5f0d6bb12 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -46,6 +46,7 @@ class ServersController extends Controller
summary: 'List',
description: 'List all servers.',
path: '/servers',
+ operationId: 'list-servers',
security: [
['bearerAuth' => []],
],
@@ -100,12 +101,13 @@ class ServersController extends Controller
summary: 'Get',
description: 'Get server by UUID.',
path: '/servers/{uuid}',
+ operationId: 'get-server-by-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
- new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
@@ -177,12 +179,13 @@ class ServersController extends Controller
summary: 'Resources',
description: 'Get resources by server.',
path: '/servers/{uuid}/resources',
+ operationId: 'get-resources-by-server-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
- new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
@@ -254,12 +257,13 @@ class ServersController extends Controller
summary: 'Domains',
description: 'Get domains by server.',
path: '/servers/{uuid}/domains',
+ operationId: 'get-domains-by-server-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
- new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
@@ -401,6 +405,7 @@ class ServersController extends Controller
summary: 'Create',
description: 'Create Server.',
path: '/servers',
+ operationId: 'create-server',
security: [
['bearerAuth' => []],
],
@@ -545,6 +550,7 @@ class ServersController extends Controller
summary: 'Update',
description: 'Update Server.',
path: '/servers/{uuid}',
+ operationId: 'update-server-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -655,6 +661,7 @@ class ServersController extends Controller
summary: 'Delete',
description: 'Delete server by UUID.',
path: '/servers/{uuid}',
+ operationId: 'delete-server-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -727,6 +734,7 @@ class ServersController extends Controller
summary: 'Validate',
description: 'Validate server by UUID.',
path: '/servers/{uuid}/validate',
+ operationId: 'validate-server-by-uuid',
security: [
['bearerAuth' => []],
],
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 3a5b59f55..0a6154410 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -38,6 +38,7 @@ class ServicesController extends Controller
summary: 'List',
description: 'List all services.',
path: '/services',
+ operationId: 'list-services',
security: [
['bearerAuth' => []],
],
@@ -88,6 +89,7 @@ class ServicesController extends Controller
summary: 'Create',
description: 'Create a one-click service',
path: '/services',
+ operationId: 'create-service',
security: [
['bearerAuth' => []],
],
@@ -365,6 +367,7 @@ class ServicesController extends Controller
summary: 'Get',
description: 'Get service by UUID.',
path: '/services/{uuid}',
+ operationId: 'get-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -375,7 +378,7 @@ class ServicesController extends Controller
responses: [
new OA\Response(
response: 200,
- description: 'Get a service by Uuid.',
+ description: 'Get a service by UUID.',
content: [
new OA\MediaType(
mediaType: 'application/json',
@@ -422,6 +425,7 @@ class ServicesController extends Controller
summary: 'Delete',
description: 'Delete service by UUID.',
path: '/services/{uuid}',
+ operationId: 'delete-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -432,7 +436,7 @@ class ServicesController extends Controller
responses: [
new OA\Response(
response: 200,
- description: 'Delete a service by Uuid',
+ description: 'Delete a service by UUID',
content: [
new OA\MediaType(
mediaType: 'application/json',
@@ -479,10 +483,521 @@ class ServicesController extends Controller
]);
}
+ #[OA\Get(
+ summary: 'List Envs',
+ description: 'List all envs by service UUID.',
+ path: '/services/{uuid}/envs',
+ operationId: 'list-envs-by-service-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Services'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the service.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'All environment variables by service UUID.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'array',
+ items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
+ )
+ ),
+ ]),
+ 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 envs(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+
+ $envs = $service->environment_variables->map(function ($env) {
+ $env->makeHidden([
+ 'application_id',
+ 'standalone_clickhouse_id',
+ 'standalone_dragonfly_id',
+ 'standalone_keydb_id',
+ 'standalone_mariadb_id',
+ 'standalone_mongodb_id',
+ 'standalone_mysql_id',
+ 'standalone_postgresql_id',
+ 'standalone_redis_id',
+ ]);
+ $env = $this->removeSensitiveData($env);
+
+ return $env;
+ });
+
+ return response()->json($envs);
+ }
+
+ #[OA\Patch(
+ summary: 'Update Env',
+ description: 'Update env by service UUID.',
+ path: '/services/{uuid}/envs',
+ operationId: 'update-env-by-service-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Services'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the service.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ requestBody: new OA\RequestBody(
+ description: 'Env updated.',
+ required: true,
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['key', 'value'],
+ properties: [
+ 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
+ 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
+ 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'],
+ 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'],
+ 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
+ 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
+ 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
+ ],
+ ),
+ ),
+ ],
+ ),
+ responses: [
+ new OA\Response(
+ response: 201,
+ description: 'Environment variable updated.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Environment variable updated.'],
+ ]
+ )
+ ),
+ ]),
+ 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 update_env_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+
+ $validator = customApiValidator($request->all(), [
+ 'key' => 'string|required',
+ 'value' => 'string|nullable',
+ 'is_build_time' => 'boolean',
+ 'is_literal' => 'boolean',
+ 'is_multiline' => 'boolean',
+ 'is_shown_once' => 'boolean',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $validator->errors(),
+ ], 422);
+ }
+
+ $env = $service->environment_variables()->where('key', $request->key)->first();
+ if (! $env) {
+ return response()->json(['message' => 'Environment variable not found.'], 404);
+ }
+
+ $env->fill($request->all());
+ $env->save();
+
+ return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
+ }
+
+ #[OA\Patch(
+ summary: 'Update Envs (Bulk)',
+ description: 'Update multiple envs by service UUID.',
+ path: '/services/{uuid}/envs/bulk',
+ operationId: 'update-envs-by-service-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Services'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the service.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ requestBody: new OA\RequestBody(
+ description: 'Bulk envs updated.',
+ required: true,
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['data'],
+ properties: [
+ 'data' => [
+ 'type' => 'array',
+ 'items' => new OA\Schema(
+ type: 'object',
+ properties: [
+ 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
+ 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
+ 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'],
+ 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'],
+ 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
+ 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
+ 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
+ ],
+ ),
+ ],
+ ],
+ ),
+ ),
+ ],
+ ),
+ responses: [
+ new OA\Response(
+ response: 201,
+ description: 'Environment variables updated.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Environment variables updated.'],
+ ]
+ )
+ ),
+ ]),
+ 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 create_bulk_envs(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+
+ $bulk_data = $request->get('data');
+ if (! $bulk_data) {
+ return response()->json(['message' => 'Bulk data is required.'], 400);
+ }
+
+ $updatedEnvs = collect();
+ foreach ($bulk_data as $item) {
+ $validator = customApiValidator($item, [
+ 'key' => 'string|required',
+ 'value' => 'string|nullable',
+ 'is_build_time' => 'boolean',
+ 'is_literal' => 'boolean',
+ 'is_multiline' => 'boolean',
+ 'is_shown_once' => 'boolean',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $validator->errors(),
+ ], 422);
+ }
+
+ $env = $service->environment_variables()->updateOrCreate(
+ ['key' => $item['key']],
+ $item
+ );
+
+ $updatedEnvs->push($this->removeSensitiveData($env));
+ }
+
+ return response()->json($updatedEnvs)->setStatusCode(201);
+ }
+
+ #[OA\Post(
+ summary: 'Create Env',
+ description: 'Create env by service UUID.',
+ path: '/services/{uuid}/envs',
+ operationId: 'create-env-by-service-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Services'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the service.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ requestBody: new OA\RequestBody(
+ required: true,
+ description: 'Env created.',
+ content: new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
+ 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
+ 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'],
+ 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'],
+ 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
+ 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
+ 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
+ ],
+ ),
+ ),
+ ),
+ responses: [
+ new OA\Response(
+ response: 201,
+ description: 'Environment variable created.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'uuid' => ['type' => 'string', 'example' => 'nc0k04gk8g0cgsk440g0koko'],
+ ]
+ )
+ ),
+ ]),
+ 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 create_env(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+
+ $validator = customApiValidator($request->all(), [
+ 'key' => 'string|required',
+ 'value' => 'string|nullable',
+ 'is_build_time' => 'boolean',
+ 'is_literal' => 'boolean',
+ 'is_multiline' => 'boolean',
+ 'is_shown_once' => 'boolean',
+ ]);
+
+ if ($validator->fails()) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $validator->errors(),
+ ], 422);
+ }
+
+ $existingEnv = $service->environment_variables()->where('key', $request->key)->first();
+ if ($existingEnv) {
+ return response()->json([
+ 'message' => 'Environment variable already exists. Use PATCH request to update it.',
+ ], 409);
+ }
+
+ $env = $service->environment_variables()->create($request->all());
+
+ return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
+ }
+
+ #[OA\Delete(
+ summary: 'Delete Env',
+ description: 'Delete env by UUID.',
+ path: '/services/{uuid}/envs/{env_uuid}',
+ operationId: 'delete-env-by-service-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Services'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the service.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ new OA\Parameter(
+ name: 'env_uuid',
+ in: 'path',
+ description: 'UUID of the environment variable.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Environment variable deleted.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'message' => ['type' => 'string', 'example' => 'Environment variable deleted.'],
+ ]
+ )
+ ),
+ ]),
+ 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 delete_env_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+
+ $env = EnvironmentVariable::where('uuid', $request->env_uuid)
+ ->where('service_id', $service->id)
+ ->first();
+
+ if (! $env) {
+ return response()->json(['message' => 'Environment variable not found.'], 404);
+ }
+
+ $env->forceDelete();
+
+ return response()->json(['message' => 'Environment variable deleted.']);
+ }
+
#[OA\Get(
summary: 'Start',
description: 'Start service. `Post` request is also accepted.',
path: '/services/{uuid}/start',
+ operationId: 'start-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -558,6 +1073,7 @@ class ServicesController extends Controller
summary: 'Stop',
description: 'Stop service. `Post` request is also accepted.',
path: '/services/{uuid}/stop',
+ operationId: 'stop-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -633,6 +1149,7 @@ class ServicesController extends Controller
summary: 'Restart',
description: 'Restart service. `Post` request is also accepted.',
path: '/services/{uuid}/restart',
+ operationId: 'restart-service-by-uuid',
security: [
['bearerAuth' => []],
],
diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php
index 1a481e5ec..3f951c6f7 100644
--- a/app/Http/Controllers/Api/TeamController.php
+++ b/app/Http/Controllers/Api/TeamController.php
@@ -32,6 +32,7 @@ class TeamController extends Controller
summary: 'List',
description: 'Get all teams.',
path: '/teams',
+ operationId: 'list-teams',
security: [
['bearerAuth' => []],
],
@@ -79,6 +80,7 @@ class TeamController extends Controller
summary: 'Get',
description: 'Get team by TeamId.',
path: '/teams/{id}',
+ operationId: 'get-team-by-id',
security: [
['bearerAuth' => []],
],
@@ -129,6 +131,7 @@ class TeamController extends Controller
summary: 'Members',
description: 'Get members by TeamId.',
path: '/teams/{id}/members',
+ operationId: 'get-members-by-team-id',
security: [
['bearerAuth' => []],
],
@@ -189,6 +192,7 @@ class TeamController extends Controller
summary: 'Authenticated Team',
description: 'Get currently authenticated team.',
path: '/teams/current',
+ operationId: 'get-current-team',
security: [
['bearerAuth' => []],
],
@@ -225,6 +229,7 @@ class TeamController extends Controller
summary: 'Authenticated Team Members',
description: 'Get currently authenticated team members.',
path: '/teams/current/members',
+ operationId: 'get-current-team-members',
security: [
['bearerAuth' => []],
],
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 42c1ba43c..d8f85cff8 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -12,6 +12,7 @@ use App\Models\ApplicationPreview;
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;
@@ -110,10 +111,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private bool $is_debug_enabled;
- private $build_args;
+ private Collection|string $build_args;
private $env_args;
+ private $environment_variables;
+
private $env_nixpacks_args;
private $docker_compose;
@@ -158,7 +161,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $coolify_variables = null;
- private bool $preserveRepository = true;
+ private bool $preserveRepository = false;
public $tries = 1;
@@ -167,6 +170,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
+ $this->build_args = collect([]);
$this->application_deployment_queue_id = $application_deployment_queue_id;
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
@@ -199,9 +203,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
if ($this->application->settings->custom_internal_name && ! $this->application->settings->is_consistent_container_name_enabled) {
- $this->container_name = $this->application->settings->custom_internal_name;
+ if ($this->pull_request_id === 0) {
+ $this->container_name = $this->application->settings->custom_internal_name;
+ } else {
+ $this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}";
+ }
}
- ray('New container name: ', $this->container_name);
+ ray('New container name: ', $this->container_name)->green();
savePrivateKeyToFs($this->server);
$this->saved_outputs = collect();
@@ -277,6 +285,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->original_server = $this->server;
} else {
$this->build_server = $buildServers->random();
+ $this->application_deployment_queue->build_server_id = $this->build_server->id;
$this->application_deployment_queue->addLogEntry("Found a suitable build server ({$this->build_server->name}).");
$this->original_server = $this->server;
$this->use_build_server = true;
@@ -415,15 +424,42 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository();
+ if ($this->preserveRepository) {
+ foreach ($this->application->fileStorages as $fileStorage) {
+ $path = $fileStorage->fs_path;
+ $saveName = 'file_stat_'.$fileStorage->id;
+ $realPathInGit = str($path)->replace($this->application->workdir(), $this->workdir)->value();
+ // check if the file is a directory or a file inside the repository
+ $this->execute_remote_command(
+ [executeInDocker($this->deployment_uuid, "stat -c '%F' {$realPathInGit}"), 'hidden' => true, 'ignore_errors' => true, 'save' => $saveName]
+ );
+ if ($this->saved_outputs->has($saveName)) {
+ $fileStat = $this->saved_outputs->get($saveName);
+ if ($fileStat->value() === 'directory' && ! $fileStorage->is_directory) {
+ $fileStorage->is_directory = true;
+ $fileStorage->content = null;
+ $fileStorage->save();
+ $fileStorage->deleteStorageOnServer();
+ $fileStorage->saveStorageOnServer();
+ } elseif ($fileStat->value() === 'regular file' && $fileStorage->is_directory) {
+ $fileStorage->is_directory = false;
+ $fileStorage->is_based_on_git = true;
+ $fileStorage->save();
+ $fileStorage->deleteStorageOnServer();
+ $fileStorage->saveStorageOnServer();
+ }
+ }
+ }
+ }
$this->generate_image_names();
$this->cleanup_git();
$this->application->loadComposeFile(isInit: false);
if ($this->application->settings->is_raw_compose_deployment_enabled) {
- $this->application->parseRawCompose();
+ $this->application->oldRawParser();
$yaml = $composeFile = $this->application->docker_compose_raw;
$this->save_environment_variables();
} else {
- $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id, preview_id: data_get($this, 'preview.id'));
+ $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables();
if (! is_null($this->env_filename)) {
$services = collect($composeFile['services']);
@@ -440,11 +476,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
return;
}
- $yaml = Yaml::dump($composeFile->toArray(), 10);
+ $yaml = Yaml::dump(convertToArray($composeFile), 10);
}
$this->docker_compose_base64 = base64_encode($yaml);
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
+ 'hidden' => true,
]);
// Build new container to limit downtime.
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
@@ -474,13 +511,18 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// TODO
} else {
$this->execute_remote_command([
- "docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true", 'hidden' => true, 'ignore_errors' => true,
+ "docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true",
+ 'hidden' => true,
+ 'ignore_errors' => true,
], [
- "docker network connect {$networkId} coolify-proxy || true", 'hidden' => true, 'ignore_errors' => true,
+ "docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true",
+ 'hidden' => true,
+ 'ignore_errors' => true,
]);
}
// Start compose file
+ $server_workdir = $this->application->workdir();
if ($this->application->settings->is_raw_compose_deployment_enabled) {
if ($this->docker_compose_custom_start_command) {
$this->write_deployment_configurations();
@@ -489,7 +531,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
} else {
$this->write_deployment_configurations();
- $server_workdir = $this->application->workdir();
$this->docker_compose_location = '/docker-compose.yaml';
$command = "{$this->coolify_variables} docker compose";
@@ -509,15 +550,26 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
} else {
$command = "{$this->coolify_variables} docker compose";
- if ($this->env_filename) {
- $command .= " --env-file {$this->workdir}/{$this->env_filename}";
- }
- $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
+ if ($this->preserveRepository) {
+ if ($this->env_filename) {
+ $command .= " --env-file {$server_workdir}/{$this->env_filename}";
+ }
+ $command .= " --project-name {$this->application->uuid} --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d";
+ $this->write_deployment_configurations();
- $this->write_deployment_configurations();
- $this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
- );
+ $this->execute_remote_command(
+ ['command' => $command, 'hidden' => true],
+ );
+ } else {
+ if ($this->env_filename) {
+ $command .= " --env-file {$this->workdir}/{$this->env_filename}";
+ }
+ $command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
+ $this->execute_remote_command(
+ [executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
+ );
+ $this->write_deployment_configurations();
+ }
}
}
@@ -611,15 +663,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
[
"mkdir -p $this->configuration_dir",
],
- // removing this now as we are using docker cp
- // [
- // "rm -rf $this->configuration_dir/{*,.*}",
- // ],
[
"docker cp {$this->deployment_uuid}:{$this->workdir}/. {$this->configuration_dir}",
],
);
}
+ foreach ($this->application->fileStorages as $fileStorage) {
+ if (! $fileStorage->is_based_on_git && ! $fileStorage->is_directory) {
+ $fileStorage->saveStorageOnServer();
+ }
+ }
if ($this->use_build_server) {
$this->server = $this->build_server;
}
@@ -699,7 +752,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name}).");
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"),
+ 'hidden' => true,
],
);
if ($this->application->docker_registry_image_tag) {
@@ -707,10 +761,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with {$this->application->docker_registry_image_tag} tag.");
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"),
+ 'ignore_errors' => true,
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"),
+ 'ignore_errors' => true,
+ 'hidden' => true,
],
);
}
@@ -807,14 +865,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function check_image_locally_or_remotely()
{
$this->execute_remote_command([
- "docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found',
+ "docker images -q {$this->production_image_name} 2>/dev/null",
+ 'hidden' => true,
+ 'save' => 'local_image_found',
]);
if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
$this->execute_remote_command([
- "docker pull {$this->production_image_name} 2>/dev/null", 'ignore_errors' => true, 'hidden' => true,
+ "docker pull {$this->production_image_name} 2>/dev/null",
+ 'ignore_errors' => true,
+ 'hidden' => true,
]);
$this->execute_remote_command([
- "docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found',
+ "docker images -q {$this->production_image_name} 2>/dev/null",
+ 'hidden' => true,
+ 'save' => 'local_image_found',
]);
}
}
@@ -847,17 +911,24 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
$envs->push("COOLIFY_FQDN={$this->preview->fqdn}");
+ $envs->push("COOLIFY_DOMAIN_URL={$this->preview->fqdn}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
$envs->push("COOLIFY_URL={$url}");
+ $envs->push("COOLIFY_DOMAIN_FQDN={$url}");
}
- if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
- $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}");
+ 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}\"");
+ }
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
+ $envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
+ }
}
+
+ add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables_preview);
+
foreach ($sorted_environment_variables_preview as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
@@ -892,19 +963,31 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
- $envs->push("COOLIFY_FQDN={$this->application->fqdn}");
+ if ($this->application->compose_parsing_version === '3') {
+ $envs->push("COOLIFY_URL={$this->application->fqdn}");
+ } else {
+ $envs->push("COOLIFY_FQDN={$this->application->fqdn}");
+ }
}
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
- $url = str_replace('http://', '', $this->application->fqdn);
- $url = str_replace('https://', '', $url);
- $envs->push("COOLIFY_URL={$url}");
+ $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
+ if ($this->application->compose_parsing_version === '3') {
+ $envs->push("COOLIFY_FQDN={$url}");
+ } else {
+ $envs->push("COOLIFY_URL={$url}");
+ }
}
- if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
- $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}");
+ 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}\"");
+ }
+ if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
+ $envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
+ }
}
+
+ add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables);
+
foreach ($sorted_environment_variables as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
@@ -981,17 +1064,58 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
}
}
+ $this->environment_variables = $envs;
+ }
+
+ private function elixir_finetunes()
+ {
+ if ($this->pull_request_id === 0) {
+ $envType = 'environment_variables';
+ } else {
+ $envType = 'environment_variables_preview';
+ }
+ $mix_env = $this->application->{$envType}->where('key', 'MIX_ENV')->first();
+ if ($mix_env) {
+ if ($mix_env->is_build_time === false) {
+ $this->application_deployment_queue->addLogEntry('MIX_ENV environment variable is not set as build time.', type: 'error');
+ $this->application_deployment_queue->addLogEntry('Please set MIX_ENV environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
+ }
+ } else {
+ $this->application_deployment_queue->addLogEntry('MIX_ENV environment variable not found.', type: 'error');
+ $this->application_deployment_queue->addLogEntry('Please add MIX_ENV environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
+ }
+ $secret_key_base = $this->application->{$envType}->where('key', 'SECRET_KEY_BASE')->first();
+ if ($secret_key_base) {
+ if ($secret_key_base->is_build_time === false) {
+ $this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable is not set as build time.', type: 'error');
+ $this->application_deployment_queue->addLogEntry('Please set SECRET_KEY_BASE environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
+ }
+ } else {
+ $this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable not found.', type: 'error');
+ $this->application_deployment_queue->addLogEntry('Please add SECRET_KEY_BASE environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
+ }
+ $database_url = $this->application->{$envType}->where('key', 'DATABASE_URL')->first();
+ if ($database_url) {
+ if ($database_url->is_build_time === false) {
+ $this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable is not set as build time.', type: 'error');
+ $this->application_deployment_queue->addLogEntry('Please set DATABASE_URL environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
+ }
+ } else {
+ $this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable not found.', type: 'error');
+ $this->application_deployment_queue->addLogEntry('Please add DATABASE_URL environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
+ }
}
private function laravel_finetunes()
{
if ($this->pull_request_id === 0) {
- $nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
- $nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
+ $envType = 'environment_variables';
} else {
- $nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
- $nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
+ $envType = 'environment_variables_preview';
}
+ $nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
+ $nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
+
if (! $nixpacks_php_fallback_path) {
$nixpacks_php_fallback_path = new EnvironmentVariable;
$nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
@@ -1211,7 +1335,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function prepare_builder_image()
{
+ $settings = InstanceSettings::get();
$helperImage = config('coolify.helper_image');
+ $helperImage = "{$helperImage}:{$settings->helper_version}";
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
@@ -1363,7 +1489,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
$this->execute_remote_command(
[
- $importCommands, 'hidden' => true,
+ $importCommands,
+ 'hidden' => true,
]
);
$this->create_workdir();
@@ -1447,6 +1574,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value);
data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value);
}
+ if ($this->nixpacks_type === 'elixir') {
+ $this->elixir_finetunes();
+ }
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
if ($this->nixpacks_type === 'rust') {
@@ -1573,7 +1703,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// Check for custom HEALTHCHECK
if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile_from_repo', 'ignore_errors' => true,
+ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"),
+ 'hidden' => true,
+ 'save' => 'dockerfile_from_repo',
+ 'ignore_errors' => true,
]);
$dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
$this->application->parseHealthcheckFromDockerfile($dockerfile);
@@ -1676,14 +1809,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$docker_compose['services'][$this->container_name]['labels'] = $labels;
}
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
- $docker_compose['services'][$this->container_name]['logging'] = [
- 'driver' => 'fluentd',
- 'options' => [
- 'fluentd-address' => 'tcp://127.0.0.1:24224',
- 'fluentd-async' => 'true',
- 'fluentd-sub-second-precision' => 'true',
- ],
- ];
+ $docker_compose['services'][$this->container_name]['logging'] = generate_fluentd_configuration();
}
if ($this->application->settings->is_gpu_enabled) {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
@@ -1710,13 +1836,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
$docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array;
}
+
if (count($persistent_storages) > 0) {
- $docker_compose['services'][$this->container_name]['volumes'] = $persistent_storages;
+ if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) {
+ $docker_compose['services'][$this->container_name]['volumes'] = [];
+ }
+ $docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_storages);
}
if (count($persistent_file_volumes) > 0) {
- $docker_compose['services'][$this->container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
+ if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) {
+ $docker_compose['services'][$this->container_name]['volumes'] = [];
+ }
+ $docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
- })->toArray();
+ })->toArray());
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
@@ -1839,13 +1972,23 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry.");
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "docker pull {$image}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "docker pull {$image}"),
+ 'hidden' => true,
]
);
}
private function build_image()
{
+ // Add Coolify related variables to the build args
+ $this->environment_variables->filter(function ($key, $value) {
+ return str($key)->startsWith('COOLIFY_');
+ })->each(function ($key, $value) {
+ $this->build_args->push("--build-arg '{$key}'");
+ });
+
+ $this->build_args = $this->build_args->implode(' ');
+
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
@@ -1889,12 +2032,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
+ 'hidden' => true,
]);
$build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
} else {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
+ 'hidden' => true,
]);
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
}
@@ -1902,10 +2047,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
@@ -1919,10 +2070,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
}
@@ -1959,10 +2116,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"),
],
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
} else {
@@ -1976,10 +2139,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
} else {
@@ -1988,22 +2157,30 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
+ 'hidden' => true,
]);
$build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
} else {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
+ 'hidden' => true,
]);
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
}
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
@@ -2017,10 +2194,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
+ 'hidden' => true,
],
[
- executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
+ 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
+ 'hidden' => true,
]
);
}
@@ -2135,15 +2318,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->build_args->push("--build-arg {$env->key}={$value}");
}
}
-
- $this->build_args = $this->build_args->implode(' ');
- ray($this->build_args);
}
private function add_build_env_variables_to_dockerfile()
{
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile',
+ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"),
+ 'hidden' => true,
+ 'save' => 'dockerfile',
]);
$dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
if ($this->pull_request_id === 0) {
@@ -2161,7 +2343,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} else {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
}
- $dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
}
}
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
@@ -2189,7 +2370,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$exec = "docker exec {$containerName} {$cmd}";
$this->execute_remote_command(
[
- 'command' => $exec, 'hidden' => true,
+ 'command' => $exec,
+ 'hidden' => true,
],
);
@@ -2216,7 +2398,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
try {
$this->execute_remote_command(
[
- 'command' => $exec, 'hidden' => true, 'save' => 'post-deployment-command-output',
+ 'command' => $exec,
+ 'hidden' => true,
+ 'save' => 'post-deployment-command-output',
],
);
} catch (Exception $e) {
diff --git a/app/Jobs/ApplicationRestartJob.php b/app/Jobs/ApplicationRestartJob.php
deleted file mode 100644
index 54c062197..000000000
--- a/app/Jobs/ApplicationRestartJob.php
+++ /dev/null
@@ -1,32 +0,0 @@
-applicationDeploymentQueueId = $applicationDeploymentQueueId;
- }
-
- public function handle()
- {
- ray('Restarting application');
- }
-}
diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php
index 86b66fbfb..ddc264839 100644
--- a/app/Jobs/CheckForUpdatesJob.php
+++ b/app/Jobs/CheckForUpdatesJob.php
@@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\File;
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -25,12 +26,14 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
+
$latest_version = data_get($versions, 'coolify.v4.version');
$current_version = config('version');
if (version_compare($latest_version, $current_version, '>')) {
// New version available
$settings->update(['new_version_available' => true]);
+ File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
} else {
$settings->update(['new_version_available' => false]);
}
diff --git a/app/Jobs/CheckLogDrainContainerJob.php b/app/Jobs/CheckLogDrainContainerJob.php
deleted file mode 100644
index 16ef85192..000000000
--- a/app/Jobs/CheckLogDrainContainerJob.php
+++ /dev/null
@@ -1,93 +0,0 @@
-server->id))->dontRelease()];
- }
-
- public function uniqueId(): int
- {
- return $this->server->id;
- }
-
- public function healthcheck()
- {
- $status = instant_remote_process(["docker inspect --format='{{json .State.Status}}' coolify-log-drain"], $this->server, false);
- if (str($status)->contains('running')) {
- return true;
- } else {
- return false;
- }
- }
-
- public function handle()
- {
- // ray("checking log drain statuses for {$this->server->id}");
- try {
- if (! $this->server->isFunctional()) {
- return;
- }
- $containers = instant_remote_process(['docker container ls -q'], $this->server, false);
- if (! $containers) {
- return;
- }
- $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
- $containers = format_docker_command_output_to_json($containers);
-
- $foundLogDrainContainer = $containers->filter(function ($value, $key) {
- return data_get($value, 'Name') === '/coolify-log-drain';
- })->first();
- if (! $foundLogDrainContainer || ! $this->healthcheck()) {
- ray('Log drain container not found or unhealthy. Restarting...');
- InstallLogDrain::run($this->server);
- Sleep::for(10)->seconds();
- if ($this->healthcheck()) {
- if ($this->server->log_drain_notification_sent) {
- $this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
- $this->server->update(['log_drain_notification_sent' => false]);
- }
-
- return;
- }
- if (! $this->server->log_drain_notification_sent) {
- ray('Log drain container still unhealthy. Sending notification...');
- // $this->server->team?->notify(new ContainerStopped('Coolify Log Drainer', $this->server, null));
- $this->server->update(['log_drain_notification_sent' => true]);
- }
- } else {
- if ($this->server->log_drain_notification_sent) {
- $this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
- $this->server->update(['log_drain_notification_sent' => false]);
- }
- }
- } catch (\Throwable $e) {
- if (! isCloud()) {
- send_internal_notification("CheckLogDrainContainerJob failed on ({$this->server->id}) with: ".$e->getMessage());
- }
- ray($e->getMessage());
-
- return handleError($e);
- }
- }
-}
diff --git a/app/Jobs/CleanupStaleMultiplexedConnections.php b/app/Jobs/CleanupStaleMultiplexedConnections.php
new file mode 100644
index 000000000..bcca77c18
--- /dev/null
+++ b/app/Jobs/CleanupStaleMultiplexedConnections.php
@@ -0,0 +1,37 @@
+cleanupStaleConnection($server);
+ }
+ });
+ }
+
+ private function cleanupStaleConnection(Server $server)
+ {
+ $muxSocket = "/tmp/mux_{$server->id}";
+ $checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
+ $checkProcess = Process::run($checkCommand);
+
+ if ($checkProcess->exitCode() !== 0) {
+ $closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
+ Process::run($closeCommand);
+ }
+ }
+}
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 79b00e9cd..3bd13564b 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Actions\Database\StopDatabase;
use App\Events\BackupCreated;
+use App\Models\InstanceSettings;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
@@ -25,6 +26,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
+use Visus\Cuid2\Cuid2;
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -56,6 +58,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $backup_output = null;
+ public ?string $postgres_password = null;
+
public ?S3Storage $s3 = null;
public function __construct($backup)
@@ -89,8 +93,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function handle(): void
{
try {
- BackupCreated::dispatch($this->team->id);
-
// Check if team is exists
if (is_null($this->team)) {
$this->backup->update(['status' => 'failed']);
@@ -99,6 +101,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
return;
}
+
+ BackupCreated::dispatch($this->team->id);
+
$status = str(data_get($this->database, 'status'));
if (! $status->startsWith('running') && $this->database->id !== 0) {
ray('database not running');
@@ -134,6 +139,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else {
$databasesToBackup = $this->database->postgres_user;
}
+ $this->postgres_password = $envs->filter(function ($env) {
+ return str($env)->startsWith('POSTGRES_PASSWORD=');
+ })->first();
+ if ($this->postgres_password) {
+ $this->postgres_password = str($this->postgres_password)->after('POSTGRES_PASSWORD=')->value();
+ }
+
} elseif (str($databaseType)->contains('mysql')) {
$this->container_name = "{$this->database->name}-$serviceUuid";
$this->directory_name = $serviceName.'-'.$this->container_name;
@@ -336,7 +348,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$url = $this->database->internal_db_url;
if ($databaseWithCollections === 'all') {
$commands[] = 'mkdir -p '.$this->backup_dir;
- if (str($this->database->image)->startsWith('mongo:4.0')) {
+ if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
@@ -351,13 +363,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
$commands[] = 'mkdir -p '.$this->backup_dir;
if ($collectionsToExclude->count() === 0) {
- if (str($this->database->image)->startsWith('mongo:4.0')) {
+ if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location";
}
} else {
- if (str($this->database->image)->startsWith('mongo:4.0')) {
+ if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
@@ -381,7 +393,14 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
- $commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
+ $backupCommand = 'docker exec';
+ if ($this->postgres_password) {
+ $backupCommand .= " -e PGPASSWORD=$this->postgres_password";
+ }
+ $backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
+
+ $commands[] = $backupCommand;
+ ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
@@ -452,7 +471,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup->number_of_backups_locally === 0) {
$deletable = $this->backup->executions()->where('status', 'success');
} else {
- $deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally - 1);
+ $deletable = $this->backup->executions()->where('status', 'success')->skip($this->backup->number_of_backups_locally - 1);
}
foreach ($deletable->get() as $execution) {
delete_backup_locally($execution->filename, $this->server);
@@ -460,6 +479,34 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
+ // private function upload_to_s3(): void
+ // {
+ // try {
+ // if (is_null($this->s3)) {
+ // return;
+ // }
+ // $key = $this->s3->key;
+ // $secret = $this->s3->secret;
+ // // $region = $this->s3->region;
+ // $bucket = $this->s3->bucket;
+ // $endpoint = $this->s3->endpoint;
+ // $this->s3->testConnection(shouldSave: true);
+ // $configName = new Cuid2;
+
+ // $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
+ // $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
+ // $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
+ // instant_remote_process($commands, $this->server);
+ // $this->add_to_backup_output('Uploaded to S3.');
+ // } catch (\Throwable $e) {
+ // $this->add_to_backup_output($e->getMessage());
+ // throw $e;
+ // } finally {
+ // $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
+ // $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
+ // instant_remote_process($removeConfigCommands, $this->server, false);
+ // }
+ // }
private function upload_to_s3(): void
{
try {
@@ -477,12 +524,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else {
$network = $this->database->destination->network;
}
- $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
+
+ $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.');
- ray('Uploaded to S3. '.$this->backup_location.' to s3://'.$bucket.$this->backup_dir);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
throw $e;
@@ -491,4 +541,42 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
instant_remote_process([$command], $this->server);
}
}
+
+ private function ensureHelperImageAvailable(): void
+ {
+ $fullImageName = $this->getFullImageName();
+
+ $imageExists = $this->checkImageExists($fullImageName);
+
+ if (! $imageExists) {
+ $this->pullHelperImage($fullImageName);
+ }
+ }
+
+ private function checkImageExists(string $fullImageName): bool
+ {
+ $result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false);
+
+ return trim($result) === 'exists';
+ }
+
+ private function pullHelperImage(string $fullImageName): void
+ {
+ try {
+ instant_remote_process(["docker pull {$fullImageName}"], $this->server);
+ } catch (\Exception $e) {
+ $errorMessage = 'Failed to pull helper image: '.$e->getMessage();
+ $this->add_to_backup_output($errorMessage);
+ throw new \RuntimeException($errorMessage);
+ }
+ }
+
+ private function getFullImageName(): string
+ {
+ $settings = InstanceSettings::get();
+ $helperImage = config('coolify.helper_image');
+ $latestVersion = $settings->helper_version;
+
+ return "{$helperImage}:{$latestVersion}";
+ }
}
diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php
index 5010263ae..f95cd2920 100644
--- a/app/Jobs/DockerCleanupJob.php
+++ b/app/Jobs/DockerCleanupJob.php
@@ -10,6 +10,7 @@ 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\Log;
@@ -17,21 +18,33 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public $timeout = 300;
+ public $timeout = 600;
- public int|string|null $usageBefore = null;
+ public $tries = 1;
+
+ 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 handle(): void
{
try {
if (! $this->server->isFunctional()) {
return;
}
- if ($this->server->settings->is_force_cleanup_enabled) {
+ if ($this->server->settings->force_docker_cleanup) {
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
- CleanupDocker::run(server: $this->server, force: true);
+ CleanupDocker::run(server: $this->server);
return;
}
@@ -39,12 +52,12 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
$this->usageBefore = $this->server->getDiskUsage();
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
- CleanupDocker::run(server: $this->server, force: true);
+ CleanupDocker::run(server: $this->server);
return;
}
- if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
- CleanupDocker::run(server: $this->server, force: false);
+ if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {
+ CleanupDocker::run(server: $this->server);
$usageAfter = $this->server->getDiskUsage();
if ($usageAfter < $this->usageBefore) {
$this->server->team?->notify(new DockerCleanup($this->server, 'Saved '.($this->usageBefore - $usageAfter).'% disk space.'));
@@ -56,7 +69,8 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
Log::info('No need to clean up '.$this->server->name);
}
} catch (\Throwable $e) {
- ray($e->getMessage());
+ CleanupDocker::run(server: $this->server);
+ Log::error('DockerCleanupJob failed: '.$e->getMessage());
throw $e;
}
}
diff --git a/app/Jobs/InstanceAutoUpdateJob.php b/app/Jobs/InstanceAutoUpdateJob.php
deleted file mode 100644
index 1bbfcf8cb..000000000
--- a/app/Jobs/InstanceAutoUpdateJob.php
+++ /dev/null
@@ -1,28 +0,0 @@
-get('https://cdn.coollabs.io/coolify/versions.json');
- if ($response->successful()) {
- $versions = $response->json();
- File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
- }
- $latest_version = get_latest_version_of_coolify();
- instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$latest_version}"], $server, false);
-
- $current_version = config('version');
- if (! $settings->is_auto_update_enabled) {
- return;
- }
- if ($latest_version === $current_version) {
- return;
- }
- if (version_compare($latest_version, $current_version, '<')) {
- return;
- }
- } catch (\Throwable $e) {
- throw $e;
- }
- }
-}
diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php
index 30a1b8026..63b7fa920 100644
--- a/app/Jobs/PullHelperImageJob.php
+++ b/app/Jobs/PullHelperImageJob.php
@@ -2,6 +2,7 @@
namespace App\Jobs;
+use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -10,6 +11,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Http;
class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -32,10 +34,20 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public function handle(): void
{
try {
- $helperImage = config('coolify.helper_image');
- ray("Pulling {$helperImage}");
- instant_remote_process(["docker pull -q {$helperImage}"], $this->server, false);
- ray('PullHelperImageJob done');
+ $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
+ if ($response->successful()) {
+ $versions = $response->json();
+ $settings = InstanceSettings::get();
+ $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);
+ $settings->update(['helper_version' => $latest_version]);
+ }
+ }
+
} catch (\Throwable $e) {
send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage());
ray($e->getMessage());
diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php
index 819e28f89..93d5fca70 100644
--- a/app/Jobs/ScheduledTaskJob.php
+++ b/app/Jobs/ScheduledTaskJob.php
@@ -36,6 +36,8 @@ class ScheduledTaskJob implements ShouldQueue
public array $containers = [];
+ public string $server_timezone;
+
public function __construct($task)
{
$this->task = $task;
@@ -47,6 +49,19 @@ class ScheduledTaskJob implements ShouldQueue
throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
}
$this->team = Team::find($task->team_id);
+ $this->server_timezone = $this->getServerTimezone();
+ }
+
+ 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
@@ -61,6 +76,7 @@ class ScheduledTaskJob implements ShouldQueue
public function handle(): void
{
+
try {
$this->task_log = ScheduledTaskExecution::create([
'scheduled_task_id' => $this->task->id,
@@ -78,12 +94,12 @@ class ScheduledTaskJob implements ShouldQueue
} 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');
}
});
}
@@ -96,8 +112,8 @@ class ScheduledTaskJob implements ShouldQueue
}
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([
@@ -121,6 +137,7 @@ class ScheduledTaskJob implements ShouldQueue
$this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
+ } finally {
}
}
}
diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php
index 1db15cfd4..540085385 100644
--- a/app/Jobs/ServerCheckJob.php
+++ b/app/Jobs/ServerCheckJob.php
@@ -26,6 +26,8 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public $tries = 3;
+ public $timeout = 60;
+
public $containers;
public $applications;
@@ -43,15 +45,15 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
- // public function middleware(): array
- // {
- // return [(new WithoutOverlapping($this->server->uuid))];
- // }
+ public function middleware(): array
+ {
+ return [(new WithoutOverlapping($this->server->id))];
+ }
- // public function uniqueId(): int
- // {
- // return $this->server->uuid;
- // }
+ public function uniqueId(): int
+ {
+ return $this->server->id;
+ }
public function handle()
{
@@ -79,7 +81,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
}
GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
$this->checkLogDrainContainer();
- $this->checkSentinel();
}
} catch (\Throwable $e) {
@@ -90,21 +91,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
}
- private function checkSentinel()
- {
- if ($this->server->isSentinelEnabled()) {
- $sentinelContainerFound = $this->containers->filter(function ($value, $key) {
- return data_get($value, 'Name') === '/coolify-sentinel';
- })->first();
- if ($sentinelContainerFound) {
- $status = data_get($sentinelContainerFound, 'State.Status');
- if ($status !== 'running') {
- PullSentinelImageJob::dispatch($this);
- }
- }
- }
- }
-
private function serverStatus()
{
['uptime' => $uptime] = $this->server->validateConnection();
@@ -140,6 +126,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
private function checkLogDrainContainer()
{
+ if (! $this->server->isLogDrainEnabled()) {
+ return;
+ }
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain';
})->first();
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index 147a1ad6f..af05ad767 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -73,6 +73,8 @@ class Index extends Component
}
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
+ $this->remoteServerPort = $this->remoteServerPort;
+ $this->remoteServerUser = $this->remoteServerUser;
if (isDev()) {
$this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
@@ -154,6 +156,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->servers->count() > 0) {
$this->selectedExistingServer = $this->servers->first()->id;
+ $this->updateServerDetails();
$this->currentState = 'select-existing-server';
return;
@@ -173,9 +176,18 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
+ $this->updateServerDetails();
$this->currentState = 'validate-server';
}
+ private function updateServerDetails()
+ {
+ if ($this->createdServer) {
+ $this->remoteServerPort = $this->createdServer->port;
+ $this->remoteServerUser = $this->createdServer->user;
+ }
+ }
+
public function getProxyType()
{
// Set Default Proxy Type
@@ -235,11 +247,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function saveServer()
{
$this->validate([
- 'remoteServerName' => 'required',
- 'remoteServerHost' => 'required',
+ 'remoteServerName' => 'required|string',
+ 'remoteServerHost' => 'required|string',
'remoteServerPort' => 'required|integer',
- 'remoteServerUser' => 'required',
+ 'remoteServerUser' => 'required|string',
]);
+
$this->privateKey = formatPrivateKey($this->privateKey);
$foundServer = Server::whereIp($this->remoteServerHost)->first();
if ($foundServer) {
@@ -269,7 +282,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function validateServer()
{
try {
- config()->set('coolify.mux_enabled', false);
+ config()->set('constants.ssh.mux_enabled', false);
// EC2 does not have `uptime` command, lol
instant_remote_process(['ls /'], $this->createdServer, true);
@@ -277,9 +290,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdServer->settings()->update([
'is_reachable' => true,
]);
+ $this->serverReachable = true;
} catch (\Throwable $e) {
$this->serverReachable = false;
- $this->createdServer->delete();
+ $this->createdServer->settings()->update([
+ 'is_reachable' => false,
+ ]);
return handleError(error: $e, livewire: $this);
}
@@ -296,6 +312,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]);
$this->getProxyType();
} catch (\Throwable $e) {
+ $this->createdServer->settings()->update([
+ 'is_usable' => false,
+ ]);
+
return handleError(error: $e, livewire: $this);
}
}
@@ -349,6 +369,21 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
);
}
+ public function saveAndValidateServer()
+ {
+ $this->validate([
+ 'remoteServerPort' => 'required|integer|min:1|max:65535',
+ 'remoteServerUser' => 'required|string',
+ ]);
+
+ $this->createdServer->update([
+ 'port' => $this->remoteServerPort,
+ 'user' => $this->remoteServerUser,
+ 'timezone' => 'UTC',
+ ]);
+ $this->validateServer();
+ }
+
private function createNewPrivateKey()
{
$this->privateKeyName = generate_random_name();
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 215d3b16c..1f0b68dd3 100644
--- a/app/Livewire/Dashboard.php
+++ b/app/Livewire/Dashboard.php
@@ -49,15 +49,6 @@ class Dashboard extends Component
])->sortBy('id')->groupBy('server_name')->toArray();
}
- // public function getIptables()
- // {
- // $servers = Server::ownedByCurrentTeam()->get();
- // foreach ($servers as $server) {
- // checkRequiredCommands($server);
- // $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server);
- // ray($iptables);
- // }
- // }
public function render()
{
return view('livewire.dashboard');
diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php
index cde3322db..a3a688f7c 100644
--- a/app/Livewire/Project/Application/Advanced.php
+++ b/app/Livewire/Project/Application/Advanced.php
@@ -66,9 +66,9 @@ class Advanced extends Component
$this->dispatch('resetDefaultLabels', false);
}
if ($this->application->settings->is_raw_compose_deployment_enabled) {
- $this->application->parseRawCompose();
+ $this->application->oldRawParser();
} else {
- $this->application->parseCompose();
+ $this->application->parse();
}
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');
@@ -96,6 +96,12 @@ class Advanced extends Component
} else {
$this->application->settings->custom_internal_name = null;
}
+ if (is_null($this->application->settings->custom_internal_name)) {
+ $this->application->settings->save();
+ $this->dispatch('success', 'Custom name saved.');
+
+ return;
+ }
$customInternalName = $this->application->settings->custom_internal_name;
$server = $this->application->destination->server;
$allApplications = $server->applications();
diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php
index 84a24255c..f2968f6d9 100644
--- a/app/Livewire/Project/Application/Deployment/Show.php
+++ b/app/Livewire/Project/Application/Deployment/Show.php
@@ -4,6 +4,7 @@ namespace App\Livewire\Project\Application\Deployment;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
+use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component
@@ -69,6 +70,20 @@ class Show extends Component
}
}
+ public function getLogLinesProperty()
+ {
+ return decode_remote_command_output($this->application_deployment_queue)->map(function ($logLine) {
+ $logLine['line'] = e($logLine['line']);
+ $logLine['line'] = preg_replace(
+ '/(https?:\/\/[^\s]+)/',
+ '$1',
+ $logLine['line'],
+ );
+
+ return $logLine;
+ });
+ }
+
public function render()
{
return view('livewire.project.application.deployment.show');
diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php
index b3e39d23d..5fccce792 100644
--- a/app/Livewire/Project/Application/DeploymentNavbar.php
+++ b/app/Livewire/Project/Application/DeploymentNavbar.php
@@ -55,9 +55,14 @@ class DeploymentNavbar extends Component
public function cancel()
{
$kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
+ $build_server_id = $this->application_deployment_queue->build_server_id;
$server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id;
try {
- $server = Server::find($server_id);
+ if ($this->application->settings->is_build_server_enabled) {
+ $server = Server::find($build_server_id);
+ } else {
+ $server = Server::find($server_id);
+ }
if ($this->application_deployment_queue->logs) {
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 77593bf0a..d2700f444 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -3,7 +3,6 @@
namespace App\Livewire\Project\Application;
use App\Models\Application;
-use App\Models\LocalFileVolume;
use Illuminate\Support\Collection;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -30,6 +29,8 @@ class General extends Component
public ?string $ports_exposes = null;
+ public bool $is_preserve_repository_enabled = false;
+
public bool $is_container_label_escape_enabled = true;
public $customLabels;
@@ -130,7 +131,7 @@ class General extends Component
public function mount()
{
try {
- $this->parsedServices = $this->application->parseCompose();
+ $this->parsedServices = $this->application->parse();
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
@@ -145,6 +146,7 @@ class General extends Component
}
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes;
+ $this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->customLabels = $this->application->parseContainerLabels();
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) {
@@ -168,9 +170,21 @@ class General extends Component
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');
$this->application->refresh();
+
+ // If port_exposes changed, reset default labels
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
$this->resetDefaultLabels(false);
}
+ if ($this->is_preserve_repository_enabled !== $this->application->settings->is_preserve_repository_enabled) {
+ if ($this->application->settings->is_preserve_repository_enabled === false) {
+ $this->application->fileStorages->each(function ($storage) {
+ $storage->is_based_on_git = $this->application->settings->is_preserve_repository_enabled;
+ $storage->save();
+ });
+ }
+
+ }
+
}
public function loadComposeFile($isInit = false)
@@ -179,39 +193,18 @@ class General extends Component
if ($isInit && $this->application->docker_compose_raw) {
return;
}
+
+ // Must reload the application to get the latest database changes
+ // Why? Not sure, but it works.
+ // $this->application->refresh();
+
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit);
if (is_null($this->parsedServices)) {
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
return;
}
- $compose = $this->application->parseCompose();
- $services = data_get($compose, 'services');
- if ($services) {
- $volumes = collect($services)->map(function ($service) {
- return data_get($service, 'volumes');
- })->flatten()->filter(function ($volume) {
- return str($volume)->startsWith('/data/coolify');
- })->unique()->values();
- foreach ($volumes as $volume) {
- $source = str($volume)->before(':');
- $target = str($volume)->after(':')->beforeLast(':');
-
- LocalFileVolume::updateOrCreate(
- [
- 'mount_path' => $target,
- 'resource_id' => $this->application->id,
- 'resource_type' => get_class($this->application),
- ],
- [
- 'fs_path' => $source,
- 'mount_path' => $target,
- 'resource_id' => $this->application->id,
- 'resource_type' => get_class($this->application),
- ]
- );
- }
- }
+ $this->application->parse();
$this->dispatch('success', 'Docker compose file loaded.');
$this->dispatch('compose_loaded');
$this->dispatch('refreshStorages');
diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php
index 78fab7e44..397e159ad 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -81,8 +81,15 @@ class Previews extends Component
return;
}
- $fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
+ if ($this->application->build_pack === 'dockercompose') {
+ $preview->generate_preview_fqdn_compose();
+ $this->application->refresh();
+ $this->dispatch('success', 'Domain generated.');
+ return;
+ }
+
+ $fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
$url = Url::fromString($fqdn);
$template = $this->application->preview_url_template;
$host = $url->getHost();
diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php
index b6827895f..e16397652 100644
--- a/app/Livewire/Project/Database/BackupExecutions.php
+++ b/app/Livewire/Project/Database/BackupExecutions.php
@@ -11,9 +11,8 @@ use Livewire\Attributes\On;
class BackupExecutions extends Component
{
public ?ScheduledDatabaseBackup $backup = null;
-
+ public $database;
public $executions = [];
-
public $setDeletableBackup;
public $delete_backup_s3 = true;
@@ -79,10 +78,56 @@ class BackupExecutions extends Component
public function refreshBackupExecutions(): void
{
if ($this->backup) {
- $this->executions = $this->backup->executions()->get()->sortBy('created_at');
+ $this->executions = $this->backup->executions()->get();
}
}
+ public function mount(ScheduledDatabaseBackup $backup)
+ {
+ $this->backup = $backup;
+ $this->database = $backup->database;
+ $this->refreshBackupExecutions();
+ }
+
+ public function server()
+ {
+ if ($this->database) {
+ $server = null;
+
+ if ($this->database instanceof \App\Models\ServiceDatabase) {
+ $server = $this->database->service->destination->server;
+ } elseif ($this->database->destination && $this->database->destination->server) {
+ $server = $this->database->destination->server;
+ }
+ if ($server) {
+ return $server;
+ }
+ }
+ return null;
+ }
+
+ public function getServerTimezone()
+ {
+ $server = $this->server();
+ if (!$server) {
+ return 'UTC';
+ }
+ $serverTimezone = $server->settings->server_timezone;
+ return $serverTimezone;
+ }
+
+ public function formatDateInServerTimezone($date)
+ {
+ $serverTimezone = $this->getServerTimezone();
+ $dateObj = new \DateTime($date);
+ try {
+ $dateObj->setTimezone(new \DateTimeZone($serverTimezone));
+ } 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', [
diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php
index ffdbe95c3..a6e2a1320 100644
--- a/app/Livewire/Project/Database/Clickhouse/General.php
+++ b/app/Livewire/Project/Database/Clickhouse/General.php
@@ -31,6 +31,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -42,6 +43,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
@@ -54,7 +56,7 @@ class General extends Component
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.');
@@ -71,14 +73,14 @@ class General extends Component
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;
@@ -93,7 +95,7 @@ class General extends Component
$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 f81f4a2f0..00e0ff09f 100644
--- a/app/Livewire/Project/Database/Dragonfly/General.php
+++ b/app/Livewire/Project/Database/Dragonfly/General.php
@@ -30,6 +30,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -40,6 +41,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
@@ -52,7 +54,7 @@ class General extends Component
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.');
@@ -86,14 +88,14 @@ class General extends Component
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;
@@ -108,7 +110,7 @@ class General extends Component
$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/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index 2b78c9f10..320feeac7 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -31,6 +31,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -42,6 +43,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
@@ -55,7 +57,7 @@ class General extends Component
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.');
@@ -92,14 +94,14 @@ class General extends Component
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;
@@ -114,7 +116,7 @@ class General extends Component
$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 858d7b383..70545910c 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -34,6 +34,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -48,6 +49,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Options',
];
public function mount()
@@ -61,7 +63,7 @@ class General extends Component
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.');
@@ -98,14 +100,14 @@ class General extends Component
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;
@@ -120,7 +122,7 @@ class General extends Component
$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 5a5ef8a62..d23b66c00 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -33,6 +33,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -46,6 +47,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
@@ -59,7 +61,7 @@ class General extends Component
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 +101,14 @@ class General extends Component
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 +123,7 @@ class General extends Component
$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 58d8e03a8..29a9cbae2 100644
--- a/app/Livewire/Project/Database/Mysql/General.php
+++ b/app/Livewire/Project/Database/Mysql/General.php
@@ -34,6 +34,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -48,6 +49,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
@@ -60,7 +62,7 @@ class General extends Component
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.');
@@ -97,14 +99,14 @@ class General extends Component
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;
@@ -119,7 +121,7 @@ class General extends Component
$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/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php
index eabbbd679..c12fa49f3 100644
--- a/app/Livewire/Project/Database/Postgresql/General.php
+++ b/app/Livewire/Project/Database/Postgresql/General.php
@@ -49,6 +49,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -65,6 +66,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Run Options',
];
public function mount()
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index a7ce0161a..fd2f9834f 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -31,6 +31,7 @@ class General extends Component
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
+ 'database.custom_docker_run_options' => 'nullable',
];
protected $validationAttributes = [
@@ -42,6 +43,7 @@ class General extends Component
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
+ 'database.custom_docker_run_options' => 'Custom Docker Options',
];
public function mount()
@@ -55,7 +57,7 @@ class General extends Component
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.');
@@ -86,14 +88,14 @@ class General extends Component
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;
@@ -108,7 +110,7 @@ class General extends Component
$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/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php
index 633ce5bda..199a20cf6 100644
--- a/app/Livewire/Project/New/DockerCompose.php
+++ b/app/Livewire/Project/New/DockerCompose.php
@@ -5,6 +5,8 @@ namespace App\Livewire\Project\New;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Service;
+use App\Models\StandaloneDocker;
+use App\Models\SwarmDocker;
use Illuminate\Support\Str;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
@@ -58,12 +60,26 @@ class DockerCompose extends Component
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
+
+ $destination_uuid = $this->query['destination'];
+ $destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
+ if (! $destination) {
+ $destination = SwarmDocker::where('uuid', $destination_uuid)->first();
+ }
+ if (! $destination) {
+ throw new \Exception('Destination not found. What?!');
+ }
+ $destination_class = $destination->getMorphClass();
+
$service = Service::create([
'name' => 'service'.Str::random(10),
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
+ 'destination_id' => $destination->id,
+ 'destination_type' => $destination_class,
]);
+
$variables = parseEnvFormatToArray($this->envFile);
foreach ($variables as $key => $variable) {
EnvironmentVariable::create([
diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php
index b29fe8cab..b5c5cb1db 100644
--- a/app/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Livewire/Project/New/PublicGitRepository.php
@@ -99,6 +99,16 @@ class PublicGitRepository extends Component
}
}
+ public function updatedDockerComposeLocation()
+ {
+ if ($this->docker_compose_location) {
+ $this->docker_compose_location = rtrim($this->docker_compose_location, '/');
+ if (! str($this->docker_compose_location)->startsWith('/')) {
+ $this->docker_compose_location = '/'.$this->docker_compose_location;
+ }
+ }
+ }
+
public function updatedBuildPack()
{
if ($this->build_pack === 'nixpacks') {
diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index b25290f71..3c5f3901b 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -45,6 +45,8 @@ class Select extends Component
public ?string $selectedEnvironment = null;
+ public string $postgresql_type = 'postgres:16-alpine';
+
public ?string $existingPostgresqlUrl = null;
public ?string $search = null;
@@ -202,6 +204,8 @@ class Select extends Component
$docker = $this->standaloneDockers->first() ?? $this->swarmDockers->first();
if ($docker) {
$this->setDestination($docker->uuid);
+
+ return $this->whatToDoNext();
}
}
$this->current_step = 'destinations';
@@ -211,15 +215,38 @@ class Select extends Component
{
$this->destination_uuid = $destination_uuid;
+ return $this->whatToDoNext();
+ }
+
+ public function setPostgresqlType(string $type)
+ {
+ $this->postgresql_type = $type;
+
return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'],
'type' => $this->type,
'destination' => $this->destination_uuid,
'server_id' => $this->server_id,
+ 'database_image' => $this->postgresql_type,
]);
}
+ public function whatToDoNext()
+ {
+ if ($this->type === 'postgresql') {
+ $this->current_step = 'select-postgresql-type';
+ } else {
+ return redirect()->route('project.resource.create', [
+ 'project_uuid' => $this->parameters['project_uuid'],
+ 'environment_name' => $this->parameters['environment_name'],
+ 'type' => $this->type,
+ 'destination' => $this->destination_uuid,
+ 'server_id' => $this->server_id,
+ ]);
+ }
+ }
+
public function loadServers()
{
$this->servers = Server::isUsable()->get();
diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php
index 341dd93d8..5c6a37d6d 100644
--- a/app/Livewire/Project/Resource/Create.php
+++ b/app/Livewire/Project/Resource/Create.php
@@ -18,6 +18,7 @@ class Create extends Component
$type = str(request()->query('type'));
$destination_uuid = request()->query('destination');
$server_id = request()->query('server_id');
+ $database_image = request()->query('database_image');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (! $project) {
@@ -33,7 +34,11 @@ class Create extends Component
if (in_array($type, DATABASE_TYPES)) {
if ($type->value() === 'postgresql') {
- $database = create_standalone_postgresql($environment->id, $destination_uuid);
+ $database = create_standalone_postgresql(
+ environmentId: $environment->id,
+ destinationUuid: $destination_uuid,
+ databaseImage: $database_image
+ );
} elseif ($type->value() === 'redis') {
$database = create_standalone_redis($environment->id, $destination_uuid);
} elseif ($type->value() === 'mongodb') {
@@ -86,18 +91,16 @@ class Create extends Component
$oneClickDotEnvs->each(function ($value) use ($service) {
$key = str()->before($value, '=');
$value = str(str()->after($value, '='));
- $generatedValue = $value;
- if ($value->contains('SERVICE_')) {
- $command = $value->after('SERVICE_')->beforeLast('_');
- $generatedValue = generateEnvValue($command->value(), $service);
+ if ($value) {
+ EnvironmentVariable::create([
+ 'key' => $key,
+ 'value' => $value,
+ 'service_id' => $service->id,
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
}
- EnvironmentVariable::create([
- 'key' => $key,
- 'value' => $generatedValue,
- 'service_id' => $service->id,
- 'is_build_time' => false,
- 'is_preview' => false,
- ]);
+
});
}
$service->parse(isNew: true);
diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php
index a55af5777..a2e48fee7 100644
--- a/app/Livewire/Project/Service/Configuration.php
+++ b/app/Livewire/Project/Service/Configuration.php
@@ -25,6 +25,7 @@ class Configuration extends Component
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
'check_status',
+ 'refresh' => '$refresh',
];
}
@@ -75,6 +76,12 @@ class Configuration extends Component
{
try {
GetContainersStatus::run($this->service->server);
+ $this->service->applications->each(function ($application) {
+ $application->refresh();
+ });
+ $this->service->databases->each(function ($database) {
+ $database->refresh();
+ });
$this->dispatch('$refresh');
} catch (\Exception $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php
index f67b95a8a..dc043e65a 100644
--- a/app/Livewire/Project/Service/EditCompose.php
+++ b/app/Livewire/Project/Service/EditCompose.php
@@ -11,7 +11,11 @@ class EditCompose extends Component
public $serviceId;
- protected $listeners = ['refreshEnvs', 'envsUpdated'];
+ protected $listeners = [
+ 'refreshEnvs',
+ 'envsUpdated',
+ 'refresh' => 'envsUpdated',
+ ];
protected $rules = [
'service.docker_compose_raw' => 'required',
@@ -39,6 +43,7 @@ class EditCompose extends Component
{
$this->dispatch('info', 'Saving new docker compose...');
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
+ $this->dispatch('refreshStorages');
}
public function instantSave()
diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 9f0d94aad..9fac897bf 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -35,6 +35,7 @@ class FileStorage extends Component
'fileStorage.fs_path' => 'required',
'fileStorage.mount_path' => 'required',
'fileStorage.content' => 'nullable',
+ 'fileStorage.is_based_on_git' => 'required|boolean',
];
public function mount()
@@ -47,6 +48,7 @@ class FileStorage extends Component
$this->workdir = null;
$this->fs_path = $this->fileStorage->fs_path;
}
+ $this->fileStorage->loadStorageOnServer();
}
public function convertToDirectory()
@@ -55,6 +57,7 @@ class FileStorage extends Component
$this->fileStorage->deleteStorageOnServer();
$this->fileStorage->is_directory = true;
$this->fileStorage->content = null;
+ $this->fileStorage->is_based_on_git = false;
$this->fileStorage->save();
$this->fileStorage->saveStorageOnServer();
} catch (\Throwable $e) {
@@ -70,6 +73,9 @@ class FileStorage extends Component
$this->fileStorage->deleteStorageOnServer();
$this->fileStorage->is_directory = false;
$this->fileStorage->content = null;
+ if (data_get($this->resource, 'settings.is_preserve_repository_enabled')) {
+ $this->fileStorage->is_based_on_git = true;
+ }
$this->fileStorage->save();
$this->fileStorage->saveStorageOnServer();
} catch (\Throwable $e) {
diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php
index 66d3079ec..51f38d2db 100644
--- a/app/Livewire/Project/Service/Navbar.php
+++ b/app/Livewire/Project/Service/Navbar.php
@@ -21,6 +21,7 @@ class Navbar extends Component
public $isDeploymentProgress = false;
public $docker_cleanup = true;
+ public $title = 'Configuration';
public function mount()
{
diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php
index 3f62202c8..04bb136db 100644
--- a/app/Livewire/Project/Service/StackForm.php
+++ b/app/Livewire/Project/Service/StackForm.php
@@ -53,7 +53,7 @@ class StackForm extends Component
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
- $this->submit();
+ $this->submit(notify: false);
}
public function instantSave()
@@ -62,7 +62,7 @@ class StackForm extends Component
$this->dispatch('success', 'Service settings saved.');
}
- public function submit()
+ public function submit($notify = true)
{
try {
$this->validate();
@@ -76,7 +76,7 @@ class StackForm extends Component
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->dispatch('refreshEnvs');
- $this->dispatch('success', 'Service saved.');
+ $notify && $this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index 9e6760293..055788b57 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -23,8 +23,9 @@ class All extends Component
public string $view = 'normal';
protected $listeners = [
- 'refreshEnvs',
'saveKey' => 'submit',
+ 'refreshEnvs',
+ 'environmentVariableDeleted' => 'refreshEnvs',
];
protected $rules = [
@@ -40,220 +41,240 @@ class All extends Component
$this->showPreview = true;
}
$this->modalId = new Cuid2;
- $this->sortMe();
- $this->getDevView();
- }
-
- public function sortMe()
- {
- if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
- if ($this->resource->settings->is_env_sorting_enabled) {
- $this->resource->environment_variables = $this->resource->environment_variables->sortBy('key');
- $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('key');
- } else {
- $this->resource->environment_variables = $this->resource->environment_variables->sortBy('id');
- $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('id');
- }
- }
- $this->getDevView();
+ $this->sortEnvironmentVariables();
}
public function instantSave()
{
- if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
- $this->resource->settings->save();
- $this->dispatch('success', 'Environment variable settings updated.');
- $this->sortMe();
+ $this->resource->settings->save();
+ $this->sortEnvironmentVariables();
+ $this->dispatch('success', 'Environment variable settings updated.');
+ }
+
+ public function sortEnvironmentVariables()
+ {
+ if ($this->resource->type() === 'application') {
+ $this->resource->load(['environment_variables', 'environment_variables_preview']);
+ } else {
+ $this->resource->load(['environment_variables']);
}
+
+ $sortBy = data_get($this->resource, 'settings.is_env_sorting_enabled') ? 'key' : 'order';
+
+ $sortFunction = function ($variables) use ($sortBy) {
+ if (! $variables) {
+ return $variables;
+ }
+ if ($sortBy === 'key') {
+ return $variables->sortBy(function ($item) {
+ return strtolower($item->key);
+ }, SORT_NATURAL | SORT_FLAG_CASE)->values();
+ } else {
+ return $variables->sortBy('order')->values();
+ }
+ };
+
+ $this->resource->environment_variables = $sortFunction($this->resource->environment_variables);
+ $this->resource->environment_variables_preview = $sortFunction($this->resource->environment_variables_preview);
+
+ $this->getDevView();
}
public function getDevView()
{
- $this->variables = $this->resource->environment_variables->map(function ($item) {
+ $this->variables = $this->formatEnvironmentVariables($this->resource->environment_variables);
+ if ($this->showPreview) {
+ $this->variablesPreview = $this->formatEnvironmentVariables($this->resource->environment_variables_preview);
+ }
+ }
+
+ private function formatEnvironmentVariables($variables)
+ {
+ return $variables->map(function ($item) {
if ($item->is_shown_once) {
- return "$item->key=(locked secret)";
+ return "$item->key=(Locked Secret, delete and add again to change)";
}
if ($item->is_multiline) {
- return "$item->key=(multiline, edit in normal view)";
+ return "$item->key=(Multiline environment variable, edit in normal view)";
}
return "$item->key=$item->value";
- })->join('
-');
- if ($this->showPreview) {
- $this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
- if ($item->is_shown_once) {
- return "$item->key=(locked secret)";
- }
- if ($item->is_multiline) {
- return "$item->key=(multiline, edit in normal view)";
- }
-
- return "$item->key=$item->value";
- })->join('
-');
- }
+ })->join("\n");
}
public function switch()
{
- if ($this->view === 'normal') {
- $this->view = 'dev';
- } else {
- $this->view = 'normal';
- }
- $this->sortMe();
+ $this->view = $this->view === 'normal' ? 'dev' : 'normal';
+ $this->sortEnvironmentVariables();
}
- public function saveVariables($isPreview)
+ public function submit($data = null)
{
- if ($isPreview) {
- $variables = parseEnvFormatToArray($this->variablesPreview);
- $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete();
- } else {
- $variables = parseEnvFormatToArray($this->variables);
- $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete();
- }
- foreach ($variables as $key => $variable) {
- if ($isPreview) {
- $found = $this->resource->environment_variables_preview()->where('key', $key)->first();
+ try {
+ if ($data === null) {
+ $this->handleBulkSubmit();
} else {
- $found = $this->resource->environment_variables()->where('key', $key)->first();
+ $this->handleSingleSubmit($data);
}
- if ($found) {
- if ($found->is_shown_once || $found->is_multiline) {
- continue;
+
+ $this->updateOrder();
+ $this->sortEnvironmentVariables();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ private function updateOrder()
+ {
+ $variables = parseEnvFormatToArray($this->variables);
+ $order = 1;
+ foreach ($variables as $key => $value) {
+ $env = $this->resource->environment_variables()->where('key', $key)->first();
+ if ($env) {
+ $env->order = $order;
+ $env->save();
+ }
+ $order++;
+ }
+
+ if ($this->showPreview) {
+ $previewVariables = parseEnvFormatToArray($this->variablesPreview);
+ $order = 1;
+ foreach ($previewVariables as $key => $value) {
+ $env = $this->resource->environment_variables_preview()->where('key', $key)->first();
+ if ($env) {
+ $env->order = $order;
+ $env->save();
}
- $found->value = $variable;
- // if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) {
- // $type = str($found->value)->after('{{')->before('.')->value;
- // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
- // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.');
+ $order++;
+ }
+ }
+ }
- // return;
- // }
- // }
- $found->save();
+ private function handleBulkSubmit()
+ {
+ $variables = parseEnvFormatToArray($this->variables);
+ $this->deleteRemovedVariables(false, $variables);
+ $this->updateOrCreateVariables(false, $variables);
- continue;
+ if ($this->showPreview) {
+ $previewVariables = parseEnvFormatToArray($this->variablesPreview);
+ $this->deleteRemovedVariables(true, $previewVariables);
+ $this->updateOrCreateVariables(true, $previewVariables);
+ }
+
+ $this->dispatch('success', 'Environment variables updated.');
+ }
+
+ private function handleSingleSubmit($data)
+ {
+ $found = $this->resource->environment_variables()->where('key', $data['key'])->first();
+ if ($found) {
+ $this->dispatch('error', 'Environment variable already exists.');
+
+ return;
+ }
+
+ $maxOrder = $this->resource->environment_variables()->max('order') ?? 0;
+ $environment = $this->createEnvironmentVariable($data);
+ $environment->order = $maxOrder + 1;
+ $environment->save();
+ }
+
+ private function createEnvironmentVariable($data)
+ {
+ $environment = new EnvironmentVariable;
+ $environment->key = $data['key'];
+ $environment->value = $data['value'];
+ $environment->is_build_time = $data['is_build_time'] ?? false;
+ $environment->is_multiline = $data['is_multiline'] ?? false;
+ $environment->is_literal = $data['is_literal'] ?? false;
+ $environment->is_preview = $data['is_preview'] ?? false;
+
+ $resourceType = $this->resource->type();
+ $resourceIdField = $this->getResourceIdField($resourceType);
+
+ if ($resourceIdField) {
+ $environment->$resourceIdField = $this->resource->id;
+ }
+
+ return $environment;
+ }
+
+ private function getResourceIdField($resourceType)
+ {
+ $resourceTypes = [
+ 'application' => 'application_id',
+ 'standalone-postgresql' => 'standalone_postgresql_id',
+ 'standalone-redis' => 'standalone_redis_id',
+ 'standalone-mongodb' => 'standalone_mongodb_id',
+ 'standalone-mysql' => 'standalone_mysql_id',
+ 'standalone-mariadb' => 'standalone_mariadb_id',
+ 'standalone-keydb' => 'standalone_keydb_id',
+ 'standalone-dragonfly' => 'standalone_dragonfly_id',
+ 'standalone-clickhouse' => 'standalone_clickhouse_id',
+ 'service' => 'service_id',
+ ];
+
+ return $resourceTypes[$resourceType] ?? null;
+ }
+
+ private function deleteRemovedVariables($isPreview, $variables)
+ {
+ $method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
+ $this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
+ }
+
+ private function updateOrCreateVariables($isPreview, $variables)
+ {
+ foreach ($variables as $key => $value) {
+ $method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
+ $found = $this->resource->$method()->where('key', $key)->first();
+
+ if ($found) {
+ if (! $found->is_shown_once && ! $found->is_multiline) {
+ $found->value = $value;
+ $found->save();
+ }
} else {
$environment = new EnvironmentVariable;
$environment->key = $key;
- $environment->value = $variable;
- // if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) {
- // $type = str($environment->value)->after('{{')->before('.')->value;
- // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
- // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.');
-
- // return;
- // }
- // }
+ $environment->value = $value;
$environment->is_build_time = false;
$environment->is_multiline = false;
- $environment->is_preview = $isPreview ? true : false;
- switch ($this->resource->type()) {
- case 'application':
- $environment->application_id = $this->resource->id;
- break;
- case 'standalone-postgresql':
- $environment->standalone_postgresql_id = $this->resource->id;
- break;
- case 'standalone-redis':
- $environment->standalone_redis_id = $this->resource->id;
- break;
- case 'standalone-mongodb':
- $environment->standalone_mongodb_id = $this->resource->id;
- break;
- case 'standalone-mysql':
- $environment->standalone_mysql_id = $this->resource->id;
- break;
- case 'standalone-mariadb':
- $environment->standalone_mariadb_id = $this->resource->id;
- break;
- case 'standalone-keydb':
- $environment->standalone_keydb_id = $this->resource->id;
- break;
- case 'standalone-dragonfly':
- $environment->standalone_dragonfly_id = $this->resource->id;
- break;
- case 'standalone-clickhouse':
- $environment->standalone_clickhouse_id = $this->resource->id;
- break;
- case 'service':
- $environment->service_id = $this->resource->id;
- break;
- }
+ $environment->is_preview = $isPreview;
+
+ $this->setEnvironmentResourceId($environment);
$environment->save();
}
}
- if ($isPreview) {
- $this->dispatch('success', 'Preview environment variables updated.');
- } else {
- $this->dispatch('success', 'Environment variables updated.');
+ }
+
+ private function setEnvironmentResourceId($environment)
+ {
+ $resourceTypes = [
+ 'application' => 'application_id',
+ 'standalone-postgresql' => 'standalone_postgresql_id',
+ 'standalone-redis' => 'standalone_redis_id',
+ 'standalone-mongodb' => 'standalone_mongodb_id',
+ 'standalone-mysql' => 'standalone_mysql_id',
+ 'standalone-mariadb' => 'standalone_mariadb_id',
+ 'standalone-keydb' => 'standalone_keydb_id',
+ 'standalone-dragonfly' => 'standalone_dragonfly_id',
+ 'standalone-clickhouse' => 'standalone_clickhouse_id',
+ 'service' => 'service_id',
+ ];
+
+ $resourceType = $this->resource->type();
+ if (isset($resourceTypes[$resourceType])) {
+ $environment->{$resourceTypes[$resourceType]} = $this->resource->id;
}
- $this->refreshEnvs();
}
public function refreshEnvs()
{
$this->resource->refresh();
+ $this->sortEnvironmentVariables();
$this->getDevView();
}
-
- public function submit($data)
- {
- try {
- $found = $this->resource->environment_variables()->where('key', $data['key'])->first();
- if ($found) {
- $this->dispatch('error', 'Environment variable already exists.');
-
- return;
- }
- $environment = new EnvironmentVariable;
- $environment->key = $data['key'];
- $environment->value = $data['value'];
- $environment->is_build_time = $data['is_build_time'];
- $environment->is_multiline = $data['is_multiline'];
- $environment->is_literal = $data['is_literal'];
- $environment->is_preview = $data['is_preview'];
-
- switch ($this->resource->type()) {
- case 'application':
- $environment->application_id = $this->resource->id;
- break;
- case 'standalone-postgresql':
- $environment->standalone_postgresql_id = $this->resource->id;
- break;
- case 'standalone-redis':
- $environment->standalone_redis_id = $this->resource->id;
- break;
- case 'standalone-mongodb':
- $environment->standalone_mongodb_id = $this->resource->id;
- break;
- case 'standalone-mysql':
- $environment->standalone_mysql_id = $this->resource->id;
- break;
- case 'standalone-mariadb':
- $environment->standalone_mariadb_id = $this->resource->id;
- break;
- case 'standalone-keydb':
- $environment->standalone_keydb_id = $this->resource->id;
- break;
- case 'standalone-dragonfly':
- $environment->standalone_dragonfly_id = $this->resource->id;
- break;
- case 'standalone-clickhouse':
- $environment->standalone_clickhouse_id = $this->resource->id;
- break;
- case 'service':
- $environment->service_id = $this->resource->id;
- break;
- }
- $environment->save();
- $this->refreshEnvs();
- $this->dispatch('success', 'Environment variable added.');
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
}
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
index e63871602..463ceecad 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
@@ -24,7 +24,8 @@ class Show extends Component
public string $type;
protected $listeners = [
- 'refresh' => 'refresh',
+ 'refreshEnvs' => 'refresh',
+ 'refresh',
'compose_loaded' => '$refresh',
];
@@ -129,7 +130,8 @@ class Show extends Component
{
try {
$this->env->delete();
- $this->dispatch('refreshEnvs');
+ $this->dispatch('environmentVariableDeleted');
+ $this->dispatch('success', 'Environment variable deleted successfully.');
} catch (\Exception $e) {
return handleError($e);
}
diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
index 343915d9c..d95443621 100644
--- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php
+++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
@@ -2,18 +2,16 @@
namespace App\Livewire\Project\Shared;
-use App\Actions\Server\RunCommand;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Support\Collection;
+use Livewire\Attributes\On;
use Livewire\Component;
class ExecuteContainerCommand extends Component
{
- public string $command;
-
- public string $container;
+ public $container;
public Collection $containers;
@@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component
public string $type;
- public string $workDir = '';
-
public Server $server;
public Collection $servers;
@@ -33,11 +29,13 @@ class ExecuteContainerCommand extends Component
'server' => 'required',
'container' => 'required',
'command' => 'required',
- 'workDir' => 'nullable',
];
public function mount()
{
+ if (! auth()->user()->isAdmin()) {
+ abort(403);
+ }
$this->parameters = get_route_parameters();
$this->containers = collect();
$this->servers = collect();
@@ -62,24 +60,13 @@ class ExecuteContainerCommand extends Component
if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
- $this->container = $this->resource->uuid;
- $this->containers->push($this->container);
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
- $this->resource->applications()->get()->each(function ($application) {
- $this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid'));
- });
- $this->resource->databases()->get()->each(function ($database) {
- $this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid'));
- });
if ($this->resource->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->server);
}
}
- if ($this->containers->count() > 0) {
- $this->container = $this->containers->first();
- }
}
public function loadContainers()
@@ -102,44 +89,65 @@ class ExecuteContainerCommand extends Component
];
$this->containers = $this->containers->push($payload);
}
+ } elseif (data_get($this->parameters, 'database_uuid')) {
+ if ($this->resource->isRunning()) {
+ $this->containers = $this->containers->push([
+ 'server' => $server,
+ 'container' => [
+ 'Names' => $this->resource->uuid,
+ ],
+ ]);
+ }
+ } elseif (data_get($this->parameters, 'service_uuid')) {
+ $this->resource->applications()->get()->each(function ($application) {
+ ray($application);
+ if ($application->isRunning()) {
+ $this->containers->push([
+ 'server' => $this->resource->server,
+ 'container' => [
+ 'Names' => data_get($application, 'name').'-'.data_get($this->resource, 'uuid'),
+ ],
+ ]);
+ }
+ });
+ $this->resource->databases()->get()->each(function ($database) {
+ if ($database->isRunning()) {
+ $this->containers->push([
+ 'server' => $this->resource->server,
+ 'container' => [
+ 'Names' => data_get($database, 'name').'-'.data_get($this->resource, 'uuid'),
+ ],
+ ]);
+ }
+ });
}
+
}
if ($this->containers->count() > 0) {
- if (data_get($this->parameters, 'application_uuid')) {
- $this->container = data_get($this->containers->first(), 'container.Names');
- } elseif (data_get($this->parameters, 'database_uuid')) {
- $this->container = $this->containers->first();
- } elseif (data_get($this->parameters, 'service_uuid')) {
- $this->container = $this->containers->first();
- }
+ $this->container = $this->containers->first();
}
}
- public function runCommand()
+ #[On('connectToContainer')]
+ public function connectToContainer()
{
try {
- if (data_get($this->parameters, 'application_uuid')) {
- $container = $this->containers->where('container.Names', $this->container)->first();
- $container_name = data_get($container, 'container.Names');
- if (is_null($container)) {
- throw new \RuntimeException('Container not found.');
- }
- $server = data_get($container, 'server');
- } else {
- $container_name = $this->container;
- $server = $this->servers->first();
+ $container_name = data_get($this->container, 'container.Names');
+ if (is_null($container_name)) {
+ throw new \RuntimeException('Container not found.');
}
+ $server = data_get($this->container, 'server');
+
if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
- $cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'";
- if (! empty($this->workDir)) {
- $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
- } else {
- $exec = "docker exec {$container_name} {$cmd}";
- }
- $activity = RunCommand::run(server: $server, command: $exec);
- $this->dispatch('activityMonitor', $activity->id);
+
+ $this->dispatch('send-terminal-command',
+ true,
+ $container_name,
+ $server->uuid,
+ );
+
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php
index edcaf0f34..deccc875c 100644
--- a/app/Livewire/Project/Shared/GetLogs.php
+++ b/app/Livewire/Project/Shared/GetLogs.php
@@ -97,7 +97,7 @@ class GetLogs extends Component
if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) {
return;
}
- if (! $this->numberOfLines) {
+ if ($this->numberOfLines <= 0) {
$this->numberOfLines = 1000;
}
if ($this->container) {
diff --git a/app/Livewire/Project/Shared/ScheduledTask/All.php b/app/Livewire/Project/Shared/ScheduledTask/All.php
index 4d8c87dbf..b383e294a 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/All.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/All.php
@@ -26,7 +26,7 @@ class All extends Component
$this->containerNames = $this->containerNames->merge($this->resource->databases()->pluck('name'));
} elseif ($this->resource->type() == 'application') {
if ($this->resource->build_pack === 'dockercompose') {
- $parsed = $this->resource->parseCompose();
+ $parsed = $this->resource->parse();
$containers = collect(data_get($parsed, 'services'))->keys();
$this->containerNames = $containers;
} else {
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php
index 7a2e14e89..5bd6b4b9b 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php
@@ -7,8 +7,8 @@ use Livewire\Component;
class Executions extends Component
{
public $executions = [];
-
public $selectedKey;
+ public $task;
public function getListeners()
{
@@ -26,4 +26,44 @@ class Executions extends Component
}
$this->selectedKey = $key;
}
+
+ public function server()
+ {
+ if (!$this->task) {
+ return null;
+ }
+
+ if ($this->task->application) {
+ if ($this->task->application->destination && $this->task->application->destination->server) {
+ return $this->task->application->destination->server;
+ }
+ } elseif ($this->task->service) {
+ if ($this->task->service->destination && $this->task->service->destination->server) {
+ return $this->task->service->destination->server;
+ }
+ }
+ return null;
+ }
+
+ public function getServerTimezone()
+ {
+ $server = $this->server();
+ if (!$server) {
+ return 'UTC';
+ }
+ $serverTimezone = $server->settings->server_timezone;
+ return $serverTimezone;
+ }
+
+ public function formatDateInServerTimezone($date)
+ {
+ $serverTimezone = $this->getServerTimezone();
+ $dateObj = new \DateTime($date);
+ try {
+ $dateObj->setTimezone(new \DateTimeZone($serverTimezone));
+ } 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/Terminal.php b/app/Livewire/Project/Shared/Terminal.php
new file mode 100644
index 000000000..802e65a30
--- /dev/null
+++ b/app/Livewire/Project/Shared/Terminal.php
@@ -0,0 +1,43 @@
+whereUuid($serverUuid)->firstOrFail();
+
+ if ($isContainer) {
+ $status = getContainerStatus($server, $identifier);
+ if ($status !== 'running') {
+ return;
+ }
+ $command = generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
+ } else {
+ $command = generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
+ }
+
+ // ssh command is sent back to frontend then to websocket
+ // this is done because the websocket connection is not available here
+ // a better solution would be to remove websocket on NodeJS and work with something like
+ // 1. Laravel Pusher/Echo connection (not possible without a sdk)
+ // 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies)
+ // 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used
+ // 4. Follow-up discussions here:
+ // - https://github.com/coollabsio/coolify/issues/2298
+ // - https://github.com/coollabsio/coolify/discussions/3362
+ $this->dispatch('send-back-command', $command);
+ }
+
+ public function render()
+ {
+ return view('livewire.project.shared.terminal');
+ }
+}
diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php
deleted file mode 100644
index c2d3adeea..000000000
--- a/app/Livewire/RunCommand.php
+++ /dev/null
@@ -1,43 +0,0 @@
- 'required',
- 'command' => 'required',
- ];
-
- protected $validationAttributes = [
- 'server' => 'server',
- 'command' => 'command',
- ];
-
- public function mount($servers)
- {
- $this->servers = $servers;
- $this->server = $servers[0]->uuid;
- }
-
- public function runCommand()
- {
- $this->validate();
- try {
- $activity = ServerRunCommand::run(server: Server::where('uuid', $this->server)->first(), command: $this->command);
- $this->dispatch('activityMonitor', $activity->id);
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-}
diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php
index 9934ea345..3b3747a81 100644
--- a/app/Livewire/Server/Form.php
+++ b/app/Livewire/Server/Form.php
@@ -18,13 +18,17 @@ class Form extends Component
public ?string $wildcard_domain = null;
- public int $cleanup_after_percentage;
-
public bool $dockerInstallationStarted = false;
public bool $revalidate = false;
- protected $listeners = ['serverInstalled', 'revalidate' => '$refresh'];
+ public $timezones;
+
+ protected $listeners = [
+ 'serverInstalled',
+ 'refreshServerShow' => 'serverInstalled',
+ 'revalidate' => '$refresh',
+ ];
protected $rules = [
'server.name' => 'required',
@@ -37,7 +41,6 @@ class Form extends Component
'server.settings.is_swarm_manager' => 'required|boolean',
'server.settings.is_swarm_worker' => 'required|boolean',
'server.settings.is_build_server' => 'required|boolean',
- 'server.settings.is_force_cleanup_enabled' => 'required|boolean',
'server.settings.concurrent_builds' => 'required|integer|min:1',
'server.settings.dynamic_timeout' => 'required|integer|min:1',
'server.settings.is_metrics_enabled' => 'required|boolean',
@@ -46,6 +49,10 @@ class Form extends Component
'server.settings.metrics_history_days' => 'required|integer|min:1',
'wildcard_domain' => 'nullable|url',
'server.settings.is_server_api_enabled' => 'required|boolean',
+ 'server.settings.server_timezone' => 'required|string|timezone',
+ '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',
];
protected $validationAttributes = [
@@ -66,12 +73,27 @@ class Form extends Component
'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval',
'server.settings.metrics_history_days' => 'Metrics History',
'server.settings.is_server_api_enabled' => 'Server API',
+ 'server.settings.server_timezone' => 'Server Timezone',
];
- public function mount()
+ public function mount(Server $server)
{
+ $this->server = $server;
+ $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->wildcard_domain = $this->server->settings->wildcard_domain;
- $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
+ $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
+ $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
+ }
+
+ public function updated($field)
+ {
+ if ($field === 'server.settings.docker_cleanup_frequency') {
+ $frequency = $this->server->settings->docker_cleanup_frequency;
+ if (empty($frequency) || ! validate_cron_expression($frequency)) {
+ $this->dispatch('error', 'Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.');
+ $this->server->settings->docker_cleanup_frequency = '*/10 * * * *';
+ }
+ }
}
public function serverInstalled()
@@ -116,7 +138,6 @@ class Form extends Component
}
if ($this->server->settings->isDirty('is_server_api_enabled') && $this->server->settings->is_server_api_enabled === true) {
ray('Starting sentinel');
-
}
} else {
ray('Sentinel is not enabled');
@@ -172,27 +193,49 @@ class Form extends Component
public function submit()
{
- if (isCloud() && ! isDev()) {
- $this->validate();
- $this->validate([
- 'server.ip' => 'required',
- ]);
- } else {
- $this->validate();
- }
- $uniqueIPs = Server::all()->reject(function (Server $server) {
- return $server->id === $this->server->id;
- })->pluck('ip')->toArray();
- if (in_array($this->server->ip, $uniqueIPs)) {
- $this->dispatch('error', 'IP address is already in use by another team.');
+ try {
+ if (isCloud() && ! isDev()) {
+ $this->validate();
+ $this->validate([
+ 'server.ip' => 'required',
+ ]);
+ } else {
+ $this->validate();
+ }
+ $uniqueIPs = Server::all()->reject(function (Server $server) {
+ return $server->id === $this->server->id;
+ })->pluck('ip')->toArray();
+ if (in_array($this->server->ip, $uniqueIPs)) {
+ $this->dispatch('error', 'IP address is already in use by another team.');
- return;
+ return;
+ }
+ refresh_server_connection($this->server->privateKey);
+ $this->server->settings->wildcard_domain = $this->wildcard_domain;
+ if ($this->server->settings->force_docker_cleanup) {
+ $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
+ } else {
+ $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
+ }
+ $currentTimezone = $this->server->settings->getOriginal('server_timezone');
+ $newTimezone = $this->server->settings->server_timezone;
+ if ($currentTimezone !== $newTimezone || $currentTimezone === '') {
+ $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);
}
- refresh_server_connection($this->server->privateKey);
- $this->server->settings->wildcard_domain = $this->wildcard_domain;
- $this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage;
+ }
+
+ public function updatedServerSettingsServerTimezone($value)
+ {
+ $this->server->settings->server_timezone = $value;
$this->server->settings->save();
- $this->server->save();
- $this->dispatch('success', 'Server updated.');
+ $this->dispatch('success', 'Server timezone updated.');
}
}
diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php
index 5f69835d7..f80152435 100644
--- a/app/Livewire/Server/New/ByIp.php
+++ b/app/Livewire/Server/New/ByIp.php
@@ -2,10 +2,10 @@
namespace App\Livewire\Server\New;
-use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use App\Models\Team;
+use Illuminate\Support\Collection;
use Livewire\Component;
class ByIp extends Component
@@ -40,7 +40,7 @@ class ByIp extends Component
public bool $is_build_server = false;
- public $swarm_managers = [];
+ public Collection $swarm_managers;
protected $rules = [
'name' => 'required|string',
@@ -102,11 +102,6 @@ class ByIp extends Component
'port' => $this->port,
'team_id' => currentTeam()->id,
'private_key_id' => $this->private_key_id,
- 'proxy' => [
- // set default proxy type to traefik v2
- 'type' => ProxyTypes::TRAEFIK->value,
- 'status' => ProxyStatus::EXITED->value,
- ],
];
if ($this->is_swarm_worker) {
$payload['swarm_cluster'] = $this->selected_swarm_cluster;
@@ -115,6 +110,9 @@ class ByIp extends Component
data_forget($payload, 'proxy');
}
$server = Server::create($payload);
+ $server->proxy->set('status', 'exited');
+ $server->proxy->set('type', ProxyTypes::TRAEFIK->value);
+ $server->save();
if ($this->is_build_server) {
$this->is_swarm_manager = false;
$this->is_swarm_worker = false;
diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php
index 0751b186e..a5e94a19a 100644
--- a/app/Livewire/Server/Show.php
+++ b/app/Livewire/Server/Show.php
@@ -14,7 +14,7 @@ class Show extends Component
public $parameters = [];
- protected $listeners = ['refreshServerShow' => '$refresh'];
+ protected $listeners = ['refreshServerShow'];
public function mount()
{
@@ -29,6 +29,12 @@ class Show extends Component
}
}
+ public function refreshServerShow()
+ {
+ $this->server->refresh();
+ $this->dispatch('$refresh');
+ }
+
public function submit()
{
$this->dispatch('serverRefresh', false);
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index f593fb78b..c52970258 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -40,6 +40,7 @@ class Index extends Component
'settings.is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string',
'update_check_frequency' => 'string',
+ 'settings.instance_timezone' => 'required|string|timezone',
];
protected $validationAttributes = [
@@ -54,6 +55,8 @@ class Index extends Component
'update_check_frequency' => 'Update Check Frequency',
];
+ public $timezones;
+
public function mount()
{
if (isInstanceAdmin()) {
@@ -65,6 +68,7 @@ class Index extends Component
$this->is_api_enabled = $this->settings->is_api_enabled;
$this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency;
+ $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
} else {
return redirect()->route('dashboard');
}
@@ -166,6 +170,13 @@ class Index extends Component
}
}
+ public function updatedSettingsInstanceTimezone($value)
+ {
+ $this->settings->instance_timezone = $value;
+ $this->settings->save();
+ $this->dispatch('success', 'Instance timezone updated.');
+ }
+
public function render()
{
return view('livewire.settings.index');
diff --git a/app/Livewire/SharedVariables/Environment/Show.php b/app/Livewire/SharedVariables/Environment/Show.php
index e025d8f7c..daf1df212 100644
--- a/app/Livewire/SharedVariables/Environment/Show.php
+++ b/app/Livewire/SharedVariables/Environment/Show.php
@@ -16,7 +16,7 @@ class Show extends Component
public array $parameters;
- protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey'];
+ protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey'];
public function saveKey($data)
{
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/Livewire/Upgrade.php b/app/Livewire/Upgrade.php
index da7b5860d..dfbd945f5 100644
--- a/app/Livewire/Upgrade.php
+++ b/app/Livewire/Upgrade.php
@@ -4,7 +4,6 @@ namespace App\Livewire;
use App\Actions\Server\UpdateCoolify;
use App\Models\InstanceSettings;
-use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Upgrade extends Component
@@ -22,13 +21,8 @@ class Upgrade extends Component
public function checkUpdate()
{
try {
- $settings = InstanceSettings::get();
- $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
- if ($response->successful()) {
- $versions = $response->json();
- $this->latestVersion = data_get($versions, 'coolify.v4.version');
- }
- $this->isUpgradeAvailable = $settings->new_version_available;
+ $this->latestVersion = get_latest_version_of_coolify();
+ $this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false);
} catch (\Throwable $e) {
return handleError($e, $this);
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 2873ee7da..8c7520eaf 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -104,6 +104,8 @@ class Application extends BaseModel
{
use SoftDeletes;
+ private static $parserVersion = '3';
+
protected $guarded = [];
protected $appends = ['server_status'];
@@ -127,7 +129,7 @@ class Application extends BaseModel
ApplicationSetting::create([
'application_id' => $application->id,
]);
- $application->compose_parsing_version = '2';
+ $application->compose_parsing_version = self::$parserVersion;
$application->save();
});
static::forceDeleting(function ($application) {
@@ -140,6 +142,7 @@ class Application extends BaseModel
$task->delete();
}
$application->tags()->detach();
+ $application->previews()->delete();
});
}
@@ -474,23 +477,6 @@ class Application extends BaseModel
);
}
- public function dockerComposePrLocation(): Attribute
- {
- return Attribute::make(
- set: function ($value) {
- if (is_null($value) || $value === '') {
- return '/docker-compose.yaml';
- } else {
- if ($value !== '/') {
- return Str::start(Str::replaceEnd('/', '', $value), '/');
- }
-
- return Str::start($value, '/');
- }
- }
- );
- }
-
public function baseDirectory(): Attribute
{
return Attribute::make(
@@ -541,12 +527,12 @@ class Application extends BaseModel
$main_server_status = $this->destination->server->isFunctional();
foreach ($additional_servers_status as $status) {
$server_status = str($status)->before(':')->value();
- if ($main_server_status !== $server_status) {
+ if ($server_status !== 'running') {
return false;
}
}
- return true;
+ return $main_server_status;
}
}
);
@@ -1102,7 +1088,7 @@ class Application extends BaseModel
}
}
- public function parseRawCompose()
+ public function oldRawParser()
{
try {
$yaml = Yaml::parse($this->docker_compose_raw);
@@ -1162,9 +1148,11 @@ class Application extends BaseModel
instant_remote_process($commands, $this->destination->server, false);
}
- public function parseCompose(int $pull_request_id = 0, ?int $preview_id = null)
+ public function parse(int $pull_request_id = 0, ?int $preview_id = null)
{
- if ($this->docker_compose_raw) {
+ if ($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);
} else {
return collect([]);
@@ -1216,7 +1204,7 @@ class Application extends BaseModel
if ($composeFileContent) {
$this->docker_compose_raw = $composeFileContent;
$this->save();
- $parsedServices = $this->parseCompose();
+ $parsedServices = $this->parse();
if ($this->docker_compose_domains) {
$json = collect(json_decode($this->docker_compose_domains));
$names = collect(data_get($parsedServices, 'services'))->keys()->toArray();
diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php
index 57d20e3aa..04a0ab27e 100644
--- a/app/Models/ApplicationPreview.php
+++ b/app/Models/ApplicationPreview.php
@@ -12,9 +12,9 @@ class ApplicationPreview extends BaseModel
protected static function booted()
{
static::deleting(function ($preview) {
- if ($preview->application->build_pack === 'dockercompose') {
+ if (data_get($preview, 'application.build_pack') === 'dockercompose') {
$server = $preview->application->destination->server;
- $composeFile = $preview->application->parseCompose(pull_request_id: $preview->pull_request_id);
+ $composeFile = $preview->application->parse(pull_request_id: $preview->pull_request_id);
$volumes = data_get($composeFile, 'volumes');
$networks = data_get($composeFile, 'networks');
$networkKeys = collect($networks)->keys();
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 5e1d8ae13..138775aba 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -6,7 +6,6 @@ use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
-use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
@@ -97,8 +96,22 @@ class EnvironmentVariable extends Model
$resource = Application::find($this->application_id);
} elseif ($this->service_id) {
$resource = Service::find($this->service_id);
- } elseif ($this->database_id) {
- $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
+ } elseif ($this->standalone_postgresql_id) {
+ $resource = StandalonePostgresql::find($this->standalone_postgresql_id);
+ } elseif ($this->standalone_redis_id) {
+ $resource = StandaloneRedis::find($this->standalone_redis_id);
+ } elseif ($this->standalone_mongodb_id) {
+ $resource = StandaloneMongodb::find($this->standalone_mongodb_id);
+ } elseif ($this->standalone_mysql_id) {
+ $resource = StandaloneMysql::find($this->standalone_mysql_id);
+ } elseif ($this->standalone_mariadb_id) {
+ $resource = StandaloneMariadb::find($this->standalone_mariadb_id);
+ } elseif ($this->standalone_keydb_id) {
+ $resource = StandaloneKeydb::find($this->standalone_keydb_id);
+ } elseif ($this->standalone_dragonfly_id) {
+ $resource = StandaloneDragonfly::find($this->standalone_dragonfly_id);
+ } elseif ($this->standalone_clickhouse_id) {
+ $resource = StandaloneClickhouse::find($this->standalone_clickhouse_id);
}
return $resource;
@@ -122,63 +135,6 @@ class EnvironmentVariable extends Model
);
}
- protected function isFoundInCompose(): Attribute
- {
- return Attribute::make(
- get: function () {
- if (! $this->application_id) {
- return true;
- }
- $found_in_compose = false;
- $found_in_args = false;
- $resource = $this->resource();
- $compose = data_get($resource, 'docker_compose_raw');
- if (! $compose) {
- return true;
- }
- $yaml = Yaml::parse($compose);
- $services = collect(data_get($yaml, 'services'));
- if ($services->isEmpty()) {
- return false;
- }
- foreach ($services as $service) {
- $environments = collect(data_get($service, 'environment'));
- $args = collect(data_get($service, 'build.args'));
- if ($environments->isEmpty() && $args->isEmpty()) {
- $found_in_compose = false;
- break;
- }
-
- $found_in_compose = $environments->contains(function ($item) {
- if (str($item)->contains('=')) {
- $item = str($item)->before('=');
- }
-
- return strpos($item, $this->key) !== false;
- });
-
- if ($found_in_compose) {
- break;
- }
-
- $found_in_args = $args->contains(function ($item) {
- if (str($item)->contains('=')) {
- $item = str($item)->before('=');
- }
-
- return strpos($item, $this->key) !== false;
- });
-
- if ($found_in_args) {
- break;
- }
- }
-
- return $found_in_compose || $found_in_args;
- }
- );
- }
-
protected function isShared(): Attribute
{
return Attribute::make(
@@ -201,8 +157,10 @@ class EnvironmentVariable extends Model
$environment_variable = trim($environment_variable);
$sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/');
if ($sharedEnvsFound->isEmpty()) {
+
return $environment_variable;
}
+
foreach ($sharedEnvsFound as $sharedEnv) {
$type = str($sharedEnv)->match('/(.*?)\./');
if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index 5bd421956..27a181ee4 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -37,6 +37,30 @@ class InstanceSettings extends Model implements SendsEmail
);
}
+ public function updateCheckFrequency(): Attribute
+ {
+ return Attribute::make(
+ set: function ($value) {
+ return translate_cron_expression($value);
+ },
+ get: function ($value) {
+ return translate_cron_expression($value);
+ }
+ );
+ }
+
+ public function autoUpdateFrequency(): Attribute
+ {
+ return Attribute::make(
+ set: function ($value) {
+ return translate_cron_expression($value);
+ },
+ get: function ($value) {
+ return translate_cron_expression($value);
+ }
+ );
+ }
+
public static function get()
{
return InstanceSettings::findOrFail(0);
diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php
index a436f5797..d528099ff 100644
--- a/app/Models/LocalFileVolume.php
+++ b/app/Models/LocalFileVolume.php
@@ -24,8 +24,9 @@ class LocalFileVolume extends BaseModel
return $this->morphTo('resource');
}
- public function deleteStorageOnServer()
+ public function loadStorageOnServer()
{
+ $this->load(['service']);
$isService = data_get($this->resource, 'service');
if ($isService) {
$workdir = $this->resource->service->workdir();
@@ -35,17 +36,46 @@ class LocalFileVolume extends BaseModel
$server = $this->resource->destination->server;
}
$commands = collect([]);
- $fs_path = data_get($this, 'fs_path');
- $isFile = instant_remote_process(["test -f $fs_path && echo OK || echo NOK"], $server);
- $isDir = instant_remote_process(["test -d $fs_path && echo OK || echo NOK"], $server);
- if ($fs_path && $fs_path != '/' && $fs_path != '.' && $fs_path != '..') {
- ray($isFile, $isDir);
+ $path = data_get_str($this, 'fs_path');
+ if ($path->startsWith('.')) {
+ $path = $path->after('.');
+ $path = $workdir.$path;
+ }
+ $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
+ if ($isFile === 'OK') {
+ $content = instant_remote_process(["cat $path"], $server, false);
+ $this->content = $content;
+ $this->is_directory = false;
+ $this->save();
+ }
+ }
+
+ public function deleteStorageOnServer()
+ {
+ $this->load(['service']);
+ $isService = data_get($this->resource, 'service');
+ if ($isService) {
+ $workdir = $this->resource->service->workdir();
+ $server = $this->resource->service->server;
+ } else {
+ $workdir = $this->resource->workdir();
+ $server = $this->resource->destination->server;
+ }
+ $commands = collect([]);
+ $path = data_get_str($this, 'fs_path');
+ if ($path->startsWith('.')) {
+ $path = $path->after('.');
+ $path = $workdir.$path;
+ }
+ $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
+ $isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
+ if ($path && $path != '/' && $path != '.' && $path != '..') {
if ($isFile === 'OK') {
- $commands->push("rm -rf $fs_path > /dev/null 2>&1 || true");
+ $commands->push("rm -rf $path > /dev/null 2>&1 || true");
} elseif ($isDir === 'OK') {
- $commands->push("rm -rf $fs_path > /dev/null 2>&1 || true");
- $commands->push("rmdir $fs_path > /dev/null 2>&1 || true");
+ $commands->push("rm -rf $path > /dev/null 2>&1 || true");
+ $commands->push("rmdir $path > /dev/null 2>&1 || true");
}
}
if ($commands->count() > 0) {
@@ -55,6 +85,7 @@ class LocalFileVolume extends BaseModel
public function saveStorageOnServer()
{
+ $this->load(['service']);
$isService = data_get($this->resource, 'service');
if ($isService) {
$workdir = $this->resource->service->workdir();
@@ -74,30 +105,36 @@ class LocalFileVolume extends BaseModel
$commands->push("mkdir -p $parent_dir > /dev/null 2>&1 || true");
}
}
- $fileVolume = $this;
- $path = str(data_get($fileVolume, 'fs_path'));
- $content = data_get($fileVolume, 'content');
+ $path = data_get_str($this, 'fs_path');
+ $content = data_get($this, 'content');
if ($path->startsWith('.')) {
$path = $path->after('.');
$path = $workdir.$path;
}
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
- if ($isFile == 'OK' && $fileVolume->is_directory) {
+ if ($isFile == 'OK' && $this->is_directory) {
$content = instant_remote_process(["cat $path"], $server, false);
- $fileVolume->is_directory = false;
- $fileVolume->content = $content;
- $fileVolume->save();
+ $this->is_directory = false;
+ $this->content = $content;
+ $this->save();
FileStorageChanged::dispatch(data_get($server, 'team_id'));
throw new \Exception('The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.');
- } elseif ($isDir == 'OK' && ! $fileVolume->is_directory) {
- $fileVolume->is_directory = true;
- $fileVolume->save();
- throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file.
Please delete the directory on the server or mark it as directory.');
+ } elseif ($isDir == 'OK' && ! $this->is_directory) {
+ if ($path == '/' || $path == '.' || $path == '..' || $path == '' || str($path)->isEmpty() || is_null($path)) {
+ $this->is_directory = true;
+ $this->save();
+ throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file.