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 @@ # Donations
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 @@ ## Github Sponsors ($40+)
+
+
+
## 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 @@ public function handle(StandaloneClickhouse $database)
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 @@ public function handle(StandaloneClickhouse $database)
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 @@ private function generate_environment_variables()
$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 @@ public function handle(StandaloneDragonfly $database)
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir().'/'.$container_name;
+ $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -75,18 +75,11 @@ public function handle(StandaloneDragonfly $database)
],
],
];
- if (! is_null($this->database->limits_cpuset)) {
+ if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $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 @@ public function handle(StandaloneDragonfly $database)
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 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -154,7 +152,7 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}");
}
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index 85cb89c1c..9290efc7c 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -24,7 +24,7 @@ public function handle(StandaloneKeydb $database)
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir().'/'.$container_name;
+ $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -74,18 +74,11 @@ public function handle(StandaloneKeydb $database)
],
],
];
- if (! is_null($this->database->limits_cpuset)) {
+ if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $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 @@ public function handle(StandaloneKeydb $database)
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
+ if (!is_null($this->database->keydb_conf) || !empty($this->database->keydb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir.'/keydb.conf',
+ 'source' => $this->configuration_dir . '/keydb.conf',
'target' => '/etc/keydb/keydb.conf',
'read_only' => true,
];
$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 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -162,10 +159,12 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
}
+ 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 @@ public function handle(StandaloneMariadb $database)
$this->database = $database;
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir().'/'.$container_name;
+ $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -69,18 +69,11 @@ public function handle(StandaloneMariadb $database)
],
],
];
- if (! is_null($this->database->limits_cpuset)) {
+ if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $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 @@ public function handle(StandaloneMariadb $database)
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
+ if (!is_null($this->database->mariadb_conf) || !empty($this->database->mariadb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir.'/custom-config.cnf',
+ 'source' => $this->configuration_dir . '/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
}
+
+ // 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 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -156,21 +154,23 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
$environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
$environment_variables->push("MARIADB_USER={$this->database->mariadb_user}");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
}
+ 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 @@ public function handle(StandaloneMongodb $database)
$startCommand = 'mongod';
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir().'/'.$container_name;
+ $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -77,18 +77,11 @@ public function handle(StandaloneMongodb $database)
],
],
];
- if (! is_null($this->database->limits_cpuset)) {
+ if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $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 @@ public function handle(StandaloneMongodb $database)
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) {
+ if (!is_null($this->database->mongo_conf) || !empty($this->database->mongo_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir.'/mongod.conf',
+ 'source' => $this->configuration_dir . '/mongod.conf',
'target' => '/etc/mongo/mongod.conf',
'read_only' => true,
];
- $docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf';
+ $docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf';
}
$this->add_default_database();
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
+ 'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d',
'target' => '/docker-entrypoint-initdb.d',
'read_only' => true,
];
+ // 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 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -173,18 +170,20 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
}
+ 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 @@ public function handle(StandaloneMysql $database)
$this->database = $database;
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir().'/'.$container_name;
+ $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -69,18 +69,11 @@ public function handle(StandaloneMysql $database)
],
],
];
- if (! is_null($this->database->limits_cpuset)) {
+ if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $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 @@ public function handle(StandaloneMysql $database)
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
+ if (!is_null($this->database->mysql_conf) || !empty($this->database->mysql_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir.'/custom-config.cnf',
+ 'source' => $this->configuration_dir . '/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
}
+
+ // 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 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -156,21 +154,23 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
$environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
$environment_variables->push("MYSQL_USER={$this->database->mysql_user}");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
}
+ 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 @@ public function handle(StandalonePostgresql $database)
$this->generate_init_scripts();
$this->add_custom_conf();
+
$docker_compose = [
'services' => [
$container_name => [
@@ -80,14 +81,7 @@ public function handle(StandalonePostgresql $database)
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 @@ public function handle(StandalonePostgresql $database)
'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 @@ private function generate_environment_variables()
$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 @@ public function handle(StandaloneRedis $database)
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
$container_name = $this->database->uuid;
- $this->configuration_dir = database_configuration_dir().'/'.$container_name;
+ $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -78,18 +78,11 @@ public function handle(StandaloneRedis $database)
],
],
];
- if (! is_null($this->database->limits_cpuset)) {
+ if (!is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- $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 @@ public function handle(StandaloneRedis $database)
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
- if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
+ if (!is_null($this->database->redis_conf) || !empty($this->database->redis_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
- 'source' => $this->configuration_dir.'/redis.conf',
+ 'source' => $this->configuration_dir . '/redis.conf',
'target' => '/usr/local/etc/redis/redis.conf',
'read_only' => true,
];
$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 @@ private function generate_local_persistent_volumes()
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
- $local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
- $local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
+ $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
}
@@ -166,10 +164,12 @@ private function generate_environment_variables()
$environment_variables->push("$env->key=$env->real_value");
}
- if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
+ if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
}
+ 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 @@ public function handle(Server $server)
[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 @@ public function handle(Server $server)
[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 @@ public function handle($manual_update = false)
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 @@ private function update()
return;
}
+
+ $all_servers = Server::all();
+ $servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
+ foreach ($servers as $server) {
+ PullHelperImageJob::dispatch($server);
+ }
+
+ instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
+
remote_process([
'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 @@ public function handle(Service $service)
$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 @@ public function handle(Service $service)
$network = $service->destination->network;
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
foreach ($serviceNames as $serviceName => $serviceConfig) {
- $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
+ $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
}
}
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
diff --git a/app/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 @@ private function cleanup_stucked_resources()
} 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 @@ private function cleanup_stucked_resources()
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 @@ private function cleanup_unnecessary_dynamic_proxy_configuration()
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 @@ public function handle()
$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 @@ public function handle()
$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 @@ public function handle()
]);
});
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 @@ public function handle()
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 @@ public function handle()
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 @@ public function handle()
}
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 @@
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 @@ protected function schedule(Schedule $schedule): void
$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 @@ private function pull_images($schedule)
$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 @@ private function schedule_updates($schedule)
$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 @@ private function check_resources($schedule)
}
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 @@ private function check_scheduled_backups($schedule)
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 @@ private function check_scheduled_tasks($schedule)
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 @@ private function removeSensitiveData($application)
summary: 'List',
description: 'List all applications.',
path: '/applications',
+ operationId: 'list-applications',
security: [
['bearerAuth' => []],
],
@@ -101,6 +102,7 @@ public function applications(Request $request)
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 @@ public function create_public_application(Request $request)
#[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 @@ public function create_private_gh_app_application(Request $request)
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 @@ public function create_private_deploy_key_application(Request $request)
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 @@ public function create_dockerfile_application(Request $request)
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 @@ public function create_dockerimage_application(Request $request)
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 @@ private function create_application(Request $request, $type)
summary: 'Get',
description: 'Get application by UUID.',
path: '/applications/{uuid}',
+ operationId: 'get-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1235,6 +1243,7 @@ public function application_by_uuid(Request $request)
summary: 'Delete',
description: 'Delete application by UUID.',
path: '/applications/{uuid}',
+ operationId: 'delete-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1321,6 +1330,7 @@ public function delete_by_uuid(Request $request)
summary: 'Update',
description: 'Update application by UUID.',
path: '/applications/{uuid}',
+ operationId: 'update-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1450,7 +1460,7 @@ public function update_by_uuid(Request $request)
], 404);
}
$server = $application->destination->server;
- $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect'];
+ $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 @@ public function update_by_uuid(Request $request)
}
$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 @@ public function update_by_uuid(Request $request)
$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 @@ public function update_by_uuid(Request $request)
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 @@ public function envs(Request $request)
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 @@ public function update_env_by_uuid(Request $request)
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 @@ public function create_bulk_envs(Request $request)
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 @@ public function create_env(Request $request)
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 @@ public function delete_env_by_uuid(Request $request)
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 @@ public function action_deploy(Request $request)
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 @@ public function action_stop(Request $request)
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 @@ private function removeSensitiveData($database)
summary: 'List',
description: 'List all databases.',
path: '/databases',
+ operationId: 'list-databases',
security: [
['bearerAuth' => []],
],
@@ -91,6 +92,7 @@ public function databases(Request $request)
summary: 'Get',
description: 'Get database by UUID.',
path: '/databases/{uuid}',
+ operationId: 'get-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -151,6 +153,7 @@ public function database_by_uuid(Request $request)
summary: 'Update',
description: 'Update database by UUID.',
path: '/databases/{uuid}',
+ operationId: 'update-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -510,6 +513,7 @@ public function update_by_uuid(Request $request)
summary: 'Create (PostgreSQL)',
description: 'Create a new PostgreSQL database.',
path: '/databases/postgresql',
+ operationId: 'create-database-postgresql',
security: [
['bearerAuth' => []],
],
@@ -575,6 +579,7 @@ public function create_database_postgresql(Request $request)
summary: 'Create (Clickhouse)',
description: 'Create a new Clickhouse database.',
path: '/databases/clickhouse',
+ operationId: 'create-database-clickhouse',
security: [
['bearerAuth' => []],
],
@@ -636,6 +641,7 @@ public function create_database_clickhouse(Request $request)
summary: 'Create (DragonFly)',
description: 'Create a new DragonFly database.',
path: '/databases/dragonfly',
+ operationId: 'create-database-dragonfly',
security: [
['bearerAuth' => []],
],
@@ -696,6 +702,7 @@ public function create_database_dragonfly(Request $request)
summary: 'Create (Redis)',
description: 'Create a new Redis database.',
path: '/databases/redis',
+ operationId: 'create-database-redis',
security: [
['bearerAuth' => []],
],
@@ -757,6 +764,7 @@ public function create_database_redis(Request $request)
summary: 'Create (KeyDB)',
description: 'Create a new KeyDB database.',
path: '/databases/keydb',
+ operationId: 'create-database-keydb',
security: [
['bearerAuth' => []],
],
@@ -818,6 +826,7 @@ public function create_database_keydb(Request $request)
summary: 'Create (MariaDB)',
description: 'Create a new MariaDB database.',
path: '/databases/mariadb',
+ operationId: 'create-database-mariadb',
security: [
['bearerAuth' => []],
],
@@ -882,6 +891,7 @@ public function create_database_mariadb(Request $request)
summary: 'Create (MySQL)',
description: 'Create a new MySQL database.',
path: '/databases/mysql',
+ operationId: 'create-database-mysql',
security: [
['bearerAuth' => []],
],
@@ -945,6 +955,7 @@ public function create_database_mysql(Request $request)
summary: 'Create (MongoDB)',
description: 'Create a new MongoDB database.',
path: '/databases/mongodb',
+ operationId: 'create-database-mongodb',
security: [
['bearerAuth' => []],
],
@@ -1514,6 +1525,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
summary: 'Delete',
description: 'Delete database by UUID.',
path: '/databases/{uuid}',
+ operationId: 'delete-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1597,6 +1609,7 @@ public function delete_by_uuid(Request $request)
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 @@ public function action_deploy(Request $request)
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 @@ public function action_stop(Request $request)
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 @@ private function removeSensitiveData($deployment)
summary: 'List',
description: 'List currently running deployments',
path: '/deployments',
+ operationId: 'list-deployments',
security: [
['bearerAuth' => []],
],
@@ -79,12 +80,13 @@ public function deployments(Request $request)
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 @@ public function deployment_by_uuid(Request $request)
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 @@ public function deployment_by_uuid(Request $request)
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 @@ public function version(Request $request)
summary: 'Enable API',
description: 'Enable API (only with root permissions).',
path: '/enable',
+ operationId: 'enable-api',
security: [
['bearerAuth' => []],
],
@@ -94,6 +96,7 @@ public function enable_api(Request $request)
summary: 'Disable API',
description: 'Disable API (only with root permissions).',
path: '/disable',
+ operationId: 'disable-api',
security: [
['bearerAuth' => []],
],
@@ -158,6 +161,7 @@ public function feedback(Request $request)
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 @@ public function projects(Request $request)
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 @@ public function projects(Request $request)
#[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 @@ public function project_by_uuid(Request $request)
summary: 'Environment',
description: 'Get environment by name.',
path: '/projects/{uuid}/{environment_name}',
+ operationId: 'get-environment-by-name',
security: [
['bearerAuth' => []],
],
@@ -136,12 +139,15 @@ public function environment_details(Request $request)
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 @@ public function environment_details(Request $request)
summary: 'Create',
description: 'Create Project.',
path: '/projects',
+ operationId: 'create-project',
security: [
['bearerAuth' => []],
],
@@ -167,7 +174,7 @@ public function environment_details(Request $request)
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 @@ public function create_project(Request $request)
summary: 'Update',
description: 'Update Project.',
path: '/projects/{uuid}',
+ operationId: 'update-project-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -333,7 +341,7 @@ public function update_project(Request $request)
}
$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 @@ public function update_project(Request $request)
summary: 'Delete',
description: 'Delete project by UUID.',
path: '/projects/{uuid}',
+ operationId: 'delete-project-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -408,7 +417,7 @@ public function delete_project(Request $request)
}
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 @@ private function removeSensitiveData($team)
summary: 'List',
description: 'List all private keys.',
path: '/security/keys',
+ operationId: 'list-private-keys',
security: [
['bearerAuth' => []],
],
@@ -68,12 +69,13 @@ public function keys(Request $request)
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 @@ public function key_by_uuid(Request $request)
summary: 'Create',
description: 'Create a new private key.',
path: '/security/keys',
+ operationId: 'create-private-key',
security: [
['bearerAuth' => []],
],
@@ -217,6 +220,7 @@ public function create_key(Request $request)
summary: 'Update',
description: 'Update a private key.',
path: '/security/keys',
+ operationId: 'update-private-key',
security: [
['bearerAuth' => []],
],
@@ -313,12 +317,13 @@ public function update_key(Request $request)
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 @@ private function removeSensitiveData($server)
summary: 'List',
description: 'List all servers.',
path: '/servers',
+ operationId: 'list-servers',
security: [
['bearerAuth' => []],
],
@@ -100,12 +101,13 @@ public function servers(Request $request)
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 @@ public function server_by_uuid(Request $request)
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 @@ public function resources_by_server(Request $request)
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 @@ public function domains_by_server(Request $request)
summary: 'Create',
description: 'Create Server.',
path: '/servers',
+ operationId: 'create-server',
security: [
['bearerAuth' => []],
],
@@ -545,6 +550,7 @@ public function create_server(Request $request)
summary: 'Update',
description: 'Update Server.',
path: '/servers/{uuid}',
+ operationId: 'update-server-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -655,6 +661,7 @@ public function update_server(Request $request)
summary: 'Delete',
description: 'Delete server by UUID.',
path: '/servers/{uuid}',
+ operationId: 'delete-server-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -727,6 +734,7 @@ public function delete_server(Request $request)
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 @@ private function removeSensitiveData($service)
summary: 'List',
description: 'List all services.',
path: '/services',
+ operationId: 'list-services',
security: [
['bearerAuth' => []],
],
@@ -88,6 +89,7 @@ public function services(Request $request)
summary: 'Create',
description: 'Create a one-click service',
path: '/services',
+ operationId: 'create-service',
security: [
['bearerAuth' => []],
],
@@ -365,6 +367,7 @@ public function create_service(Request $request)
summary: 'Get',
description: 'Get service by UUID.',
path: '/services/{uuid}',
+ operationId: 'get-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -375,7 +378,7 @@ public function create_service(Request $request)
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 @@ public function service_by_uuid(Request $request)
summary: 'Delete',
description: 'Delete service by UUID.',
path: '/services/{uuid}',
+ operationId: 'delete-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -432,7 +436,7 @@ public function service_by_uuid(Request $request)
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 @@ public function delete_by_uuid(Request $request)
]);
}
+ #[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 @@ public function action_deploy(Request $request)
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 @@ public function action_stop(Request $request)
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 @@ private function removeSensitiveData($team)
summary: 'List',
description: 'Get all teams.',
path: '/teams',
+ operationId: 'list-teams',
security: [
['bearerAuth' => []],
],
@@ -79,6 +80,7 @@ public function teams(Request $request)
summary: 'Get',
description: 'Get team by TeamId.',
path: '/teams/{id}',
+ operationId: 'get-team-by-id',
security: [
['bearerAuth' => []],
],
@@ -129,6 +131,7 @@ public function team_by_id(Request $request)
summary: 'Members',
description: 'Get members by TeamId.',
path: '/teams/{id}/members',
+ operationId: 'get-members-by-team-id',
security: [
['bearerAuth' => []],
],
@@ -189,6 +192,7 @@ public function members_by_id(Request $request)
summary: 'Authenticated Team',
description: 'Get currently authenticated team.',
path: '/teams/current',
+ operationId: 'get-current-team',
security: [
['bearerAuth' => []],
],
@@ -225,6 +229,7 @@ public function current_team(Request $request)
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\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 @@ public function __construct(int $application_deployment_queue_id)
$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 @@ public function __construct(int $application_deployment_queue_id)
$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 @@ public function handle(): void
$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 @@ private function deploy_docker_compose_buildpack()
$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 @@ private function deploy_docker_compose_buildpack()
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 @@ private function deploy_docker_compose_buildpack()
// 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 @@ private function deploy_docker_compose_buildpack()
);
} 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 @@ private function deploy_docker_compose_buildpack()
);
} 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 @@ private function write_deployment_configurations()
[
"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 @@ private function push_to_docker_registry()
$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 @@ private function push_to_docker_registry()
$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 @@ private function should_skip_build()
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 @@ private function save_environment_variables()
}
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 @@ private function save_environment_variables()
}
}
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 @@ private function save_environment_variables()
);
}
}
+ $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 @@ private function create_workdir()
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 @@ private function clone_repository()
}
$this->execute_remote_command(
[
- $importCommands, 'hidden' => true,
+ $importCommands,
+ 'hidden' => true,
]
);
$this->create_workdir();
@@ -1447,6 +1574,9 @@ private function generate_nixpacks_confs()
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 @@ private function generate_compose_file()
// 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 @@ private function generate_compose_file()
$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 @@ private function generate_compose_file()
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 @@ private function pull_latest_image($image)
$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 @@ private function build_image()
$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 @@ private function build_image()
$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 @@ private function build_image()
}
$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 @@ private function build_image()
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 @@ private function build_image()
$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 @@ private function build_image()
$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 @@ private function build_image()
}
$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 @@ private function generate_build_env_variables()
$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 @@ private function add_build_env_variables_to_dockerfile()
} 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 @@ private function run_pre_deployment_command()
$exec = "docker exec {$containerName} {$cmd}";
$this->execute_remote_command(
[
- 'command' => $exec, 'hidden' => true,
+ 'command' => $exec,
+ 'hidden' => true,
],
);
@@ -2216,7 +2398,9 @@ private function run_post_deployment_command()
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\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 @@ public function handle(): void
$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 @@
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\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 @@ public function uniqueId(): int
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 @@ public function handle(): void
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 @@ public function handle(): void
} 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 @@ private function backup_standalone_mongodb(string $databaseWithCollections): voi
$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 @@ private function backup_standalone_mongodb(string $databaseWithCollections): voi
}
$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 @@ private function backup_standalone_postgresql(string $database): void
{
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 @@ private function remove_old_backups(): void
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 @@ private function remove_old_backups(): void
}
}
+ // private function upload_to_s3(): void
+ // {
+ // try {
+ // if (is_null($this->s3)) {
+ // return;
+ // }
+ // $key = $this->s3->key;
+ // $secret = $this->s3->secret;
+ // // $region = $this->s3->region;
+ // $bucket = $this->s3->bucket;
+ // $endpoint = $this->s3->endpoint;
+ // $this->s3->testConnection(shouldSave: true);
+ // $configName = new Cuid2;
+
+ // $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
+ // $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
+ // $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
+ // instant_remote_process($commands, $this->server);
+ // $this->add_to_backup_output('Uploaded to S3.');
+ // } catch (\Throwable $e) {
+ // $this->add_to_backup_output($e->getMessage());
+ // throw $e;
+ // } finally {
+ // $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
+ // $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
+ // instant_remote_process($removeConfigCommands, $this->server, false);
+ // }
+ // }
private function upload_to_s3(): void
{
try {
@@ -477,12 +524,15 @@ private function upload_to_s3(): void
} 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 @@ private function upload_to_s3(): void
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\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 @@ public function handle(): void
$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 @@ public function handle(): void
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\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 @@ public function __construct(public Server $server) {}
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 @@ public function __construct($task)
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 @@ public function uniqueId(): int
public function handle(): void
{
+
try {
$this->task_log = ScheduledTaskExecution::create([
'scheduled_task_id' => $this->task->id,
@@ -78,12 +94,12 @@ public function handle(): void
} elseif ($this->resource->type() == 'service') {
$this->resource->applications()->get()->each(function ($application) {
if (str(data_get($application, 'status'))->contains('running')) {
- $this->containers[] = data_get($application, 'name').'-'.data_get($this->resource, 'uuid');
+ $this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid');
}
});
$this->resource->databases()->get()->each(function ($database) {
if (str(data_get($database, 'status'))->contains('running')) {
- $this->containers[] = data_get($database, 'name').'-'.data_get($this->resource, 'uuid');
+ $this->containers[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid');
}
});
}
@@ -96,8 +112,8 @@ public function handle(): void
}
foreach ($this->containers as $containerName) {
- if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) {
- $cmd = "sh -c '".str_replace("'", "'\''", $this->task->command)."'";
+ if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) {
+ $cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'";
$exec = "docker exec {$containerName} {$cmd}";
$this->task_output = instant_remote_process([$exec], $this->server, true);
$this->task_log->update([
@@ -121,6 +137,7 @@ public function handle(): void
$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 @@ public function backoff(): int
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 @@ public function handle()
}
GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
$this->checkLogDrainContainer();
- $this->checkSentinel();
}
} catch (\Throwable $e) {
@@ -90,21 +91,6 @@ public function handle()
}
- 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 @@ private function serverStatus()
private function checkLogDrainContainer()
{
+ if (! $this->server->isLogDrainEnabled()) {
+ return;
+ }
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain';
})->first();
diff --git a/app/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 @@ public function mount()
}
$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 @@ public function setServerType(string $type)
$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 @@ public function selectExistingServer()
}
$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 @@ public function savePrivateKey()
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 @@ public function installServer()
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 @@ public function validateServer()
$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 @@ public function validateServer()
]);
$this->getProxyType();
} catch (\Throwable $e) {
+ $this->createdServer->settings()->update([
+ 'is_usable' => false,
+ ]);
+
return handleError(error: $e, livewire: $this);
}
}
@@ -349,6 +369,21 @@ public function showNewResource()
);
}
+ 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 @@ public function get_deployments()
])->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 @@ public function instantSave()
$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 @@ public function saveCustomName()
} 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 @@
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
+use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component
@@ -69,6 +70,20 @@ public function polling()
}
}
+ 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 @@ public function force_start()
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 @@ public function mount()
}
$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 @@ public function instantSave()
$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 @@ public function loadComposeFile($isInit = false)
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 @@ public function generate_preview($preview_id)
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 @@
class BackupExecutions extends Component
{
public ?ScheduledDatabaseBackup $backup = null;
-
+ public $database;
public $executions = [];
-
public $setDeletableBackup;
public $delete_backup_s3 = true;
@@ -79,10 +78,56 @@ public function download_file($exeuctionId)
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 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (! $this->server->isLogDrainEnabled()) {
+ if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -71,14 +73,14 @@ public function instantSaveAdvanced()
public function instantSave()
{
try {
- if ($this->database->is_public && ! $this->database->public_port) {
+ if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (! str($this->database->status)->startsWith('running')) {
+ if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -93,7 +95,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
+ $this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index 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 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (! $this->server->isLogDrainEnabled()) {
+ if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -86,14 +88,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && ! $this->database->public_port) {
+ if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (! str($this->database->status)->startsWith('running')) {
+ if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -108,7 +110,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
+ $this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/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 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (! $this->server->isLogDrainEnabled()) {
+ if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -92,14 +94,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && ! $this->database->public_port) {
+ if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (! str($this->database->status)->startsWith('running')) {
+ if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -114,7 +116,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
+ $this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index 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 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (! $this->server->isLogDrainEnabled()) {
+ if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -98,14 +100,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && ! $this->database->public_port) {
+ if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (! str($this->database->status)->startsWith('running')) {
+ if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -120,7 +122,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
+ $this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index 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 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (! $this->server->isLogDrainEnabled()) {
+ if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -99,14 +101,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && ! $this->database->public_port) {
+ if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (! str($this->database->status)->startsWith('running')) {
+ if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -121,7 +123,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
+ $this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php
index 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 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (! $this->server->isLogDrainEnabled()) {
+ if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -97,14 +99,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && ! $this->database->public_port) {
+ if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (! str($this->database->status)->startsWith('running')) {
+ if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -119,7 +121,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
+ $this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Database/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 @@ public function getListeners()
'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 @@ public function getListeners()
'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 @@ public function mount()
public function instantSaveAdvanced()
{
try {
- if (! $this->server->isLogDrainEnabled()) {
+ if (!$this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -86,14 +88,14 @@ public function submit()
public function instantSave()
{
try {
- if ($this->database->is_public && ! $this->database->public_port) {
+ if ($this->database->is_public && !$this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
- if (! str($this->database->status)->startsWith('running')) {
+ if (!str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -108,7 +110,7 @@ public function instantSave()
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
- $this->database->is_public = ! $this->database->is_public;
+ $this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/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 @@
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 @@ public function submit()
$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 @@ public function updatedBaseDirectory()
}
}
+ 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 @@ public function setServer(Server $server)
$docker = $this->standaloneDockers->first() ?? $this->swarmDockers->first();
if ($docker) {
$this->setDestination($docker->uuid);
+
+ return $this->whatToDoNext();
}
}
$this->current_step = 'destinations';
@@ -211,15 +215,38 @@ public function setDestination(string $destination_uuid)
{
$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 @@ public function mount()
$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 @@ public function mount()
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 @@ public function mount()
$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 @@ public function getListeners()
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
'check_status',
+ 'refresh' => '$refresh',
];
}
@@ -75,6 +76,12 @@ public function check_status()
{
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 @@ public function saveEditedCompose()
{
$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 @@ public function mount()
$this->workdir = null;
$this->fs_path = $this->fileStorage->fs_path;
}
+ $this->fileStorage->loadStorageOnServer();
}
public function convertToDirectory()
@@ -55,6 +57,7 @@ public function convertToDirectory()
$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 @@ public function convertToFile()
$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 @@ public function mount()
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
- $this->submit();
+ $this->submit(notify: false);
}
public function instantSave()
@@ -62,7 +62,7 @@ public function instantSave()
$this->dispatch('success', 'Service settings saved.');
}
- public function submit()
+ public function submit($notify = true)
{
try {
$this->validate();
@@ -76,7 +76,7 @@ public function submit()
$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 @@ public function mount()
$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 @@ public function delete()
{
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 @@ public function mount()
if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
- $this->container = $this->resource->uuid;
- $this->containers->push($this->container);
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
- $this->resource->applications()->get()->each(function ($application) {
- $this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid'));
- });
- $this->resource->databases()->get()->each(function ($database) {
- $this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid'));
- });
if ($this->resource->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->server);
}
}
- if ($this->containers->count() > 0) {
- $this->container = $this->containers->first();
- }
}
public function loadContainers()
@@ -102,44 +89,65 @@ public function loadContainers()
];
$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 @@ public function getLogs($refresh = false)
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 @@ public function mount()
$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 @@
class Executions extends Component
{
public $executions = [];
-
public $selectedKey;
+ public $task;
public function getListeners()
{
@@ -26,4 +26,44 @@ public function selectTask($key): void
}
$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 @@ public function instantSave()
}
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 @@ public function validateServer($install = true)
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 @@ public function submit()
'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 @@ public function submit()
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 @@ public function mount()
}
}
+ 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 @@ public function mount()
$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 @@ public function checkManually()
}
}
+ 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 @@
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 @@ protected static function booted()
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 @@ protected static function booted()
$task->delete();
}
$application->tags()->detach();
+ $application->previews()->delete();
});
}
@@ -474,23 +477,6 @@ public function dockerComposeLocation(): Attribute
);
}
- 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 @@ protected function serverStatus(): Attribute
$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 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
}
}
- public function parseRawCompose()
+ public function oldRawParser()
{
try {
$yaml = Yaml::parse($this->docker_compose_raw);
@@ -1162,9 +1148,11 @@ public function parseRawCompose()
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 @@ public function loadComposeFile($isInit = false)
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 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 @@ public function resource()
$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 @@ public function realValue(): Attribute
);
}
- 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 @@ private function get_real_environment_variables(?string $environment_variable =
$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 @@ public function fqdn(): Attribute
);
}
+ 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 @@ public function service()
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 @@ public function deleteStorageOnServer()
$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 @@ public function deleteStorageOnServer()
public function saveStorageOnServer()
{
+ $this->load(['service']);
$isService = data_get($this->resource, 'service');
if ($isService) {
$workdir = $this->resource->service->workdir();
@@ -74,30 +105,36 @@ public function saveStorageOnServer()
$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.