diff --git a/.dockerignore b/.dockerignore
index d6abd1451..3a0ec49f7 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -22,3 +22,4 @@ yarn-error.log
/_data
.rnd
/.ssh
+.ignition.json
diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/01_BUG_REPORT.yml
similarity index 100%
rename from .github/ISSUE_TEMPLATE/BUG_REPORT.yml
rename to .github/ISSUE_TEMPLATE/01_BUG_REPORT.yml
diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml b/.github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml
similarity index 100%
rename from .github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml
rename to .github/ISSUE_TEMPLATE/02_ENHANCEMENT_BOUNTY.yml
diff --git a/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml
new file mode 100644
index 000000000..d00853964
--- /dev/null
+++ b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml
@@ -0,0 +1,17 @@
+name: Lock closed Issues, Discussions, and PRs
+
+on:
+ schedule:
+ - cron: '0 1 * * *'
+
+jobs:
+ lock-threads:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Lock threads after 30 days of inactivity
+ uses: dessant/lock-threads@v5
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ issue-inactive-days: '30'
+ pr-inactive-days: '30'
+ discussion-inactive-days: '30'
diff --git a/.github/workflows/chore-manage-stale-issues-and-prs.yml b/.github/workflows/chore-manage-stale-issues-and-prs.yml
new file mode 100644
index 000000000..2afc996cb
--- /dev/null
+++ b/.github/workflows/chore-manage-stale-issues-and-prs.yml
@@ -0,0 +1,28 @@
+name: Manage Stale Issues and PRs
+
+on:
+ schedule:
+ - cron: '0 2 * * *'
+
+jobs:
+ manage-stale:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Manage stale issues and PRs
+ uses: actions/stale@v9
+ id: stale
+ with:
+ stale-issue-message: 'This issue will be automatically closed in a few days if no response is received. Please provide an update with the requested information.'
+ stale-pr-message: 'This pull request will be automatically closed in a few days if no response is received. Please update your PR or comment if you would like to continue working on it.'
+ close-issue-message: 'This issue has been automatically closed due to inactivity.'
+ close-pr-message: 'This pull request has been automatically closed due to inactivity.'
+ days-before-stale: 14
+ days-before-close: 7
+ stale-issue-label: '⏱︎ Stale'
+ stale-pr-label: '⏱︎ Stale'
+ only-labels: '💤 Waiting for feedback'
+ remove-stale-when-updated: true
+ operations-per-run: 100
+ labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback'
+ close-issue-reason: 'not_planned'
+ exempt-all-milestones: false
diff --git a/.github/workflows/remove-labels-and-assignees-on-close.yml b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml
similarity index 100%
rename from .github/workflows/remove-labels-and-assignees-on-close.yml
rename to .github/workflows/chore-remove-labels-and-assignees-on-close.yml
diff --git a/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml
index 4add8516e..4354294b1 100644
--- a/.github/workflows/coolify-helper-next.yml
+++ b/.github/workflows/coolify-helper-next.yml
@@ -1,4 +1,4 @@
-name: Coolify Helper Image Development (v4)
+name: Coolify Helper Image Development
on:
push:
@@ -8,7 +8,8 @@ on:
- docker/coolify-helper/Dockerfile
env:
- REGISTRY: ghcr.io
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-helper"
jobs:
@@ -19,25 +20,36 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
labels: |
coolify.managed=true
aarch64:
@@ -47,27 +59,39 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
labels: |
coolify.managed=true
+
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -75,25 +99,42 @@ jobs:
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: actions/checkout@v4
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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
+
+ - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: |
- 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
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next
+
- uses: sarisia/actions-status-discord@v1
if: always()
with:
diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml
index fd4be2f11..6d852a2b3 100644
--- a/.github/workflows/coolify-helper.yml
+++ b/.github/workflows/coolify-helper.yml
@@ -1,4 +1,4 @@
-name: Coolify Helper Image (v4)
+name: Coolify Helper Image
on:
push:
@@ -8,7 +8,8 @@ on:
- docker/coolify-helper/Dockerfile
env:
- REGISTRY: ghcr.io
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-helper"
jobs:
@@ -19,25 +20,36 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
labels: |
coolify.managed=true
aarch64:
@@ -47,25 +59,36 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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
+ 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 and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
labels: |
coolify.managed=true
merge-manifest:
@@ -75,26 +98,45 @@ jobs:
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: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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
+ 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 on ${{ env.GITHUB_REGISTRY }}
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
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.DOCKER_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/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml
new file mode 100644
index 000000000..771687d4b
--- /dev/null
+++ b/.github/workflows/coolify-production-build.yml
@@ -0,0 +1,143 @@
+name: Production Build (v4)
+
+on:
+ push:
+ branches: ["main"]
+ paths-ignore:
+ - .github/workflows/coolify-helper.yml
+ - .github/workflows/coolify-helper-next.yml
+ - .github/workflows/coolify-realtime.yml
+ - .github/workflows/coolify-realtime-next.yml
+ - docker/coolify-helper/Dockerfile
+ - docker/coolify-realtime/Dockerfile
+ - docker/testing-host/Dockerfile
+ - templates/service-templates.json
+
+env:
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
+ IMAGE_NAME: "coollabsio/coolify"
+
+jobs:
+ amd64:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT]
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/prod/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ labels: |
+ coolify.managed=true
+
+ aarch64:
+ runs-on: [self-hosted, arm64]
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT]
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/prod/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ ${{ env.GITHUB_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:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Get Version
+ id: version
+ run: |
+ echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
+
+ - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.DOCKER_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/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml
new file mode 100644
index 000000000..7e937d17a
--- /dev/null
+++ b/.github/workflows/coolify-realtime-next.yml
@@ -0,0 +1,146 @@
+name: Coolify Realtime Development
+
+on:
+ push:
+ branches: [ "next" ]
+ paths:
+ - .github/workflows/coolify-realtime-next.yml
+ - docker/coolify-realtime/Dockerfile
+ - docker/coolify-realtime/terminal-server.js
+ - docker/coolify-realtime/package.json
+ - docker/coolify-realtime/soketi-entrypoint.sh
+
+env:
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.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 ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
+ labels: |
+ coolify.managed=true
+
+ aarch64:
+ runs-on: [ self-hosted, arm64 ]
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/coolify-realtime/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
+ labels: |
+ coolify.managed=true
+
+ merge-manifest:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [ amd64, aarch64 ]
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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 on ${{ env.GITHUB_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
+ --tag ${{ env.DOCKER_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-realtime.yml b/.github/workflows/coolify-realtime.yml
index 75e3f1681..97bfd52eb 100644
--- a/.github/workflows/coolify-realtime.yml
+++ b/.github/workflows/coolify-realtime.yml
@@ -1,8 +1,8 @@
-name: Coolify Realtime (v4)
+name: Coolify Realtime
on:
push:
- branches: [ "main", "next" ]
+ branches: [ "main" ]
paths:
- .github/workflows/coolify-realtime.yml
- docker/coolify-realtime/Dockerfile
@@ -11,7 +11,8 @@ on:
- docker/coolify-realtime/soketi-entrypoint.sh
env:
- REGISTRY: ghcr.io
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-realtime"
jobs:
@@ -22,27 +23,39 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
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 }}
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
labels: |
coolify.managed=true
+
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
@@ -50,27 +63,39 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
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
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
labels: |
coolify.managed=true
+
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -78,25 +103,43 @@ jobs:
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: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_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
+
+ - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
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
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
- uses: sarisia/actions-status-discord@v1
if: always()
with:
diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml
new file mode 100644
index 000000000..dd5e6ebd6
--- /dev/null
+++ b/.github/workflows/coolify-staging-build.yml
@@ -0,0 +1,129 @@
+name: Staging Build
+
+on:
+ push:
+ branches-ignore: ["main", "v3"]
+ paths-ignore:
+ - .github/workflows/coolify-helper.yml
+ - .github/workflows/coolify-helper-next.yml
+ - .github/workflows/coolify-realtime.yml
+ - .github/workflows/coolify-realtime-next.yml
+ - docker/coolify-helper/Dockerfile
+ - docker/coolify-realtime/Dockerfile
+ - docker/testing-host/Dockerfile
+ - templates/service-templates.json
+
+env:
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
+ IMAGE_NAME: "coollabsio/coolify"
+
+jobs:
+ amd64:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/prod/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
+ labels: |
+ coolify.managed=true
+
+ aarch64:
+ runs-on: [self-hosted, arm64]
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: docker/prod/Dockerfile
+ platforms: linux/aarch64
+ push: true
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64
+ labels: |
+ coolify.managed=true
+
+ merge-manifest:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ needs: [amd64, aarch64]
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.GITHUB_REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
+
+ - uses: sarisia/actions-status-discord@v1
+ if: always()
+ with:
+ webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
diff --git a/.github/workflows/coolify-testing-host.yml b/.github/workflows/coolify-testing-host.yml
index 5fdc32991..95a228114 100644
--- a/.github/workflows/coolify-testing-host.yml
+++ b/.github/workflows/coolify-testing-host.yml
@@ -1,14 +1,15 @@
-name: Coolify Testing Host (v4-non-prod)
+name: Coolify Testing Host
on:
push:
- branches: [ "main", "next" ]
+ branches: [ "next" ]
paths:
- .github/workflows/coolify-testing-host.yml
- docker/testing-host/Dockerfile
env:
- REGISTRY: ghcr.io
+ GITHUB_REGISTRY: ghcr.io
+ DOCKER_REGISTRY: docker.io
IMAGE_NAME: "coollabsio/coolify-testing-host"
jobs:
@@ -19,21 +20,34 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/testing-host/Dockerfile
platforms: linux/amd64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+ labels: |
+ coolify.managed=true
+
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
@@ -41,21 +55,34 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- - name: Login to ghcr.io
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Build and Push Image
+ uses: docker/build-push-action@v6
with:
- no-cache: true
context: .
file: docker/testing-host/Dockerfile
platforms: linux/aarch64
push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
+ tags: |
+ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
+ ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
+ labels: |
+ coolify.managed=true
+
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -63,21 +90,36 @@ jobs:
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: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Create & publish manifest
+
+ - name: Login to ${{ env.DOCKER_REGISTRY }}
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.DOCKER_REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
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.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
+ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
+ - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
+ run: |
+ docker buildx imagetools create \
+ --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
+ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
+
- uses: sarisia/actions-status-discord@v1
if: always()
with:
diff --git a/.github/workflows/development-build.yml b/.github/workflows/development-build.yml
deleted file mode 100644
index 268b885ac..000000000
--- a/.github/workflows/development-build.yml
+++ /dev/null
@@ -1,79 +0,0 @@
-name: Development Build (v4)
-
-on:
- push:
- branches-ignore: ["main", "v3"]
- paths-ignore:
- - .github/workflows/coolify-helper.yml
- - docker/coolify-helper/Dockerfile
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: "coollabsio/coolify"
-
-jobs:
- amd64:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Login to ghcr.io
- uses: docker/login-action@v3
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
- with:
- context: .
- file: docker/prod/Dockerfile
- platforms: linux/amd64
- push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
- 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: Build image and push to registry
- uses: docker/build-push-action@v5
- with:
- context: .
- file: docker/prod/Dockerfile
- platforms: linux/aarch64
- push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64
- 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: Create & publish manifest
- run: |
- docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
- - uses: sarisia/actions-status-discord@v1
- if: always()
- with:
- webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
deleted file mode 100644
index 0edaa4f1c..000000000
--- a/.github/workflows/docker-image.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-name: Docker Image CI
-
-on:
- # push:
- # branches: [ "main" ]
- # pull_request:
- # branches: [ "*" ]
- push:
- branches: ["this-does-not-exist"]
- pull_request:
- branches: ["this-does-not-exist"]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Cache Docker layers
- uses: actions/cache@v2
- with:
- path: |
- /usr/local/share/ca-certificates
- /var/cache/apt/archives
- /var/lib/apt/lists
- ~/.cache
- key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
- restore-keys: |
- ${{ runner.os }}-docker-
- - name: Build the Docker image
- run: |
- cp .env.example .env
- docker run --rm -u "$(id -u):$(id -g)" \
- -v "$(pwd):/app" \
- -w /app composer:2 \
- composer install --ignore-platform-reqs
- ./vendor/bin/spin build
- - name: Start the stack
- run: |
- ./vendor/bin/spin up -d
- ./vendor/bin/spin exec coolify php artisan key:generate
- ./vendor/bin/spin exec coolify php artisan migrate:fresh --seed
- - name: Test (missing E2E tests)
- run: |
- ./vendor/bin/spin exec coolify php artisan test
diff --git a/.github/workflows/fix-php-code-style-issues b/.github/workflows/fix-php-code-style-issues
deleted file mode 100644
index aebce91bc..000000000
--- a/.github/workflows/fix-php-code-style-issues
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Fix PHP code style issues
-
-on: [push]
-
-permissions:
- contents: write
-
-jobs:
- php-code-styling:
- runs-on: ubuntu-latest
- timeout-minutes: 5
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- ref: ${{ github.head_ref }}
-
- - name: Fix PHP code style issues
- uses: aglipanci/laravel-pint-action@2.4
-
- - name: Commit changes
- uses: stefanzweifel/git-auto-commit-action@v5
- with:
- commit_message: Fix styling
diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml
deleted file mode 100644
index d7a680170..000000000
--- a/.github/workflows/pr-build.yml
+++ /dev/null
@@ -1,93 +0,0 @@
-name: PR Build (v4)
-
-on:
- pull_request:
- types:
- - opened
- branches-ignore: ["main", "v3"]
- paths-ignore:
- - .github/workflows/coolify-helper.yml
- - docker/coolify-helper/Dockerfile
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: "coollabsio/coolify"
-
-jobs:
- amd64:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
- attestations: write
- id-token: write
- actions: write
- steps:
- - uses: actions/checkout@v4
- - name: Login to ghcr.io
- uses: docker/login-action@v3
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
- with:
- context: .
- file: docker/prod/Dockerfile
- platforms: linux/amd64
- push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}
- aarch64:
- runs-on: [self-hosted, arm64]
- permissions:
- contents: read
- packages: write
- attestations: write
- id-token: write
- actions: write
- steps:
- - uses: actions/checkout@v4
- - name: Login to ghcr.io
- uses: docker/login-action@v3
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
- with:
- context: .
- file: docker/prod/Dockerfile
- platforms: linux/aarch64
- push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}-aarch64
- merge-manifest:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
- attestations: write
- id-token: write
- actions: write
- needs: [amd64, aarch64]
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
- - name: Login to ghcr.io
- uses: docker/login-action@v3
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Create & publish manifest
- run: |
- docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}
- - uses: sarisia/actions-status-discord@v1
- if: always()
- with:
- webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml
deleted file mode 100644
index c78c865bf..000000000
--- a/.github/workflows/production-build.yml
+++ /dev/null
@@ -1,89 +0,0 @@
-name: Production Build (v4)
-
-on:
- push:
- branches: ["main"]
- paths-ignore:
- - .github/workflows/coolify-helper.yml
- - docker/coolify-helper/Dockerfile
- - templates/service-templates.json
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: "coollabsio/coolify"
-
-jobs:
- amd64:
- runs-on: ubuntu-latest
- 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 php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
- with:
- context: .
- file: docker/prod/Dockerfile
- platforms: linux/amd64
- push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
- aarch64:
- runs-on: [self-hosted, arm64]
- 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 php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
- - name: Build image and push to registry
- uses: docker/build-push-action@v5
- with:
- context: .
- file: docker/prod/Dockerfile
- platforms: linux/aarch64
- push: true
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
- 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 php:8.2-alpine3.16 php bootstrap/getVersion.php)"|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/.gitignore b/.gitignore
index ac8a1e090..09504afee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,4 @@ _ide_helper_models.php
.rnd
/.ssh
scripts/load-test/*
+.ignition.json
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4a3e0e538..80ec0614e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,19 +6,16 @@ # Contributing to Coolify
## Table of Contents
-- [Contributing to Coolify](#contributing-to-coolify)
- - [Table of Contents](#table-of-contents)
- - [1. Setup Development Environment](#1-setup-development-environment)
- - [2. Verify Installation (Optional)](#2-verify-installation-optional)
- - [3. Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
- - [4. Set up Environment Variables](#4-set-up-environment-variables)
- - [5. Start Coolify](#5-start-coolify)
- - [6. Start Development](#6-start-development)
- - [7. Development Notes](#7-development-notes)
- - [8. Create a Pull Request](#8-create-a-pull-request)
- - [Additional Contribution Guidelines](#additional-contribution-guidelines)
- - [Contributing a New Service](#contributing-a-new-service)
- - [Contributing to Documentation](#contributing-to-documentation)
+1. [Setup Development Environment](#1-setup-development-environment)
+2. [Verify Installation](#2-verify-installation-optional)
+3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
+4. [Set up Environment Variables](#4-set-up-environment-variables)
+5. [Start Coolify](#5-start-coolify)
+6. [Start Development](#6-start-development)
+7. [Create a Pull Request](#7-create-a-pull-request)
+8. [Development Notes](#development-notes)
+9. [Resetting Development Environment](#resetting-development-environment)
+10. [Additional Contribution Guidelines](#additional-contribution-guidelines)
## 1. Setup Development Environment
@@ -29,15 +26,15 @@ ## 1. Setup Development Environment
1. Install `docker-ce`, Docker Desktop (or similar):
- Docker CE (recommended):
- - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install)
- - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/)
+ - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify)
+ - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify)
- Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide
- Install Docker Desktop (easier):
- - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/)
+ - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify)
- Ensure WSL2 backend is enabled in Docker Desktop settings
2. Install Spin:
- - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2)
+ - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2?ref=coolify)
@@ -46,12 +43,12 @@ ## 1. Setup Development Environment
1. Install Orbstack, Docker Desktop (or similar):
- Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
- - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation)
+ - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify)
- Docker Desktop:
- - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/)
+ - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify)
2. Install Spin:
- - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin)
+ - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin?ref=coolify)
@@ -60,12 +57,12 @@ ## 1. Setup Development Environment
1. Install Docker Engine, Docker Desktop (or similar):
- Docker Engine (recommended, as there is no VM overhead):
- - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/) for your Linux distribution
+ - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution
- Docker Desktop:
- - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/)
+ - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify)
2. Install Spin:
- - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions)
+ - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions?ref=coolify)
@@ -89,14 +86,14 @@ ## 3. Fork and Setup Local Repository
| Editor | Platform | Download Link |
|--------|----------|---------------|
- | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download) |
- | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/) |
- | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download) |
+ | Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download?ref=coolify) |
+ | Cursor (recommended but paid) | Windows/macOS/Linux | [Download](https://www.cursor.com/?ref=coolify) |
+ | Zed (very fast) | macOS/Linux | [Download](https://zed.dev/download?ref=coolify) |
3. Clone the Coolify Repository from your fork to your local machine
- Use `git clone` in the command line, or
- Use GitHub Desktop (recommended):
- - Download and install from [https://desktop.github.com/](https://desktop.github.com/)
+ - Download and install from [https://desktop.github.com/](https://desktop.github.com/?ref=coolify)
- Open GitHub Desktop and login with your GitHub account
- Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone`
@@ -149,7 +146,36 @@ ## 6. Start Development
> TELESCOPE_ENABLED=true
> ```
-## 7. Development Notes
+## 7. Create a Pull Request
+
+1. After making changes or adding a new service:
+ - Commit your changes to your forked repository.
+ - Push the changes to your GitHub account.
+
+2. Creating the Pull Request (PR):
+ - Navigate to the main Coolify repository on GitHub.
+ - Click the "Pull requests" tab.
+ - Click the green "New pull request" button.
+ - Choose your fork and branch as the compare branch.
+ - Click "Create pull request".
+
+3. Filling out the PR details:
+ - Give your PR a descriptive title.
+ - Use the Pull Request Template provided and fill in the details.
+
+> [!IMPORTANT]
+> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch.
+
+4. Submit your PR:
+ - Review your changes one last time.
+ - Click "Create pull request" to submit.
+
+> [!NOTE]
+> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers.
+
+After submission, maintainers will review your PR and may request changes or provide feedback.
+
+## Development Notes
When working on Coolify, keep the following in mind:
@@ -168,35 +194,41 @@ ## 7. Development Notes
> [!IMPORTANT]
> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
-## 8. Create a Pull Request
+## Resetting Development Environment
-1. After making changes or adding a new service:
- - Commit your changes to your forked repository.
- - Push the changes to your GitHub account.
+If you encounter issues or break your database or something else, follow these steps to start from a clean slate (works since `v4.0.0-beta.342`):
-2. Creating the Pull Request (PR):
- - Navigate to the main Coolify repository on GitHub.
- - Click the "Pull requests" tab.
- - Click the green "New pull request" button.
- - Choose your fork and branch as the compare branch.
- - Click "Create pull request".
+1. Stop all running containers `ctrl + c`.
-3. Filling out the PR details:
- - Give your PR a descriptive title.
- - In the description, explain the changes you've made.
- - Reference any related issues by using keywords like "Fixes #123" or "Closes #456".
+2. Remove all Coolify containers:
+ ```bash
+ docker rm coolify coolify-db coolify-redis coolify-realtime coolify-testing-host coolify-minio coolify-vite-1 coolify-mail
+ ```
+
+3. Remove Coolify volumes (it is possible that the volumes have no `coolify` prefix on your machine, in that case remove the prefix from the command):
+ ```bash
+ docker volume rm coolify_dev_backups_data coolify_dev_postgres_data coolify_dev_redis_data coolify_dev_coolify_data coolify_dev_minio_data
+ ```
+
+4. Remove unused images:
+ ```bash
+ docker image prune -a
+ ```
+
+5. Start Coolify again:
+ ```bash
+ spin up
+ ```
+
+6. Run database migrations and seeders:
+ ```bash
+ docker exec -it coolify php artisan migrate:fresh --seed
+ ```
+
+After completing these steps, you'll have a fresh development setup.
> [!IMPORTANT]
-> Always set the base branch for your PR to the `next` branch of the Coolify repository, not the `main` branch.
-
-4. Submit your PR:
- - Review your changes one last time.
- - Click "Create pull request" to submit.
-
-> [!NOTE]
-> Make sure your PR is out of draft mode as soon as it's ready for review. PRs that are in draft mode for a long time may be closed by maintainers.
-
-After submission, maintainers will review your PR and may request changes or provide feedback.
+> Always run database migrations and seeders after switching branches or pulling updates to ensure your local database structure matches the current codebase and includes necessary seed data.
## Additional Contribution Guidelines
diff --git a/RELEASE.md b/RELEASE.md
index 2cb96b72b..d9f05f17d 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -2,35 +2,120 @@ # Coolify Release Guide
This guide outlines the release process for Coolify, intended for developers and those interested in understanding how releases are managed and deployed.
+## Table of Contents
+- [Release Process](#release-process)
+- [Version Types](#version-types)
+ - [Stable](#stable)
+ - [Nightly](#nightly)
+ - [Beta](#beta)
+- [Version Availability](#version-availability)
+ - [Self-Hosted](#self-hosted)
+ - [Cloud](#cloud)
+- [Manually Update to Specific Versions](#manually-update-to-specific-versions)
+
## Release Process
-1. **Development on `next` or separate branches**
- - Changes, fixes and new features are developed on the `next` or even separate branches.
+1. **Development on `next` or Feature Branches**
+ - Improvements, fixes, and new features are developed on the `next` branch or separate feature branches.
2. **Merging to `main`**
- - Once changes are ready, they are merged from `next` into the `main` branch.
+ - Once ready, changes are merged from the `next` branch into the `main` branch.
-3. **Building the release**
- - After merging to `main`, a new release is built.
- - Note: A push to `main` does not automatically mean a new version is released.
+3. **Building the Release**
+ - After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry with the version tag and the `latest` tag.
-4. **Creating a GitHub release**
- - A new release is created on GitHub with the new version details.
+4. **Creating a GitHub Release**
+ - A new GitHub release is manually created with details of the changes made in the version.
5. **Updating the CDN**
- - The final step is updating the version information on the CDN:
- [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json)
+ - To make a new version publicly available, the version information on the CDN needs to be updated: [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json)
> [!NOTE]
-> The CDN update may not occur immediately after the GitHub release. It can happen hours or even days later due to additional testing, stability checks, or potential hotfixes.
+> The CDN update may not occur immediately after the GitHub release. It can take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated.**
+## Version Types
+
+
+ Stable (coming soon)
+
+- **Stable**
+ - The production version suitable for stable, production environments (generally recommended).
+ - **Update Frequency:** Every 2 to 4 weeks, with more frequent possible hotfixes.
+ - **Release Size:** Larger but less frequent releases. Multiple nightly versions are consolidated into a single stable release.
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `v4.0.0`).
+ - **Installation Command:**
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
+ ```
+
+
+
+
+ Nightly
+
+- **Nightly**
+ - The latest development version, suitable for testing the latest changes and experimenting with new features.
+ - **Update Frequency:** Daily or bi-weekly updates.
+ - **Release Size:** Smaller, more frequent releases.
+ - **Versioning Scheme:** TO BE DETERMINED
+ - **Installation Command:**
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify-nightly/install.sh | bash -s next
+ ```
+
+
+
+
+ Beta
+
+- **Beta**
+ - Test releases for the upcoming stable version.
+ - **Purpose:** Allows users to test and provide feedback on new features and changes before they become stable.
+ - **Update Frequency:** Available if we think beta testing is necessary.
+ - **Release Size:** Same size as stable release as it will become the next stabe release after some time.
+ - **Versioning Scheme:** Follows semantic versioning (e.g., `4.1.0-beta.1`).
+ - **Installation Command:**
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
+ ```
+
+
+
+> [!WARNING]
+> Do not use nightly/beta builds in production as there is no guarantee of stability.
## Version Availability
-It's important to understand that a new version released on GitHub may not immediately become available for users to update (through manual or auto-update).
+When a new version is released and a new GitHub release is created, it doesn't immediately become available for your instance. Here's how version availability works for different instance types.
+
+### Self-Hosted
+
+- **Update Frequency:** More frequent updates, especially on the nightly release channel.
+- **Update Availability:** New versions are available once the CDN has been updated.
+- **Update Methods:**
+ 1. **Manual Update in Instance Settings:**
+ - Go to `Settings > Update Check Frequency` and click the `Check Manually` button.
+ - If an update is available, an upgrade button will appear on the sidebar.
+ 2. **Automatic Update:**
+ - If enabled, the instance will update automatically at the time set in the settings.
+ 3. **Re-run Installation Script:**
+ - Run the installation script again to upgrade to the latest version available on the CDN:
+ ```bash
+ curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
+ ```
> [!IMPORTANT]
-> If you see a new release on GitHub but haven't received the update, it's likely because the CDN hasn't been updated yet. This is intentional and ensures stability and allows for hotfixes before the new version is officially released.
+> If a new release is available on GitHub but your instance hasn't updated yet or no upgrade button is shown in the UI, the CDN might not have been updated yet. This intentional delay ensures stability and allows for hotfixes before official release.
+
+### Cloud
+
+- **Update Frequency:** Less frequent as it's a managed service.
+- **Update Availability:** New versions are available once Andras has updated the cloud version manually.
+- **Update Method:**
+ - Updates are managed by Andras, who ensures each cloud version is thoroughly tested and stable before releasing it.
+
+> [!IMPORTANT]
+> The cloud version of Coolify may be several versions behind the latest GitHub releases even if the CDN is updated. This is intentional to ensure stability and reliability for cloud users and Andras will manully update the cloud version when the update is ready.
## Manually Update to Specific Versions
@@ -42,4 +127,4 @@ ## Manually Update to Specific Versions
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s
```
--> Replace `` with the version you want to update to (for example `4.0.0-beta.332`).
+Replace `` with the version you want to update to (for example `4.0.0-beta.332`).
diff --git a/app/Actions/Application/GenerateConfig.php b/app/Actions/Application/GenerateConfig.php
new file mode 100644
index 000000000..69365f921
--- /dev/null
+++ b/app/Actions/Application/GenerateConfig.php
@@ -0,0 +1,17 @@
+clearAll();
+ return $application->generateConfig(is_json: $is_json);
+ }
+}
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index 352c6a59f..3ee46a2e1 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -46,9 +46,6 @@ public function handle(StandaloneDragonfly $database)
'networks' => [
$this->database->destination->network,
],
- 'ulimits' => [
- 'memlock' => '-1',
- ],
'labels' => [
'coolify.managed' => 'true',
],
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index 0779da31d..ed563eaae 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -651,8 +651,9 @@ private function old_way()
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
- // Check if proxy is running
- $this->server->proxyType();
+ if (! $this->server->proxySet() || $this->server->proxy->force_stop) {
+ return;
+ }
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php
index f8882d12a..481757162 100644
--- a/app/Actions/Fortify/CreateNewUser.php
+++ b/app/Actions/Fortify/CreateNewUser.php
@@ -2,7 +2,6 @@
namespace App\Actions\Fortify;
-use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
@@ -20,7 +19,7 @@ class CreateNewUser implements CreatesNewUsers
*/
public function create(array $input): User
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->is_registration_enabled) {
abort(403);
}
@@ -48,7 +47,7 @@ public function create(array $input): User
$team = $user->teams()->first();
// Disable registration after first user is created
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$settings->is_registration_enabled = false;
$settings->save();
} else {
diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php
index dcb4058c0..55af1a8c0 100644
--- a/app/Actions/License/CheckResaleLicense.php
+++ b/app/Actions/License/CheckResaleLicense.php
@@ -2,7 +2,6 @@
namespace App\Actions\License;
-use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -13,7 +12,7 @@ class CheckResaleLicense
public function handle()
{
try {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (isDev()) {
$settings->update([
'is_resale_license_active' => true,
diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php
index f4fe650c5..bdeafd061 100644
--- a/app/Actions/Proxy/CheckConfiguration.php
+++ b/app/Actions/Proxy/CheckConfiguration.php
@@ -22,7 +22,7 @@ public function handle(Server $server, bool $reset = false)
];
$proxy_configuration = instant_remote_process($payload, $server, false);
if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) {
- $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value;
+ $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value();
}
if (! $proxy_configuration || is_null($proxy_configuration)) {
throw new \Exception('Could not generate proxy configuration');
diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php
index cf0f6015c..03a0beddf 100644
--- a/app/Actions/Proxy/CheckProxy.php
+++ b/app/Actions/Proxy/CheckProxy.php
@@ -2,14 +2,17 @@
namespace App\Actions\Proxy;
+use App\Enums\ProxyTypes;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
+use Symfony\Component\Yaml\Yaml;
class CheckProxy
{
use AsAction;
- public function handle(Server $server, $fromUI = false)
+ // It should return if the proxy should be started (true) or not (false)
+ public function handle(Server $server, $fromUI = false): bool
{
if (! $server->isFunctional()) {
return false;
@@ -62,22 +65,42 @@ public function handle(Server $server, $fromUI = false)
$ip = 'host.docker.internal';
}
- $connection80 = @fsockopen($ip, '80');
- $connection443 = @fsockopen($ip, '443');
- $port80 = is_resource($connection80) && fclose($connection80);
- $port443 = is_resource($connection443) && fclose($connection443);
- if ($port80) {
- if ($fromUI) {
- throw new \Exception("Port 80 is in use. You must stop the process using this port. Docs: https://coolify.io/docs Discord: https://coollabs.io/discord");
+ $portsToCheck = ['80', '443'];
+
+ try {
+ if ($server->proxyType() !== ProxyTypes::NONE->value) {
+ $proxyCompose = CheckConfiguration::run($server);
+ if (isset($proxyCompose)) {
+ $yaml = Yaml::parse($proxyCompose);
+ $portsToCheck = [];
+ if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
+ $ports = data_get($yaml, 'services.traefik.ports');
+ } elseif ($server->proxyType() === ProxyTypes::CADDY->value) {
+ $ports = data_get($yaml, 'services.caddy.ports');
+ }
+ if (isset($ports)) {
+ foreach ($ports as $port) {
+ $portsToCheck[] = str($port)->before(':')->value();
+ }
+ }
+ }
} else {
- return false;
+ $portsToCheck = [];
}
+ } catch (\Exception $e) {
+ ray($e->getMessage());
}
- if ($port443) {
- if ($fromUI) {
- throw new \Exception("Port 443 is in use. You must stop the process using this port. Docs: https://coolify.io/docs Discord: https://coollabs.io/discord");
- } else {
- return false;
+ if (count($portsToCheck) === 0) {
+ return false;
+ }
+ foreach ($portsToCheck as $port) {
+ $connection = @fsockopen($ip, $port);
+ if (is_resource($connection) && fclose($connection)) {
+ if ($fromUI) {
+ throw new \Exception("Port $port is in use. You must stop the process using this port. Docs: https://coolify.io/docs Discord: https://coollabs.io/discord");
+ } else {
+ return false;
+ }
}
}
diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php
index f025e5661..f20c10123 100644
--- a/app/Actions/Proxy/StartProxy.php
+++ b/app/Actions/Proxy/StartProxy.php
@@ -26,7 +26,7 @@ public function handle(Server $server, bool $async = true, bool $force = false):
}
SaveConfiguration::run($server, $configuration);
$docker_compose_yml_base64 = base64_encode($configuration);
- $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value;
+ $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
$server->save();
if ($server->isSwarm()) {
$commands = $commands->merge([
@@ -35,7 +35,7 @@ public function handle(Server $server, bool $async = true, bool $force = false):
"echo 'Creating required Docker Compose file.'",
"echo 'Starting coolify-proxy.'",
'docker stack deploy -c docker-compose.yml coolify-proxy',
- "echo 'Proxy started successfully.'",
+ "echo 'Successfully started coolify-proxy.'",
]);
} else {
$caddfile = 'import /dynamic/*.caddy';
@@ -46,12 +46,14 @@ public function handle(Server $server, bool $async = true, bool $force = false):
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
- "echo 'Stopping existing coolify-proxy.'",
- 'docker stop -t 10 coolify-proxy || true',
- 'docker rm coolify-proxy || true',
+ 'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then',
+ " echo 'Stopping and removing existing coolify-proxy.'",
+ ' docker rm -f coolify-proxy || true',
+ " echo 'Successfully stopped and removed existing coolify-proxy.'",
+ 'fi',
"echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
- "echo 'Proxy started successfully.'",
+ "echo 'Successfully started coolify-proxy.'",
]);
$commands = $commands->merge(connectProxyToNetworks($server));
}
diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php
index 1034c13d6..dc6ac12bf 100644
--- a/app/Actions/Server/CleanupDocker.php
+++ b/app/Actions/Server/CleanupDocker.php
@@ -2,7 +2,6 @@
namespace App\Actions\Server;
-use App\Models\InstanceSettings;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -12,28 +11,29 @@ class CleanupDocker
public function handle(Server $server)
{
+ $settings = instanceSettings();
+ $helperImageVersion = data_get($settings, 'helper_version');
+ $helperImage = config('coolify.helper_image');
+ $helperImageWithVersion = "$helperImage:$helperImageVersion";
- $commands = $this->getCommands();
+ $commands = [
+ 'docker container prune -f --filter "label=coolify.managed=true" --filter "label!=coolify.proxy=true"',
+ 'docker image prune -af --filter "label!=coolify.managed=true"',
+ 'docker builder prune -af',
+ "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
+ ];
+
+ $serverSettings = $server->settings;
+ if ($serverSettings->delete_unused_volumes) {
+ $commands[] = 'docker volume prune -af';
+ }
+
+ if ($serverSettings->delete_unused_networks) {
+ $commands[] = 'docker network prune -f';
+ }
foreach ($commands as $command) {
instant_remote_process([$command], $server, false);
}
}
-
- private function getCommands(): array
- {
- $settings = InstanceSettings::get();
- $helperImageVersion = data_get($settings, 'helper_version');
- $helperImage = config('coolify.helper_image');
- $helperImageWithVersion = config('coolify.helper_image').':'.$helperImageVersion;
-
- $commonCommands = [
- 'docker container prune -f --filter "label=coolify.managed=true"',
- 'docker image prune -af --filter "label!=coolify.managed=true"',
- 'docker builder prune -af',
- "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi",
- ];
-
- return $commonCommands;
- }
}
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index f671f2d2a..2e1df8185 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -17,7 +17,7 @@ public function handle(Server $server)
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.');
}
ray('Installing Docker on server: '.$server->name.' ('.$server->ip.')'.' with OS type: '.$supported_os_type);
- $dockerVersion = '24.0';
+ $dockerVersion = '26.0';
$config = base64_encode('{
"log-driver": "json-file",
"log-opts": {
diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php
index b79bc8f67..a1d36a041 100644
--- a/app/Actions/Server/StartSentinel.php
+++ b/app/Actions/Server/StartSentinel.php
@@ -2,6 +2,7 @@
namespace App\Actions\Server;
+use App\Models\InstanceSettings;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,18 +10,48 @@ class StartSentinel
{
use AsAction;
- public function handle(Server $server, $version = 'latest', bool $restart = false)
+ public function handle(Server $server, $version = 'next', bool $restart = false)
{
if ($restart) {
StopSentinel::run($server);
}
- $metrics_history = $server->settings->metrics_history_days;
- $refresh_rate = $server->settings->metrics_refresh_rate_seconds;
- $token = $server->settings->metrics_token;
+ $metrics_history = data_get($server, 'settings.sentinel_metrics_history_days');
+ $refresh_rate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
+ $push_interval = data_get($server, 'settings.sentinel_push_interval_seconds');
+ $token = data_get($server, 'settings.sentinel_token');
+ $endpoint = data_get($server, 'settings.sentinel_custom_url');
+ $mount_dir = '/data/coolify/sentinel';
+ $image = "ghcr.io/coollabsio/sentinel:$version";
+ if (! $endpoint) {
+ throw new \Exception('You should set FQDN in Instance Settings.');
+ }
+ $environments = [
+ 'TOKEN' => $token,
+ 'PUSH_ENDPOINT' => $endpoint,
+ 'PUSH_INTERVAL_SECONDS' => $push_interval,
+ 'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false',
+ 'COLLECTOR_REFRESH_RATE_SECONDS' => $refresh_rate,
+ 'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history,
+ ];
+ if (isDev()) {
+ data_set($environments, 'DEBUG', 'true');
+ $mount_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel';
+ $image = 'sentinel';
+ }
+ $docker_environments = '-e "' . implode('" -e "', array_map(fn($key, $value) => "$key=$value", array_keys($environments), $environments)) . '"';
+
+ $docker_command = "docker run -d $docker_environments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mount_dir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway $image";
+
instant_remote_process([
- "docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version",
- 'chown -R 9999:root /data/coolify/metrics /data/coolify/logs',
- 'chmod -R 700 /data/coolify/metrics /data/coolify/logs',
- ], $server, true);
+ 'docker rm -f coolify-sentinel || true',
+ "mkdir -p $mount_dir",
+ $docker_command,
+ "chown -R 9999:root $mount_dir",
+ "chmod -R 700 $mount_dir",
+ ], $server);
+
+ $server->settings->is_sentinel_enabled = true;
+ $server->settings->save();
+ $server->sentinelUpdateAt();
}
}
diff --git a/app/Actions/Server/StopSentinel.php b/app/Actions/Server/StopSentinel.php
index 21ffca3bd..68972f0f2 100644
--- a/app/Actions/Server/StopSentinel.php
+++ b/app/Actions/Server/StopSentinel.php
@@ -3,6 +3,7 @@
namespace App\Actions\Server;
use App\Models\Server;
+use Carbon\Carbon;
use Lorisleiva\Actions\Concerns\AsAction;
class StopSentinel
@@ -12,5 +13,6 @@ class StopSentinel
public function handle(Server $server)
{
instant_remote_process(['docker rm -f coolify-sentinel'], $server, false);
+ $server->sentinelUpdateAt(isReset: true);
}
}
diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php
index 901f2cf77..30664df26 100644
--- a/app/Actions/Server/UpdateCoolify.php
+++ b/app/Actions/Server/UpdateCoolify.php
@@ -3,7 +3,6 @@
namespace App\Actions\Server;
use App\Jobs\PullHelperImageJob;
-use App\Models\InstanceSettings;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -20,7 +19,7 @@ class UpdateCoolify
public function handle($manual_update = false)
{
try {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$this->server = Server::find(0);
if (! $this->server) {
return;
diff --git a/app/Console/Commands/CheckApplicationDeploymentQueue.php b/app/Console/Commands/CheckApplicationDeploymentQueue.php
new file mode 100644
index 000000000..e89d26f2c
--- /dev/null
+++ b/app/Console/Commands/CheckApplicationDeploymentQueue.php
@@ -0,0 +1,50 @@
+option('seconds');
+ $deployments = ApplicationDeploymentQueue::whereIn('status', [
+ ApplicationDeploymentStatus::IN_PROGRESS,
+ ApplicationDeploymentStatus::QUEUED,
+ ])->where('created_at', '<=', now()->subSeconds($seconds))->get();
+ if ($deployments->isEmpty()) {
+ $this->info('No deployments found in the last '.$seconds.' seconds.');
+
+ return;
+ }
+
+ $this->info('Found '.$deployments->count().' deployments created in the last '.$seconds.' seconds.');
+
+ foreach ($deployments as $deployment) {
+ if ($this->option('force')) {
+ $this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
+ $this->cancelDeployment($deployment);
+ } else {
+ $this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
+ if ($this->confirm('Do you want to cancel this deployment?', true)) {
+ $this->cancelDeployment($deployment);
+ }
+ }
+ }
+ }
+
+ private function cancelDeployment(ApplicationDeploymentQueue $deployment)
+ {
+ $deployment->update(['status' => ApplicationDeploymentStatus::FAILED]);
+ if ($deployment->server?->isFunctional()) {
+ remote_process(['docker rm -f '.$deployment->deployment_uuid], $deployment->server, false);
+ }
+ }
+}
diff --git a/app/Console/Commands/CleanupApplicationDeploymentQueue.php b/app/Console/Commands/CleanupApplicationDeploymentQueue.php
index f068e3eb2..3aae28ae6 100644
--- a/app/Console/Commands/CleanupApplicationDeploymentQueue.php
+++ b/app/Console/Commands/CleanupApplicationDeploymentQueue.php
@@ -7,9 +7,9 @@
class CleanupApplicationDeploymentQueue extends Command
{
- protected $signature = 'cleanup:application-deployment-queue {--team-id=}';
+ protected $signature = 'cleanup:deployment-queue {--team-id=}';
- protected $description = 'CleanupApplicationDeploymentQueue';
+ protected $description = 'Cleanup application deployment queue.';
public function handle()
{
diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php
index dfd09d4b7..66c25ec27 100644
--- a/app/Console/Commands/CleanupStuckedResources.php
+++ b/app/Console/Commands/CleanupStuckedResources.php
@@ -4,6 +4,7 @@
use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application;
+use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
@@ -47,6 +48,17 @@ private function cleanup_stucked_resources()
} catch (\Throwable $e) {
echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
}
+ try {
+ $applicationsDeploymentQueue = ApplicationDeploymentQueue::get();
+ foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) {
+ if (is_null($applicationDeploymentQueue->application)) {
+ echo "Deleting stuck application deployment queue: {$applicationDeploymentQueue->id}\n";
+ $applicationDeploymentQueue->delete();
+ }
+ }
+ } catch (\Throwable $e) {
+ echo "Error in cleaning stuck application deployment queue: {$e->getMessage()}\n";
+ }
try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) {
diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php
index 964b8e46e..20a2667c3 100644
--- a/app/Console/Commands/Dev.php
+++ b/app/Console/Commands/Dev.php
@@ -48,6 +48,13 @@ public function init()
echo "Generating APP_KEY.\n";
Artisan::call('key:generate');
}
+
+ // Generate STORAGE link if not exists
+ if (! file_exists(public_path('storage'))) {
+ echo "Generating STORAGE link.\n";
+ Artisan::call('storage:link');
+ }
+
// Seed database if it's empty
$settings = InstanceSettings::find(0);
if (! $settings) {
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index 2f5d36140..ad7bff86d 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -7,7 +7,6 @@
use App\Enums\ApplicationDeploymentStatus;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
-use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
@@ -69,7 +68,7 @@ public function handle()
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (! is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]);
@@ -196,7 +195,7 @@ private function send_alive_signal()
{
$id = config('app.id');
$version = config('version');
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
echo "Skipping alive as do_not_track is enabled\n";
diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php
index de64afefa..9720e81ac 100644
--- a/app/Console/Commands/ServicesGenerate.php
+++ b/app/Console/Commands/ServicesGenerate.php
@@ -39,8 +39,8 @@ public function handle()
$serviceTemplatesJson[$name] = $parsed;
}
}
- $serviceTemplatesJson = json_encode($serviceTemplatesJson);
- file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson);
+ $serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+ file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson.PHP_EOL);
}
private function process_file($file)
@@ -78,7 +78,7 @@ private function process_file($file)
if ($logo->count() > 0) {
$logo = str($logo[0])->after('# logo:')->trim()->value();
} else {
- $logo = 'svgs/unknown.svg';
+ $logo = 'svgs/coolify.png';
}
$minversion = collect(preg_grep('/^# minversion:/', explode("\n", $content)))->values();
if ($minversion->count() > 0) {
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 03d479400..a689b35b8 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -13,13 +13,13 @@
use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob;
use App\Jobs\UpdateCoolifyJob;
-use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
+use Illuminate\Support\Carbon;
class Kernel extends ConsoleKernel
{
@@ -28,7 +28,7 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
$this->all_servers = Server::all();
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
@@ -66,7 +66,7 @@ protected function schedule(Schedule $schedule): void
private function pull_images($schedule)
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
if ($server->isSentinelEnabled()) {
@@ -88,7 +88,7 @@ private function pull_images($schedule)
private function schedule_updates($schedule)
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$updateCheckFrequency = $settings->update_check_frequency;
$schedule->job(new CheckForUpdatesJob)
@@ -115,7 +115,11 @@ private function check_resources($schedule)
$servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
}
foreach ($servers as $server) {
- $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
+ $last_sentinel_update = $server->sentinel_updated_at;
+ if (Carbon::parse($last_sentinel_update)->isBefore(now()->subMinutes(4))) {
+ $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
+ }
+ // $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer();
$serverTimezone = $server->settings->server_timezone;
if ($server->settings->force_docker_cleanup) {
$schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
diff --git a/app/Enums/ContainerStatusTypes.php b/app/Enums/ContainerStatusTypes.php
new file mode 100644
index 000000000..ffcb6d5b5
--- /dev/null
+++ b/app/Enums/ContainerStatusTypes.php
@@ -0,0 +1,14 @@
+settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
if ($this->settings->do_not_track) {
return;
}
diff --git a/app/Helpers/SshMultiplexingHelper.php b/app/Helpers/SshMultiplexingHelper.php
index b0a832605..1a2146799 100644
--- a/app/Helpers/SshMultiplexingHelper.php
+++ b/app/Helpers/SshMultiplexingHelper.php
@@ -94,7 +94,9 @@ public static function generateScpCommand(Server $server, string $source, string
$muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp ";
-
+ if ($server->isIpv6()) {
+ $scp_command .= '-6 ';
+ }
if (self::isMultiplexingEnabled()) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
@@ -136,8 +138,8 @@ public static function generateSshCommand(Server $server, string $command)
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
- $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$delimiter = Hash::make($command);
+ $delimiter = base64_encode($delimiter);
$command = str_replace($delimiter, '', $command);
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index 48e126f27..2a1f846d3 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -132,6 +132,7 @@ public function applications(Request $request)
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
+ 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
@@ -177,6 +178,7 @@ public function applications(Request $request)
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -235,6 +237,7 @@ public function create_public_application(Request $request)
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
+ 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
@@ -279,6 +282,7 @@ public function create_public_application(Request $request)
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -337,6 +341,7 @@ public function create_private_gh_app_application(Request $request)
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
+ 'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
@@ -381,6 +386,7 @@ public function create_private_gh_app_application(Request $request)
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -468,6 +474,7 @@ public function create_private_deploy_key_application(Request $request)
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'],
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -552,6 +559,7 @@ public function create_dockerfile_application(Request $request)
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'],
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -602,6 +610,7 @@ public function create_dockerimage_application(Request $request)
'name' => ['type' => 'string', 'description' => 'The application name.'],
'description' => ['type' => 'string', 'description' => 'The application description.'],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -627,7 +636,7 @@ public function create_dockercompose_application(Request $request)
private function create_application(Request $request, $type)
{
- $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths'];
+ $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -665,6 +674,8 @@ private function create_application(Request $request, $type)
$fqdn = $request->domains;
$instantDeploy = $request->instant_deploy;
$githubAppUuid = $request->github_app_uuid;
+ $useBuildServer = $request->use_build_server;
+ $isStatic = $request->is_static;
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
@@ -693,8 +704,7 @@ private function create_application(Request $request, $type)
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
- $validator = customApiValidator($request->all(), [
- sharedDataApplications(),
+ $validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
@@ -702,19 +712,21 @@ private function create_application(Request $request, $type)
'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
- 'docker_compose_custom_start_command' => 'string|nullable',
- 'docker_compose_custom_build_command' => 'string|nullable',
- ]);
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
+
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
+
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
@@ -738,6 +750,14 @@ private function create_application(Request $request, $type)
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
$application->save();
+ if (isset($isStatic)) {
+ $application->settings->is_static = $isStatic;
+ $application->settings->save();
+ }
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
@@ -771,8 +791,7 @@ private function create_application(Request $request, $type)
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
- $validator = customApiValidator($request->all(), [
- sharedDataApplications(),
+ $validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
@@ -781,10 +800,10 @@ private function create_application(Request $request, $type)
'watch_paths' => 'string|nullable',
'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
- 'docker_compose_domains' => 'array|nullable',
- 'docker_compose_custom_start_command' => 'string|nullable',
- 'docker_compose_custom_build_command' => 'string|nullable',
- ]);
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+
+ $validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -833,6 +852,10 @@ private function create_application(Request $request, $type)
$application->environment_id = $environment->id;
$application->source_type = $githubApp->getMorphClass();
$application->source_id = $githubApp->id;
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
@@ -867,8 +890,8 @@ private function create_application(Request $request, $type)
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
- $validator = customApiValidator($request->all(), [
- sharedDataApplications(),
+
+ $validationRules = [
'git_repository' => 'string|required',
'git_branch' => 'string|required',
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
@@ -877,10 +900,10 @@ private function create_application(Request $request, $type)
'watch_paths' => 'string|nullable',
'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
- 'docker_compose_domains' => 'array|nullable',
- 'docker_compose_custom_start_command' => 'string|nullable',
- 'docker_compose_custom_build_command' => 'string|nullable',
- ]);
+ ];
+
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
return response()->json([
@@ -925,6 +948,10 @@ private function create_application(Request $request, $type)
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
$application->save();
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
@@ -956,10 +983,13 @@ private function create_application(Request $request, $type)
if (! $request->has('name')) {
$request->offsetSet('name', 'dockerfile-'.new Cuid2);
}
- $validator = customApiValidator($request->all(), [
- sharedDataApplications(),
+
+ $validationRules = [
'dockerfile' => 'string|required',
- ]);
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
+
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -1004,6 +1034,10 @@ private function create_application(Request $request, $type)
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
@@ -1034,12 +1068,14 @@ private function create_application(Request $request, $type)
if (! $request->has('name')) {
$request->offsetSet('name', 'docker-image-'.new Cuid2);
}
- $validator = customApiValidator($request->all(), [
- sharedDataApplications(),
+ $validationRules = [
'docker_registry_image_name' => 'string|required',
'docker_registry_image_tag' => 'string',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
- ]);
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
+
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -1062,6 +1098,10 @@ private function create_application(Request $request, $type)
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id;
+ if (isset($useBuildServer)) {
+ $application->settings->is_build_server_enabled = $useBuildServer;
+ $application->settings->save();
+ }
$application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main';
@@ -1108,10 +1148,12 @@ private function create_application(Request $request, $type)
if (! $request->has('name')) {
$request->offsetSet('name', 'service'.new Cuid2);
}
- $validator = customApiValidator($request->all(), [
- sharedDataApplications(),
+ $validationRules = [
'docker_compose_raw' => 'string|required',
- ]);
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
+
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
@@ -1259,16 +1301,10 @@ public function application_by_uuid(Request $request)
format: 'uuid',
)
),
- new OA\Parameter(
- name: 'cleanup',
- in: 'query',
- description: 'Delete configurations and volumes.',
- required: false,
- schema: new OA\Schema(
- type: 'boolean',
- default: true,
- )
- ),
+ new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
],
responses: [
new OA\Response(
@@ -1316,10 +1352,14 @@ public function delete_by_uuid(Request $request)
'message' => 'Application not found',
], 404);
}
+
DeleteResourceJob::dispatch(
resource: $application,
- deleteConfigurations: $cleanup,
- deleteVolumes: $cleanup);
+ deleteConfigurations: $request->query->get('delete_configurations', true),
+ deleteVolumes: $request->query->get('delete_volumes', true),
+ dockerCleanup: $request->query->get('docker_cleanup', true),
+ deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
+ );
return response()->json([
'message' => 'Application deletion request queued.',
@@ -1404,6 +1444,7 @@ public function delete_by_uuid(Request $request)
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
+ 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
)),
]),
@@ -1460,10 +1501,9 @@ public function update_by_uuid(Request $request)
], 404);
}
$server = $application->destination->server;
- $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy'];
+ $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server'];
- $validator = customApiValidator($request->all(), [
- sharedDataApplications(),
+ $validationRules = [
'name' => 'string|max:255',
'description' => 'string|nullable',
'static_image' => 'string',
@@ -1473,7 +1513,9 @@ public function update_by_uuid(Request $request)
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
- ]);
+ ];
+ $validationRules = array_merge($validationRules, sharedDataApplications());
+ $validator = customApiValidator($request->all(), $validationRules);
// Validate ports_exposes
if ($request->has('ports_exposes')) {
@@ -1538,6 +1580,13 @@ public function update_by_uuid(Request $request)
}
$instantDeploy = $request->instant_deploy;
+ $use_build_server = $request->use_build_server;
+
+ if (isset($use_build_server)) {
+ $application->settings->is_build_server_enabled = $use_build_server;
+ $application->settings->save();
+ }
+
removeUnnecessaryFieldsFromRequest($request);
$data = $request->all();
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index a205704cc..65873f818 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -1541,16 +1541,10 @@ public function create_database(Request $request, NewDatabaseTypes $type)
format: 'uuid',
)
),
- new OA\Parameter(
- name: 'cleanup',
- in: 'query',
- description: 'Delete configurations and volumes.',
- required: false,
- schema: new OA\Schema(
- type: 'boolean',
- default: true,
- )
- ),
+ new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
],
responses: [
new OA\Response(
@@ -1595,10 +1589,14 @@ public function delete_by_uuid(Request $request)
if (! $database) {
return response()->json(['message' => 'Database not found.'], 404);
}
+
DeleteResourceJob::dispatch(
resource: $database,
- deleteConfigurations: $cleanup,
- deleteVolumes: $cleanup);
+ deleteConfigurations: $request->query->get('delete_configurations', true),
+ deleteVolumes: $request->query->get('delete_volumes', true),
+ dockerCleanup: $request->query->get('docker_cleanup', true),
+ deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
+ );
return response()->json([
'message' => 'Database deletion request queued.',
diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php
index c085b88a5..2414b7a42 100644
--- a/app/Http/Controllers/Api/OtherController.php
+++ b/app/Http/Controllers/Api/OtherController.php
@@ -86,7 +86,7 @@ public function enable_api(Request $request)
if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to enable the API.'], 403);
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$settings->update(['is_api_enabled' => true]);
return response()->json(['message' => 'API enabled.'], 200);
@@ -138,7 +138,7 @@ public function disable_api(Request $request)
if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to disable the API.'], 403);
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$settings->update(['is_api_enabled' => false]);
return response()->json(['message' => 'API disabled.'], 200);
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index 5f0d6bb12..6d512e578 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -23,7 +23,7 @@ private function removeSensitiveDataFromSettings($settings)
return serializeApiResponse($settings);
}
$settings = $settings->makeHidden([
- 'metrics_token',
+ 'sentinel_token',
]);
return serializeApiResponse($settings);
@@ -308,7 +308,7 @@ public function domains_by_server(Request $request)
$projects = Project::where('team_id', $teamId)->get();
$domains = collect();
$applications = $projects->pluck('applications')->flatten();
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($applications->count() > 0) {
foreach ($applications as $application) {
$ip = $application->destination->server->ip;
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 0a6154410..89418517b 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -432,6 +432,10 @@ public function service_by_uuid(Request $request)
tags: ['Services'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')),
+ new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
+ new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
],
responses: [
new OA\Response(
@@ -476,7 +480,14 @@ public function delete_by_uuid(Request $request)
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
- DeleteResourceJob::dispatch($service);
+
+ DeleteResourceJob::dispatch(
+ resource: $service,
+ deleteConfigurations: $request->query->get('delete_configurations', true),
+ deleteVolumes: $request->query->get('delete_volumes', true),
+ dockerCleanup: $request->query->get('docker_cleanup', true),
+ deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
+ );
return response()->json([
'message' => 'Service deletion request queued.',
@@ -516,7 +527,8 @@ public function delete_by_uuid(Request $request)
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -619,7 +631,8 @@ public function envs(Request $request)
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -738,7 +751,8 @@ public function update_env_by_uuid(Request $request)
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -853,7 +867,8 @@ public function create_bulk_envs(Request $request)
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -953,7 +968,8 @@ public function create_env(Request $request)
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1025,9 +1041,11 @@ public function delete_env_by_uuid(Request $request)
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service starting request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1101,9 +1119,11 @@ public function action_deploy(Request $request)
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1177,9 +1197,11 @@ public function action_stop(Request $request)
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php
index 9569e8cfa..630d01045 100644
--- a/app/Http/Controllers/OauthController.php
+++ b/app/Http/Controllers/OauthController.php
@@ -2,7 +2,6 @@
namespace App\Http\Controllers;
-use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -22,7 +21,7 @@ public function callback(string $provider)
$oauthUser = get_socialite_provider($provider)->user();
$user = User::whereEmail($oauthUser->email)->first();
if (! $user) {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->is_registration_enabled) {
abort(403, 'Registration is disabled');
}
diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php
index 648720ba4..471e6d602 100644
--- a/app/Http/Middleware/ApiAllowed.php
+++ b/app/Http/Middleware/ApiAllowed.php
@@ -14,7 +14,7 @@ public function handle(Request $request, Closure $next): Response
if (isCloud()) {
return $next($request);
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->is_api_enabled === false) {
return response()->json(['success' => true, 'message' => 'API is disabled.'], 403);
}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 24565b389..9ae383a9f 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -12,7 +12,6 @@
use App\Models\EnvironmentVariable;
use App\Models\GithubApp;
use App\Models\GitlabApp;
-use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
@@ -962,7 +961,7 @@ private function save_environment_variables()
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
- if ($this->application->compose_parsing_version === '3') {
+ if ((int) $this->application->compose_parsing_version >= 3) {
$envs->push("COOLIFY_URL={$this->application->fqdn}");
} else {
$envs->push("COOLIFY_FQDN={$this->application->fqdn}");
@@ -970,7 +969,7 @@ private function save_environment_variables()
}
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
- if ($this->application->compose_parsing_version === '3') {
+ if ((int) $this->application->compose_parsing_version >= 3) {
$envs->push("COOLIFY_FQDN={$url}");
} else {
$envs->push("COOLIFY_URL={$url}");
@@ -1334,7 +1333,7 @@ private function create_workdir()
private function prepare_builder_image()
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$helperImage = config('coolify.helper_image');
$helperImage = "{$helperImage}:{$settings->helper_version}";
// Get user home directory
diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php
index 747a9a98a..f2348118a 100644
--- a/app/Jobs/CheckForUpdatesJob.php
+++ b/app/Jobs/CheckForUpdatesJob.php
@@ -2,7 +2,6 @@
namespace App\Jobs;
-use App\Models\InstanceSettings;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -22,7 +21,7 @@ public function handle(): void
if (isDev() || isCloud()) {
return;
}
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 947dc4317..769739d5e 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -2,9 +2,7 @@
namespace App\Jobs;
-use App\Actions\Database\StopDatabase;
use App\Events\BackupCreated;
-use App\Models\InstanceSettings;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
@@ -25,7 +23,6 @@
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
-use Visus\Cuid2\Cuid2;
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -64,32 +61,32 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct($backup)
{
$this->backup = $backup;
- $this->team = Team::find($backup->team_id);
- if (is_null($this->team)) {
- return;
- }
- if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
- $this->database = data_get($this->backup, 'database');
- $this->server = $this->database->service->server;
- $this->s3 = $this->backup->s3;
- } else {
- $this->database = data_get($this->backup, 'database');
- $this->server = $this->database->destination->server;
- $this->s3 = $this->backup->s3;
- }
}
public function handle(): void
{
try {
- // Check if team is exists
- if (is_null($this->team)) {
- $this->backup->update(['status' => 'failed']);
- StopDatabase::run($this->database);
- $this->database->delete();
+ $this->team = Team::find($this->backup->team_id);
+ if (! $this->team) {
+ $this->backup->delete();
return;
}
+ if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
+ $this->database = data_get($this->backup, 'database');
+ $this->server = $this->database->service->server;
+ $this->s3 = $this->backup->s3;
+ } else {
+ $this->database = data_get($this->backup, 'database');
+ $this->server = $this->database->destination->server;
+ $this->s3 = $this->backup->s3;
+ }
+ if (is_null($this->server)) {
+ throw new \Exception('Server not found?!');
+ }
+ if (is_null($this->database)) {
+ throw new \Exception('Database not found?!');
+ }
BackupCreated::dispatch($this->team->id);
@@ -239,7 +236,6 @@ public function handle(): void
}
}
$this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
-
if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify'];
$this->directory_name = $this->container_name = 'coolify-db';
@@ -252,6 +248,9 @@ public function handle(): void
try {
if (str($databaseType)->contains('postgres')) {
$this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp';
+ if ($this->backup->dump_all) {
+ $this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.gz';
+ }
$this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
@@ -280,6 +279,9 @@ public function handle(): void
$this->backup_standalone_mongodb($database);
} elseif (str($databaseType)->contains('mysql')) {
$this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp';
+ if ($this->backup->dump_all) {
+ $this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.gz';
+ }
$this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
@@ -289,6 +291,9 @@ public function handle(): void
$this->backup_standalone_mysql($database);
} elseif (str($databaseType)->contains('mariadb')) {
$this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp';
+ if ($this->backup->dump_all) {
+ $this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.gz';
+ }
$this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
@@ -327,7 +332,9 @@ public function handle(): void
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e;
} finally {
- BackupCreated::dispatch($this->team->id);
+ if ($this->team) {
+ BackupCreated::dispatch($this->team->id);
+ }
}
}
@@ -386,7 +393,11 @@ private function backup_standalone_postgresql(string $database): void
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";
+ if ($this->backup->dump_all) {
+ $backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
+ } else {
+ $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);
@@ -407,8 +418,11 @@ private function backup_standalone_mysql(string $database): void
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
- $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
- ray($commands);
+ if ($this->backup->dump_all) {
+ $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
+ } else {
+ $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
+ }
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
@@ -426,7 +440,11 @@ private function backup_standalone_mariadb(string $database): void
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
- $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
+ if ($this->backup->dump_all) {
+ $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
+ } else {
+ $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
+ }
ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
@@ -468,34 +486,6 @@ 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 {
@@ -517,10 +507,27 @@ private function upload_to_s3(): void
$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";
+
+ if (isDev()) {
+ if ($this->database->name === 'coolify-db') {
+ $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
+ $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
+ } else {
+ $backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file;
+ $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
+ }
+ } else {
+ $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
+ }
+ if ($this->s3->isHetzner()) {
+ $endpointWithoutBucket = 'https://'.str($endpoint)->after('https://')->after('.')->value();
+ $commands[] = "docker exec backup-of-{$this->backup->uuid} mc alias set --path=off --api=S3v4 temporary {$endpointWithoutBucket} $key $secret";
+ } else {
+ $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
+ }
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server);
+
$this->add_to_backup_output('Uploaded to S3.');
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
@@ -562,7 +569,7 @@ private function pullHelperImage(string $fullImageName): void
private function getFullImageName(): string
{
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$helperImage = config('coolify.helper_image');
$latestVersion = $settings->helper_version;
diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php
index ac34d064e..2442d5b06 100644
--- a/app/Jobs/DeleteResourceJob.php
+++ b/app/Jobs/DeleteResourceJob.php
@@ -31,10 +31,10 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
- public bool $deleteConfigurations,
- public bool $deleteVolumes,
- public bool $dockerCleanup,
- public bool $deleteConnectedNetworks
+ public bool $deleteConfigurations = true,
+ public bool $deleteVolumes = true,
+ public bool $dockerCleanup = true,
+ public bool $deleteConnectedNetworks = true
) {}
public function handle()
diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php
index a961fae4c..900bae99c 100644
--- a/app/Jobs/DockerCleanupJob.php
+++ b/app/Jobs/DockerCleanupJob.php
@@ -23,7 +23,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $usageBefore = null;
- public function __construct(public Server $server) {}
+ public function __construct(public Server $server, public bool $manualCleanup = false) {}
public function handle(): void
{
@@ -31,8 +31,9 @@ public function handle(): void
if (! $this->server->isFunctional()) {
return;
}
- if ($this->server->settings->force_docker_cleanup) {
- Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
+
+ if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
+ Log::info('DockerCleanupJob '.($this->manualCleanup ? 'manual' : 'force').' cleanup on '.$this->server->name);
CleanupDocker::run(server: $this->server);
return;
diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php
index ef1659680..4b208fc31 100644
--- a/app/Jobs/PullHelperImageJob.php
+++ b/app/Jobs/PullHelperImageJob.php
@@ -2,7 +2,6 @@
namespace App\Jobs;
-use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -26,7 +25,7 @@ public function handle(): void
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$latest_version = data_get($versions, 'coolify.helper.version');
$current_version = $settings->helper_version;
if (version_compare($latest_version, $current_version, '>')) {
diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php
new file mode 100644
index 000000000..82e311d47
--- /dev/null
+++ b/app/Jobs/PushServerUpdateJob.php
@@ -0,0 +1,404 @@
+containers = collect();
+ $this->foundApplicationIds = collect();
+ $this->foundDatabaseUuids = collect();
+ $this->foundServiceApplicationIds = collect();
+ $this->foundApplicationPreviewsIds = collect();
+ $this->foundServiceDatabaseIds = collect();
+ $this->allApplicationIds = collect();
+ $this->allDatabaseUuids = collect();
+ $this->allTcpProxyUuids = collect();
+ $this->allServiceApplicationIds = collect();
+ $this->allServiceDatabaseIds = collect();
+ }
+
+ public function handle()
+ {
+ try {
+ if (! $this->data) {
+ throw new \Exception('No data provided');
+ }
+ $data = collect($this->data);
+
+ $this->serverStatus();
+
+ $this->server->sentinelUpdateAt();
+
+ $this->containers = collect(data_get($data, 'containers'));
+ if ($this->containers->isEmpty()) {
+ return;
+ }
+ $this->applications = $this->server->applications();
+ $this->databases = $this->server->databases();
+ $this->previews = $this->server->previews();
+ $this->services = $this->server->services()->get();
+ $this->allApplicationIds = $this->applications->filter(function ($application) {
+ return $application->additional_servers->count() === 0;
+ })->pluck('id');
+ $this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) {
+ return $application->additional_servers->count() > 0;
+ });
+ $this->allApplicationPreviewsIds = $this->previews->pluck('id');
+ $this->allDatabaseUuids = $this->databases->pluck('uuid');
+ $this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid');
+ $this->services->each(function ($service) {
+ $service->applications()->pluck('id')->each(function ($applicationId) {
+ $this->allServiceApplicationIds->push($applicationId);
+ });
+ $service->databases()->pluck('id')->each(function ($databaseId) {
+ $this->allServiceDatabaseIds->push($databaseId);
+ });
+ });
+
+ ray('allServiceApplicationIds', ['allServiceApplicationIds' => $this->allServiceApplicationIds]);
+
+ foreach ($this->containers as $container) {
+ $containerStatus = data_get($container, 'state', 'exited');
+ $containerHealth = data_get($container, 'health_status', 'unhealthy');
+ $containerStatus = "$containerStatus ($containerHealth)";
+ $labels = collect(data_get($container, 'labels'));
+ $coolify_managed = $labels->has('coolify.managed');
+ if ($coolify_managed) {
+ $name = data_get($container, 'name');
+ if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) {
+ $this->foundLogDrainContainer = true;
+ }
+ if ($labels->has('coolify.applicationId')) {
+ $applicationId = $labels->get('coolify.applicationId');
+ $pullRequestId = data_get($labels, 'coolify.pullRequestId', '0');
+ try {
+ if ($pullRequestId === '0') {
+ if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) {
+ $this->foundApplicationIds->push($applicationId);
+ }
+ $this->updateApplicationStatus($applicationId, $containerStatus);
+ } else {
+ if ($this->allApplicationPreviewsIds->contains($applicationId) && $this->isRunning($containerStatus)) {
+ $this->foundApplicationPreviewsIds->push($applicationId);
+ }
+ $this->updateApplicationPreviewStatus($applicationId, $containerStatus);
+ }
+ } catch (\Exception $e) {
+ ray()->error($e);
+ }
+ } elseif ($labels->has('coolify.serviceId')) {
+ $serviceId = $labels->get('coolify.serviceId');
+ $subType = $labels->get('coolify.service.subType');
+ $subId = $labels->get('coolify.service.subId');
+ if ($subType === 'application' && $this->isRunning($containerStatus)) {
+ $this->foundServiceApplicationIds->push($subId);
+ $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
+ } elseif ($subType === 'database' && $this->isRunning($containerStatus)) {
+ $this->foundServiceDatabaseIds->push($subId);
+ $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
+ }
+
+ } else {
+ $uuid = $labels->get('com.docker.compose.service');
+ $type = $labels->get('coolify.type');
+ if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) {
+ $this->foundProxy = true;
+ } elseif ($type === 'service' && $this->isRunning($containerStatus)) {
+ ray("Service: $uuid, $containerStatus");
+ } else {
+ if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) {
+ $this->foundDatabaseUuids->push($uuid);
+ if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
+ $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
+ } else {
+ $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ $this->updateProxyStatus();
+
+ $this->updateNotFoundApplicationStatus();
+ $this->updateNotFoundApplicationPreviewStatus();
+ $this->updateNotFoundDatabaseStatus();
+ $this->updateNotFoundServiceStatus();
+
+ $this->updateAdditionalServersStatus();
+
+ $this->checkLogDrainContainer();
+
+ } catch (\Exception $e) {
+ throw $e;
+ }
+
+ }
+
+ private function serverStatus(){
+ if ($this->server->isFunctional() === false) {
+ throw new \Exception('Server is not ready.');
+ }
+ if ($this->server->status() === false) {
+ throw new \Exception('Server is not reachable.');
+ }
+ }
+ private function updateApplicationStatus(string $applicationId, string $containerStatus)
+ {
+ $application = $this->applications->where('id', $applicationId)->first();
+ if (! $application) {
+ return;
+ }
+ $application->status = $containerStatus;
+ $application->save();
+ ray('Application updated', ['application_id' => $applicationId, 'status' => $containerStatus]);
+ }
+
+ private function updateApplicationPreviewStatus(string $applicationId, string $containerStatus)
+ {
+ $application = $this->previews->where('id', $applicationId)->first();
+ if (! $application) {
+ return;
+ }
+ $application->status = $containerStatus;
+ $application->save();
+ ray('Application preview updated', ['application_id' => $applicationId, 'status' => $containerStatus]);
+ }
+
+ private function updateNotFoundApplicationStatus()
+ {
+ $notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds);
+ if ($notFoundApplicationIds->isNotEmpty()) {
+ ray('Not found application ids', ['application_ids' => $notFoundApplicationIds]);
+ $notFoundApplicationIds->each(function ($applicationId) {
+ ray('Updating application status', ['application_id' => $applicationId, 'status' => 'exited']);
+ $application = Application::find($applicationId);
+ if ($application) {
+ $application->status = 'exited';
+ $application->save();
+ ray('Application status updated', ['application_id' => $applicationId, 'status' => 'exited']);
+ }
+ });
+ }
+ }
+
+ private function updateNotFoundApplicationPreviewStatus()
+ {
+ $notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds);
+ if ($notFoundApplicationPreviewsIds->isNotEmpty()) {
+ ray('Not found application previews ids', ['application_previews_ids' => $notFoundApplicationPreviewsIds]);
+ $notFoundApplicationPreviewsIds->each(function ($applicationPreviewId) {
+ ray('Updating application preview status', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']);
+ $applicationPreview = ApplicationPreview::find($applicationPreviewId);
+ if ($applicationPreview) {
+ $applicationPreview->status = 'exited';
+ $applicationPreview->save();
+ ray('Application preview status updated', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']);
+ }
+ });
+ }
+ }
+
+ private function updateProxyStatus()
+ {
+ // If proxy is not found, start it
+ if ($this->server->isProxyShouldRun()) {
+ if ($this->foundProxy === false) {
+ try {
+ if (CheckProxy::run($this->server)) {
+ StartProxy::run($this->server, false);
+ }
+ } catch (\Throwable $e) {
+ logger()->error($e);
+ }
+ } else {
+ $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
+ instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
+ }
+ }
+
+ }
+
+ private function updateDatabaseStatus(string $databaseUuid, string $containerStatus, bool $tcpProxy = false)
+ {
+ $database = $this->databases->where('uuid', $databaseUuid)->first();
+ if (! $database) {
+ return;
+ }
+ $database->status = $containerStatus;
+ $database->save();
+ ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => $containerStatus]);
+ if ($this->isRunning($containerStatus) && $tcpProxy) {
+ $tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) {
+ return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running';
+ })->first();
+ if (! $tcpProxyContainerFound) {
+ ray('Starting TCP proxy for database', ['database_uuid' => $databaseUuid]);
+ StartDatabaseProxy::dispatch($database);
+ } else {
+ ray('TCP proxy for database found in containers', ['database_uuid' => $databaseUuid]);
+ }
+ }
+ }
+
+ private function updateNotFoundDatabaseStatus()
+ {
+ $notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids);
+ if ($notFoundDatabaseUuids->isNotEmpty()) {
+ ray('Not found database uuids', ['database_uuids' => $notFoundDatabaseUuids]);
+ $notFoundDatabaseUuids->each(function ($databaseUuid) {
+ ray('Updating database status', ['database_uuid' => $databaseUuid, 'status' => 'exited']);
+ $database = $this->databases->where('uuid', $databaseUuid)->first();
+ if ($database) {
+ $database->status = 'exited';
+ $database->save();
+ ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => 'exited']);
+ ray('Database is public', ['database_uuid' => $databaseUuid, 'is_public' => $database->is_public]);
+ if ($database->is_public) {
+ ray('Stopping TCP proxy for database', ['database_uuid' => $databaseUuid]);
+ StopDatabaseProxy::dispatch($database);
+ }
+ }
+ });
+ }
+ }
+
+ private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus)
+ {
+ $service = $this->services->where('id', $serviceId)->first();
+ if (! $service) {
+ return;
+ }
+ if ($subType === 'application') {
+ $application = $service->applications()->where('id', $subId)->first();
+ $application->status = $containerStatus;
+ $application->save();
+ ray('Service application updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
+ } elseif ($subType === 'database') {
+ $database = $service->databases()->where('id', $subId)->first();
+ $database->status = $containerStatus;
+ $database->save();
+ ray('Service database updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
+ } else {
+ ray()->warning('Unknown sub type', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
+ }
+ }
+
+ private function updateNotFoundServiceStatus()
+ {
+ $notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds);
+ $notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds);
+ if ($notFoundServiceApplicationIds->isNotEmpty()) {
+ ray('Not found service application ids', ['service_application_ids' => $notFoundServiceApplicationIds]);
+ $notFoundServiceApplicationIds->each(function ($serviceApplicationId) {
+ ray('Updating service application status', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']);
+ $application = ServiceApplication::find($serviceApplicationId);
+ if ($application) {
+ $application->status = 'exited';
+ $application->save();
+ ray('Service application status updated', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']);
+ }
+ });
+ }
+ if ($notFoundServiceDatabaseIds->isNotEmpty()) {
+ ray('Not found service database ids', ['service_database_ids' => $notFoundServiceDatabaseIds]);
+ $notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) {
+ ray('Updating service database status', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']);
+ $database = ServiceDatabase::find($serviceDatabaseId);
+ if ($database) {
+ $database->status = 'exited';
+ $database->save();
+ ray('Service database status updated', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']);
+ }
+ });
+ }
+ }
+
+ private function updateAdditionalServersStatus()
+ {
+ $this->allApplicationsWithAdditionalServers->each(function ($application) {
+ ray('Updating additional servers status for application', ['application_id' => $application->id]);
+ ComplexStatusCheck::run($application);
+ });
+ }
+
+ private function isRunning(string $containerStatus)
+ {
+ return str($containerStatus)->contains('running');
+ }
+
+ private function checkLogDrainContainer(){
+ if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
+ InstallLogDrain::dispatch($this->server);
+ }
+ }
+}
diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php
index 7fde44f49..39d4aa0c0 100644
--- a/app/Jobs/ServerCheckJob.php
+++ b/app/Jobs/ServerCheckJob.php
@@ -69,7 +69,9 @@ public function handle()
return 'No containers found.';
}
GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
- $this->checkLogDrainContainer();
+ if ($this->server->isLogDrainEnabled()) {
+ $this->checkLogDrainContainer();
+ }
}
} catch (\Throwable $e) {
@@ -115,9 +117,6 @@ private function serverStatus()
private function checkLogDrainContainer()
{
- if (! $this->server->isLogDrainEnabled()) {
- return;
- }
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain';
})->first();
diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php
index b2c816f5d..1f09d5a3b 100644
--- a/app/Jobs/ServerLimitCheckJob.php
+++ b/app/Jobs/ServerLimitCheckJob.php
@@ -10,7 +10,6 @@
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
-use Illuminate\Queue\Middleware\;
use Illuminate\Queue\SerializesModels;
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php
new file mode 100644
index 000000000..376cb8532
--- /dev/null
+++ b/app/Jobs/ServerStorageCheckJob.php
@@ -0,0 +1,59 @@
+server->isFunctional()) {
+ ray('Server is not ready.');
+
+ return 'Server is not ready.';
+ }
+ $team = $this->server->team;
+ $percentage = $this->server->storageCheck();
+ if ($percentage > 1) {
+ ray('Server storage is at '.$percentage.'%');
+ }
+
+ } catch (\Throwable $e) {
+ ray($e->getMessage());
+
+ return handleError($e);
+ }
+
+ }
+}
diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php
index 4c65a711f..2cc705e4a 100644
--- a/app/Jobs/UpdateCoolifyJob.php
+++ b/app/Jobs/UpdateCoolifyJob.php
@@ -3,7 +3,6 @@
namespace App\Jobs;
use App\Actions\Server\UpdateCoolify;
-use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -23,7 +22,7 @@ public function handle(): void
{
try {
CheckForUpdatesJob::dispatchSync();
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->new_version_available) {
Log::info('No new version available. Skipping update.');
diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php
index 1f0b68dd3..d18a7689e 100644
--- a/app/Livewire/Dashboard.php
+++ b/app/Livewire/Dashboard.php
@@ -30,7 +30,7 @@ public function mount()
public function cleanup_queue()
{
- Artisan::queue('cleanup:application-deployment-queue', [
+ Artisan::queue('cleanup:deployment-queue', [
'--team-id' => currentTeam()->id,
]);
}
diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php
index d6dc0d521..934e81661 100644
--- a/app/Livewire/Help.php
+++ b/app/Livewire/Help.php
@@ -47,7 +47,7 @@ public function submit()
]
);
$mail->subject("[HELP]: {$this->subject}");
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$type = set_transanctional_email_settings($settings);
if (! $type) {
$url = 'https://app.coolify.io/api/feedback';
@@ -61,6 +61,7 @@ public function submit()
send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io');
}
$this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.');
+ $this->reset('description', 'subject');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index 2960ed226..53673292e 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -172,7 +172,7 @@ public function submitResend()
public function copyFromInstanceSettings()
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->smtp_enabled) {
$team = currentTeam();
$team->update([
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index d2700f444..2e327d80f 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -2,9 +2,11 @@
namespace App\Livewire\Project\Application;
+use App\Actions\Application\GenerateConfig;
use App\Models\Application;
use Illuminate\Support\Collection;
use Livewire\Component;
+use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
class General extends Component
@@ -182,9 +184,7 @@ public function instantSave()
$storage->save();
});
}
-
}
-
}
public function loadComposeFile($isInit = false)
@@ -241,16 +241,6 @@ public function updatedApplicationBaseDirectory()
}
}
- public function updatedApplicationFqdn()
- {
- $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
- $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
- $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
- return str($domain)->trim()->lower();
- });
- $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
- $this->resetDefaultLabels();
- }
public function updatedApplicationBuildPack()
{
@@ -287,18 +277,22 @@ public function getWildcardDomain()
public function resetDefaultLabels()
{
- if ($this->application->settings->is_container_label_readonly_enabled) {
- return;
+ try {
+ if ($this->application->settings->is_container_label_readonly_enabled) {
+ return;
+ }
+ $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
+ $this->ports_exposes = $this->application->ports_exposes;
+ $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
+ $this->application->custom_labels = base64_encode($this->customLabels);
+ $this->application->save();
+ if ($this->application->build_pack === 'dockercompose') {
+ $this->loadComposeFile();
+ }
+ $this->dispatch('configurationChanged');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
- $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
- $this->ports_exposes = $this->application->ports_exposes;
- $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
- $this->application->custom_labels = base64_encode($this->customLabels);
- $this->application->save();
- if ($this->application->build_pack === 'dockercompose') {
- $this->loadComposeFile();
- }
- $this->dispatch('configurationChanged');
}
public function checkFqdns($showToaster = true)
@@ -320,7 +314,7 @@ public function checkFqdns($showToaster = true)
public function set_redirect()
{
try {
- $has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
+ $has_www = collect($this->application->fqdns)->filter(fn($fqdn) => str($fqdn)->contains('www.'))->count();
if ($has_www === 0 && $this->application->redirect === 'www') {
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.
Please add www to your domain list and as an A DNS record (if applicable).');
@@ -337,15 +331,18 @@ public function set_redirect()
public function submit($showToaster = true)
{
try {
- if ($this->application->isDirty('redirect')) {
- $this->set_redirect();
- }
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
+ Url::fromString($domain, ['http', 'https']);
return str($domain)->trim()->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
+ $this->resetDefaultLabels();
+
+ if ($this->application->isDirty('redirect')) {
+ $this->set_redirect();
+ }
$this->checkFqdns();
@@ -408,9 +405,25 @@ public function submit($showToaster = true)
$this->application->save();
$showToaster && $this->dispatch('success', 'Application settings updated!');
} catch (\Throwable $e) {
+ $originalFqdn = $this->application->getOriginal('fqdn');
+ if ($originalFqdn !== $this->application->fqdn) {
+ $this->application->fqdn = $originalFqdn;
+ }
return handleError($e, $this);
} finally {
$this->dispatch('configurationChanged');
}
}
+ public function downloadConfig()
+ {
+ $config = GenerateConfig::run($this->application, true);
+ $fileName = str($this->application->name)->slug()->append('_config.json');
+
+ return response()->streamDownload(function () use ($config) {
+ echo $config;
+ }, $fileName, [
+ 'Content-Type' => 'application/json',
+ 'Content-Disposition' => 'attachment; filename=' . $fileName,
+ ]);
+ }
}
diff --git a/app/Livewire/Project/Application/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php
index e4f100fcf..9a0b9b851 100644
--- a/app/Livewire/Project/Application/Preview/Form.php
+++ b/app/Livewire/Project/Application/Preview/Form.php
@@ -31,10 +31,14 @@ public function resetToDefault()
public function generate_real_url()
{
if (data_get($this->application, 'fqdn')) {
- $firstFqdn = str($this->application->fqdn)->before(',');
- $url = Url::fromString($firstFqdn);
- $host = $url->getHost();
- $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host);
+ try {
+ $firstFqdn = str($this->application->fqdn)->before(',');
+ $url = Url::fromString($firstFqdn);
+ $host = $url->getHost();
+ $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host);
+ } catch (\Exception $e) {
+ $this->dispatch('error', 'Invalid FQDN.');
+ }
}
}
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index ec87beead..7e2e4a12b 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -31,6 +31,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'required|boolean',
'backup.s3_storage_id' => 'nullable|integer',
'backup.databases_to_backup' => 'nullable',
+ 'backup.dump_all' => 'required|boolean',
];
protected $validationAttributes = [
@@ -40,6 +41,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'Save to S3',
'backup.s3_storage_id' => 'S3 Storage',
'backup.databases_to_backup' => 'Databases to Backup',
+ 'backup.dump_all' => 'Backup All Databases',
];
protected $messages = [
@@ -182,7 +184,7 @@ public function render()
{
return view('livewire.project.database.backup-edit', [
'checkboxes' => [
- ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
+ ['id' => 'delete_associated_backups_locally', 'label' => __('database.delete_backups_locally')],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
],
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index 765213f60..49884ff9a 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -78,7 +78,7 @@ public function render()
{
return view('livewire.project.database.heading', [
'checkboxes' => [
- ['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
+ ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
]);
}
diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php
index beb5a9c39..8021e25d3 100644
--- a/app/Livewire/Project/Database/ScheduledBackups.php
+++ b/app/Livewire/Project/Database/ScheduledBackups.php
@@ -26,7 +26,7 @@ class ScheduledBackups extends Component
public function mount(): void
{
if ($this->selectedBackupId) {
- $this->setSelectedBackup($this->selectedBackupId);
+ $this->setSelectedBackup($this->selectedBackupId, true);
}
$this->parameters = get_route_parameters();
if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
@@ -37,10 +37,13 @@ public function mount(): void
$this->s3s = currentTeam()->s3s;
}
- public function setSelectedBackup($backupId)
+ public function setSelectedBackup($backupId, $force = false)
{
+ if ($this->selectedBackupId === $backupId && ! $force) {
+ return;
+ }
$this->selectedBackupId = $backupId;
- $this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId);
+ $this->selectedBackup = $this->database->scheduledBackups->find($backupId);
if (is_null($this->selectedBackup)) {
$this->selectedBackupId = null;
}
diff --git a/app/Livewire/Project/Index.php b/app/Livewire/Project/Index.php
index 0e4f15a5c..f8eb838be 100644
--- a/app/Livewire/Project/Index.php
+++ b/app/Livewire/Project/Index.php
@@ -18,7 +18,11 @@ class Index extends Component
public function mount()
{
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
- $this->projects = Project::ownedByCurrentTeam()->get();
+ $this->projects = Project::ownedByCurrentTeam()->get()->map(function ($project) {
+ $project->settingsRoute = route('project.edit', ['project_uuid' => $project->uuid]);
+
+ return $project;
+ });
$this->servers = Server::ownedByCurrentTeam()->count();
}
diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php
index b5c5cb1db..971d4700b 100644
--- a/app/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Livewire/Project/New/PublicGitRepository.php
@@ -31,10 +31,12 @@ class PublicGitRepository extends Component
public bool $isStatic = false;
+ public bool $checkCoolifyConfig = true;
+
public ?string $publish_directory = null;
// In case of docker compose
- public ?string $base_directory = null;
+ public string $base_directory = '/';
public ?string $docker_compose_location = '/docker-compose.yaml';
// End of docker compose
@@ -97,6 +99,7 @@ public function updatedBaseDirectory()
$this->base_directory = '/'.$this->base_directory;
}
}
+
}
public function updatedDockerComposeLocation()
@@ -275,6 +278,7 @@ public function submit()
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'build_pack' => $this->build_pack,
+ 'base_directory' => $this->base_directory,
];
} else {
$application_init = [
@@ -289,6 +293,7 @@ public function submit()
'source_id' => $this->git_source->id,
'source_type' => $this->git_source->getMorphClass(),
'build_pack' => $this->build_pack,
+ 'base_directory' => $this->base_directory,
];
}
@@ -303,11 +308,15 @@ public function submit()
$application->settings->is_static = $this->isStatic;
$application->settings->save();
-
$fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = $fqdn;
$application->save();
-
+ if ($this->checkCoolifyConfig) {
+ // $config = loadConfigFromGit($this->repository_url, $this->git_branch, $this->base_directory, $this->query['server_id'], auth()->user()->currentTeam()->id);
+ // if ($config) {
+ // $application->setConfig($config);
+ // }
+ }
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index 3c5f3901b..7f8247597 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -49,11 +49,8 @@ class Select extends Component
public ?string $existingPostgresqlUrl = null;
- public ?string $search = null;
-
protected $queryString = [
'server_id',
- 'search',
];
public function mount()
@@ -90,40 +87,119 @@ public function updatedSelectedEnvironment()
// }
// }
- public function updatedSearch()
+ public function loadServices()
{
- $this->loadServices();
- }
+ $services = get_service_templates(true);
+ $services = collect($services)->map(function ($service, $key) {
+ return [
+ 'name' => str($key)->headline(),
+ 'logo' => asset(data_get($service, 'logo', 'svgs/coolify.png')),
+ ] + (array) $service;
+ })->all();
+ $gitBasedApplications = [
+ [
+ 'id' => 'public',
+ 'name' => 'Public Repository',
+ 'description' => 'You can deploy any kind of public repositories from the supported git providers.',
+ 'logo' => asset('svgs/git.svg'),
+ ],
+ [
+ 'id' => 'private-gh-app',
+ 'name' => 'Private Repository (with GitHub App)',
+ 'description' => 'You can deploy public & private repositories through your GitHub Apps.',
+ 'logo' => asset('svgs/github.svg'),
+ ],
+ [
+ 'id' => 'private-deploy-key',
+ 'name' => 'Private Repository (with Deploy Key)',
+ 'description' => 'You can deploy private repositories with a deploy key.',
+ 'logo' => asset('svgs/git.svg'),
+ ],
+ ];
+ $dockerBasedApplications = [
+ [
+ 'id' => 'dockerfile',
+ 'name' => 'Dockerfile',
+ 'description' => 'You can deploy a simple Dockerfile, without Git.',
+ 'logo' => asset('svgs/docker.svg'),
+ ],
+ [
+ 'id' => 'docker-compose-empty',
+ 'name' => 'Docker Compose Empty',
+ 'description' => 'You can deploy complex application easily with Docker Compose, without Git.',
+ 'logo' => asset('svgs/docker.svg'),
+ ],
+ [
+ 'id' => 'docker-image',
+ 'name' => 'Docker Image',
+ 'description' => 'You can deploy an existing Docker Image from any Registry, without Git.',
+ 'logo' => asset('svgs/docker.svg'),
+ ],
+ ];
+ $databases = [
+ [
+ 'id' => 'postgresql',
+ 'name' => 'PostgreSQL',
+ 'description' => 'PostgreSQL is an object-relational database known for its robustness, advanced features, and strong standards compliance.',
+ 'logo' => '
+',
+ ],
+ [
+ 'id' => 'mysql',
+ 'name' => 'MySQL',
+ 'description' => 'MySQL is an open-source relational database management system. ',
+ 'logo' => '',
- public function loadServices(bool $force = false)
- {
- try {
- $this->loadingServices = true;
- if (count($this->allServices) > 0 && ! $force) {
- if (! $this->search) {
- $this->services = $this->allServices;
+ ],
+ [
+ 'id' => 'mariadb',
+ 'name' => 'MariaDB',
+ 'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source software under the GNU General Public License.',
+ 'logo' => '',
+ ],
+ [
+ 'id' => 'redis',
+ 'name' => 'Redis',
+ 'description' => 'Redis is a source-available, in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability.',
+ 'logo' => '',
+ ],
+ [
+ 'id' => 'keydb',
+ 'name' => 'KeyDB',
+ 'description' => 'KeyDB is a database that offers high performance, low latency, and scalability for various data structures and workloads.',
+ 'logo' => ' ',
+ ],
+ [
+ 'id' => 'dragonfly',
+ 'name' => 'Dragonfly',
+ 'description' => 'Dragonfly DB is a drop-in Redis replacement that delivers 25x more throughput and 12x faster snapshotting than Redis.',
+ 'logo' => '',
+ ],
+ [
+ 'id' => 'mongodb',
+ 'name' => 'MongoDB',
+ 'description' => 'MongoDB is a source-available, cross-platform, document-oriented database program.',
+ 'logo' => '',
+ ],
+ [
+ 'id' => 'clickhouse',
+ 'name' => 'ClickHouse',
+ 'description' => 'ClickHouse is a column-oriented database that supports real-time analytics, business intelligence, observability, ML and GenAI, and more.',
+ 'logo' => '',
+ ],
- return;
- }
- $this->services = $this->allServices->filter(function ($service, $key) {
- $tags = collect(data_get($service, 'tags', []));
+ ];
- return str_contains(strtolower($key), strtolower($this->search)) || $tags->contains(function ($tag) {
- return str_contains(strtolower($tag), strtolower($this->search));
- });
- });
- } else {
- $this->search = null;
- $this->allServices = get_service_templates($force);
- $this->services = $this->allServices->filter(function ($service, $key) {
- return str_contains(strtolower($key), strtolower($this->search));
- });
- }
- } catch (\Throwable $e) {
- return handleError($e, $this);
- } finally {
- $this->loadingServices = false;
- }
+ return [
+ 'services' => $services,
+ 'gitBasedApplications' => $gitBasedApplications,
+ 'dockerBasedApplications' => $dockerBasedApplications,
+ 'databases' => $databases,
+ ];
}
public function instantSave()
@@ -141,6 +217,7 @@ public function instantSave()
public function setType(string $type)
{
+ $type = str($type)->lower()->slug()->value();
if ($this->loading) {
return;
}
diff --git a/app/Livewire/Project/Resource/Index.php b/app/Livewire/Project/Resource/Index.php
index 71ce2c356..283496887 100644
--- a/app/Livewire/Project/Resource/Index.php
+++ b/app/Livewire/Project/Resource/Index.php
@@ -32,8 +32,11 @@ class Index extends Component
public $services = [];
+ public array $parameters;
+
public function mount()
{
+ $this->parameters = get_route_parameters();
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (! $project) {
return redirect()->route('dashboard');
@@ -44,7 +47,6 @@ public function mount()
}
$this->project = $project;
$this->environment = $environment;
-
$this->applications = $this->environment->applications->load(['tags']);
$this->applications = $this->applications->map(function ($application) {
if (data_get($application, 'environment.project.uuid')) {
diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php
index 70e8006c7..4138f720e 100644
--- a/app/Livewire/Project/Service/EditDomain.php
+++ b/app/Livewire/Project/Service/EditDomain.php
@@ -4,6 +4,7 @@
use App\Models\ServiceApplication;
use Livewire\Component;
+use Spatie\Url\Url;
class EditDomain extends Component
{
@@ -20,21 +21,16 @@ public function mount()
{
$this->application = ServiceApplication::find($this->applicationId);
}
-
- public function updatedApplicationFqdn()
- {
- $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
- $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
- $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
- return str($domain)->trim()->lower();
- });
- $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
- $this->application->save();
- }
-
public function submit()
{
try {
+ $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
+ $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
+ $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
+ Url::fromString($domain, ['http', 'https']);
+ return str($domain)->trim()->lower();
+ });
+ $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
check_domain_usage(resource: $this->application);
$this->validate();
$this->application->save();
@@ -44,12 +40,15 @@ public function submit()
} else {
$this->dispatch('success', 'Service saved.');
}
- } catch (\Throwable $e) {
- return handleError($e, $this);
- } finally {
$this->application->service->parse();
$this->dispatch('refresh');
$this->dispatch('configurationChanged');
+ } catch (\Throwable $e) {
+ $originalFqdn = $this->application->getOriginal('fqdn');
+ if ($originalFqdn !== $this->application->fqdn) {
+ $this->application->fqdn = $originalFqdn;
+ }
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php
index 42c9357fd..fa76ee26f 100644
--- a/app/Livewire/Project/Service/Navbar.php
+++ b/app/Livewire/Project/Service/Navbar.php
@@ -39,6 +39,7 @@ public function getListeners()
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted',
+ "envsUpdated" => '$refresh',
];
}
@@ -108,8 +109,23 @@ public function restart()
return;
}
+ StopService::run(service: $this->service, dockerCleanup: false);
+ $this->service->parse();
+ $this->dispatch('imagePulled');
+ $activity = StartService::run($this->service);
+ $this->dispatch('activityMonitor', $activity->id);
+ }
+
+ public function pullAndRestartEvent()
+ {
+ $this->checkDeployments();
+ if ($this->isDeploymentProgress) {
+ $this->dispatch('error', 'There is a deployment in progress.');
+
+ return;
+ }
PullImage::run($this->service);
- StopService::run($this->service);
+ StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse();
$this->dispatch('imagePulled');
$activity = StartService::run($this->service);
diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php
index 56b506043..ba37313fd 100644
--- a/app/Livewire/Project/Service/ServiceApplicationView.php
+++ b/app/Livewire/Project/Service/ServiceApplicationView.php
@@ -6,6 +6,7 @@
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
+use Spatie\Url\Url;
class ServiceApplicationView extends Component
{
@@ -31,13 +32,7 @@ class ServiceApplicationView extends Component
public function updatedApplicationFqdn()
{
- $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
- $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
- $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
- return str($domain)->trim()->lower();
- });
- $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
- $this->application->save();
+
}
public function instantSave()
@@ -83,6 +78,14 @@ public function mount()
public function submit()
{
try {
+ $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
+ $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
+ $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
+ Url::fromString($domain, ['http', 'https']);
+ return str($domain)->trim()->lower();
+ });
+ $this->application->fqdn = $this->application->fqdn->unique()->implode(',');
+
check_domain_usage(resource: $this->application);
$this->validate();
$this->application->save();
@@ -92,10 +95,13 @@ public function submit()
} else {
$this->dispatch('success', 'Service saved.');
}
- } catch (\Throwable $e) {
- return handleError($e, $this);
- } finally {
$this->dispatch('generateDockerCompose');
+ } catch (\Throwable $e) {
+ $originalFqdn = $this->application->getOriginal('fqdn');
+ if ($originalFqdn !== $this->application->fqdn) {
+ $this->application->fqdn = $originalFqdn;
+ }
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php
index 7f2416e3e..2c751aa92 100644
--- a/app/Livewire/Project/Service/StackForm.php
+++ b/app/Livewire/Project/Service/StackForm.php
@@ -34,6 +34,7 @@ public function mount()
$value = data_get($field, 'value');
$rules = data_get($field, 'rules', 'nullable');
$isPassword = data_get($field, 'isPassword', false);
+ $customHelper = data_get($field, 'customHelper', false);
$this->fields->put($key, [
'serviceName' => $serviceName,
'key' => $key,
@@ -41,6 +42,7 @@ public function mount()
'value' => $value,
'isPassword' => $isPassword,
'rules' => $rules,
+ 'customHelper' => $customHelper,
]);
$this->rules["fields.$key.value"] = $rules;
diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php
index 543e64539..c05260899 100644
--- a/app/Livewire/Project/Shared/Danger.php
+++ b/app/Livewire/Project/Shared/Danger.php
@@ -91,10 +91,12 @@ public function mount()
public function delete($password)
{
- if (! Hash::check($password, Auth::user()->password)) {
- $this->addError('password', 'The provided password is incorrect.');
+ if (isProduction()) {
+ if (! Hash::check($password, Auth::user()->password)) {
+ $this->addError('password', 'The provided password is incorrect.');
- return;
+ return;
+ }
}
if (! $this->resource) {
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
index a859c90b0..0dbf0f957 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
@@ -48,14 +48,6 @@ public function mount()
public function submit()
{
$this->validate();
- // if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) {
- // $type = str($this->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;
- // }
- // }
$this->dispatch('saveKey', [
'key' => $this->key,
'value' => $this->value,
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index 055788b57..5a711259b 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -53,30 +53,16 @@ public function instantSave()
public function sortEnvironmentVariables()
{
- if ($this->resource->type() === 'application') {
- $this->resource->load(['environment_variables', 'environment_variables_preview']);
- } else {
- $this->resource->load(['environment_variables']);
+ if (! data_get($this->resource, 'settings.is_env_sorting_enabled')) {
+ if ($this->resource->environment_variables) {
+ $this->resource->environment_variables = $this->resource->environment_variables->sortBy('order')->values();
+ }
+
+ if ($this->resource->environment_variables_preview) {
+ $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('order')->values();
+ }
}
- $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();
}
@@ -121,6 +107,8 @@ public function submit($data = null)
$this->sortEnvironmentVariables();
} catch (\Throwable $e) {
return handleError($e, $this);
+ } finally {
+ $this->refreshEnvs();
}
}
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
index 463ceecad..0538a6bdb 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
@@ -37,6 +37,7 @@ class Show extends Component
'env.is_literal' => 'required|boolean',
'env.is_shown_once' => 'required|boolean',
'env.real_value' => 'nullable',
+ 'env.is_required' => 'required|boolean',
];
protected $validationAttributes = [
@@ -46,6 +47,7 @@ class Show extends Component
'env.is_multiline' => 'Multiline',
'env.is_literal' => 'Literal',
'env.is_shown_once' => 'Shown Once',
+ 'env.is_required' => 'Required',
];
public function refresh()
@@ -109,14 +111,14 @@ public function submit()
} else {
$this->validate();
}
- // if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) {
- // $type = str($this->env->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;
- // }
- // }
+ if ($this->env->is_required && str($this->env->real_value)->isEmpty()) {
+ $oldValue = $this->env->getOriginal('value');
+ $this->env->value = $oldValue;
+ $this->dispatch('error', 'Required environment variable cannot be empty.');
+
+ return;
+ }
$this->serialize();
$this->env->save();
$this->dispatch('success', 'Environment variable updated.');
diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
index d95443621..90419caed 100644
--- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php
+++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
@@ -11,6 +11,8 @@
class ExecuteContainerCommand extends Component
{
+ public $selected_container = 'default';
+
public $container;
public Collection $containers;
@@ -83,11 +85,14 @@ public function loadContainers()
$containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true);
}
foreach ($containers as $container) {
- $payload = [
- 'server' => $server,
- 'container' => $container,
- ];
- $this->containers = $this->containers->push($payload);
+ // if container state is running
+ if (data_get($container, 'State') === 'running') {
+ $payload = [
+ 'server' => $server,
+ 'container' => $container,
+ ];
+ $this->containers = $this->containers->push($payload);
+ }
}
} elseif (data_get($this->parameters, 'database_uuid')) {
if ($this->resource->isRunning()) {
@@ -100,7 +105,6 @@ public function loadContainers()
}
} 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,
@@ -131,9 +135,14 @@ public function loadContainers()
#[On('connectToContainer')]
public function connectToContainer()
{
+ if ($this->selected_container === 'default') {
+ $this->dispatch('error', 'Please select a container.');
+
+ return;
+ }
try {
- $container_name = data_get($this->container, 'container.Names');
- if (is_null($container_name)) {
+ $container = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
+ if (is_null($container)) {
throw new \RuntimeException('Container not found.');
}
$server = data_get($this->container, 'server');
@@ -141,11 +150,11 @@ public function connectToContainer()
if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
-
- $this->dispatch('send-terminal-command',
- true,
- $container_name,
- $server->uuid,
+ $this->dispatch(
+ 'send-terminal-command',
+ isset($container),
+ data_get($container, 'container.Names'),
+ data_get($container, 'server.uuid')
);
} catch (\Throwable $e) {
diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php
index d9d7dd3ef..fdc35fc0f 100644
--- a/app/Livewire/Project/Shared/Metrics.php
+++ b/app/Livewire/Project/Shared/Metrics.php
@@ -31,13 +31,8 @@ public function pollData()
public function loadData()
{
try {
- $metrics = $this->resource->getMetrics($this->interval);
- $cpuMetrics = collect($metrics)->map(function ($metric) {
- return [$metric[0], $metric[1]];
- });
- $memoryMetrics = collect($metrics)->map(function ($metric) {
- return [$metric[0], $metric[2]];
- });
+ $cpuMetrics = $this->resource->getCpuMetrics($this->interval);
+ $memoryMetrics = $this->resource->getMemoryMetrics($this->interval);
$this->dispatch("refreshChartData-{$this->chartId}-cpu", [
'seriesData' => $cpuMetrics,
]);
diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php
index 27be46227..916db650f 100644
--- a/app/Livewire/Project/Shared/Terminal.php
+++ b/app/Livewire/Project/Shared/Terminal.php
@@ -34,9 +34,9 @@ public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
if ($status !== 'running') {
return;
}
- $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
+ $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} else {
- $command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
+ $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && 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
diff --git a/app/Livewire/Project/Shared/UploadConfig.php b/app/Livewire/Project/Shared/UploadConfig.php
new file mode 100644
index 000000000..dea842651
--- /dev/null
+++ b/app/Livewire/Project/Shared/UploadConfig.php
@@ -0,0 +1,41 @@
+config = '{
+ "build_pack": "nixpacks",
+ "base_directory": "/nodejs",
+ "publish_directory": "/",
+ "ports_exposes": "3000",
+ "settings": {
+ "is_static": false
+ }
+}';
+ }
+ }
+ public function uploadConfig()
+ {
+ try {
+ $application = Application::findOrFail($this->applicationId);
+ $application->setConfig($this->config);
+ $this->dispatch('success', 'Application settings updated');
+ } catch (\Exception $e) {
+ $this->dispatch('error', $e->getMessage());
+ return;
+ }
+
+ }
+ public function render()
+ {
+ return view('livewire.project.shared.upload-config');
+ }
+}
diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php
index ff8679d21..fe68a8ba5 100644
--- a/app/Livewire/Security/ApiTokens.php
+++ b/app/Livewire/Security/ApiTokens.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Security;
+use App\Models\InstanceSettings;
use Livewire\Component;
class ApiTokens extends Component
@@ -14,8 +15,12 @@ class ApiTokens extends Component
public bool $readOnly = true;
+ public bool $rootAccess = false;
+
public array $permissions = ['read-only'];
+ public $isApiEnabled;
+
public function render()
{
return view('livewire.security.api-tokens');
@@ -23,6 +28,7 @@ public function render()
public function mount()
{
+ $this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
$this->tokens = auth()->user()->tokens->sortByDesc('created_at');
}
@@ -31,12 +37,11 @@ public function updatedViewSensitiveData()
if ($this->viewSensitiveData) {
$this->permissions[] = 'view:sensitive';
$this->permissions = array_diff($this->permissions, ['*']);
+ $this->rootAccess = false;
} else {
$this->permissions = array_diff($this->permissions, ['view:sensitive']);
}
- if (count($this->permissions) == 0) {
- $this->permissions = ['*'];
- }
+ $this->makeSureOneIsSelected();
}
public function updatedReadOnly()
@@ -44,11 +49,30 @@ public function updatedReadOnly()
if ($this->readOnly) {
$this->permissions[] = 'read-only';
$this->permissions = array_diff($this->permissions, ['*']);
+ $this->rootAccess = false;
} else {
$this->permissions = array_diff($this->permissions, ['read-only']);
}
- if (count($this->permissions) == 0) {
+ $this->makeSureOneIsSelected();
+ }
+
+ public function updatedRootAccess()
+ {
+ if ($this->rootAccess) {
$this->permissions = ['*'];
+ $this->readOnly = false;
+ $this->viewSensitiveData = false;
+ } else {
+ $this->readOnly = true;
+ $this->permissions = ['read-only'];
+ }
+ }
+
+ public function makeSureOneIsSelected()
+ {
+ if (count($this->permissions) == 0) {
+ $this->permissions = ['read-only'];
+ $this->readOnly = true;
}
}
@@ -58,12 +82,6 @@ public function addNewToken()
$this->validate([
'description' => 'required|min:3|max:255',
]);
- // if ($this->viewSensitiveData) {
- // $this->permissions[] = 'view:sensitive';
- // }
- // if ($this->readOnly) {
- // $this->permissions[] = 'read-only';
- // }
$token = auth()->user()->createToken($this->description, $this->permissions);
$this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken);
diff --git a/app/Livewire/Server/Charts.php b/app/Livewire/Server/Charts.php
index 0921c7fa4..09b31c0b0 100644
--- a/app/Livewire/Server/Charts.php
+++ b/app/Livewire/Server/Charts.php
@@ -34,12 +34,12 @@ public function loadData()
try {
$cpuMetrics = $this->server->getCpuMetrics($this->interval);
$memoryMetrics = $this->server->getMemoryMetrics($this->interval);
- $cpuMetrics = collect($cpuMetrics)->map(function ($metric) {
- return [$metric[0], $metric[1]];
- });
- $memoryMetrics = collect($memoryMetrics)->map(function ($metric) {
- return [$metric[0], $metric[1]];
- });
+ // $cpuMetrics = collect($cpuMetrics)->map(function ($metric) {
+ // return [$metric[0], $metric[1]];
+ // });
+ // $memoryMetrics = collect($memoryMetrics)->map(function ($metric) {
+ // return [$metric[0], $metric[1]];
+ // });
$this->dispatch("refreshChartData-{$this->chartId}-cpu", [
'seriesData' => $cpuMetrics,
]);
diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php
index a69a5e15d..f58d7b6be 100644
--- a/app/Livewire/Server/ConfigureCloudflareTunnels.php
+++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php
@@ -30,6 +30,11 @@ public function alreadyConfigured()
public function submit()
{
try {
+ if (str($this->ssh_domain)->contains('https://')) {
+ $this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
+ // remove / from the end
+ $this->ssh_domain = str($this->ssh_domain)->replace('/', '');
+ }
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true;
diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php
index 3cb3305b5..dadf9033d 100644
--- a/app/Livewire/Server/Form.php
+++ b/app/Livewire/Server/Form.php
@@ -4,8 +4,10 @@
use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel;
+use App\Jobs\DockerCleanupJob;
use App\Jobs\PullSentinelImageJob;
use App\Models\Server;
+use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Form extends Component
@@ -24,6 +26,10 @@ class Form extends Component
public $timezones;
+ public $delete_unused_volumes = false;
+
+ public $delete_unused_networks = false;
+
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@@ -49,15 +55,19 @@ public function getListeners()
'server.settings.concurrent_builds' => 'required|integer|min:1',
'server.settings.dynamic_timeout' => 'required|integer|min:1',
'server.settings.is_metrics_enabled' => 'required|boolean',
- 'server.settings.metrics_token' => 'required',
- 'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1',
- 'server.settings.metrics_history_days' => 'required|integer|min:1',
+ 'server.settings.sentinel_token' => 'required',
+ 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'required|integer|min:1',
+ 'server.settings.sentinel_metrics_history_days' => 'required|integer|min:1',
+ 'server.settings.sentinel_push_interval_seconds' => 'required|integer|min:10',
'wildcard_domain' => 'nullable|url',
- 'server.settings.is_server_api_enabled' => 'required|boolean',
+ 'server.settings.sentinel_custom_url' => 'nullable|url',
+ 'server.settings.is_sentinel_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',
+ 'server.settings.delete_unused_volumes' => 'boolean',
+ 'server.settings.delete_unused_networks' => 'boolean',
];
protected $validationAttributes = [
@@ -74,11 +84,15 @@ public function getListeners()
'server.settings.concurrent_builds' => 'Concurrent Builds',
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
'server.settings.is_metrics_enabled' => 'Metrics',
- 'server.settings.metrics_token' => 'Metrics Token',
- '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.sentinel_token' => 'Metrics Token',
+ 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval',
+ 'server.settings.sentinel_metrics_history_days' => 'Metrics History',
+ 'server.settings.sentinel_push_interval_seconds' => 'Push Interval',
+ 'server.settings.is_sentinel_enabled' => 'Server API',
+ 'server.settings.sentinel_custom_url' => 'Sentinel URL',
'server.settings.server_timezone' => 'Server Timezone',
+ 'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
+ 'server.settings.delete_unused_networks' => 'Delete Unused Networks',
];
public function mount(Server $server)
@@ -88,6 +102,24 @@ public function mount(Server $server)
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
+ $this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes;
+ $this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks;
+ }
+
+ public function checkSyncStatus(){
+ $this->server->refresh();
+ $this->server->settings->refresh();
+ }
+
+ public function regenerateSentinelToken()
+ {
+ try {
+ $this->server->generateSentinelToken();
+ $this->server->settings->refresh();
+ $this->dispatch('success', 'Sentinel token regenerated. Please restart your Sentinel.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function updated($field)
@@ -120,40 +152,47 @@ public function updatedServerSettingsIsBuildServer()
$this->dispatch('proxyStatusUpdated');
}
- public function checkPortForServerApi()
- {
- try {
- if ($this->server->settings->is_server_api_enabled === true) {
- $this->server->checkServerApi();
- $this->dispatch('success', 'Server API is reachable.');
- }
- } catch (\Throwable $e) {
- return handleError($e, $this);
+ public function updatedServerSettingsIsSentinelEnabled($value){
+ if($value === false){
+ StopSentinel::dispatch($this->server);
+ $this->server->settings->is_metrics_enabled = false;
+ $this->server->settings->save();
+ $this->server->sentinelUpdateAt(isReset: true);
+ } else {
+ StartSentinel::run($this->server);
}
}
+ public function updatedServerSettingsIsMetricsEnabled(){
+ $this->restartSentinel();
+ }
+
+
public function instantSave()
{
try {
refresh_server_connection($this->server->privateKey);
$this->validateServer(false);
+
$this->server->settings->save();
$this->server->save();
$this->dispatch('success', 'Server updated.');
$this->dispatch('refreshServerShow');
- if ($this->server->isSentinelEnabled()) {
- PullSentinelImageJob::dispatchSync($this->server);
- ray('Sentinel is enabled');
- if ($this->server->settings->isDirty('is_metrics_enabled')) {
- $this->dispatch('reloadWindow');
- }
- 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');
- StopSentinel::dispatch($this->server);
- }
+
+ // if ($this->server->isSentinelEnabled()) {
+ // PullSentinelImageJob::dispatchSync($this->server);
+ // ray('Sentinel is enabled');
+ // if ($this->server->settings->isDirty('is_metrics_enabled')) {
+ // $this->dispatch('reloadWindow');
+ // }
+ // if ($this->server->settings->isDirty('is_sentinel_enabled') && $this->server->settings->is_sentinel_enabled === true) {
+ // ray('Starting sentinel');
+ // }
+ // } else {
+ // ray('Sentinel is not enabled');
+ // StopSentinel::dispatch($this->server);
+ // }
+ $this->server->settings->save();
// $this->checkPortForServerApi();
} catch (\Throwable $e) {
@@ -166,7 +205,7 @@ public function restartSentinel()
try {
$version = get_latest_sentinel_version();
StartSentinel::run($this->server, $version, true);
- $this->dispatch('success', 'Sentinel restarted.');
+ $this->dispatch('success', 'Sentinel started.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -232,22 +271,24 @@ public function submit()
$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);
}
}
- public function updatedServerSettingsServerTimezone($value)
+ public function manualCleanup()
{
- $this->server->settings->server_timezone = $value;
- $this->server->settings->save();
- $this->dispatch('success', 'Server timezone updated.');
+ try {
+ DockerCleanupJob::dispatch($this->server, true);
+ $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function manualCloudflareConfig()
diff --git a/app/Livewire/Server/Proxy/Status.php b/app/Livewire/Server/Proxy/Status.php
index d23d7fc20..f4f18381f 100644
--- a/app/Livewire/Server/Proxy/Status.php
+++ b/app/Livewire/Server/Proxy/Status.php
@@ -4,7 +4,7 @@
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy;
-use App\Jobs\ContainerStatusJob;
+use App\Actions\Proxy\StartProxy;
use App\Models\Server;
use Livewire\Component;
@@ -44,11 +44,18 @@ public function checkProxy(bool $notification = false)
}
$this->numberOfPolls++;
}
- CheckProxy::run($this->server, true);
+ $shouldStart = CheckProxy::run($this->server, true);
+ if ($shouldStart) {
+ StartProxy::run($this->server, false);
+ }
$this->dispatch('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') {
$this->polling = false;
$notification && $this->dispatch('success', 'Proxy is running.');
+ } elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) {
+ $notification && $this->dispatch('error', 'Proxy has exited.');
+ } elseif ($this->server->proxy->force_stop) {
+ $notification && $this->dispatch('error', 'Proxy is stopped manually.');
} else {
$notification && $this->dispatch('error', 'Proxy is not running.');
}
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index c52970258..eb492e691 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -28,6 +28,7 @@ class Index extends Component
protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
protected Server $server;
+ public $timezones;
protected $rules = [
'settings.fqdn' => 'nullable',
@@ -53,14 +54,14 @@ class Index extends Component
'settings.is_auto_update_enabled' => 'Auto Update Enabled',
'auto_update_frequency' => 'Auto Update Frequency',
'update_check_frequency' => 'Update Check Frequency',
+ 'settings.instance_timezone' => 'Instance Timezone',
];
- public $timezones;
public function mount()
{
if (isInstanceAdmin()) {
- $this->settings = InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled;
@@ -162,7 +163,7 @@ public function checkManually()
{
CheckForUpdatesJob::dispatchSync();
$this->dispatch('updateAvailable');
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->new_version_available) {
$this->dispatch('success', 'New version available!');
} else {
@@ -170,12 +171,6 @@ public function checkManually()
}
}
- public function updatedSettingsInstanceTimezone($value)
- {
- $this->settings->instance_timezone = $value;
- $this->settings->save();
- $this->dispatch('success', 'Instance timezone updated.');
- }
public function render()
{
diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php
index f9402fd7b..ca0c9c1ae 100644
--- a/app/Livewire/Settings/License.php
+++ b/app/Livewire/Settings/License.php
@@ -29,7 +29,7 @@ public function mount()
abort(404);
}
$this->instance_id = config('app.id');
- $this->settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
}
public function render()
diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php
index 99b8f8d49..9240aa96d 100644
--- a/app/Livewire/SettingsBackup.php
+++ b/app/Livewire/SettingsBackup.php
@@ -42,7 +42,7 @@ class SettingsBackup extends Component
public function mount()
{
if (isInstanceAdmin()) {
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
if ($this->database) {
diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php
index 3eb8ea646..4515df9a7 100644
--- a/app/Livewire/SettingsEmail.php
+++ b/app/Livewire/SettingsEmail.php
@@ -43,7 +43,7 @@ class SettingsEmail extends Component
public function mount()
{
if (isInstanceAdmin()) {
- $this->settings = InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->emails = auth()->user()->email;
} else {
return redirect()->route('dashboard');
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index 75d7fd04a..193b650ff 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -99,7 +99,7 @@ public function mount()
return redirect()->route('source.all');
}
$this->applications = $this->github_app->applications;
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->name = str($this->github_app->name)->kebab();
diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php
index f85e8646e..103c5c9fb 100644
--- a/app/Livewire/Source/Github/Create.php
+++ b/app/Livewire/Source/Github/Create.php
@@ -23,7 +23,7 @@ class Create extends Component
public function mount()
{
- $this->name = generate_random_name();
+ $this->name = substr(generate_random_name(), 0, 34); // GitHub Apps names can only be 34 characters long
}
public function createGitHubApp()
diff --git a/app/Livewire/Storage/Create.php b/app/Livewire/Storage/Create.php
index a05834ecc..c5250e1e3 100644
--- a/app/Livewire/Storage/Create.php
+++ b/app/Livewire/Storage/Create.php
@@ -43,15 +43,17 @@ class Create extends Component
'endpoint' => 'Endpoint',
];
- public function mount()
+ public function updatedEndpoint($value)
{
- if (isDev()) {
- $this->name = 'Local MinIO';
- $this->description = 'Local MinIO';
- $this->key = 'minioadmin';
- $this->secret = 'minioadmin';
- $this->bucket = 'local';
- $this->endpoint = 'http://coolify-minio:9000';
+ if (! str($value)->startsWith('https://') && ! str($value)->startsWith('http://')) {
+ $this->endpoint = 'https://'.$value;
+ $value = $this->endpoint;
+ }
+
+ if (str($value)->contains('your-objectstorage.com') && ! isset($this->bucket)) {
+ $this->bucket = str($value)->after('//')->before('.');
+ } elseif (str($value)->contains('your-objectstorage.com')) {
+ $this->bucket = $this->bucket ?: str($value)->after('//')->before('.');
}
}
diff --git a/app/Livewire/Subscription/Index.php b/app/Livewire/Subscription/Index.php
index c278bf58e..df450cf7e 100644
--- a/app/Livewire/Subscription/Index.php
+++ b/app/Livewire/Subscription/Index.php
@@ -23,7 +23,7 @@ public function mount()
if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) {
return redirect()->route('subscription.show');
}
- $this->settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->alreadySubscribed = currentTeam()->subscription()->exists();
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 55006745a..3002f1f45 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -9,6 +9,7 @@
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
+use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
use RuntimeException;
@@ -104,7 +105,7 @@ class Application extends BaseModel
{
use SoftDeletes;
- private static $parserVersion = '3';
+ private static $parserVersion = '4';
protected $guarded = [];
@@ -143,6 +144,9 @@ protected static function booted()
}
$application->tags()->detach();
$application->previews()->delete();
+ foreach ($application->deployment_queue as $deployment) {
+ $deployment->delete();
+ }
});
}
@@ -209,7 +213,7 @@ public function delete_configurations()
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
- instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
+ instant_remote_process(['rm -rf ' . $this->workdir()], $server, false);
}
}
@@ -304,7 +308,7 @@ public function failedTaskLink($task_uuid)
'application_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid,
]);
- $settings = InstanceSettings::get();
+ $settings = instanceSettings();
if (data_get($settings, 'fqdn')) {
$url = Url::fromString($route);
$url = $url->withPort(null);
@@ -344,7 +348,7 @@ public function type()
public function publishDirectory(): Attribute
{
return Attribute::make(
- set: fn ($value) => $value ? '/'.ltrim($value, '/') : null,
+ set: fn($value) => $value ? '/' . ltrim($value, '/') : null,
);
}
@@ -426,7 +430,7 @@ public function gitCommitLink($link): string
$git_repository = str_replace('.git', '', $this->git_repository);
$url = Url::fromString($git_repository);
$url = $url->withUserInfo('');
- $url = $url->withPath($url->getPath().'/commits/'.$link);
+ $url = $url->withPath($url->getPath() . '/commits/' . $link);
return $url->__toString();
}
@@ -479,21 +483,21 @@ public function dockerComposeLocation(): Attribute
public function baseDirectory(): Attribute
{
return Attribute::make(
- set: fn ($value) => '/'.ltrim($value, '/'),
+ set: fn($value) => '/' . ltrim($value, '/'),
);
}
public function portsMappings(): Attribute
{
return Attribute::make(
- set: fn ($value) => $value === '' ? null : $value,
+ set: fn($value) => $value === '' ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
- get: fn () => is_null($this->ports_mappings)
+ get: fn() => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
@@ -610,7 +614,7 @@ public function status(): Attribute
public function portsExposesArray(): Attribute
{
return Attribute::make(
- get: fn () => is_null($this->ports_exposes)
+ get: fn() => is_null($this->ports_exposes)
? []
: explode(',', $this->ports_exposes)
);
@@ -710,6 +714,11 @@ public function previews()
return $this->hasMany(ApplicationPreview::class);
}
+ public function deployment_queue()
+ {
+ return $this->hasMany(ApplicationDeploymentQueue::class);
+ }
+
public function destination()
{
return $this->morphTo();
@@ -822,7 +831,7 @@ public function isHealthcheckDisabled(): bool
public function workdir()
{
- return application_configuration_dir()."/{$this->uuid}";
+ return application_configuration_dir() . "/{$this->uuid}";
}
public function isLogDrainEnabled()
@@ -832,7 +841,7 @@ public function isLogDrainEnabled()
public function isConfigurationChanged(bool $save = false)
{
- $newConfigHash = $this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect;
+ $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->ports_exposes . $this->ports_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels . $this->custom_docker_run_options . $this->dockerfile_target_build . $this->redirect;
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
} else {
@@ -886,7 +895,7 @@ public function generateBaseDir(string $uuid)
public function dirOnServer()
{
- return application_configuration_dir()."/{$this->uuid}";
+ return application_configuration_dir() . "/{$this->uuid}";
}
public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false)
@@ -1010,7 +1019,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
@@ -1018,14 +1027,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
}
}
@@ -1054,7 +1063,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
@@ -1062,14 +1071,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
- $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
+ $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
}
}
@@ -1122,7 +1131,7 @@ public function oldRawParser()
}
if ($source->startsWith('.')) {
$source = $source->after('.');
- $source = $workdir.$source;
+ $source = $workdir . $source;
}
$commands->push("mkdir -p $source > /dev/null 2>&1 || true");
}
@@ -1133,7 +1142,7 @@ public function oldRawParser()
$labels->push('coolify.managed=true');
}
if (! $labels->contains('coolify.applicationId')) {
- $labels->push('coolify.applicationId='.$this->id);
+ $labels->push('coolify.applicationId=' . $this->id);
}
if (! $labels->contains('coolify.type')) {
$labels->push('coolify.type=application');
@@ -1150,7 +1159,7 @@ public function oldRawParser()
public function parse(int $pull_request_id = 0, ?int $preview_id = null)
{
- if ($this->compose_parsing_version === '3') {
+ if ((int) $this->compose_parsing_version >= 3) {
return newParser($this, $pull_request_id, $preview_id);
} elseif ($this->docker_compose_raw) {
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
@@ -1255,7 +1264,7 @@ public function parseContainerLabels(?ApplicationPreview $preview = null)
public function fqdns(): Attribute
{
return Attribute::make(
- get: fn () => is_null($this->fqdn)
+ get: fn() => is_null($this->fqdn)
? []
: explode(',', $this->fqdn),
);
@@ -1316,10 +1325,10 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false
continue;
}
if (isset($healthcheckCommand) && str_contains($trimmedLine, '\\')) {
- $healthcheckCommand .= ' '.trim($trimmedLine, '\\ ');
+ $healthcheckCommand .= ' ' . trim($trimmedLine, '\\ ');
}
if (isset($healthcheckCommand) && ! str_contains($trimmedLine, '\\') && ! empty($healthcheckCommand)) {
- $healthcheckCommand .= ' '.$trimmedLine;
+ $healthcheckCommand .= ' ' . $trimmedLine;
break;
}
}
@@ -1391,13 +1400,21 @@ public static function getDomainsByUuid(string $uuid): array
return [];
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ if (isDev() && $server->id === 0) {
+ $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/cpu/history?from=$from");
+ if ($process->failed()) {
+ throw new \Exception($process->errorOutput());
+ }
+ $metrics = $process->output();
+ } else {
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ }
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -1406,17 +1423,106 @@ public function getMetrics(int $mins = 5)
}
throw new \Exception($error);
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
-
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int)$metric['time'], (float)$metric['percent']];
});
-
return $parsedCollection->toArray();
}
}
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ if ($server->isMetricsEnabled()) {
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ if (isDev() && $server->id === 0) {
+ $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/memory/history?from=$from");
+ if ($process->failed()) {
+ throw new \Exception($process->errorOutput());
+ }
+ $metrics = $process->output();
+ } else {
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ }
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error == 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ logger($metric);
+ return [(int)$metric['time'], (float)$metric['used']];
+ });
+ return $parsedCollection->toArray();
+ }
+ }
+
+ public function generateConfig($is_json = false)
+ {
+ $config = collect([]);
+ if ($this->build_pack = 'nixpacks') {
+ $config = collect([
+ 'build_pack' => 'nixpacks',
+ 'docker_registry_image_name' => $this->docker_registry_image_name,
+ 'docker_registry_image_tag' => $this->docker_registry_image_tag,
+ 'install_command' => $this->install_command,
+ 'build_command' => $this->build_command,
+ 'start_command' => $this->start_command,
+ 'base_directory' => $this->base_directory,
+ 'publish_directory' => $this->publish_directory,
+ 'custom_docker_run_options' => $this->custom_docker_run_options,
+ 'ports_exposes' => $this->ports_exposes,
+ 'ports_mappings' => $this->ports_mapping,
+ 'settings' => collect([
+ 'is_static' => $this->settings->is_static,
+ ]),
+ ]);
+ }
+ $config = $config->filter(function ($value) {
+ return str($value)->isNotEmpty();
+ });
+ if ($is_json) {
+ return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+ }
+
+ return $config;
+ }
+ public function setConfig($config)
+ {
+
+ $config = $config;
+ $validator = Validator::make(['config' => $config], [
+ 'config' => 'required|json',
+ ]);
+ if ($validator->fails()) {
+ throw new \Exception('Invalid JSON format');
+ }
+ $config = json_decode($config, true);
+
+ $deepValidator = Validator::make(['config' => $config], [
+ 'config.build_pack' => 'required|string',
+ 'config.base_directory' => 'required|string',
+ 'config.publish_directory' => 'required|string',
+ 'config.ports_exposes' => 'required|string',
+ 'config.settings.is_static' => 'required|boolean',
+ ]);
+ if ($deepValidator->fails()) {
+ throw new \Exception('Invalid data');
+ }
+ $config = $deepValidator->validated()['config'];
+
+ try {
+ $settings = data_get($config, 'settings', []);
+ data_forget($config, 'settings');
+ $this->update($config);
+ $this->settings()->update($settings);
+ } catch (\Exception $e) {
+ throw new \Exception('Failed to update application settings');
+ }
+ }
}
diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php
index 90d7608cc..c261c30c6 100644
--- a/app/Models/ApplicationDeploymentQueue.php
+++ b/app/Models/ApplicationDeploymentQueue.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use OpenApi\Attributes as OA;
@@ -39,6 +40,20 @@ class ApplicationDeploymentQueue extends Model
{
protected $guarded = [];
+ public function application(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => Application::find($this->application_id),
+ );
+ }
+
+ public function server(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => Server::find($this->server_id),
+ );
+ }
+
public function setStatus(string $status)
{
$this->update([
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 138775aba..531c8fa40 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -44,7 +44,7 @@ class EnvironmentVariable extends Model
'version' => 'string',
];
- protected $appends = ['real_value', 'is_shared'];
+ protected $appends = ['real_value', 'is_shared', 'is_really_required'];
protected static function booted()
{
@@ -126,15 +126,17 @@ public function realValue(): Attribute
$env = $this->get_real_environment_variables($this->value, $resource);
return data_get($env, 'value', $env);
- if (is_string($env)) {
- return $env;
- }
-
- return $env->value;
}
);
}
+ protected function isReallyRequired(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => $this->is_required && str($this->real_value)->isEmpty(),
+ );
+ }
+
protected function isShared(): Attribute
{
return Attribute::make(
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index 27a181ee4..3ee142050 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -21,6 +21,7 @@ class InstanceSettings extends Model implements SendsEmail
'is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string',
'update_check_frequency' => 'string',
+ 'sentinel_token' => 'encrypted',
];
public function fqdn(): Attribute
@@ -85,4 +86,17 @@ public function getTitleDisplayName(): string
return "[{$instanceName}]";
}
+
+ public function helperVersion(): Attribute
+ {
+ return Attribute::make(
+ get: function ($value) {
+ if (isDev()) {
+ return 'latest';
+ }
+
+ return $value;
+ }
+ );
+ }
}
diff --git a/app/Models/Project.php b/app/Models/Project.php
index 18481751c..5a9dd964a 100644
--- a/app/Models/Project.php
+++ b/app/Models/Project.php
@@ -24,9 +24,11 @@ class Project extends BaseModel
{
protected $guarded = [];
+ protected $appends = ['default_environment'];
+
public static function ownedByCurrentTeam()
{
- return Project::whereTeamId(currentTeam()->id)->orderBy('name');
+ return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)');
}
protected static function booted()
@@ -131,7 +133,7 @@ public function databases()
return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get());
}
- public function default_environment()
+ public function getDefaultEnvironmentAttribute()
{
$default = $this->environments()->where('name', 'production')->first();
if ($default) {
diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php
index 4c7faaa6f..a432a6e9c 100644
--- a/app/Models/S3Storage.php
+++ b/app/Models/S3Storage.php
@@ -40,6 +40,16 @@ public function awsUrl()
return "{$this->endpoint}/{$this->bucket}";
}
+ public function isHetzner()
+ {
+ return str($this->endpoint)->contains('your-objectstorage.com');
+ }
+
+ public function isDigitalOcean()
+ {
+ return str($this->endpoint)->contains('digitaloceanspaces.com');
+ }
+
public function testConnection(bool $shouldSave = false)
{
try {
diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php
index ce5d3a87f..3921e32e4 100644
--- a/app/Models/ScheduledDatabaseBackup.php
+++ b/app/Models/ScheduledDatabaseBackup.php
@@ -39,13 +39,19 @@ public function get_last_days_backup_status($days = 7)
public function server()
{
if ($this->database) {
- if ($this->database->destination && $this->database->destination->server) {
- $server = $this->database->destination->server;
-
+ if ($this->database instanceof ServiceDatabase) {
+ $destination = data_get($this->database->service, 'destination');
+ $server = data_get($destination, 'server');
+ } else {
+ $destination = data_get($this->database, 'destination');
+ $server = data_get($destination, 'server');
+ }
+ if ($server) {
return $server;
}
}
+
return null;
}
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index adc8aa7e4..3639d9263 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -7,6 +7,7 @@
use App\Jobs\PullSentinelImageJob;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process;
@@ -16,6 +17,7 @@
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Spatie\Url\Url;
+use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
#[OA\Schema(
@@ -36,6 +38,8 @@
'validation_logs' => ['type' => 'string'],
'log_drain_notification_sent' => ['type' => 'boolean'],
'swarm_cluster' => ['type' => 'string'],
+ 'delete_unused_volumes' => ['type' => 'boolean'],
+ 'delete_unused_networks' => ['type' => 'boolean'],
]
)]
@@ -105,6 +109,8 @@ protected static function booted()
'proxy' => SchemalessAttributes::class,
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
+ 'delete_unused_volumes' => 'boolean',
+ 'delete_unused_networks' => 'boolean',
];
protected $schemalessAttributes = [
@@ -162,7 +168,7 @@ public function proxySet()
public function setupDefault404Redirect()
{
- $dynamic_conf_path = $this->proxyPath().'/dynamic';
+ $dynamic_conf_path = $this->proxyPath() . '/dynamic';
$proxy_type = $this->proxyType();
$redirect_url = $this->proxy->redirect_url;
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
@@ -176,8 +182,8 @@ public function setupDefault404Redirect()
respond 404
}';
$conf =
- "# This file is automatically generated by Coolify.\n".
- "# Do not edit it manually (only if you know what are you doing).\n\n".
+ "# This file is automatically generated by Coolify.\n" .
+ "# Do not edit it manually (only if you know what are you doing).\n\n" .
$conf;
$base64 = base64_encode($conf);
instant_remote_process([
@@ -205,10 +211,13 @@ public function setupDefault404Redirect()
1 => 'https',
],
'service' => 'noop',
- 'rule' => 'HostRegexp(`{catchall:.*}`)',
+ 'rule' => 'HostRegexp(`.+`)',
+ 'tls' => [
+ 'certResolver' => 'letsencrypt',
+ ],
'priority' => 1,
'middlewares' => [
- 0 => 'redirect-regexp@file',
+ 0 => 'redirect-regexp',
],
],
],
@@ -236,8 +245,8 @@ public function setupDefault404Redirect()
];
$conf = Yaml::dump($dynamic_conf, 12, 2);
$conf =
- "# This file is automatically generated by Coolify.\n".
- "# Do not edit it manually (only if you know what are you doing).\n\n".
+ "# This file is automatically generated by Coolify.\n" .
+ "# Do not edit it manually (only if you know what are you doing).\n\n" .
$conf;
$base64 = base64_encode($conf);
@@ -246,8 +255,8 @@ public function setupDefault404Redirect()
redir $redirect_url
}";
$conf =
- "# This file is automatically generated by Coolify.\n".
- "# Do not edit it manually (only if you know what are you doing).\n\n".
+ "# This file is automatically generated by Coolify.\n" .
+ "# Do not edit it manually (only if you know what are you doing).\n\n" .
$conf;
$base64 = base64_encode($conf);
}
@@ -264,8 +273,8 @@ public function setupDefault404Redirect()
public function setupDynamicProxyConfiguration()
{
- $settings = \App\Models\InstanceSettings::get();
- $dynamic_config_path = $this->proxyPath().'/dynamic';
+ $settings = instanceSettings();
+ $dynamic_config_path = $this->proxyPath() . '/dynamic';
if ($this->proxyType() === ProxyTypes::TRAEFIK->value) {
$file = "$dynamic_config_path/coolify.yaml";
if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) {
@@ -384,8 +393,8 @@ public function setupDynamicProxyConfiguration()
}
$yaml = Yaml::dump($traefik_dynamic_conf, 12, 2);
$yaml =
- "# This file is automatically generated by Coolify.\n".
- "# Do not edit it manually (only if you know what are you doing).\n\n".
+ "# This file is automatically generated by Coolify.\n" .
+ "# Do not edit it manually (only if you know what are you doing).\n\n" .
$yaml;
$base64 = base64_encode($yaml);
@@ -444,11 +453,19 @@ public function proxyPath()
// Should move everything except /caddy and /nginx to /traefik
// The code needs to be modified as well, so maybe it does not worth it
if ($proxyType === ProxyTypes::TRAEFIK->value) {
- $proxy_path = $proxy_path;
+ // Do nothing
} elseif ($proxyType === ProxyTypes::CADDY->value) {
- $proxy_path = $proxy_path.'/caddy';
+ if (isDev()) {
+ $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy';
+ } else {
+ $proxy_path = $proxy_path . '/caddy';
+ }
} elseif ($proxyType === ProxyTypes::NGINX->value) {
- $proxy_path = $proxy_path.'/nginx';
+ if (isDev()) {
+ $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx';
+ } else {
+ $proxy_path = $proxy_path . '/nginx';
+ }
}
return $proxy_path;
@@ -456,15 +473,6 @@ public function proxyPath()
public function proxyType()
{
- // $proxyType = $this->proxy->get('type');
- // if ($proxyType === ProxyTypes::NONE->value) {
- // return $proxyType;
- // }
- // if (is_null($proxyType)) {
- // $this->proxy->type = ProxyTypes::TRAEFIK->value;
- // $this->proxy->status = ProxyStatus::EXITED->value;
- // $this->save();
- // }
return data_get($this->proxy, 'type');
}
@@ -519,6 +527,45 @@ public function forceDisableServer()
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
+ public function generateSentinelUrl() {
+ if ($this->isLocalhost()) {
+ return 'http://host.docker.internal:8000';
+ }
+ $settings = InstanceSettings::get();
+ if ($settings->fqdn) {
+ return $settings->fqdn;
+ }
+ if ($settings->ipv4) {
+ return $settings->ipv4 . ':8000';
+ }
+ if ($settings->ipv6) {
+ return $settings->ipv6 . ':8000';
+ }
+ return null;
+ }
+ public function generateSentinelToken()
+ {
+ $data = [
+ 'server_uuid' => $this->uuid,
+ ];
+ $token = json_encode($data);
+ $encrypted = encrypt($token);
+ $this->settings->sentinel_token = $encrypted;
+ $this->settings->save();
+
+ return $encrypted;
+ }
+
+ public function sentinelUpdateAt(bool $isReset = false)
+ {
+ $this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now();
+ $this->save();
+ }
+ public function isSentinelLive()
+ {
+ return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subMinutes(4));
+ }
+
public function isSentinelEnabled()
{
return $this->isMetricsEnabled() || $this->isServerApiEnabled();
@@ -531,7 +578,7 @@ public function isMetricsEnabled()
public function isServerApiEnabled()
{
- return $this->settings->is_server_api_enabled;
+ return $this->settings->is_sentinel_enabled;
}
public function checkServerApi()
@@ -549,7 +596,6 @@ public function checkServerApi()
ray($process->exitCode(), $process->output(), $process->errorOutput());
throw new \Exception("Server API is not reachable on http://{$server_ip}:12172");
}
-
}
}
@@ -573,7 +619,15 @@ public function getCpuMetrics(int $mins = 5)
{
if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
+ if (isDev() && $this->id === 0) {
+ $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/cpu/history?from=$from");
+ if ($process->failed()) {
+ throw new \Exception($process->errorOutput());
+ }
+ $cpu = $process->output();
+ } else {
+ $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
+ }
if (str($cpu)->contains('error')) {
$error = json_decode($cpu, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -582,17 +636,12 @@ public function getCpuMetrics(int $mins = 5)
}
throw new \Exception($error);
}
- $cpu = str($cpu)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($cpu)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 0);
-
- return [(int) $time, (float) $cpu_usage_percent];
- });
+ $cpu = json_decode($cpu, true);
+ $parsedCollection = collect($cpu)->map(function ($metric) {
+ return [(int)$metric['time'], (float)$metric['percent']];
});
+ return $parsedCollection;
- return $parsedCollection->toArray();
}
}
@@ -600,7 +649,15 @@ public function getMemoryMetrics(int $mins = 5)
{
if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
+ if (isDev() && $this->id === 0) {
+ $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/memory/history?from=$from");
+ if ($process->failed()) {
+ throw new \Exception($process->errorOutput());
+ }
+ $memory = $process->output();
+ } else {
+ $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
+ }
if (str($memory)->contains('error')) {
$error = json_decode($memory, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -609,14 +666,9 @@ public function getMemoryMetrics(int $mins = 5)
}
throw new \Exception($error);
}
- $memory = str($memory)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($memory)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $used, $free, $usedPercent] = explode(',', trim($line));
- $usedPercent = number_format($usedPercent, 0);
-
- return [(int) $time, (float) $usedPercent];
- });
+ $memory = json_decode($memory, true);
+ $parsedCollection = collect($memory)->map(function ($metric) {
+ return [(int)$metric['time'], (float)$metric['usedPercent']];
});
return $parsedCollection->toArray();
@@ -971,7 +1023,8 @@ public function team()
public function isProxyShouldRun()
{
- if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
+ // TODO: Do we need "|| $this->proxy->force_stop" here?
+ if ($this->proxyType() === ProxyTypes::NONE->value || $this->isBuildServer()) {
return false;
}
@@ -1035,6 +1088,37 @@ public function isSwarmWorker()
return data_get($this, 'settings.is_swarm_worker');
}
+ public function status(): bool
+ {
+ ['uptime' => $uptime] = $this->validateConnection(false);
+ if ($uptime) {
+ if ($this->unreachable_notification_sent === true) {
+ $this->update(['unreachable_notification_sent' => false]);
+ }
+ } else {
+ // $this->server->team?->notify(new Unreachable($this->server));
+ foreach ($this->applications as $application) {
+ $application->update(['status' => 'exited']);
+ }
+ foreach ($this->databases as $database) {
+ $database->update(['status' => 'exited']);
+ }
+ foreach ($this->services as $service) {
+ $apps = $service->applications()->get();
+ $dbs = $service->databases()->get();
+ foreach ($apps as $app) {
+ $app->update(['status' => 'exited']);
+ }
+ foreach ($dbs as $db) {
+ $db->update(['status' => 'exited']);
+ }
+ }
+
+ return false;
+ }
+
+ return true;
+ }
public function validateConnection($isManualCheck = true)
{
config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
@@ -1048,6 +1132,10 @@ public function validateConnection($isManualCheck = true)
return ['uptime' => false, 'error' => 'Server skipped.'];
}
try {
+ // Make sure the private key is stored
+ if ($server->privateKey) {
+ $server->privateKey->storeInFileSystem();
+ }
instant_remote_process(['ls /'], $server);
$server->settings()->update([
'is_reachable' => true,
@@ -1204,4 +1292,18 @@ public function updateWithPrivateKey(array $data, ?PrivateKey $privateKey = null
return $this;
}
+
+ public function storageCheck(): ?string
+ {
+ $commands = [
+ 'df / --output=pcent | tr -cd 0-9',
+ ];
+
+ return instant_remote_process($commands, $this, false);
+ }
+
+ public function isIpv6(): bool
+ {
+ return str($this->ip)->contains(':');
+ }
}
diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php
index c44a393b4..f2eba4854 100644
--- a/app/Models/ServerSetting.php
+++ b/app/Models/ServerSetting.php
@@ -24,7 +24,7 @@
'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
'is_metrics_enabled' => ['type' => 'boolean'],
'is_reachable' => ['type' => 'boolean'],
- 'is_server_api_enabled' => ['type' => 'boolean'],
+ 'is_sentinel_enabled' => ['type' => 'boolean'],
'is_swarm_manager' => ['type' => 'boolean'],
'is_swarm_worker' => ['type' => 'boolean'],
'is_usable' => ['type' => 'boolean'],
@@ -35,9 +35,9 @@
'logdrain_highlight_project_id' => ['type' => 'string'],
'logdrain_newrelic_base_uri' => ['type' => 'string'],
'logdrain_newrelic_license_key' => ['type' => 'string'],
- 'metrics_history_days' => ['type' => 'integer'],
- 'metrics_refresh_rate_seconds' => ['type' => 'integer'],
- 'metrics_token' => ['type' => 'string'],
+ 'sentinel_metrics_history_days' => ['type' => 'integer'],
+ 'sentinel_metrics_refresh_rate_seconds' => ['type' => 'integer'],
+ 'sentinel_token' => ['type' => 'string'],
'docker_cleanup_frequency' => ['type' => 'string'],
'docker_cleanup_threshold' => ['type' => 'integer'],
'server_id' => ['type' => 'integer'],
@@ -53,8 +53,8 @@ class ServerSetting extends Model
protected $casts = [
'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer',
+ 'sentinel_token' => 'encrypted',
];
-
public function server()
{
return $this->belongsTo(Server::class);
diff --git a/app/Models/Service.php b/app/Models/Service.php
index d236869ba..16e11ecb6 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -42,7 +42,7 @@ class Service extends BaseModel
{
use HasFactory, SoftDeletes;
- private static $parserVersion = '3';
+ private static $parserVersion = '4';
protected $guarded = [];
@@ -283,9 +283,162 @@ public function extraFields()
$fields = collect([]);
$applications = $this->applications()->get();
foreach ($applications as $application) {
- $image = str($application->image)->before(':')->value();
+ $image = str($application->image)->before(':');
+ if ($image->isEmpty()) {
+ continue;
+ }
switch ($image) {
- case str($image)?->contains('rabbitmq'):
+ case $image->contains('castopod'):
+ $data = collect([]);
+ $disable_https = $this->environment_variables()->where('key', 'CP_DISABLE_HTTPS')->first();
+ if ($disable_https) {
+ $data = $data->merge([
+ 'Disable HTTPS' => [
+ 'key' => 'CP_DISABLE_HTTPS',
+ 'value' => data_get($disable_https, 'value'),
+ 'rules' => 'required',
+ 'customHelper' => "If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS",
+ ],
+ ]);
+ }
+ $fields->put('Castopod', $data->toArray());
+ break;
+ case $image->contains('label-studio'):
+ $data = collect([]);
+ $username = $this->environment_variables()->where('key', 'LABEL_STUDIO_USERNAME')->first();
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LABELSTUDIO')->first();
+ if ($username) {
+ $data = $data->merge([
+ 'Username' => [
+ 'key' => 'LABEL_STUDIO_USERNAME',
+ 'value' => data_get($username, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($password) {
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($password, 'key'),
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $fields->put('Label Studio', $data->toArray());
+ break;
+ case $image->contains('litellm'):
+ $data = collect([]);
+ $username = $this->environment_variables()->where('key', 'SERVICE_USER_UI')->first();
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UI')->first();
+ if ($username) {
+ $data = $data->merge([
+ 'Username' => [
+ 'key' => data_get($username, 'key'),
+ 'value' => data_get($username, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($password) {
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($password, 'key'),
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $fields->put('Litellm', $data->toArray());
+ break;
+ case $image->contains('langfuse'):
+ $data = collect([]);
+ $email = $this->environment_variables()->where('key', 'LANGFUSE_INIT_USER_EMAIL')->first();
+ if ($email) {
+ $data = $data->merge([
+ 'Admin Email' => [
+ 'key' => data_get($email, 'key'),
+ 'value' => data_get($email, 'value'),
+ 'rules' => 'required|email',
+ ],
+ ]);
+ }
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first();
+ ray('password', $password);
+ if ($password) {
+ $data = $data->merge([
+ 'Admin Password' => [
+ 'key' => data_get($password, 'key'),
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $fields->put('Langfuse', $data->toArray());
+ break;
+ case $image->contains('invoiceninja'):
+ $data = collect([]);
+ $email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first();
+ $data = $data->merge([
+ 'Email' => [
+ 'key' => data_get($email, 'key'),
+ 'value' => data_get($email, 'value'),
+ 'rules' => 'required|email',
+ ],
+ ]);
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first();
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($password, 'key'),
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ $fields->put('Invoice Ninja', $data->toArray());
+ break;
+ case $image->contains('argilla'):
+ $data = collect([]);
+ $api_key = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_APIKEY')->first();
+ $data = $data->merge([
+ 'API Key' => [
+ 'key' => data_get($api_key, 'key'),
+ 'value' => data_get($api_key, 'value'),
+ 'isPassword' => true,
+ 'rules' => 'required',
+ ],
+ ]);
+ $data = $data->merge([
+ 'API Key' => [
+ 'key' => data_get($api_key, 'key'),
+ 'value' => data_get($api_key, 'value'),
+ 'isPassword' => true,
+ 'rules' => 'required',
+ ],
+ ]);
+ $username = $this->environment_variables()->where('key', 'ARGILLA_USERNAME')->first();
+ $data = $data->merge([
+ 'Username' => [
+ 'key' => data_get($username, 'key'),
+ 'value' => data_get($username, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ARGILLA')->first();
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($password, 'key'),
+ 'value' => data_get($password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ $fields->put('Argilla', $data->toArray());
+ break;
+ case $image->contains('rabbitmq'):
$data = collect([]);
$host_port = $this->environment_variables()->where('key', 'PORT')->first();
$username = $this->environment_variables()->where('key', 'SERVICE_USER_RABBITMQ')->first();
@@ -320,7 +473,7 @@ public function extraFields()
}
$fields->put('RabbitMQ', $data->toArray());
break;
- case str($image)?->contains('tolgee'):
+ case $image->contains('tolgee'):
$data = collect([]);
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_TOLGEE')->first();
$data = $data->merge([
@@ -334,7 +487,7 @@ public function extraFields()
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
- 'key' => 'SERVICE_PASSWORD_TOLGEE',
+ 'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -343,7 +496,7 @@ public function extraFields()
}
$fields->put('Tolgee', $data->toArray());
break;
- case str($image)?->contains('logto'):
+ case $image->contains('logto'):
$data = collect([]);
$logto_endpoint = $this->environment_variables()->where('key', 'LOGTO_ENDPOINT')->first();
$logto_admin_endpoint = $this->environment_variables()->where('key', 'LOGTO_ADMIN_ENDPOINT')->first();
@@ -367,7 +520,7 @@ public function extraFields()
}
$fields->put('Logto', $data->toArray());
break;
- case str($image)?->contains('unleash-server'):
+ case $image->contains('unleash-server'):
$data = collect([]);
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UNLEASH')->first();
$data = $data->merge([
@@ -381,7 +534,7 @@ public function extraFields()
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
- 'key' => 'SERVICE_PASSWORD_UNLEASH',
+ 'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -390,7 +543,7 @@ public function extraFields()
}
$fields->put('Unleash', $data->toArray());
break;
- case str($image)?->contains('grafana'):
+ case $image->contains('grafana'):
$data = collect([]);
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GRAFANA')->first();
$data = $data->merge([
@@ -404,7 +557,7 @@ public function extraFields()
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
- 'key' => 'GF_SECURITY_ADMIN_PASSWORD',
+ 'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
@@ -413,7 +566,7 @@ public function extraFields()
}
$fields->put('Grafana', $data->toArray());
break;
- case str($image)?->contains('directus'):
+ case $image->contains('directus'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
@@ -439,7 +592,7 @@ public function extraFields()
}
$fields->put('Directus', $data->toArray());
break;
- case str($image)?->contains('kong'):
+ case $image->contains('kong'):
$data = collect([]);
$dashboard_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
$dashboard_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
@@ -463,7 +616,7 @@ public function extraFields()
]);
}
$fields->put('Supabase', $data->toArray());
- case str($image)?->contains('minio'):
+ case $image->contains('minio'):
$data = collect([]);
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
@@ -516,7 +669,7 @@ public function extraFields()
$fields->put('MinIO', $data->toArray());
break;
- case str($image)?->contains('weblate'):
+ case $image->contains('weblate'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
@@ -542,7 +695,7 @@ public function extraFields()
}
$fields->put('Weblate', $data->toArray());
break;
- case str($image)?->contains('meilisearch'):
+ case $image->contains('meilisearch'):
$data = collect([]);
$SERVICE_PASSWORD_MEILISEARCH = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MEILISEARCH')->first();
if ($SERVICE_PASSWORD_MEILISEARCH) {
@@ -556,7 +709,7 @@ public function extraFields()
}
$fields->put('Meilisearch', $data->toArray());
break;
- case str($image)?->contains('ghost'):
+ case $image->contains('ghost'):
$data = collect([]);
$MAIL_OPTIONS_AUTH_PASS = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_PASS')->first();
$MAIL_OPTIONS_AUTH_USER = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_USER')->first();
@@ -616,45 +769,8 @@ public function extraFields()
$fields->put('Ghost', $data->toArray());
break;
- default:
- $data = collect([]);
- $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
- // Chaskiq
- $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();
- $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
- if ($admin_user) {
- $data = $data->merge([
- 'User' => [
- 'key' => 'SERVICE_USER_ADMIN',
- 'value' => data_get($admin_user, 'value', 'admin'),
- 'readonly' => true,
- 'rules' => 'required',
- ],
- ]);
- }
- if ($admin_password) {
- $data = $data->merge([
- 'Password' => [
- 'key' => 'SERVICE_PASSWORD_ADMIN',
- 'value' => data_get($admin_password, 'value'),
- 'rules' => 'required',
- 'isPassword' => true,
- ],
- ]);
- }
- if ($admin_email) {
- $data = $data->merge([
- 'Email' => [
- 'key' => 'ADMIN_EMAIL',
- 'value' => data_get($admin_email, 'value'),
- 'rules' => 'required|email',
- ],
- ]);
- }
- $fields->put('Admin', $data->toArray());
- break;
- case str($image)?->contains('vaultwarden'):
+ case $image->contains('vaultwarden'):
$data = collect([]);
$DATABASE_URL = $this->environment_variables()->where('key', 'DATABASE_URL')->first();
@@ -720,7 +836,7 @@ public function extraFields()
$fields->put('Vaultwarden', $data);
break;
- case str($image)->contains('gitlab/gitlab'):
+ case $image->contains('gitlab/gitlab'):
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GITLAB')->first();
$data = collect([]);
if ($password) {
@@ -744,7 +860,7 @@ public function extraFields()
$fields->put('GitLab', $data->toArray());
break;
- case str($image)->contains('code-server'):
+ case $image->contains('code-server'):
$data = collect([]);
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_64_PASSWORDCODESERVER')->first();
if ($password) {
@@ -770,14 +886,78 @@ public function extraFields()
}
$fields->put('Code Server', $data->toArray());
break;
+ case $image->contains('elestio/strapi'):
+ $data = collect([]);
+ $license = $this->environment_variables()->where('key', 'STRAPI_LICENSE')->first();
+ if ($license) {
+ $data = $data->merge([
+ 'License' => [
+ 'key' => data_get($license, 'key'),
+ 'value' => data_get($license, 'value'),
+ ],
+ ]);
+ }
+ $nodeEnv = $this->environment_variables()->where('key', 'NODE_ENV')->first();
+ if ($nodeEnv) {
+ $data = $data->merge([
+ 'Node Environment' => [
+ 'key' => data_get($nodeEnv, 'key'),
+ 'value' => data_get($nodeEnv, 'value'),
+ ],
+ ]);
+ }
+
+ $fields->put('Strapi', $data->toArray());
+ break;
+ default:
+ $data = collect([]);
+ $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
+ // Chaskiq
+ $admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();
+
+ $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
+ if ($admin_user) {
+ $data = $data->merge([
+ 'User' => [
+ 'key' => data_get($admin_user, 'key'),
+ 'value' => data_get($admin_user, 'value', 'admin'),
+ 'readonly' => true,
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($admin_password) {
+ $data = $data->merge([
+ 'Password' => [
+ 'key' => data_get($admin_password, 'key'),
+ 'value' => data_get($admin_password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ if ($admin_email) {
+ $data = $data->merge([
+ 'Email' => [
+ 'key' => data_get($admin_email, 'key'),
+ 'value' => data_get($admin_email, 'value'),
+ 'rules' => 'required|email',
+ ],
+ ]);
+ }
+ $fields->put('Admin', $data->toArray());
+ break;
}
}
$databases = $this->databases()->get();
foreach ($databases as $database) {
- $image = str($database->image)->before(':')->value();
+ $image = str($database->image)->before(':');
+ if ($image->isEmpty()) {
+ continue;
+ }
switch ($image) {
- case str($image)->contains('postgres'):
+ case $image->contains('postgres'):
$userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL'];
$passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL'];
$dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB'];
@@ -815,10 +995,10 @@ public function extraFields()
}
$fields->put('PostgreSQL', $data->toArray());
break;
- case str($image)->contains('mysql'):
+ case $image->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
- $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD'];
- $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
+ $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL'];
+ $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
@@ -865,7 +1045,7 @@ public function extraFields()
}
$fields->put('MySQL', $data->toArray());
break;
- case str($image)->contains('mariadb'):
+ case $image->contains('mariadb'):
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER', 'SERVICE_USER_MYSQL', 'MYSQL_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS', 'MYSQL_PASSWORD'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS', 'MYSQL_ROOT_PASSWORD'];
@@ -928,6 +1108,7 @@ public function saveExtraFields($fields)
foreach ($fields as $field) {
$key = data_get($field, 'key');
$value = data_get($field, 'value');
+ ray($key, $value);
$found = $this->environment_variables()->where('key', $key)->first();
if ($found) {
$found->value = $value;
@@ -1051,13 +1232,12 @@ public function scheduled_tasks(): HasMany
public function environment_variables(): HasMany
{
-
- return $this->hasMany(EnvironmentVariable::class)->orderByRaw("key LIKE 'SERVICE%' DESC, value ASC");
+ return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
public function environment_variables_preview(): HasMany
{
- return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
+ return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
public function workdir()
@@ -1095,7 +1275,21 @@ public function saveComposeConfigs()
return 3;
});
foreach ($sorted as $env) {
- $commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
+ if (version_compare($env->version, '4.0.0-beta.347', '<=')) {
+ $commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
+ } else {
+ $real_value = $env->real_value;
+ if ($env->version === '4.0.0-beta.239') {
+ $real_value = $env->real_value;
+ } else {
+ if ($env->is_literal || $env->is_multiline) {
+ $real_value = '\''.$real_value.'\'';
+ } else {
+ $real_value = escapeEnvVariables($env->real_value);
+ }
+ }
+ $commands[] = "echo \"{$env->key}={$real_value}\" >> .env";
+ }
}
if ($sorted->count() === 0) {
$commands[] = 'touch .env';
@@ -1105,7 +1299,7 @@ public function saveComposeConfigs()
public function parse(bool $isNew = false): Collection
{
- if ($this->compose_parsing_version === '3') {
+ if ((int) $this->compose_parsing_version >= 3) {
return newParser($this);
} elseif ($this->docker_compose_raw) {
return parseDockerComposeFile($this, $isNew);
@@ -1121,4 +1315,20 @@ public function networks()
return $networks;
}
+
+ protected function isDeployable(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ $envs = $this->environment_variables()->where('is_required', true)->get();
+ foreach ($envs as $env) {
+ if ($env->is_really_required) {
+ return false;
+ }
+ }
+ return true;
+ }
+ );
+ }
+
}
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
index d312fab96..0e79e1e2e 100644
--- a/app/Models/ServiceApplication.php
+++ b/app/Models/ServiceApplication.php
@@ -112,4 +112,9 @@ public function getFilesFromServer(bool $isInit = false)
{
getFilesystemVolumesFromServer($this, $isInit);
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php
index 6b96738e8..927527118 100644
--- a/app/Models/ServiceDatabase.php
+++ b/app/Models/ServiceDatabase.php
@@ -115,4 +115,13 @@ public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return str($this->databaseType())->contains('mysql') ||
+ str($this->databaseType())->contains('postgres') ||
+ str($this->databaseType())->contains('postgis') ||
+ str($this->databaseType())->contains('mariadb') ||
+ str($this->databaseType())->contains('mongodb');
+ }
}
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index ee5c3becc..6274f51b2 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -294,4 +294,9 @@ public function getMetrics(int $mins = 5)
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 361abf110..3555e7afd 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -294,4 +294,9 @@ public function getMetrics(int $mins = 5)
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index e05879371..4725ca533 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -294,4 +294,9 @@ public function getMetrics(int $mins = 5)
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
}
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index c1e6c85d7..8f1a2c1ee 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -294,4 +294,9 @@ public function getMetrics(int $mins = 5)
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
}
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index e5ed0a5f4..41b2ce9eb 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -292,7 +292,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -314,4 +314,9 @@ public function getMetrics(int $mins = 5)
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
}
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index bd4a7abb7..da2ac070f 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -273,7 +273,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -295,4 +295,9 @@ public function getMetrics(int $mins = 5)
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
}
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index db771c7cd..e0f42269d 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -274,7 +274,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -296,4 +296,9 @@ public function getMetrics(int $mins = 5)
return $parsedCollection->toArray();
}
}
+
+ public function isBackupSolutionAvailable()
+ {
+ return true;
+ }
}
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index ba582733c..e71eacc45 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -287,7 +287,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -309,4 +309,9 @@ public function getMetrics(int $mins = 5)
return $parsedCollection->toArray();
}
}
-}
\ No newline at end of file
+ public function isBackupSolutionAvailable()
+ {
+ return false;
+ }
+}
+
diff --git a/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php
index 549fc6cd3..cc7d76ebf 100644
--- a/app/Notifications/Channels/TransactionalEmailChannel.php
+++ b/app/Notifications/Channels/TransactionalEmailChannel.php
@@ -13,7 +13,7 @@ class TransactionalEmailChannel
{
public function send(User $notifiable, Notification $notification): void
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) {
Log::info('SMTP/Resend not enabled');
diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php
index 8b1c02d39..3938a8da7 100644
--- a/app/Notifications/TransactionalEmails/ResetPassword.php
+++ b/app/Notifications/TransactionalEmails/ResetPassword.php
@@ -18,7 +18,7 @@ class ResetPassword extends Notification
public function __construct($token)
{
- $this->settings = \App\Models\InstanceSettings::get();
+ $this->settings = instanceSettings();
$this->token = $token;
}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index cd90918ad..8b4c2eef2 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -2,10 +2,8 @@
namespace App\Providers;
-use App\Models\InstanceSettings;
use App\Models\PersonalAccessToken;
use Illuminate\Support\Facades\Http;
-use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Laravel\Sanctum\Sanctum;
@@ -30,9 +28,5 @@ public function boot(): void
])->baseUrl($api_url);
}
});
- // if (! env('CI')) {
- // View::share('instanceSettings', InstanceSettings::get());
- // }
-
}
}
diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php
index 53a2e9281..b916b6234 100644
--- a/app/Providers/FortifyServiceProvider.php
+++ b/app/Providers/FortifyServiceProvider.php
@@ -46,7 +46,7 @@ public function boot(): void
Fortify::registerView(function () {
$isFirstUser = User::count() === 0;
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if (! $settings->is_registration_enabled) {
return redirect()->route('login');
}
@@ -60,7 +60,7 @@ public function boot(): void
});
Fortify::loginView(function () {
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$enabled_oauth_providers = OauthSetting::where('enabled', true)->get();
$users = User::count();
if ($users == 0) {
diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php
index 6c9378cac..fbd7b0b15 100644
--- a/app/View/Components/Forms/Input.php
+++ b/app/View/Components/Forms/Input.php
@@ -22,6 +22,7 @@ public function __construct(
public bool $allowToPeak = true,
public bool $isMultiline = false,
public string $defaultClass = 'input',
+ public string $autocomplete = 'off',
) {}
public function render(): View|Closure|string
diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php
index 8e14ef9ee..d7c16b607 100644
--- a/bootstrap/helpers/api.php
+++ b/bootstrap/helpers/api.php
@@ -2,6 +2,7 @@
use App\Enums\BuildPackTypes;
use App\Enums\RedirectTypes;
+use App\Enums\StaticImageTypes;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
@@ -89,6 +90,7 @@ function sharedDataApplications()
'git_branch' => 'string',
'build_pack' => Rule::enum(BuildPackTypes::class),
'is_static' => 'boolean',
+ 'static_image' => Rule::enum(StaticImageTypes::class),
'domains' => 'string',
'redirect' => Rule::enum(RedirectTypes::class),
'git_commit_sha' => 'string',
@@ -175,4 +177,6 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('instant_deploy');
$request->offsetUnset('github_app_uuid');
$request->offsetUnset('private_key_uuid');
+ $request->offsetUnset('use_build_server');
+ $request->offsetUnset('is_static');
}
diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php
index 1eeec8f94..303fcab8e 100644
--- a/bootstrap/helpers/constants.php
+++ b/bootstrap/helpers/constants.php
@@ -20,12 +20,16 @@
const DATABASE_DOCKER_IMAGES = [
'bitnami/mariadb',
'bitnami/mongodb',
- 'bitnami/mysql',
- 'bitnami/postgresql',
'bitnami/redis',
'mysql',
+ 'bitnami/mysql',
+ 'mysql/mysql-server',
'mariadb',
+ 'postgis/postgis',
'postgres',
+ 'bitnami/postgresql',
+ 'supabase/postgres',
+ 'elestio/postgres',
'mongo',
'redis',
'memcached',
@@ -33,10 +37,10 @@
'neo4j',
'influxdb',
'clickhouse/clickhouse-server',
- 'supabase/postgres',
];
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
+ 'minio/minio',
'svhd/logto',
];
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index e252bda10..397bce029 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -325,38 +325,20 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push('traefik.http.middlewares.gzip.compress=true');
$labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https');
- $basic_auth = false;
- $basic_auth_middleware = null;
- $redirect = false;
- $redirect_middleware = null;
+ $middlewares_from_labels = collect([]);
if ($serviceLabels) {
- $basic_auth = $serviceLabels->contains(function ($value) {
- return str_contains($value, 'basicauth');
- });
- if ($basic_auth) {
- $basic_auth_middleware = $serviceLabels
- ->map(function ($item) {
- if (preg_match('/traefik\.http\.middlewares\.(.*?)\.basicauth\.users/', $item, $matches)) {
- return $matches[1];
- }
- })
- ->filter()
- ->first();
- }
- $redirect = $serviceLabels->contains(function ($value) {
- return str_contains($value, 'redirectregex');
- });
- if ($redirect) {
- $redirect_middleware = $serviceLabels
- ->map(function ($item) {
- if (preg_match('/traefik\.http\.middlewares\.(.*?)\.redirectregex\.regex/', $item, $matches)) {
- return $matches[1];
- }
- })
- ->filter()
- ->first();
- }
+ $middlewares_from_labels = $serviceLabels->map(function ($item) {
+ if (preg_match('/traefik\.http\.middlewares\.(.*?)(\.|$)/', $item, $matches)) {
+ return $matches[1];
+ }
+ if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) {
+ return explode(',', $matches[1]);
+ }
+ return null;
+ })->flatten()
+ ->filter()
+ ->unique();
}
foreach ($domains as $loop => $domain) {
try {
@@ -404,20 +386,15 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port");
}
if ($path !== '/') {
+ // Middleware handling
$middlewares = collect([]);
- if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) {
+ if ($is_stripprefix_enabled && !str($image)->contains('ghost')) {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares->push("{$https_label}-stripprefix");
}
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
- if ($basic_auth && $basic_auth_middleware) {
- $middlewares->push($basic_auth_middleware);
- }
- if ($redirect && $redirect_middleware) {
- $middlewares->push($redirect_middleware);
- }
if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost');
}
@@ -425,10 +402,13 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_non_www);
$middlewares->push($to_non_www_name);
}
- if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) {
+ if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
+ $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
+ $middlewares->push($middleware_name);
+ });
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
@@ -437,13 +417,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares = collect([]);
if ($is_gzip_enabled) {
$middlewares->push('gzip');
- }
- if ($basic_auth && $basic_auth_middleware) {
- $middlewares->push($basic_auth_middleware);
- }
- if ($redirect && $redirect_middleware) {
- $middlewares->push($redirect_middleware);
- }
+ }
if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost');
}
@@ -455,6 +429,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
+ $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
+ $middlewares->push($middleware_name);
+ });
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
@@ -490,12 +467,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
- if ($basic_auth && $basic_auth_middleware) {
- $middlewares->push($basic_auth_middleware);
- }
- if ($redirect && $redirect_middleware) {
- $middlewares->push($redirect_middleware);
- }
if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost');
}
@@ -507,6 +478,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
+ $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
+ $middlewares->push($middleware_name);
+ });
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
@@ -516,12 +490,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_gzip_enabled) {
$middlewares->push('gzip');
}
- if ($basic_auth && $basic_auth_middleware) {
- $middlewares->push($basic_auth_middleware);
- }
- if ($redirect && $redirect_middleware) {
- $middlewares->push($redirect_middleware);
- }
if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost');
}
@@ -533,6 +501,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
+ $middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
+ $middlewares->push($middleware_name);
+ });
if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index c4c15b8fe..e2693a2cd 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -96,6 +96,8 @@ function connectProxyToNetworks(Server $server)
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
+ "echo 'Successfully connected coolify-proxy to $network network.'",
+ "echo 'Proxy started and configured successfully!'",
];
});
} else {
@@ -104,6 +106,8 @@ function connectProxyToNetworks(Server $server)
"echo 'Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
+ "echo 'Successfully connected coolify-proxy to $network network.'",
+ "echo 'Proxy started and configured successfully!'",
];
});
}
@@ -144,6 +148,7 @@ function generate_default_proxy_configuration(Server $server)
'traefik.http.routers.traefik.service=api@internal',
'traefik.http.services.traefik.loadbalancer.server.port=8080',
'coolify.managed=true',
+ 'coolify.proxy=true',
];
$config = [
'networks' => $array_of_networks->toArray(),
@@ -159,6 +164,7 @@ function generate_default_proxy_configuration(Server $server)
'ports' => [
'80:80',
'443:443',
+ '443:443/udp',
'8080:8080',
],
'healthcheck' => [
@@ -182,6 +188,7 @@ function generate_default_proxy_configuration(Server $server)
'--entryPoints.http.http2.maxConcurrentStreams=50',
'--entrypoints.https.http.encodequerysemicolons=true',
'--entryPoints.https.http2.maxConcurrentStreams=50',
+ '--entrypoints.https.http3',
'--providers.docker.exposedbydefault=false',
'--providers.file.directory=/traefik/dynamic/',
'--providers.file.watch=true',
@@ -217,7 +224,6 @@ function generate_default_proxy_configuration(Server $server)
}
} elseif ($proxy_type === 'CADDY') {
$config = [
- 'version' => '3.8',
'networks' => $array_of_networks->toArray(),
'services' => [
'caddy' => [
@@ -235,13 +241,11 @@ function generate_default_proxy_configuration(Server $server)
'ports' => [
'80:80',
'443:443',
+ '443:443/udp',
+ ],
+ 'labels' => [
+ 'coolify.managed=true',
],
- // "healthcheck" => [
- // "test" => "wget -qO- http://localhost:80|| exit 1",
- // "interval" => "4s",
- // "timeout" => "2s",
- // "retries" => 5,
- // ],
'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro',
"{$proxy_path}/dynamic:/dynamic",
diff --git a/bootstrap/helpers/s3.php b/bootstrap/helpers/s3.php
index 4a2252016..2ee7bf44a 100644
--- a/bootstrap/helpers/s3.php
+++ b/bootstrap/helpers/s3.php
@@ -1,14 +1,11 @@
endpoint) {
- $is_digital_ocean = Str::contains($s3->endpoint, 'digitaloceanspaces.com');
- }
+
config()->set('filesystems.disks.custom-s3', [
'driver' => 's3',
'region' => $s3['region'],
@@ -17,7 +14,7 @@ function set_s3_target(S3Storage $s3)
'bucket' => $s3['bucket'],
'endpoint' => $s3['endpoint'],
'use_path_style_endpoint' => true,
- 'bucket_endpoint' => $is_digital_ocean,
+ 'bucket_endpoint' => $s3->isHetzner() || $s3->isDigitalOcean(),
'aws_url' => $s3->awsUrl(),
]);
}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 350f16837..f70c705c7 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -164,10 +164,10 @@ function get_route_parameters(): array
function get_latest_sentinel_version(): string
{
try {
- $response = Http::get('https://cdn.coollabs.io/sentinel/versions.json');
+ $response = Http::get('https://cdn.coollabs.io/coolify/versions.json');
$versions = $response->json();
- return data_get($versions, 'sentinel.version');
+ return data_get($versions, 'coolify.sentinel.version');
} catch (\Throwable $e) {
//throw $e;
ray($e->getMessage());
@@ -247,7 +247,7 @@ function is_transactional_emails_active(): bool
function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string
{
if (! $settings) {
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
}
config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
@@ -281,7 +281,7 @@ function base_ip(): string
if (isDev()) {
return 'localhost';
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->public_ipv4) {
return "$settings->public_ipv4";
}
@@ -309,7 +309,7 @@ function getFqdnWithoutPort(string $fqdn)
*/
function base_url(bool $withPort = true): string
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if ($settings->fqdn) {
return $settings->fqdn;
}
@@ -343,6 +343,11 @@ function isSubscribed()
{
return isSubscriptionActive() || auth()->user()->isInstanceAdmin();
}
+
+function isProduction(): bool
+{
+ return ! isDev();
+}
function isDev(): bool
{
return config('app.env') === 'local';
@@ -384,7 +389,7 @@ function send_internal_notification(string $message): void
}
function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null): void
{
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$type = set_transanctional_email_settings($settings);
if (! $type) {
throw new Exception('No email settings found.');
@@ -517,6 +522,11 @@ function sslip(Server $server)
function get_service_templates(bool $force = false): Collection
{
+ if (isDev()) {
+ $services = File::get(base_path('templates/service-templates.json'));
+
+ return collect(json_decode($services))->sortKeys();
+ }
if ($force) {
try {
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
@@ -703,7 +713,9 @@ function getTopLevelNetworks(Service|Application $resource)
return $value == $networkName || $key == $networkName;
});
if (! $networkExists) {
- $topLevelNetworks->put($networkDetails, null);
+ if (is_string($networkDetails) || is_int($networkDetails)) {
+ $topLevelNetworks->put($networkDetails, null);
+ }
}
}
}
@@ -753,7 +765,9 @@ function getTopLevelNetworks(Service|Application $resource)
return $value == $networkName || $key == $networkName;
});
if (! $networkExists) {
- $topLevelNetworks->put($networkDetails, null);
+ if (is_string($networkDetails) || is_int($networkDetails)) {
+ $topLevelNetworks->put($networkDetails, null);
+ }
}
}
}
@@ -819,6 +833,31 @@ function convertToArray($collection)
return $collection;
}
+function parseCommandFromMagicEnvVariable(Str|string $key): Stringable
+{
+ $value = str($key);
+ $count = substr_count($value->value(), '_');
+ if ($count === 2) {
+ if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
+ // SERVICE_FQDN_UMAMI
+ $command = $value->after('SERVICE_')->beforeLast('_');
+ } else {
+ // SERVICE_BASE64_UMAMI
+ $command = $value->after('SERVICE_')->beforeLast('_');
+ }
+ }
+ if ($count === 3) {
+ if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
+ // SERVICE_FQDN_UMAMI_1000
+ $command = $value->after('SERVICE_')->before('_');
+ } else {
+ // SERVICE_BASE64_64_UMAMI
+ $command = $value->after('SERVICE_')->beforeLast('_');
+ }
+ }
+
+ return str($command);
+}
function parseEnvVariable(Str|string $value)
{
$value = str($value);
@@ -850,6 +889,7 @@ function parseEnvVariable(Str|string $value)
} else {
// SERVICE_BASE64_64_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
+ ray($command);
}
}
}
@@ -970,7 +1010,7 @@ function validate_dns_entry(string $fqdn, Server $server)
if (str($host)->contains('sslip.io')) {
return true;
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
$is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled');
if (! $is_dns_validation_enabled) {
return true;
@@ -1090,7 +1130,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId =
if ($domainFound) {
return true;
}
- $settings = \App\Models\InstanceSettings::get();
+ $settings = instanceSettings();
if (data_get($settings, 'fqdn')) {
$domain = data_get($settings, 'fqdn');
if (str($domain)->endsWith('/')) {
@@ -1134,10 +1174,10 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) {
if ($resource->uuid !== $app->uuid) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:
{$app->name}.");
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
Link: {$app->name}");
}
} elseif ($domain) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:
{$app->name}.");
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
Link: {$app->name}");
}
}
}
@@ -1153,16 +1193,16 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) {
if ($resource->uuid !== $app->uuid) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:
{$app->name}.");
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource:
Link: {$app->service->name}");
}
} elseif ($domain) {
- throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:
{$app->name}.");
+ throw new \RuntimeException("Domain $naked_domain is already in use by another resource: