Merge branch 'new-services' into upgrade_authentik

This commit is contained in:
🏔️ Peak
2024-10-07 15:34:13 +02:00
committed by GitHub
507 changed files with 17827 additions and 6395 deletions

View File

@@ -1,16 +1,34 @@
APP_NAME=Coolify-localhost
APP_ID=development
# Coolify Configuration
APP_ENV=local
APP_NAME="Coolify Development"
APP_ID=development
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_PORT=8000
MUX_ENABLED=false
APP_DEBUG=true
SSH_MUX_ENABLED=true
# PostgreSQL Database Configuration
DB_DATABASE=coolify
DB_USERNAME=coolify
DB_PASSWORD=password
DB_HOST=host.docker.internal
DB_PORT=5432
# Ray Configuration
# Set to true to enable Ray
RAY_ENABLED=false
# Set custom ray port
# RAY_PORT=
# Enable Laravel Telescope for debugging
TELESCOPE_ENABLED=false
# Selenium Driver URL for Dusk
DUSK_DRIVER_URL=http://selenium:4444
## For Andras only
# To purge cache
# Special Keys for Andras
# For cache purging
BUNNY_API_KEY=
# To upload assets
# For asset uploads
BUNNY_STORAGE_API_KEY=

View File

@@ -1,10 +1,16 @@
# Coolify Configuration
APP_ID=
APP_NAME=Coolify
APP_KEY=
# PostgreSQL Database Configuration
DB_USERNAME=coolify
DB_PASSWORD=
# Redis Configuration
REDIS_PASSWORD=
# Pusher Configuration
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=

View File

@@ -0,0 +1,65 @@
name: 🐞 Bug Report
description: "File a new bug report."
title: "[Bug]: "
labels: ["🐛 Bug", "🔍 Triage"]
body:
- type: markdown
attributes:
value: |
> [!IMPORTANT]
> **Please ensure you are using the latest version of Coolify before submitting an issue, as the bug may have already been fixed in a recent update.** (Of course, if you're experiencing an issue on the latest version that wasn't present in a previous version, please let us know.)
# 💎 Bounty Program (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
- If you would like to prioritize the issue resolution, consider adding a bounty to this issue through our [Bounty Program](https://console.algora.io/org/coollabsio/bounties/new).
- type: textarea
attributes:
label: Error Message and Logs
description: Provide a detailed description of the error or exception you encountered, along with any relevant log output.
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: Please provide a step-by-step guide to reproduce the issue. Be as detailed as possible, otherwise we may not be able to assist you.
value: |
1.
2.
3.
4.
validations:
required: true
- type: input
attributes:
label: Example Repository URL
description: If applicable, provide a URL to a repository demonstrating the issue.
- type: input
attributes:
label: Coolify Version
description: Please provide the Coolify version you are using. This can be found in the top left corner of your Coolify dashboard.
placeholder: "v4.0.0-beta.335"
validations:
required: true
- type: dropdown
attributes:
label: Are you using Coolify Cloud?
options:
- "No (self-hosted)"
- "Yes (Coolify Cloud)"
validations:
required: true
- type: input
attributes:
label: Operating System and Version (self-hosted)
description: Run `cat /etc/os-release` or `lsb_release -a` in your terminal and provide the operating system and version.
placeholder: "Ubuntu 22.04"
- type: textarea
attributes:
label: Additional Information
description: Any other relevant details about the issue.

View File

@@ -0,0 +1,31 @@
name: 💎 Enhancement Bounty
description: "Propose a new feature, service, or improvement with an attached bounty."
title: "[Enhancement]: "
labels: ["✨ Enhancement", "🔍 Triage"]
body:
- type: markdown
attributes:
value: |
> [!IMPORTANT]
> **This issue template is exclusively for proposing new features, services, or improvements with an attached bounty.** Enhancements without a bounty can be discussed in the appropriate category of [Github Discussions](https://github.com/coollabsio/coolify/discussions).
# 💎 Add a Bounty (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
- [Click here to add the required bounty](https://console.algora.io/org/coollabsio/bounties/new)
- type: dropdown
attributes:
label: Request Type
description: Select the type of request you are making.
options:
- New Feature
- New Service
- Improvement
validations:
required: true
- type: textarea
attributes:
label: Description
description: Provide a detailed description of the feature, improvement, or service you are proposing.
validations:
required: true

View File

@@ -1,46 +0,0 @@
name: Bug report
description: "Create a new bug report."
title: "[Bug]: "
body:
- type: markdown
attributes:
value: >-
# 💎 Bounty program (with
[algora.io](https://console.algora.io/org/coollabsio/bounties/new))
If you would like to prioritize the issue resolution, you can add bounty
to this issue.
Click [here](https://console.algora.io/org/coollabsio/bounties/new) to
get started.
- type: textarea
attributes:
label: Description
description: A clear and concise description of the problem
- type: textarea
attributes:
label: Minimal Reproduction (if possible, example repository)
description: Please provide a step by step guide to reproduce the issue.
validations:
required: true
- type: textarea
attributes:
label: Exception or Error
description: Please provide error logs if possible.
- type: input
attributes:
label: Version
description: Coolify's version (see top of your screen).
validations:
required: true
- type: checkboxes
attributes:
label: Cloud?
description: "Are you using the cloud version of Coolify?"
options:
- label: 'Yes'
required: false
- label: 'No'
required: false

View File

@@ -1,8 +1,18 @@
blank_issues_enabled: false
contact_links:
- name: 🤔 Community Support (Chat)
- name: 🤔 Questions and Community Support
url: https://coollabs.io/discord
about: Reach out to us on Discord.
- name: 🙋‍♂️ Feature Requests
url: https://github.com/coollabsio/coolify/discussions/categories/new-features
about: All feature requests will be discussed here.
about: If you have any questions, reach out to us on Discord inside the "#support" channel.
- name: 💡 Feature Request
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests
about: Suggest a new feature for Coolify.
- name: ⚙️ Service Request
url: https://github.com/coollabsio/coolify/discussions/categories/service-requests
about: Request a new service integration for Coolify.
- name: 🔧 Improvements
url: https://github.com/coollabsio/coolify/discussions/categories/improvements
about: Suggest improvements to existing features for Coolify.

View File

@@ -1 +1,13 @@
> Always use `next` branch as destination branch for PRs, not `main`
## Submit Checklist (REMOVE THIS SECTION BEFORE SUBMITTING)
- [ ] I have selected the `next` branch as the destination for my PR, not `main`.
- [ ] I have listed all changes in the `Changes` section.
- [ ] I have filled out the `Issues` section with the issue/discussion link(s) (if applicable).
- [ ] I have tested my changes.
- [ ] I have considered backwards compatibility.
- [ ] I have removed this checklist and any unused sections.
## Changes
-
## Issues
- fix #

View File

@@ -25,6 +25,10 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
@@ -33,7 +37,9 @@ jobs:
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
labels: |
coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
@@ -47,6 +53,10 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
@@ -55,7 +65,9 @@ jobs:
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -75,10 +87,15 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -25,6 +25,10 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
@@ -33,7 +37,9 @@ jobs:
file: docker/coolify-helper/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
labels: |
coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
@@ -47,6 +53,10 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
@@ -55,7 +65,9 @@ jobs:
file: docker/coolify-helper/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
@@ -75,10 +87,15 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}

View File

@@ -0,0 +1,103 @@
name: Coolify Realtime Development (v4)
on:
push:
branches: [ "next" ]
paths:
- .github/workflows/coolify-realtime.yml
- docker/coolify-realtime/Dockerfile
- docker/coolify-realtime/terminal-server.js
- docker/coolify-realtime/package.json
- docker/coolify-realtime/soketi-entrypoint.sh
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify-realtime"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
labels: |
coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}

103
.github/workflows/coolify-realtime.yml vendored Normal file
View File

@@ -0,0 +1,103 @@
name: Coolify Realtime (v4)
on:
push:
branches: [ "main" ]
paths:
- .github/workflows/coolify-realtime.yml
- docker/coolify-realtime/Dockerfile
- docker/coolify-realtime/terminal-server.js
- docker/coolify-realtime/package.json
- docker/coolify-realtime/soketi-entrypoint.sh
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify-realtime"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
labels: |
coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -1,81 +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
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
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
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 }}

View File

@@ -4,6 +4,8 @@ on:
push:
branches: ["main"]
paths-ignore:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
- templates/service-templates.json
env:

View File

@@ -0,0 +1,78 @@
name: Remove Labels and Assignees on Issue Close
on:
issues:
types: [closed]
pull_request:
types: [closed]
pull_request_target:
types: [closed]
jobs:
remove-labels-and-assignees:
runs-on: ubuntu-latest
steps:
- name: Remove labels and assignees
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
async function processIssue(issueNumber) {
try {
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: issueNumber
});
const labelsToKeep = currentLabels
.filter(label => label.name === '⏱︎ Stale')
.map(label => label.name);
await github.rest.issues.setLabels({
owner,
repo,
issue_number: issueNumber,
labels: labelsToKeep
});
const { data: issue } = await github.rest.issues.get({
owner,
repo,
issue_number: issueNumber
});
if (issue.assignees && issue.assignees.length > 0) {
await github.rest.issues.removeAssignees({
owner,
repo,
issue_number: issueNumber,
assignees: issue.assignees.map(assignee => assignee.login)
});
}
} catch (error) {
if (error.status !== 404) {
console.error(`Error processing issue ${issueNumber}:`, error);
}
}
}
if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
const issue = context.payload.issue || context.payload.pull_request;
await processIssue(issue.number);
}
if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
const pr = context.payload.pull_request;
if (pr.body) {
const issueReferences = pr.body.match(/#(\d+)/g);
if (issueReferences) {
for (const reference of issueReferences) {
const issueNumber = parseInt(reference.substring(1));
await processIssue(issueNumber);
}
}
}
}

243
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,243 @@
# Contributing to Coolify
> "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel.
## Table of Contents
1. [Setup Development Environment](#1-setup-development-environment)
2. [Verify Installation](#2-verify-installation-optional)
3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
4. [Set up Environment Variables](#4-set-up-environment-variables)
5. [Start Coolify](#5-start-coolify)
6. [Start Development](#6-start-development)
7. [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
Follow the steps below for your operating system:
<details>
<summary><strong>Windows</strong></summary>
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?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/?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?ref=coolify)
</details>
<details>
<summary><strong>MacOS</strong></summary>
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?ref=coolify)
- Docker Desktop:
- 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?ref=coolify)
</details>
<details>
<summary><strong>Linux</strong></summary>
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/?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/?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?ref=coolify)
</details>
## 2. Verify Installation (Optional)
After installing Docker (or Orbstack) and Spin, verify the installation:
1. Open a terminal or command prompt
2. Run the following commands:
```bash
docker --version
spin --version
```
You should see version information for both Docker and Spin.
## 3. Fork and Setup Local Repository
1. Fork the [Coolify](https://github.com/coollabsio/coolify) repository to your GitHub account.
2. Install a code editor on your machine (choose one):
| Editor | Platform | Download Link |
|--------|----------|---------------|
| Visual Studio Code (recommended free) | Windows/macOS/Linux | [Download](https://code.visualstudio.com/download?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/?ref=coolify)
- Open GitHub Desktop and login with your GitHub account
- Click on `File` -> `Clone Repository` select `github.com` as the repository location, then select your forked Coolify repository, choose the local path and then click `Clone`
4. Open the cloned Coolify Repository in your chosen code editor.
## 4. Set up Environment Variables
1. In the Code Editor, locate the `.env.development.example` file in the root directory of your local Coolify repository.
2. Duplicate the `.env.development.example` file and rename the copy to `.env`.
3. Open the new `.env` file and review its contents. Adjust any environment variables as needed for your development setup.
4. If you encounter errors during database migrations, update the database connection settings in your `.env` file. Use the IP address or hostname of your PostgreSQL database container. You can find this information by running `docker ps` after executing `spin up`.
5. Save the changes to your `.env` file.
## 5. Start Coolify
1. Open a terminal in the local Coolify directory.
2. Run the following command in the terminal (leave that terminal open):
```bash
spin up
```
> [!NOTE]
> You may see some errors, but don't worry; this is expected.
3. If you encounter permission errors, especially on macOS, use:
```bash
sudo spin up
```
> [!NOTE]
> If you change environment variables afterwards or anything seems broken, press Ctrl + C to stop the process and run `spin up` again.
## 6. Start Development
1. Access your Coolify instance:
- URL: `http://localhost:8000`
- Login: `test@example.com`
- Password: `password`
2. Additional development tools:
| Tool | URL | Note |
|------|-----|------|
| Laravel Horizon (scheduler) | `http://localhost:8000/horizon` | Only accessible when logged in as root user |
| Mailpit (email catcher) | `http://localhost:8025` | |
| Telescope (debugging tool) | `http://localhost:8000/telescope` | Disabled by default |
> [!NOTE]
> To enable Telescope, add the following to your `.env` file:
> ```env
> TELESCOPE_ENABLED=true
> ```
## 7. 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:
1. **Database Migrations**: After switching branches or making changes to the database structure, always run migrations:
```bash
docker exec -it coolify php artisan migrate
```
2. **Resetting Development Setup**: To reset your development setup to a clean database with default values:
```bash
docker exec -it coolify php artisan migrate:fresh --seed
```
3. **Troubleshooting**: If you encounter unexpected behavior, ensure your database is up-to-date with the latest migrations and if possible reset the development setup to eliminate any environment-specific issues.
> [!IMPORTANT]
> Forgetting to migrate the database can cause problems, so make it a habit to run migrations after pulling changes or switching branches.
## Resetting Development Environment
If you encounter issues or break your database or something else, follow these steps to start from a clean slate (works since `v4.0.0-beta.342`):
1. Stop all running containers `ctrl + c`.
2. Remove all Coolify containers:
```bash
docker rm coolify coolify-db coolify-redis coolify-realtime coolify-testing-host coolify-minio coolify-vite-1 coolify-mail
```
3. Remove Coolify volumes (it is possible that the volumes have no `coolify` prefix on your machine, in that case remove the prefix from the command):
```bash
docker volume rm coolify_dev_backups_data coolify_dev_postgres_data coolify_dev_redis_data coolify_dev_coolify_data coolify_dev_minio_data
```
4. Remove unused images:
```bash
docker image prune -a
```
5. Start Coolify again:
```bash
spin up
```
6. Run database migrations and seeders:
```bash
docker exec -it coolify php artisan migrate:fresh --seed
```
After completing these steps, you'll have a fresh development setup.
> [!IMPORTANT]
> Always run database migrations and seeders after switching branches or pulling updates to ensure your local database structure matches the current codebase and includes necessary seed data.
## Additional Contribution Guidelines
### Contributing a New Service
To add a new service to Coolify, please refer to our documentation:
[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service)
### Contributing to Documentation
To contribute to the Coolify documentation, please refer to this guide:
[Contributing to the Coolify Documentation](https://github.com/coollabsio/documentation-coolify/blob/main/CONTRIBUTING.md)

View File

@@ -1,34 +0,0 @@
# Contributing
> "First, thanks for considering to contribute to my project.
It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
You can ask for guidance anytime on our
[Discord server](https://coollabs.io/discord) in the `#contribution` channel.
## Code Contribution
### 1) Setup your development environment
- You need to have Docker Engine (or equivalent) [installed](https://docs.docker.com/engine/install/) on your system.
- For better DX, install [Spin](https://serversideup.net/open-source/spin/).
### 2) Set your environment variables
- Copy [.env.development.example](./.env.development.example) to .env.
## 3) Start & setup Coolify
- Run `spin up` - You can notice that errors will be thrown. Don't worry.
- If you see weird permission errors, especially on Mac, run `sudo spin up` instead.
### 4) Start development
You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user.
Mails are caught by Mailpit: `localhost:8025`
## New Service Contribution
Check out the docs [here](https://coolify.io/docs/knowledge-base/add-a-service).

View File

@@ -35,20 +35,32 @@ Thank you so much!
Special thanks to our biggest sponsors!
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="cccareers logo" width="200"/></a>
<a href="http://htznr.li/CoolifyXHetzner" target="_blank"><img src="./other/logos/hetzner.jpg" alt="hetzner logo" width="150"/></a>
<a href="https://logto.io/?ref=coolify" target="_blank"><img src="./other/logos/logto.webp" alt="logto logo" width="150"/></a>
<a href="https://bc.direct/?ref=coolify.io" target="_blank"><img src="./other/logos/bc.png" alt="bc direct logo" width="200"/></a>
<a href="https://www.quantcdn.io/?ref=coolify.io" target="_blank"><img src="./other/logos/quant.svg" alt="quantcdn logo" width="150"/></a>
<a href="https://arcjet.com/?ref=coolify.io" target="_blank"><img src="./other/logos/arcjet.svg" alt="arcjet logo" width="200"/></a>
<a href="https://supa.guide/?ref=coolify.io" target="_blank"><img src="./other/logos/supaguide.png" alt="supaguide logo" width="200"/></a>
<a href="https://tigrisdata.com/?ref=coolify.io" target="_blank"><img src="./other/logos/tigris.svg" alt="tigris logo" width="140"/></a>
<a href="https://fractalnetworks.co/?ref=coolify.io" target="_blank"><img src="./other/logos/fractal.svg" alt="fractal logo" width="180"/></a>
<a href="https://coolify.ad.vin/?ref=coolify.io" target="_blank"><img src="./other/logos/advin.png" alt="advin logo" width="250"/></a>
<a href="https://trieve.ai/?ref=coolify.io" target="_blank"><img src="./other/logos/trieve_bg.png" alt="trieve logo" width="180"/></a>
<a href="https://blacksmith.sh/?ref=coolify.io" target="_blank"><img src="./other/logos/blacksmith.svg" alt="blacksmith logo" width="200"/></a>
<a href="https://latitude.sh/?ref=coolify.io" target="_blank"><img src="./other/logos/latitude.svg" alt="latitude logo" width="200"/></a>
<a href="https://brand.dev/?ref=coolify.io" target="_blank"><img src="./other/logos/branddev.png" alt="branddev logo" width="200"/></a>
### Special Sponsors
![image](https://github.com/user-attachments/assets/c95a07df-7c5a-4e77-a35a-81f25fcbece1)
* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry.
* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions.
* [Logto](https://logto.io/?ref=coolify) - An open-source authentication and authorization solution for building secure login systems and managing user identities.
* [BC Direct](https://bc.direct/?ref=coolify.io) - A digital marketing agency specializing in e-commerce solutions and online business growth strategies.
* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
* [Fractal Networks](https://fractalnetworks.co/?ref=coolify.io) - A decentralized network infrastructure company focusing on secure and private communication solutions.
* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies.
* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers.
* [Latitude](https://latitude.sh/?ref=coolify.io) - A cloud computing platform offering bare metal servers and cloud instances for developers and businesses.
* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities.
* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries.
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
* [Glueops](https://www.glueops.dev/?ref=coolify.io) - A DevOps consulting company providing infrastructure automation and cloud optimization services.
* [Ubicloud](https://ubicloud.com/?ref=coolify.io) - An open-source alternative to hyperscale cloud providers, offering high-performance cloud computing services.
* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses.
* [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly.
* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
## Github Sponsors ($40+)
<a href="https://serpapi.com/?ref=coolify.io"><img width="60px" alt="SerpAPI" src="https://github.com/serpapi.png"/></a>
@@ -71,8 +83,11 @@ Special thanks to our biggest sponsors!
<a href="https://github.com/aniftyco"><img src="https://github.com/aniftyco.png" width="60px" alt="NiftyCo" /></a>
<a href="https://github.com/iujlaki"><img src="https://github.com/iujlaki.png" width="60px" alt="Imre Ujlaki" /></a>
<a href="https://il.ly"><img src="https://github.com/Illyism.png" width="60px" alt="Ilias Ism" /></a>
<a href="https://www.breakcold.com/?utm_source=coolify.io"><img src="https://github.com/breakcold.png" width="60px" alt="Breakcold" /></a>
<a href="https://github.com/urtho"><img src="https://github.com/urtho.png" width="60px" alt="Paweł Pierścionek" /></a>
<a href="https://github.com/monocursive"><img src="https://github.com/monocursive.png" width="60px" alt="Michael Mazurczak" /></a>
<a href="https://formbricks.com/?utm_source=coolify.io"><img src="https://github.com/formbricks.png" width="60px" alt="Formbricks" /></a>
<a href="https://x.com/adithsuhas17?utm_source=coolify.io"><img src="https://github.com/adith-suhas-sv.png" width="60px" alt="Adith Suhas" /></a>
## Organizations
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>

130
RELEASE.md Normal file
View File

@@ -0,0 +1,130 @@
# 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 Feature Branches**
- Improvements, fixes, and new features are developed on the `next` branch or separate feature branches.
2. **Merging to `main`**
- Once ready, changes are merged from the `next` branch into the `main` branch.
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 GitHub release is manually created with details of the changes made in the version.
5. **Updating the CDN**
- 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 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
<details>
<summary><strong>Stable (coming soon)</strong></summary>
- **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
```
</details>
<details>
<summary><strong>Nightly</strong></summary>
- **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
```
</details>
<details>
<summary><strong>Beta</strong></summary>
- **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
```
</details>
> [!WARNING]
> Do not use nightly/beta builds in production as there is no guarantee of stability.
## Version Availability
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 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
> [!CAUTION]
> Updating to unreleased versions is not recommended and may cause issues. Use at your own risk!
To update your Coolify instance to a specific (unreleased) version, use the following command:
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash -s <version>
```
Replace `<version>` with the version you want to update to (for example `4.0.0-beta.332`).

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Application;
use App\Actions\Server\CleanupDocker;
use App\Models\Application;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,44 +10,35 @@ class StopApplication
{
use AsAction;
public function handle(Application $application, bool $previewDeployments = false)
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{
if ($application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
return;
}
$servers = collect([]);
$servers->push($application->destination->server);
$application->additional_servers->map(function ($server) use ($servers) {
$servers->push($server);
});
foreach ($servers as $server) {
try {
$server = $application->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
if ($previewDeployments) {
$containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
} else {
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
}
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false);
}
}
ray('Stopping application: '.$application->name);
if ($server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
return;
}
$containersToStop = $application->getContainersToStop($previewDeployments);
$application->stopContainers($containersToStop, $server);
if ($application->build_pack === 'dockercompose') {
// remove network
$uuid = $application->uuid;
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
$application->delete_connected_networks($application->uuid);
}
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
} catch (\Exception $e) {
ray($e->getMessage());
return $e->getMessage();
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Actions\CoolifyTask;
use App\Enums\ActivityTypes;
use App\Enums\ProcessStatus;
use App\Helpers\SshMultiplexingHelper;
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Server;
use Illuminate\Process\ProcessResult;
@@ -137,7 +138,7 @@ class RunRemoteProcess
$command = $this->activity->getExtraProperty('command');
$server = Server::whereUuid($server_uuid)->firstOrFail();
return generateSshCommand($server, $command);
return SshMultiplexingHelper::generateSshCommand($server, $command);
}
protected function handleOutput(string $type, string $output)

View File

@@ -79,14 +79,7 @@ class StartClickhouse
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -102,6 +95,11 @@ class StartClickhouse
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -162,6 +160,8 @@ class StartClickhouse
$environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}
}

View File

@@ -46,9 +46,6 @@ class StartDragonfly
'networks' => [
$this->database->destination->network,
],
'ulimits' => [
'memlock' => '-1',
],
'labels' => [
'coolify.managed' => 'true',
],
@@ -79,14 +76,7 @@ class StartDragonfly
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -102,6 +92,11 @@ class StartDragonfly
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";

View File

@@ -78,14 +78,7 @@ class StartKeydb
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -110,6 +103,10 @@ class StartKeydb
];
$docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -166,6 +163,8 @@ class StartKeydb
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}

View File

@@ -73,14 +73,7 @@ class StartMariadb
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -104,6 +97,11 @@ class StartMariadb
'read_only' => true,
];
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -171,6 +169,8 @@ class StartMariadb
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}

View File

@@ -81,14 +81,7 @@ class StartMongodb
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -121,6 +114,10 @@ class StartMongodb
'read_only' => true,
];
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -185,6 +182,8 @@ class StartMongodb
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}

View File

@@ -73,14 +73,7 @@ class StartMysql
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -104,6 +97,11 @@ class StartMysql
'read_only' => true,
];
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -171,6 +169,8 @@ class StartMysql
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}

View File

@@ -80,14 +80,7 @@ class StartPostgresql
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -126,6 +119,10 @@ class StartPostgresql
'config_file=/etc/postgresql/postgresql.conf',
];
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -193,6 +190,8 @@ class StartPostgresql
$environment_variables->push("POSTGRES_DB={$this->database->postgres_db}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}

View File

@@ -82,14 +82,7 @@ class StartRedis
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$container_name]['logging'] = generate_fluentd_configuration();
}
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
@@ -114,6 +107,11 @@ class StartRedis
];
$docker_compose['services'][$container_name]['command'] = "redis-server /usr/local/etc/redis/redis.conf --requirepass {$this->database->redis_password} --appendonly yes";
}
// Add custom docker run options
$docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options);
$docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
@@ -170,6 +168,8 @@ class StartRedis
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
}
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
return $environment_variables->all();
}

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Database;
use App\Actions\Server\CleanupDocker;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
@@ -10,25 +11,65 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Process;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
$server = $database->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false);
$this->stopContainer($database, $database->uuid, 300);
if (! $isDeleteOperation) {
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
}
if ($database->is_public) {
StopDatabaseProxy::run($database);
}
return 'Database stopped successfully';
}
private function stopContainer($database, string $containerName, int $timeout = 300): void
{
$server = $database->destination->server;
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->forceStopContainer($containerName, $server);
break;
}
usleep(100000);
}
$this->removeContainer($containerName, $server);
}
private function forceStopContainer(string $containerName, $server): void
{
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
}
private function removeContainer(string $containerName, $server): void
{
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
}
private function deleteConnectedNetworks($uuid, $server)
{
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
}

View File

@@ -543,7 +543,7 @@ class GetContainersStatus
}
}
}
$exitedServices = $exitedServices->unique('id');
$exitedServices = $exitedServices->unique('uuid');
foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) {
continue;
@@ -651,8 +651,9 @@ class GetContainersStatus
// $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';

View File

@@ -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 @@ class CreateNewUser implements CreatesNewUsers
$team = $user->teams()->first();
// Disable registration after first user is created
$settings = InstanceSettings::get();
$settings = instanceSettings();
$settings->is_registration_enabled = false;
$settings->save();
} else {

View File

@@ -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,

View File

@@ -22,7 +22,7 @@ class CheckConfiguration
];
$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');

View File

@@ -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;
@@ -26,7 +29,7 @@ class CheckProxy
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
return false;
}
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
if (! $uptime) {
throw new \Exception($error);
}
@@ -62,22 +65,42 @@ class CheckProxy
$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.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
$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.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} 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.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
}

View File

@@ -26,7 +26,7 @@ class StartProxy
}
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 @@ class StartProxy
"echo 'Creating required Docker Compose file.'",
"echo 'Starting coolify-proxy.'",
'docker stack deploy -c docker-compose.yml coolify-proxy',
"echo 'Proxy started successfully.'",
"echo 'Successfully started coolify-proxy.'",
]);
} else {
$caddfile = 'import /dynamic/*.caddy';
@@ -46,11 +46,14 @@ class StartProxy
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
"echo 'Stopping existing coolify-proxy.'",
'docker compose down -v --remove-orphans > /dev/null 2>&1',
'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then',
" echo 'Stopping and removing existing coolify-proxy.'",
' docker rm -f coolify-proxy || true',
" echo 'Successfully stopped and removed existing coolify-proxy.'",
'fi',
"echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo 'Proxy started successfully.'",
"echo 'Successfully started coolify-proxy.'",
]);
$commands = $commands->merge(connectProxyToNetworks($server));
}

View File

@@ -9,17 +9,31 @@ class CleanupDocker
{
use AsAction;
public function handle(Server $server, bool $force = true)
public function handle(Server $server)
{
// cleanup docker images, containers, and builder caches
if ($force) {
instant_remote_process(['docker image prune -af'], $server, false);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
instant_remote_process(['docker builder prune -af'], $server, false);
} else {
instant_remote_process(['docker image prune -f'], $server, false);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
instant_remote_process(['docker builder prune -f'], $server, false);
$settings = instanceSettings();
$helperImageVersion = data_get($settings, 'helper_version');
$helperImage = config('coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion";
$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);
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Server;
use App\Events\CloudflareTunnelConfigured;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -40,12 +41,17 @@ class ConfigureCloudflared
instant_remote_process($commands, $server);
} catch (\Throwable $e) {
ray($e);
$server->settings->is_cloudflare_tunnel = false;
$server->settings->save();
throw $e;
} finally {
CloudflareTunnelConfigured::dispatch($server->team_id);
$commands = collect([
'rm -fr /tmp/cloudflared',
]);
instant_remote_process($commands, $server);
}
}
}

View File

@@ -47,7 +47,11 @@ class InstallLogDrain
[FILTER]
Name modify
Match *
Set server_name {$server->name}
Set coolify.server_name {$server->name}
Rename COOLIFY_APP_NAME coolify.app_name
Rename COOLIFY_PROJECT_NAME coolify.project_name
Rename COOLIFY_SERVER_IP coolify.server_ip
Rename COOLIFY_ENVIRONMENT_NAME coolify.environment_name
[OUTPUT]
Name nrlogs
Match *
@@ -98,7 +102,11 @@ class InstallLogDrain
[FILTER]
Name modify
Match *
Set server_name {$server->name}
Set coolify.server_name {$server->name}
Rename COOLIFY_APP_NAME coolify.app_name
Rename COOLIFY_PROJECT_NAME coolify.project_name
Rename COOLIFY_SERVER_IP coolify.server_ip
Rename COOLIFY_ENVIRONMENT_NAME coolify.environment_name
[OUTPUT]
Name http
Match *

View File

@@ -2,10 +2,8 @@
namespace App\Actions\Server;
use App\Models\InstanceSettings;
use App\Jobs\PullHelperImageJob;
use App\Models\Server;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
class UpdateCoolify
@@ -21,17 +19,12 @@ class UpdateCoolify
public function handle($manual_update = false)
{
try {
$settings = InstanceSettings::get();
$settings = instanceSettings();
$this->server = Server::find(0);
if (! $this->server) {
return;
}
CleanupDocker::dispatch($this->server, false)->onQueue('high');
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
}
CleanupDocker::dispatch($this->server)->onQueue('high');
$this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version');
if (! $manual_update) {
@@ -62,10 +55,18 @@ class UpdateCoolify
return;
}
$all_servers = Server::all();
$servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
PullHelperImageJob::dispatch($server);
}
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
remote_process([
'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',
"bash /data/coolify/source/upgrade.sh $this->latestVersion",
], $this->server);
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Service;
use App\Actions\Server\CleanupDocker;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,11 +10,11 @@ class DeleteService
{
use AsAction;
public function handle(Service $service)
public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
{
try {
$server = data_get($service, 'server');
if ($server->isFunctional()) {
if ($deleteVolumes && $server->isFunctional()) {
$storagesToDelete = collect([]);
$service->environment_variables()->delete();
@@ -33,13 +34,29 @@ class DeleteService
foreach ($storagesToDelete as $storage) {
$commands[] = "docker volume rm -f $storage->name";
}
$commands[] = "docker rm -f $service->uuid";
instant_remote_process($commands, $server, false);
// Execute volume deletion first, this must be done first otherwise volumes will not be deleted.
if (! empty($commands)) {
foreach ($commands as $command) {
$result = instant_remote_process([$command], $server, false);
if ($result !== 0) {
ray("Failed to execute: $command");
}
}
}
}
if ($deleteConnectedNetworks) {
$service->delete_connected_networks($service->uuid);
}
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
} finally {
if ($deleteConfigurations) {
$service->delete_configurations();
}
foreach ($service->applications()->get() as $application) {
$application->forceDelete();
}
@@ -50,6 +67,11 @@ class DeleteService
$task->delete();
}
$service->tags()->detach();
$service->forceDelete();
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
}
}
}

View File

@@ -16,8 +16,10 @@ class StartService
$service->saveComposeConfigs();
$commands[] = 'cd '.$service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
if ($service->networks()->count() > 0) {
$commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
}
$commands[] = 'echo Starting service.';
$commands[] = "echo 'Pulling images.'";
$commands[] = 'docker compose pull';
@@ -29,7 +31,7 @@ class StartService
$network = $service->destination->network;
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
foreach ($serviceNames as $serviceName => $serviceConfig) {
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
}
}
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Service;
use App\Actions\Server\CleanupDocker;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,35 +10,27 @@ class StopService
{
use AsAction;
public function handle(Service $service)
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
try {
$server = $service->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
ray('Stopping service: '.$service->name);
$applications = $service->applications()->get();
foreach ($applications as $application) {
instant_remote_process(command: ["docker stop --time=30 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
$application->update(['status' => 'exited']);
$containersToStop = $service->getContainersToStop();
$service->stopContainers($containersToStop, $server);
if (! $isDeleteOperation) {
$service->delete_connected_networks($service->uuid);
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
}
$dbs = $service->databases()->get();
foreach ($dbs as $db) {
instant_remote_process(command: ["docker stop --time=30 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
$db->update(['status' => 'exited']);
}
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
} catch (\Exception $e) {
ray($e->getMessage());
return $e->getMessage();
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Console\Command;
class CheckApplicationDeploymentQueue extends Command
{
protected $signature = 'check:deployment-queue {--force} {--seconds=3600}';
protected $description = 'Check application deployment queue.';
public function handle()
{
$seconds = $this->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);
}
}
}

View File

@@ -7,9 +7,9 @@ use Illuminate\Console\Command;
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()
{

View File

@@ -1,24 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class CleanupQueue extends Command
{
protected $signature = 'cleanup:queue';
protected $description = 'Cleanup Queue';
public function handle()
{
echo "Running queue cleanup...\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
foreach ($keys as $key) {
$keyWithoutPrefix = str_replace($prefix, '', $key);
Redis::connection()->del($keyWithoutPrefix);
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class CleanupRedis extends Command
{
protected $signature = 'cleanup:redis';
protected $description = 'Cleanup Redis';
public function handle()
{
echo "Cleanup Redis keys.\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
collect($keys)->each(function ($key) use ($prefix) {
$keyWithoutPrefix = str_replace($prefix, '', $key);
Redis::connection()->del($keyWithoutPrefix);
});
$queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*');
collect($queueOverlaps)->each(function ($key) {
Redis::connection()->del($key);
});
}
}

View File

@@ -2,8 +2,13 @@
namespace App\Console\Commands;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
@@ -33,6 +38,27 @@ class CleanupStuckedResources extends Command
private function cleanup_stucked_resources()
{
try {
$servers = Server::all()->filter(function ($server) {
return $server->isFunctional();
});
foreach ($servers as $server) {
CleanupHelperContainersJob::dispatch($server);
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
}
try {
$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) {
@@ -42,6 +68,17 @@ class CleanupStuckedResources extends Command
} catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
}
try {
$applicationsPreviews = ApplicationPreview::get();
foreach ($applicationsPreviews as $applicationPreview) {
if (! data_get($applicationPreview, 'application')) {
echo "Deleting stuck application preview: {$applicationPreview->uuid}\n";
$applicationPreview->delete();
}
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
}
try {
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($postgresqls as $postgresql) {
@@ -153,6 +190,18 @@ class CleanupStuckedResources extends Command
echo "Error in cleaning stuck scheduledtasks: {$e->getMessage()}\n";
}
try {
$scheduled_backups = ScheduledDatabaseBackup::all();
foreach ($scheduled_backups as $scheduled_backup) {
if (! $scheduled_backup->server()) {
echo "Deleting stuck scheduledbackup: {$scheduled_backup->name}\n";
$scheduled_backup->delete();
}
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck scheduledbackups: {$e->getMessage()}\n";
}
// Cleanup any resources that are not attached to any environment or destination or server
try {
$applications = Application::all();

View File

@@ -48,6 +48,13 @@ class Dev extends Command
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) {

View File

@@ -5,10 +5,8 @@ namespace App\Console\Commands;
use App\Actions\Server\StopSentinel;
use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
@@ -18,7 +16,7 @@ use Illuminate\Support\Facades\Http;
class Init extends Command
{
protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments} {--cleanup-proxy-networks}';
protected $signature = 'app:init {--force-cloud}';
protected $description = 'Cleanup instance related stuffs';
@@ -26,9 +24,63 @@ class Init extends Command
public function handle()
{
if (isCloud() && ! $this->option('force-cloud')) {
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
return;
}
$this->servers = Server::all();
$this->alive();
get_public_ips();
if (isCloud()) {
} else {
$this->send_alive_signal();
get_public_ips();
}
// Backward compatibility
$this->disable_metrics();
$this->replace_slash_in_environment_name();
$this->restore_coolify_db_backup();
//
$this->update_traefik_labels();
if (! isCloud() || $this->option('force-cloud')) {
$this->cleanup_unused_network_from_coolify_proxy();
}
if (isCloud()) {
$this->cleanup_unnecessary_dynamic_proxy_configuration();
} else {
$this->cleanup_in_progress_application_deployments();
}
$this->call('cleanup:redis');
$this->call('cleanup:stucked-resources');
if (isCloud()) {
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
}
} else {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
$settings = instanceSettings();
if (! is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
}
}
}
}
private function disable_metrics()
{
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
foreach ($this->servers as $server) {
if ($server->settings->is_metrics_enabled === true) {
@@ -39,62 +91,6 @@ class Init extends Command
}
}
}
$full_cleanup = $this->option('full-cleanup');
$cleanup_deployments = $this->option('cleanup-deployments');
$cleanup_proxy_networks = $this->option('cleanup-proxy-networks');
$this->replace_slash_in_environment_name();
if ($cleanup_deployments) {
echo "Running cleanup deployments.\n";
$this->cleanup_in_progress_application_deployments();
return;
}
if ($cleanup_proxy_networks) {
echo "Running cleanup proxy networks.\n";
$this->cleanup_unused_network_from_coolify_proxy();
return;
}
if ($full_cleanup) {
// Required for falsely deleted coolify db
$this->restore_coolify_db_backup();
$this->update_traefik_labels();
$this->cleanup_unused_network_from_coolify_proxy();
$this->cleanup_unnecessary_dynamic_proxy_configuration();
$this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_helper_containers();
$this->call('cleanup:queue');
$this->call('cleanup:stucked-resources');
if (! isCloud()) {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
}
$settings = InstanceSettings::get();
if (! is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
}
}
if (isCloud()) {
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
}
}
return;
}
$this->cleanup_stucked_helper_containers();
$this->call('cleanup:stucked-resources');
}
private function update_traefik_labels()
@@ -108,25 +104,23 @@ class Init extends Command
private function cleanup_unnecessary_dynamic_proxy_configuration()
{
if (isCloud()) {
foreach ($this->servers as $server) {
try {
if (! $server->isFunctional()) {
continue;
}
if ($server->id === 0) {
continue;
}
$file = $server->proxyPath().'/dynamic/coolify.yaml';
return instant_remote_process([
"rm -f $file",
], $server, false);
} catch (\Throwable $e) {
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
foreach ($this->servers as $server) {
try {
if (! $server->isFunctional()) {
continue;
}
if ($server->id === 0) {
continue;
}
$file = $server->proxyPath().'/dynamic/coolify.yaml';
return instant_remote_process([
"rm -f $file",
], $server, false);
} catch (\Throwable $e) {
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
}
}
}
@@ -172,43 +166,36 @@ class Init extends Command
private function restore_coolify_db_backup()
{
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
echo "Restoring coolify db backup\n";
$database->restore();
$scheduledBackup = ScheduledDatabaseBackup::find(0);
if (! $scheduledBackup) {
ScheduledDatabaseBackup::create([
'id' => 0,
'enabled' => true,
'save_s3' => false,
'frequency' => '0 0 * * *',
'database_id' => $database->id,
'database_type' => 'App\Models\StandalonePostgresql',
'team_id' => 0,
]);
if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
echo "Restoring coolify db backup\n";
$database->restore();
$scheduledBackup = ScheduledDatabaseBackup::find(0);
if (! $scheduledBackup) {
ScheduledDatabaseBackup::create([
'id' => 0,
'enabled' => true,
'save_s3' => false,
'frequency' => '0 0 * * *',
'database_id' => $database->id,
'database_type' => 'App\Models\StandalonePostgresql',
'team_id' => 0,
]);
}
}
}
} catch (\Throwable $e) {
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
}
}
private function cleanup_stucked_helper_containers()
{
foreach ($this->servers as $server) {
if ($server->isFunctional()) {
CleanupHelperContainersJob::dispatch($server);
} catch (\Throwable $e) {
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
}
}
}
private function alive()
private function send_alive_signal()
{
$id = config('app.id');
$version = config('version');
$settings = InstanceSettings::get();
$settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
echo "Skipping alive as do_not_track is enabled\n";
@@ -222,23 +209,7 @@ class Init extends Command
echo "Error in alive: {$e->getMessage()}\n";
}
}
// private function cleanup_ssh()
// {
// TODO: it will cleanup id.root@host.docker.internal
// try {
// $files = Storage::allFiles('ssh/keys');
// foreach ($files as $file) {
// Storage::delete($file);
// }
// $files = Storage::allFiles('ssh/mux');
// foreach ($files as $file) {
// Storage::delete($file);
// }
// } catch (\Throwable $e) {
// echo "Error in cleaning ssh: {$e->getMessage()}\n";
// }
// }
private function cleanup_in_progress_application_deployments()
{
// Cleanup any failed deployments
@@ -260,11 +231,13 @@ class Init extends Command
private function replace_slash_in_environment_name()
{
$environments = Environment::all();
foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) {
$environment->name = str_replace('/', '-', $environment->name);
$environment->save();
if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
$environments = Environment::all();
foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) {
$environment->name = str_replace('/', '-', $environment->name);
$environment->save();
}
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Process;
class OpenApi extends Command
{
protected $signature = 'openapi';
protected $description = 'Generate OpenApi file.';
public function handle()
{
// Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n";
$process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
$error = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error);
echo $error;
echo $process->output();
}
}

View File

@@ -78,7 +78,7 @@ class ServicesGenerate extends Command
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) {

View File

@@ -16,7 +16,7 @@ class SyncBunny extends Command
*
* @var string
*/
protected $signature = 'sync:bunny {--templates} {--release}';
protected $signature = 'sync:bunny {--templates} {--release} {--nightly}';
/**
* The console command description.
@@ -33,6 +33,7 @@ class SyncBunny extends Command
$that = $this;
$only_template = $this->option('templates');
$only_version = $this->option('release');
$nightly = $this->option('nightly');
$bunny_cdn = 'https://cdn.coollabs.io';
$bunny_cdn_path = 'coolify';
$bunny_cdn_storage_name = 'coolcdn';
@@ -45,9 +46,15 @@ class SyncBunny extends Command
$upgrade_script = 'upgrade.sh';
$production_env = '.env.production';
$service_template = 'service-templates.json';
$versions = 'versions.json';
$compose_file_location = "$parent_dir/$compose_file";
$compose_file_prod_location = "$parent_dir/$compose_file_prod";
$install_script_location = "$parent_dir/scripts/install.sh";
$upgrade_script_location = "$parent_dir/scripts/upgrade.sh";
$production_env_location = "$parent_dir/.env.production";
$versions_location = "$parent_dir/$versions";
PendingRequest::macro('storage', function ($fileName) use ($that) {
$headers = [
'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
@@ -73,8 +80,26 @@ class SyncBunny extends Command
]);
});
try {
if ($nightly) {
$bunny_cdn_path = 'coolify-nightly';
$compose_file_location = "$parent_dir/other/nightly/$compose_file";
$compose_file_prod_location = "$parent_dir/other/nightly/$compose_file_prod";
$production_env_location = "$parent_dir/other/nightly/$production_env";
$upgrade_script_location = "$parent_dir/other/nightly/$upgrade_script";
$install_script_location = "$parent_dir/other/nightly/$install_script";
$versions_location = "$parent_dir/other/nightly/$versions";
}
if (! $only_template && ! $only_version) {
$this->info('About to sync files (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
if ($nightly) {
$this->info('About to sync files NIGHTLY (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
} else {
$this->info('About to sync files PRODUCTION (docker-compose.yml, docker-compose.prod.yml, upgrade.sh, install.sh, etc) to BunnyCDN.');
}
$confirmed = confirm('Are you sure you want to sync?');
if (! $confirmed) {
return;
}
}
if ($only_template) {
$this->info('About to sync service-templates.json to BunnyCDN.');
@@ -90,8 +115,12 @@ class SyncBunny extends Command
return;
} elseif ($only_version) {
$this->info('About to sync versions.json to BunnyCDN.');
$file = file_get_contents("$parent_dir/$versions");
if ($nightly) {
$this->info('About to sync NIGHLTY versions.json to BunnyCDN.');
} else {
$this->info('About to sync PRODUCTION versions.json to BunnyCDN.');
}
$file = file_get_contents($versions_location);
$json = json_decode($file, true);
$actual_version = data_get($json, 'coolify.v4.version');
@@ -100,7 +129,7 @@ class SyncBunny extends Command
return;
}
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->storage(fileName: $versions_location)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
]);
$this->info('versions.json uploaded & purged...');
@@ -109,11 +138,11 @@ class SyncBunny extends Command
}
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
$pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
$pool->storage(fileName: "$parent_dir/$production_env")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
$pool->storage(fileName: "$parent_dir/scripts/$upgrade_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
$pool->storage(fileName: "$parent_dir/scripts/$install_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
$pool->storage(fileName: "$compose_file_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
$pool->storage(fileName: "$compose_file_prod_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
$pool->storage(fileName: "$production_env_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
$pool->storage(fileName: "$upgrade_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
$pool->storage(fileName: "$install_script_location")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
]);
Http::pool(fn (Pool $pool) => [
$pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file"),

View File

@@ -4,16 +4,16 @@ namespace App\Console;
use App\Jobs\CheckForUpdatesJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\CleanupStaleMultiplexedConnections;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\PullCoolifyImageJob;
use App\Jobs\PullHelperImageJob;
use App\Jobs\PullSentinelImageJob;
use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob;
use App\Jobs\ServerStorageCheckJob;
use App\Jobs\UpdateCoolifyJob;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Server;
@@ -28,24 +28,28 @@ 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();
if (isDev()) {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
// Server Jobs
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->check_scheduled_tasks($schedule);
$schedule->command('uploads:clear')->everyTwoMinutes();
$schedule->command('telescope:prune')->daily();
$schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->command('cleanup:unreachable-servers')->daily();
$schedule->job(new PullCoolifyImageJob)->cron($settings->update_check_frequency)->onOneServer();
$schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->onOneServer();
$schedule->command('cleanup:unreachable-servers')->daily()->onOneServer();
$schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$this->schedule_updates($schedule);
@@ -62,26 +66,42 @@ class Kernel extends ConsoleKernel
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()) {
$schedule->job(new PullSentinelImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
$schedule->job(function () use ($server) {
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $server, false);
$sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited');
if ($status !== 'running') {
PullSentinelImageJob::dispatch($server);
}
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
}
$schedule->job(new PullHelperImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
}
$schedule->job(new PullHelperImageJob)
->cron($settings->update_check_frequency)
->timezone($settings->instance_timezone)
->onOneServer();
}
private function schedule_updates($schedule)
{
$settings = InstanceSettings::get();
$settings = instanceSettings();
$updateCheckFrequency = $settings->update_check_frequency;
$schedule->job(new CheckForUpdatesJob)->cron($updateCheckFrequency)->onOneServer();
$schedule->job(new CheckForUpdatesJob)
->cron($updateCheckFrequency)
->timezone($settings->instance_timezone)
->onOneServer();
if ($settings->is_auto_update_enabled) {
$autoUpdateFrequency = $settings->auto_update_frequency;
$schedule->job(new UpdateCoolifyJob)->cron($autoUpdateFrequency)->onOneServer();
$schedule->job(new UpdateCoolifyJob)
->cron($autoUpdateFrequency)
->timezone($settings->instance_timezone)
->onOneServer();
}
}
@@ -96,7 +116,13 @@ class Kernel extends ConsoleKernel
}
foreach ($servers as $server) {
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->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();
} else {
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
}
}
}
@@ -117,12 +143,19 @@ class Kernel extends ConsoleKernel
continue;
}
$server = $scheduled_backup->server();
if (! $server) {
continue;
}
$serverTimezone = $server->settings->server_timezone;
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
$schedule->job(new DatabaseBackupJob(
backup: $scheduled_backup
))->cron($scheduled_backup->frequency)->onOneServer();
))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
}
}
@@ -155,12 +188,19 @@ class Kernel extends ConsoleKernel
continue;
}
}
$server = $scheduled_task->server();
if (! $server) {
continue;
}
$serverTimezone = $server->settings->server_timezone ?: config('app.timezone');
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}
$schedule->job(new ScheduledTaskJob(
task: $scheduled_task
))->cron($scheduled_task->frequency)->onOneServer();
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Enums;
enum ContainerStatusTypes: string
{
case PAUSED = 'paused';
case RESTARTING = 'restarting';
case REMOVING = 'removing';
case RUNNING = 'running';
case DEAD = 'dead';
case CREATED = 'created';
case EXITED = 'exited';
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CloudflareTunnelConfigured implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
}

View File

@@ -65,7 +65,7 @@ class Handler extends ExceptionHandler
if ($e instanceof RuntimeException) {
return;
}
$this->settings = \App\Models\InstanceSettings::get();
$this->settings = instanceSettings();
if ($this->settings->do_not_track) {
return;
}

View File

@@ -0,0 +1,186 @@
<?php
namespace App\Helpers;
use App\Models\PrivateKey;
use App\Models\Server;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Process;
class SshMultiplexingHelper
{
public static function serverSshConfiguration(Server $server)
{
$privateKey = PrivateKey::findOrFail($server->private_key_id);
$sshKeyLocation = $privateKey->getKeyLocation();
$muxFilename = '/var/www/html/storage/app/ssh/mux/mux_'.$server->uuid;
return [
'sshKeyLocation' => $sshKeyLocation,
'muxFilename' => $muxFilename,
];
}
public static function ensureMultiplexedConnection(Server $server)
{
if (! self::isMultiplexingEnabled()) {
return;
}
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
$sshKeyLocation = $sshConfig['sshKeyLocation'];
self::validateSshKey($sshKeyLocation);
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$checkCommand .= "{$server->user}@{$server->ip}";
$process = Process::run($checkCommand);
if ($process->exitCode() !== 0) {
self::establishNewMultiplexedConnection($server);
}
}
public static function establishNewMultiplexedConnection(Server $server)
{
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
$establishCommand .= "{$server->user}@{$server->ip}";
$establishProcess = Process::run($establishCommand);
if ($establishProcess->exitCode() !== 0) {
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
}
}
public static function removeMuxFile(Server $server)
{
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$closeCommand .= "{$server->user}@{$server->ip}";
Process::run($closeCommand);
}
public static function generateScpCommand(Server $server, string $source, string $dest)
{
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp ";
if ($server->isIpv6()) {
$scp_command .= '-6 ';
}
if (self::isMultiplexingEnabled()) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
$scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
return $scp_command;
}
public static function generateSshCommand(Server $server, string $command)
{
if ($server->settings->force_disabled) {
throw new \RuntimeException('Server is disabled.');
}
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$ssh_command = "timeout $timeout ssh ";
if (self::isMultiplexingEnabled()) {
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
}
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
$delimiter = Hash::make($command);
$delimiter = base64_encode($delimiter);
$command = str_replace($delimiter, '', $command);
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
.$command.PHP_EOL
.$delimiter;
return $ssh_command;
}
private static function isMultiplexingEnabled(): bool
{
return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
}
private static function validateSshKey(string $sshKeyLocation): void
{
$checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
$keyCheckProcess = Process::run($checkKeyCommand);
if ($keyCheckProcess->exitCode() !== 0) {
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
}
}
private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
{
$options = "-i {$sshKeyLocation} "
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
.'-o PasswordAuthentication=no '
."-o ConnectTimeout=$connectionTimeout "
."-o ServerAliveInterval=$serverInterval "
.'-o RequestTTY=no '
.'-o LogLevel=ERROR ';
// Bruh
if ($isScp) {
$options .= "-P {$server->port} ";
} else {
$options .= "-p {$server->port} ";
}
return $options;
}
}

View File

@@ -53,6 +53,7 @@ class ApplicationsController extends Controller
summary: 'List',
description: 'List all applications.',
path: '/applications',
operationId: 'list-applications',
security: [
['bearerAuth' => []],
],
@@ -101,6 +102,7 @@ class ApplicationsController extends Controller
summary: 'Create (Public)',
description: 'Create new application based on a public git repository.',
path: '/applications/public',
operationId: 'create-public-application',
security: [
['bearerAuth' => []],
],
@@ -175,6 +177,7 @@ class ApplicationsController extends Controller
'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.'],
],
)),
]),
@@ -201,7 +204,8 @@ class ApplicationsController extends Controller
#[OA\Post(
summary: 'Create (Private - GH App)',
description: 'Create new application based on a private repository through a Github App.',
path: '/applications/private-gh-app',
path: '/applications/private-github-app',
operationId: 'create-private-github-app-application',
security: [
['bearerAuth' => []],
],
@@ -276,6 +280,7 @@ class ApplicationsController extends Controller
'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.'],
],
)),
]),
@@ -303,6 +308,7 @@ class ApplicationsController extends Controller
summary: 'Create (Private - Deploy Key)',
description: 'Create new application based on a private repository through a Deploy Key.',
path: '/applications/private-deploy-key',
operationId: 'create-private-deploy-key-application',
security: [
['bearerAuth' => []],
],
@@ -377,6 +383,7 @@ class ApplicationsController extends Controller
'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.'],
],
)),
]),
@@ -404,6 +411,7 @@ class ApplicationsController extends Controller
summary: 'Create (Dockerfile)',
description: 'Create new application based on a simple Dockerfile.',
path: '/applications/dockerfile',
operationId: 'create-dockerfile-application',
security: [
['bearerAuth' => []],
],
@@ -463,6 +471,7 @@ class ApplicationsController extends Controller
'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.'],
],
)),
]),
@@ -490,6 +499,7 @@ class ApplicationsController extends Controller
summary: 'Create (Docker Image)',
description: 'Create new application based on a prebuilt docker image',
path: '/applications/dockerimage',
operationId: 'create-dockerimage-application',
security: [
['bearerAuth' => []],
],
@@ -546,6 +556,7 @@ class ApplicationsController extends Controller
'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.'],
],
)),
]),
@@ -573,6 +584,7 @@ class ApplicationsController extends Controller
summary: 'Create (Docker Compose)',
description: 'Create new application based on a docker-compose file.',
path: '/applications/dockercompose',
operationId: 'create-dockercompose-application',
security: [
['bearerAuth' => []],
],
@@ -595,6 +607,7 @@ class ApplicationsController extends Controller
'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.'],
],
)),
]),
@@ -620,7 +633,7 @@ class ApplicationsController extends Controller
private function create_application(Request $request, $type)
{
$allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths'];
$allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -658,6 +671,7 @@ class ApplicationsController extends Controller
$fqdn = $request->domains;
$instantDeploy = $request->instant_deploy;
$githubAppUuid = $request->github_app_uuid;
$useBuildServer = $request->use_build_server;
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
@@ -730,6 +744,10 @@ class ApplicationsController extends Controller
$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) {
@@ -826,6 +844,10 @@ class ApplicationsController extends Controller
$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) {
@@ -918,6 +940,10 @@ class ApplicationsController extends Controller
$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) {
@@ -997,6 +1023,10 @@ class ApplicationsController extends Controller
$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';
@@ -1055,6 +1085,10 @@ class ApplicationsController extends Controller
$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';
@@ -1171,6 +1205,7 @@ class ApplicationsController extends Controller
summary: 'Get',
description: 'Get application by UUID.',
path: '/applications/{uuid}',
operationId: 'get-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1235,6 +1270,7 @@ class ApplicationsController extends Controller
summary: 'Delete',
description: 'Delete application by UUID.',
path: '/applications/{uuid}',
operationId: 'delete-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1250,16 +1286,10 @@ class ApplicationsController extends Controller
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(
@@ -1307,10 +1337,14 @@ class ApplicationsController extends Controller
'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.',
@@ -1321,6 +1355,7 @@ class ApplicationsController extends Controller
summary: 'Update',
description: 'Update application by UUID.',
path: '/applications/{uuid}',
operationId: 'update-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1394,6 +1429,7 @@ class ApplicationsController extends Controller
'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.'],
],
)),
]),
@@ -1450,7 +1486,7 @@ class ApplicationsController extends Controller
], 404);
}
$server = $application->destination->server;
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect'];
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server'];
$validator = customApiValidator($request->all(), [
sharedDataApplications(),
@@ -1526,6 +1562,17 @@ class ApplicationsController extends Controller
}
$request->offsetUnset('docker_compose_domains');
}
$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();
data_set($data, 'fqdn', $domains);
if ($dockerComposeDomainsJson->count() > 0) {
@@ -1534,6 +1581,16 @@ class ApplicationsController extends Controller
$application->fill($data);
$application->save();
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
is_api: true,
);
}
return response()->json([
'uuid' => $application->uuid,
]);
@@ -1543,6 +1600,7 @@ class ApplicationsController extends Controller
summary: 'List Envs',
description: 'List all envs by application UUID.',
path: '/applications/{uuid}/envs',
operationId: 'list-envs-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -1625,6 +1683,7 @@ class ApplicationsController extends Controller
summary: 'Update Env',
description: 'Update env by application UUID.',
path: '/applications/{uuid}/envs',
operationId: 'update-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -1807,6 +1866,7 @@ class ApplicationsController extends Controller
summary: 'Update Envs (Bulk)',
description: 'Update multiple envs by application UUID.',
path: '/applications/{uuid}/envs/bulk',
operationId: 'update-envs-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -1998,6 +2058,7 @@ class ApplicationsController extends Controller
summary: 'Create Env',
description: 'Create env by application UUID.',
path: '/applications/{uuid}/envs',
operationId: 'create-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -2157,6 +2218,7 @@ class ApplicationsController extends Controller
summary: 'Delete Env',
description: 'Delete env by UUID.',
path: '/applications/{uuid}/envs/{env_uuid}',
operationId: 'delete-env-by-application-uuid',
security: [
['bearerAuth' => []],
],
@@ -2242,6 +2304,7 @@ class ApplicationsController extends Controller
summary: 'Start',
description: 'Start application. `Post` request is also accepted.',
path: '/applications/{uuid}/start',
operationId: 'start-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -2345,6 +2408,7 @@ class ApplicationsController extends Controller
summary: 'Stop',
description: 'Stop application. `Post` request is also accepted.',
path: '/applications/{uuid}/stop',
operationId: 'stop-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -2417,6 +2481,7 @@ class ApplicationsController extends Controller
summary: 'Restart',
description: 'Restart application. `Post` request is also accepted.',
path: '/applications/{uuid}/restart',
operationId: 'restart-application-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -2497,6 +2562,131 @@ class ApplicationsController extends Controller
}
#[OA\Post(
summary: 'Execute Command',
description: "Execute a command on the application's current container.",
path: '/applications/{uuid}/execute',
operationId: 'execute-command-application',
security: [
['bearerAuth' => []],
],
tags: ['Applications'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the application.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
requestBody: new OA\RequestBody(
required: true,
description: 'Command to execute.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'command' => ['type' => 'string', 'description' => 'Command to execute.'],
],
),
),
),
responses: [
new OA\Response(
response: 200,
description: "Execute a command on the application's current container.",
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Command executed.'],
'response' => ['type' => 'string'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function execute_command_by_uuid(Request $request)
{
// TODO: Need to review this from security perspective, to not allow arbitrary command execution
$allowedFields = ['command'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json(['message' => 'Application not found.'], 404);
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'command' => 'string|required',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
$container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
$status = getContainerStatus($application->destination->server, $container['Names']);
if ($status !== 'running') {
return response()->json([
'message' => 'Application is not running.',
], 400);
}
$commands = collect([
executeInDocker($container['Names'], $request->command),
]);
$res = instant_remote_process(command: $commands, server: $application->destination->server);
return response()->json([
'message' => 'Command executed.',
'response' => $res,
]);
}
private function validateDataApplications(Request $request, Server $server)
{
$teamId = getTeamIdFromToken();

View File

@@ -46,6 +46,7 @@ class DatabasesController extends Controller
summary: 'List',
description: 'List all databases.',
path: '/databases',
operationId: 'list-databases',
security: [
['bearerAuth' => []],
],
@@ -91,6 +92,7 @@ class DatabasesController extends Controller
summary: 'Get',
description: 'Get database by UUID.',
path: '/databases/{uuid}',
operationId: 'get-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -151,6 +153,7 @@ class DatabasesController extends Controller
summary: 'Update',
description: 'Update database by UUID.',
path: '/databases/{uuid}',
operationId: 'update-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -510,6 +513,7 @@ class DatabasesController extends Controller
summary: 'Create (PostgreSQL)',
description: 'Create a new PostgreSQL database.',
path: '/databases/postgresql',
operationId: 'create-database-postgresql',
security: [
['bearerAuth' => []],
],
@@ -575,6 +579,7 @@ class DatabasesController extends Controller
summary: 'Create (Clickhouse)',
description: 'Create a new Clickhouse database.',
path: '/databases/clickhouse',
operationId: 'create-database-clickhouse',
security: [
['bearerAuth' => []],
],
@@ -636,6 +641,7 @@ class DatabasesController extends Controller
summary: 'Create (DragonFly)',
description: 'Create a new DragonFly database.',
path: '/databases/dragonfly',
operationId: 'create-database-dragonfly',
security: [
['bearerAuth' => []],
],
@@ -696,6 +702,7 @@ class DatabasesController extends Controller
summary: 'Create (Redis)',
description: 'Create a new Redis database.',
path: '/databases/redis',
operationId: 'create-database-redis',
security: [
['bearerAuth' => []],
],
@@ -757,6 +764,7 @@ class DatabasesController extends Controller
summary: 'Create (KeyDB)',
description: 'Create a new KeyDB database.',
path: '/databases/keydb',
operationId: 'create-database-keydb',
security: [
['bearerAuth' => []],
],
@@ -818,6 +826,7 @@ class DatabasesController extends Controller
summary: 'Create (MariaDB)',
description: 'Create a new MariaDB database.',
path: '/databases/mariadb',
operationId: 'create-database-mariadb',
security: [
['bearerAuth' => []],
],
@@ -882,6 +891,7 @@ class DatabasesController extends Controller
summary: 'Create (MySQL)',
description: 'Create a new MySQL database.',
path: '/databases/mysql',
operationId: 'create-database-mysql',
security: [
['bearerAuth' => []],
],
@@ -945,6 +955,7 @@ class DatabasesController extends Controller
summary: 'Create (MongoDB)',
description: 'Create a new MongoDB database.',
path: '/databases/mongodb',
operationId: 'create-database-mongodb',
security: [
['bearerAuth' => []],
],
@@ -1514,6 +1525,7 @@ class DatabasesController extends Controller
summary: 'Delete',
description: 'Delete database by UUID.',
path: '/databases/{uuid}',
operationId: 'delete-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1529,16 +1541,10 @@ class DatabasesController extends Controller
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(
@@ -1583,10 +1589,14 @@ class DatabasesController extends Controller
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.',
@@ -1597,6 +1607,7 @@ class DatabasesController extends Controller
summary: 'Start',
description: 'Start database. `Post` request is also accepted.',
path: '/databases/{uuid}/start',
operationId: 'start-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1672,6 +1683,7 @@ class DatabasesController extends Controller
summary: 'Stop',
description: 'Stop database. `Post` request is also accepted.',
path: '/databases/{uuid}/stop',
operationId: 'stop-database-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -1747,6 +1759,7 @@ class DatabasesController extends Controller
summary: 'Restart',
description: 'Restart database. `Post` request is also accepted.',
path: '/databases/{uuid}/restart',
operationId: 'restart-database-by-uuid',
security: [
['bearerAuth' => []],
],

View File

@@ -32,6 +32,7 @@ class DeployController extends Controller
summary: 'List',
description: 'List currently running deployments',
path: '/deployments',
operationId: 'list-deployments',
security: [
['bearerAuth' => []],
],
@@ -79,12 +80,13 @@ class DeployController extends Controller
summary: 'Get',
description: 'Get deployment by UUID.',
path: '/deployments/{uuid}',
operationId: 'get-deployment-by-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Deployments'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
@@ -134,6 +136,7 @@ class DeployController extends Controller
summary: 'Deploy',
description: 'Deploy by tag or uuid. `Post` request also accepted.',
path: '/deploy',
operationId: 'deploy-by-tag-or-uuid',
security: [
['bearerAuth' => []],
],
@@ -147,7 +150,7 @@ class DeployController extends Controller
responses: [
new OA\Response(
response: 200,
description: 'Get deployment(s) Uuid\'s',
description: 'Get deployment(s) UUID\'s',
content: [
new OA\MediaType(
mediaType: 'application/json',

View File

@@ -1,35 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\EnvironmentVariable;
use Illuminate\Http\Request;
class EnvironmentVariablesController extends Controller
{
public function delete_env_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$env = EnvironmentVariable::where('uuid', $request->env_uuid)->first();
if (! $env) {
return response()->json([
'message' => 'Environment variable not found.',
], 404);
}
$found_app = $env->resource()->whereRelation('environment.project.team', 'id', $teamId)->first();
if (! $found_app) {
return response()->json([
'message' => 'Environment variable not found.',
], 404);
}
$env->delete();
return response()->json([
'message' => 'Environment variable deleted.',
]);
}
}

View File

@@ -13,6 +13,7 @@ class OtherController extends Controller
summary: 'Version',
description: 'Get Coolify version.',
path: '/version',
operationId: 'version',
security: [
['bearerAuth' => []],
],
@@ -43,6 +44,7 @@ class OtherController extends Controller
summary: 'Enable API',
description: 'Enable API (only with root permissions).',
path: '/enable',
operationId: 'enable-api',
security: [
['bearerAuth' => []],
],
@@ -84,7 +86,7 @@ class OtherController extends Controller
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);
@@ -94,6 +96,7 @@ class OtherController extends Controller
summary: 'Disable API',
description: 'Disable API (only with root permissions).',
path: '/disable',
operationId: 'disable-api',
security: [
['bearerAuth' => []],
],
@@ -135,7 +138,7 @@ class OtherController extends Controller
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);
@@ -158,6 +161,7 @@ class OtherController extends Controller
summary: 'Healthcheck',
description: 'Healthcheck endpoint.',
path: '/healthcheck',
operationId: 'healthcheck',
responses: [
new OA\Response(
response: 200,

View File

@@ -11,8 +11,9 @@ class ProjectController extends Controller
{
#[OA\Get(
summary: 'List',
description: 'list projects.',
description: 'List projects.',
path: '/projects',
operationId: 'list-projects',
security: [
['bearerAuth' => []],
],
@@ -46,7 +47,7 @@ class ProjectController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
$projects = Project::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
$projects = Project::whereTeamId($teamId)->select('id', 'name', 'description', 'uuid')->get();
return response()->json(serializeApiResponse($projects),
);
@@ -54,8 +55,9 @@ class ProjectController extends Controller
#[OA\Get(
summary: 'Get',
description: 'Get project by Uuid.',
description: 'Get project by UUID.',
path: '/projects/{uuid}',
operationId: 'get-project-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -102,6 +104,7 @@ class ProjectController extends Controller
summary: 'Environment',
description: 'Get environment by name.',
path: '/projects/{uuid}/{environment_name}',
operationId: 'get-environment-by-name',
security: [
['bearerAuth' => []],
],
@@ -136,12 +139,15 @@ class ProjectController extends Controller
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
return response()->json(['message' => 'UUID is required.'], 422);
}
if (! $request->environment_name) {
return response()->json(['message' => 'Environment name is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$environment = $project->environments()->whereName($request->environment_name)->first();
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
@@ -155,6 +161,7 @@ class ProjectController extends Controller
summary: 'Create',
description: 'Create Project.',
path: '/projects',
operationId: 'create-project',
security: [
['bearerAuth' => []],
],
@@ -167,7 +174,7 @@ class ProjectController extends Controller
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'description' => 'The name of the project.'],
'name' => ['type' => 'string', 'description' => 'The name of the project.'],
'description' => ['type' => 'string', 'description' => 'The description of the project.'],
],
),
@@ -250,6 +257,7 @@ class ProjectController extends Controller
summary: 'Update',
description: 'Update Project.',
path: '/projects/{uuid}',
operationId: 'update-project-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -333,7 +341,7 @@ class ProjectController extends Controller
}
$uuid = $request->uuid;
if (! $uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
return response()->json(['message' => 'UUID is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($uuid)->first();
@@ -355,6 +363,7 @@ class ProjectController extends Controller
summary: 'Delete',
description: 'Delete project by UUID.',
path: '/projects/{uuid}',
operationId: 'delete-project-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -408,7 +417,7 @@ class ProjectController extends Controller
}
if (! $request->uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
return response()->json(['message' => 'UUID is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $project) {

View File

@@ -13,6 +13,7 @@ class ResourcesController extends Controller
summary: 'List',
description: 'Get all resources.',
path: '/resources',
operationId: 'list-resources',
security: [
['bearerAuth' => []],
],

View File

@@ -26,6 +26,7 @@ class SecurityController extends Controller
summary: 'List',
description: 'List all private keys.',
path: '/security/keys',
operationId: 'list-private-keys',
security: [
['bearerAuth' => []],
],
@@ -68,12 +69,13 @@ class SecurityController extends Controller
summary: 'Get',
description: 'Get key by UUID.',
path: '/security/keys/{uuid}',
operationId: 'get-private-key-by-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
@@ -124,6 +126,7 @@ class SecurityController extends Controller
summary: 'Create',
description: 'Create a new private key.',
path: '/security/keys',
operationId: 'create-private-key',
security: [
['bearerAuth' => []],
],
@@ -217,6 +220,7 @@ class SecurityController extends Controller
summary: 'Update',
description: 'Update a private key.',
path: '/security/keys',
operationId: 'update-private-key',
security: [
['bearerAuth' => []],
],
@@ -313,12 +317,13 @@ class SecurityController extends Controller
summary: 'Delete',
description: 'Delete a private key.',
path: '/security/keys/{uuid}',
operationId: 'delete-private-key-by-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(

View File

@@ -46,6 +46,7 @@ class ServersController extends Controller
summary: 'List',
description: 'List all servers.',
path: '/servers',
operationId: 'list-servers',
security: [
['bearerAuth' => []],
],
@@ -100,12 +101,13 @@ class ServersController extends Controller
summary: 'Get',
description: 'Get server by UUID.',
path: '/servers/{uuid}',
operationId: 'get-server-by-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
@@ -177,12 +179,13 @@ class ServersController extends Controller
summary: 'Resources',
description: 'Get resources by server.',
path: '/servers/{uuid}/resources',
operationId: 'get-resources-by-server-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
@@ -254,12 +257,13 @@ class ServersController extends Controller
summary: 'Domains',
description: 'Get domains by server.',
path: '/servers/{uuid}/domains',
operationId: 'get-domains-by-server-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
@@ -304,7 +308,7 @@ class ServersController extends Controller
$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;
@@ -401,6 +405,7 @@ class ServersController extends Controller
summary: 'Create',
description: 'Create Server.',
path: '/servers',
operationId: 'create-server',
security: [
['bearerAuth' => []],
],
@@ -545,6 +550,7 @@ class ServersController extends Controller
summary: 'Update',
description: 'Update Server.',
path: '/servers/{uuid}',
operationId: 'update-server-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -655,6 +661,7 @@ class ServersController extends Controller
summary: 'Delete',
description: 'Delete server by UUID.',
path: '/servers/{uuid}',
operationId: 'delete-server-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -727,6 +734,7 @@ class ServersController extends Controller
summary: 'Validate',
description: 'Validate server by UUID.',
path: '/servers/{uuid}/validate',
operationId: 'validate-server-by-uuid',
security: [
['bearerAuth' => []],
],

View File

@@ -38,6 +38,7 @@ class ServicesController extends Controller
summary: 'List',
description: 'List all services.',
path: '/services',
operationId: 'list-services',
security: [
['bearerAuth' => []],
],
@@ -88,6 +89,7 @@ class ServicesController extends Controller
summary: 'Create',
description: 'Create a one-click service',
path: '/services',
operationId: 'create-service',
security: [
['bearerAuth' => []],
],
@@ -365,6 +367,7 @@ class ServicesController extends Controller
summary: 'Get',
description: 'Get service by UUID.',
path: '/services/{uuid}',
operationId: 'get-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -375,7 +378,7 @@ class ServicesController extends Controller
responses: [
new OA\Response(
response: 200,
description: 'Get a service by Uuid.',
description: 'Get a service by UUID.',
content: [
new OA\MediaType(
mediaType: 'application/json',
@@ -422,17 +425,22 @@ class ServicesController extends Controller
summary: 'Delete',
description: 'Delete service by UUID.',
path: '/services/{uuid}',
operationId: 'delete-service-by-uuid',
security: [
['bearerAuth' => []],
],
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(
response: 200,
description: 'Delete a service by Uuid',
description: 'Delete a service by UUID',
content: [
new OA\MediaType(
mediaType: 'application/json',
@@ -472,17 +480,540 @@ class ServicesController extends Controller
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.',
]);
}
#[OA\Get(
summary: 'List Envs',
description: 'List all envs by service UUID.',
path: '/services/{uuid}/envs',
operationId: 'list-envs-by-service-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the service.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'All environment variables by service UUID.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function envs(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
$envs = $service->environment_variables->map(function ($env) {
$env->makeHidden([
'application_id',
'standalone_clickhouse_id',
'standalone_dragonfly_id',
'standalone_keydb_id',
'standalone_mariadb_id',
'standalone_mongodb_id',
'standalone_mysql_id',
'standalone_postgresql_id',
'standalone_redis_id',
]);
$env = $this->removeSensitiveData($env);
return $env;
});
return response()->json($envs);
}
#[OA\Patch(
summary: 'Update Env',
description: 'Update env by service UUID.',
path: '/services/{uuid}/envs',
operationId: 'update-env-by-service-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the service.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
requestBody: new OA\RequestBody(
description: 'Env updated.',
required: true,
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['key', 'value'],
properties: [
'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'],
'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'],
'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
],
),
),
],
),
responses: [
new OA\Response(
response: 201,
description: 'Environment variable updated.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Environment variable updated.'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function update_env_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'value' => 'string|nullable',
'is_build_time' => 'boolean',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
'is_shown_once' => 'boolean',
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$env = $service->environment_variables()->where('key', $request->key)->first();
if (! $env) {
return response()->json(['message' => 'Environment variable not found.'], 404);
}
$env->fill($request->all());
$env->save();
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
}
#[OA\Patch(
summary: 'Update Envs (Bulk)',
description: 'Update multiple envs by service UUID.',
path: '/services/{uuid}/envs/bulk',
operationId: 'update-envs-by-service-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the service.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
requestBody: new OA\RequestBody(
description: 'Bulk envs updated.',
required: true,
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['data'],
properties: [
'data' => [
'type' => 'array',
'items' => new OA\Schema(
type: 'object',
properties: [
'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'],
'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'],
'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
],
),
],
],
),
),
],
),
responses: [
new OA\Response(
response: 201,
description: 'Environment variables updated.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Environment variables updated.'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function create_bulk_envs(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
$bulk_data = $request->get('data');
if (! $bulk_data) {
return response()->json(['message' => 'Bulk data is required.'], 400);
}
$updatedEnvs = collect();
foreach ($bulk_data as $item) {
$validator = customApiValidator($item, [
'key' => 'string|required',
'value' => 'string|nullable',
'is_build_time' => 'boolean',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
'is_shown_once' => 'boolean',
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$env = $service->environment_variables()->updateOrCreate(
['key' => $item['key']],
$item
);
$updatedEnvs->push($this->removeSensitiveData($env));
}
return response()->json($updatedEnvs)->setStatusCode(201);
}
#[OA\Post(
summary: 'Create Env',
description: 'Create env by service UUID.',
path: '/services/{uuid}/envs',
operationId: 'create-env-by-service-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the service.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
requestBody: new OA\RequestBody(
required: true,
description: 'Env created.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'],
'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'],
'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'],
'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'],
'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'],
'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'],
'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Environment variable created.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'example' => 'nc0k04gk8g0cgsk440g0koko'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function create_env(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'value' => 'string|nullable',
'is_build_time' => 'boolean',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
'is_shown_once' => 'boolean',
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$existingEnv = $service->environment_variables()->where('key', $request->key)->first();
if ($existingEnv) {
return response()->json([
'message' => 'Environment variable already exists. Use PATCH request to update it.',
], 409);
}
$env = $service->environment_variables()->create($request->all());
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
}
#[OA\Delete(
summary: 'Delete Env',
description: 'Delete env by UUID.',
path: '/services/{uuid}/envs/{env_uuid}',
operationId: 'delete-env-by-service-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the service.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
new OA\Parameter(
name: 'env_uuid',
in: 'path',
description: 'UUID of the environment variable.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Environment variable deleted.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Environment variable deleted.'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function delete_env_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
$env = EnvironmentVariable::where('uuid', $request->env_uuid)
->where('service_id', $service->id)
->first();
if (! $env) {
return response()->json(['message' => 'Environment variable not found.'], 404);
}
$env->forceDelete();
return response()->json(['message' => 'Environment variable deleted.']);
}
#[OA\Get(
summary: 'Start',
description: 'Start service. `Post` request is also accepted.',
path: '/services/{uuid}/start',
operationId: 'start-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -510,9 +1041,11 @@ class ServicesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service starting request queued.'],
])
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -558,6 +1091,7 @@ class ServicesController extends Controller
summary: 'Stop',
description: 'Stop service. `Post` request is also accepted.',
path: '/services/{uuid}/stop',
operationId: 'stop-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -585,9 +1119,11 @@ class ServicesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'],
])
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -633,6 +1169,7 @@ class ServicesController extends Controller
summary: 'Restart',
description: 'Restart service. `Post` request is also accepted.',
path: '/services/{uuid}/restart',
operationId: 'restart-service-by-uuid',
security: [
['bearerAuth' => []],
],
@@ -660,9 +1197,11 @@ class ServicesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'],
])
]
)
),
]),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',

View File

@@ -32,6 +32,7 @@ class TeamController extends Controller
summary: 'List',
description: 'Get all teams.',
path: '/teams',
operationId: 'list-teams',
security: [
['bearerAuth' => []],
],
@@ -79,6 +80,7 @@ class TeamController extends Controller
summary: 'Get',
description: 'Get team by TeamId.',
path: '/teams/{id}',
operationId: 'get-team-by-id',
security: [
['bearerAuth' => []],
],
@@ -129,6 +131,7 @@ class TeamController extends Controller
summary: 'Members',
description: 'Get members by TeamId.',
path: '/teams/{id}/members',
operationId: 'get-members-by-team-id',
security: [
['bearerAuth' => []],
],
@@ -189,6 +192,7 @@ class TeamController extends Controller
summary: 'Authenticated Team',
description: 'Get currently authenticated team.',
path: '/teams/current',
operationId: 'get-current-team',
security: [
['bearerAuth' => []],
],
@@ -225,6 +229,7 @@ class TeamController extends Controller
summary: 'Authenticated Team Members',
description: 'Get currently authenticated team members.',
path: '/teams/current/members',
operationId: 'get-current-team-members',
security: [
['bearerAuth' => []],
],

View File

@@ -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 @@ class OauthController extends Controller
$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');
}

View File

@@ -14,7 +14,7 @@ class ApiAllowed
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);
}

View File

@@ -26,6 +26,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
use RuntimeException;
@@ -109,10 +110,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private bool $is_debug_enabled;
private $build_args;
private Collection|string $build_args;
private $env_args;
private $environment_variables;
private $env_nixpacks_args;
private $docker_compose;
@@ -157,7 +160,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $coolify_variables = null;
private bool $preserveRepository = true;
private bool $preserveRepository = false;
public $tries = 1;
@@ -166,6 +169,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
$this->build_args = collect([]);
$this->application_deployment_queue_id = $application_deployment_queue_id;
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
@@ -198,11 +202,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
if ($this->application->settings->custom_internal_name && ! $this->application->settings->is_consistent_container_name_enabled) {
$this->container_name = $this->application->settings->custom_internal_name;
if ($this->pull_request_id === 0) {
$this->container_name = $this->application->settings->custom_internal_name;
} else {
$this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}";
}
}
ray('New container name: ', $this->container_name);
ray('New container name: ', $this->container_name)->green();
savePrivateKeyToFs($this->server);
$this->saved_outputs = collect();
// Set preview fqdn
@@ -276,6 +283,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->original_server = $this->server;
} else {
$this->build_server = $buildServers->random();
$this->application_deployment_queue->build_server_id = $this->build_server->id;
$this->application_deployment_queue->addLogEntry("Found a suitable build server ({$this->build_server->name}).");
$this->original_server = $this->server;
$this->use_build_server = true;
@@ -414,15 +422,42 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository();
if ($this->preserveRepository) {
foreach ($this->application->fileStorages as $fileStorage) {
$path = $fileStorage->fs_path;
$saveName = 'file_stat_'.$fileStorage->id;
$realPathInGit = str($path)->replace($this->application->workdir(), $this->workdir)->value();
// check if the file is a directory or a file inside the repository
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "stat -c '%F' {$realPathInGit}"), 'hidden' => true, 'ignore_errors' => true, 'save' => $saveName]
);
if ($this->saved_outputs->has($saveName)) {
$fileStat = $this->saved_outputs->get($saveName);
if ($fileStat->value() === 'directory' && ! $fileStorage->is_directory) {
$fileStorage->is_directory = true;
$fileStorage->content = null;
$fileStorage->save();
$fileStorage->deleteStorageOnServer();
$fileStorage->saveStorageOnServer();
} elseif ($fileStat->value() === 'regular file' && $fileStorage->is_directory) {
$fileStorage->is_directory = false;
$fileStorage->is_based_on_git = true;
$fileStorage->save();
$fileStorage->deleteStorageOnServer();
$fileStorage->saveStorageOnServer();
}
}
}
}
$this->generate_image_names();
$this->cleanup_git();
$this->application->loadComposeFile(isInit: false);
if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->application->parseRawCompose();
$this->application->oldRawParser();
$yaml = $composeFile = $this->application->docker_compose_raw;
$this->save_environment_variables();
} else {
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id, preview_id: data_get($this, 'preview.id'));
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables();
if (! is_null($this->env_filename)) {
$services = collect($composeFile['services']);
@@ -439,11 +474,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
return;
}
$yaml = Yaml::dump($composeFile->toArray(), 10);
$yaml = Yaml::dump(convertToArray($composeFile), 10);
}
$this->docker_compose_base64 = base64_encode($yaml);
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
'hidden' => true,
]);
// Build new container to limit downtime.
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
@@ -473,13 +509,18 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// TODO
} else {
$this->execute_remote_command([
"docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true", 'hidden' => true, 'ignore_errors' => true,
"docker network inspect '{$networkId}' >/dev/null 2>&1 || docker network create --attachable '{$networkId}' >/dev/null || true",
'hidden' => true,
'ignore_errors' => true,
], [
"docker network connect {$networkId} coolify-proxy || true", 'hidden' => true, 'ignore_errors' => true,
"docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true",
'hidden' => true,
'ignore_errors' => true,
]);
}
// Start compose file
$server_workdir = $this->application->workdir();
if ($this->application->settings->is_raw_compose_deployment_enabled) {
if ($this->docker_compose_custom_start_command) {
$this->write_deployment_configurations();
@@ -488,7 +529,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
} else {
$this->write_deployment_configurations();
$server_workdir = $this->application->workdir();
$this->docker_compose_location = '/docker-compose.yaml';
$command = "{$this->coolify_variables} docker compose";
@@ -508,15 +548,26 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
} else {
$command = "{$this->coolify_variables} docker compose";
if ($this->env_filename) {
$command .= " --env-file {$this->workdir}/{$this->env_filename}";
}
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
if ($this->preserveRepository) {
if ($this->env_filename) {
$command .= " --env-file {$server_workdir}/{$this->env_filename}";
}
$command .= " --project-name {$this->application->uuid} --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d";
$this->write_deployment_configurations();
$this->write_deployment_configurations();
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
$this->execute_remote_command(
['command' => $command, 'hidden' => true],
);
} else {
if ($this->env_filename) {
$command .= " --env-file {$this->workdir}/{$this->env_filename}";
}
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
$this->write_deployment_configurations();
}
}
}
@@ -610,15 +661,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
[
"mkdir -p $this->configuration_dir",
],
// removing this now as we are using docker cp
// [
// "rm -rf $this->configuration_dir/{*,.*}",
// ],
[
"docker cp {$this->deployment_uuid}:{$this->workdir}/. {$this->configuration_dir}",
],
);
}
foreach ($this->application->fileStorages as $fileStorage) {
if (! $fileStorage->is_based_on_git && ! $fileStorage->is_directory) {
$fileStorage->saveStorageOnServer();
}
}
if ($this->use_build_server) {
$this->server = $this->build_server;
}
@@ -698,7 +750,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name}).");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"),
'hidden' => true,
],
);
if ($this->application->docker_registry_image_tag) {
@@ -706,10 +759,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with {$this->application->docker_registry_image_tag} tag.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true,
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"),
'ignore_errors' => true,
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true,
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"),
'ignore_errors' => true,
'hidden' => true,
],
);
}
@@ -806,14 +863,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function check_image_locally_or_remotely()
{
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found',
"docker images -q {$this->production_image_name} 2>/dev/null",
'hidden' => true,
'save' => 'local_image_found',
]);
if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
$this->execute_remote_command([
"docker pull {$this->production_image_name} 2>/dev/null", 'ignore_errors' => true, 'hidden' => true,
"docker pull {$this->production_image_name} 2>/dev/null",
'ignore_errors' => true,
'hidden' => true,
]);
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", 'hidden' => true, 'save' => 'local_image_found',
"docker images -q {$this->production_image_name} 2>/dev/null",
'hidden' => true,
'save' => 'local_image_found',
]);
}
}
@@ -846,17 +909,24 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
$envs->push("COOLIFY_FQDN={$this->preview->fqdn}");
$envs->push("COOLIFY_DOMAIN_URL={$this->preview->fqdn}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
$envs->push("COOLIFY_URL={$url}");
$envs->push("COOLIFY_DOMAIN_FQDN={$url}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$local_branch}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
}
}
add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables_preview);
foreach ($sorted_environment_variables_preview as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
@@ -891,18 +961,31 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
$envs->push("COOLIFY_FQDN={$this->application->fqdn}");
if ((int) $this->application->compose_parsing_version >= 3) {
$envs->push("COOLIFY_URL={$this->application->fqdn}");
} else {
$envs->push("COOLIFY_FQDN={$this->application->fqdn}");
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
$envs->push("COOLIFY_URL={$url}");
if ((int) $this->application->compose_parsing_version >= 3) {
$envs->push("COOLIFY_FQDN={$url}");
} else {
$envs->push("COOLIFY_URL={$url}");
}
}
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$local_branch}");
}
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
}
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
}
}
add_coolify_default_environment_variables($this->application, $envs, $this->application->environment_variables);
foreach ($sorted_environment_variables as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
@@ -979,17 +1062,58 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
);
}
}
$this->environment_variables = $envs;
}
private function elixir_finetunes()
{
if ($this->pull_request_id === 0) {
$envType = 'environment_variables';
} else {
$envType = 'environment_variables_preview';
}
$mix_env = $this->application->{$envType}->where('key', 'MIX_ENV')->first();
if ($mix_env) {
if ($mix_env->is_build_time === false) {
$this->application_deployment_queue->addLogEntry('MIX_ENV environment variable is not set as build time.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please set MIX_ENV environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
}
} else {
$this->application_deployment_queue->addLogEntry('MIX_ENV environment variable not found.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please add MIX_ENV environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
}
$secret_key_base = $this->application->{$envType}->where('key', 'SECRET_KEY_BASE')->first();
if ($secret_key_base) {
if ($secret_key_base->is_build_time === false) {
$this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable is not set as build time.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please set SECRET_KEY_BASE environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
}
} else {
$this->application_deployment_queue->addLogEntry('SECRET_KEY_BASE environment variable not found.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please add SECRET_KEY_BASE environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
}
$database_url = $this->application->{$envType}->where('key', 'DATABASE_URL')->first();
if ($database_url) {
if ($database_url->is_build_time === false) {
$this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable is not set as build time.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please set DATABASE_URL environment variable to be build time variable if you facing any issues with the deployment.', type: 'error');
}
} else {
$this->application_deployment_queue->addLogEntry('DATABASE_URL environment variable not found.', type: 'error');
$this->application_deployment_queue->addLogEntry('Please add DATABASE_URL environment variable and set it to be build time variable if you facing any issues with the deployment.', type: 'error');
}
}
private function laravel_finetunes()
{
if ($this->pull_request_id === 0) {
$nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
$nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
$envType = 'environment_variables';
} else {
$nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
$nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
$envType = 'environment_variables_preview';
}
$nixpacks_php_fallback_path = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
$nixpacks_php_root_dir = $this->application->{$envType}->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
if (! $nixpacks_php_fallback_path) {
$nixpacks_php_fallback_path = new EnvironmentVariable;
$nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
@@ -1209,7 +1333,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function prepare_builder_image()
{
$settings = instanceSettings();
$helperImage = config('coolify.helper_image');
$helperImage = "{$helperImage}:{$settings->helper_version}";
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
@@ -1329,10 +1455,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
],
[
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
'hidden' => true,
'save' => 'git_commit_sha',
],
]
);
} else {
$this->execute_remote_command(
@@ -1361,7 +1487,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
$this->execute_remote_command(
[
$importCommands, 'hidden' => true,
$importCommands,
'hidden' => true,
]
);
$this->create_workdir();
@@ -1445,6 +1572,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value);
data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value);
}
if ($this->nixpacks_type === 'elixir') {
$this->elixir_finetunes();
}
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
if ($this->nixpacks_type === 'rust') {
@@ -1571,7 +1701,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// Check for custom HEALTHCHECK
if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile_from_repo', 'ignore_errors' => true,
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"),
'hidden' => true,
'save' => 'dockerfile_from_repo',
'ignore_errors' => true,
]);
$dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
$this->application->parseHealthcheckFromDockerfile($dockerfile);
@@ -1674,14 +1807,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$docker_compose['services'][$this->container_name]['labels'] = $labels;
}
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
$docker_compose['services'][$this->container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => 'tcp://127.0.0.1:24224',
'fluentd-async' => 'true',
'fluentd-sub-second-precision' => 'true',
],
];
$docker_compose['services'][$this->container_name]['logging'] = generate_fluentd_configuration();
}
if ($this->application->settings->is_gpu_enabled) {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
@@ -1708,13 +1834,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
$docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$this->container_name]['volumes'] = $persistent_storages;
if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) {
$docker_compose['services'][$this->container_name]['volumes'] = [];
}
$docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_storages);
}
if (count($persistent_file_volumes) > 0) {
$docker_compose['services'][$this->container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
if (! data_get($docker_compose, 'services.'.$this->container_name.'.volumes')) {
$docker_compose['services'][$this->container_name]['volumes'] = [];
}
$docker_compose['services'][$this->container_name]['volumes'] = array_merge($docker_compose['services'][$this->container_name]['volumes'], $persistent_file_volumes->map(function ($item) {
return "$item->fs_path:$item->mount_path";
})->toArray();
})->toArray());
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
@@ -1837,13 +1970,23 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker pull {$image}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "docker pull {$image}"),
'hidden' => true,
]
);
}
private function build_image()
{
// Add Coolify related variables to the build args
$this->environment_variables->filter(function ($key, $value) {
return str($key)->startsWith('COOLIFY_');
})->each(function ($key, $value) {
$this->build_args->push("--build-arg '{$key}'");
});
$this->build_args = $this->build_args->implode(' ');
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
@@ -1887,12 +2030,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
]);
$build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
]);
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
}
@@ -1900,10 +2045,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
@@ -1917,10 +2068,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
}
@@ -1957,10 +2114,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"),
],
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
} else {
@@ -1974,10 +2137,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
} else {
@@ -1986,22 +2155,30 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
]);
$build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
'hidden' => true,
]);
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
}
$base64_build_command = base64_encode($build_command);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
@@ -2015,10 +2192,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
]
);
}
@@ -2027,24 +2210,43 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
}
/**
* @param int $timeout in seconds
*/
private function graceful_shutdown_container(string $containerName, int $timeout = 30)
private function graceful_shutdown_container(string $containerName, int $timeout = 300)
{
try {
$this->execute_remote_command(
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
);
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->execute_remote_command(
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
);
break;
}
usleep(100000);
}
$isRunning = $this->execute_remote_command(
["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
) === 'true';
if ($isRunning) {
$this->execute_remote_command(
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
);
}
} catch (\Exception $error) {
// report error if needed
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
}
$this->remove_container($containerName);
}
private function remove_container(string $containerName)
{
$this->execute_remote_command(
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);
}
private function stop_running_container(bool $force = false)
@@ -2114,15 +2316,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->build_args->push("--build-arg {$env->key}={$value}");
}
}
$this->build_args = $this->build_args->implode(' ');
ray($this->build_args);
}
private function add_build_env_variables_to_dockerfile()
{
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile',
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"),
'hidden' => true,
'save' => 'dockerfile',
]);
$dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
if ($this->pull_request_id === 0) {
@@ -2140,7 +2341,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} else {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
}
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->real_value}");
}
}
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
@@ -2168,7 +2368,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$exec = "docker exec {$containerName} {$cmd}";
$this->execute_remote_command(
[
'command' => $exec, 'hidden' => true,
'command' => $exec,
'hidden' => true,
],
);
@@ -2195,7 +2396,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
try {
$this->execute_remote_command(
[
'command' => $exec, 'hidden' => true, 'save' => 'post-deployment-command-output',
'command' => $exec,
'hidden' => true,
'save' => 'post-deployment-command-output',
],
);
} catch (Exception $e) {

View File

@@ -1,32 +0,0 @@
<?php
namespace App\Jobs;
use App\Traits\ExecuteRemoteCommand;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ApplicationRestartJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle()
{
ray('Restarting application');
}
}

View File

@@ -2,13 +2,13 @@
namespace App\Jobs;
use App\Models\InstanceSettings;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
@@ -21,16 +21,18 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
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();
$latest_version = data_get($versions, 'coolify.v4.version');
$current_version = config('version');
if (version_compare($latest_version, $current_version, '>')) {
// New version available
$settings->update(['new_version_available' => true]);
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
} else {
$settings->update(['new_version_available' => false]);
}

View File

@@ -1,93 +0,0 @@
<?php
namespace App\Jobs;
use App\Actions\Server\InstallLogDrain;
use App\Models\Server;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Sleep;
class CheckLogDrainContainerJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function healthcheck()
{
$status = instant_remote_process(["docker inspect --format='{{json .State.Status}}' coolify-log-drain"], $this->server, false);
if (str($status)->contains('running')) {
return true;
} else {
return false;
}
}
public function handle()
{
// ray("checking log drain statuses for {$this->server->id}");
try {
if (! $this->server->isFunctional()) {
return;
}
$containers = instant_remote_process(['docker container ls -q'], $this->server, false);
if (! $containers) {
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
$containers = format_docker_command_output_to_json($containers);
$foundLogDrainContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain';
})->first();
if (! $foundLogDrainContainer || ! $this->healthcheck()) {
ray('Log drain container not found or unhealthy. Restarting...');
InstallLogDrain::run($this->server);
Sleep::for(10)->seconds();
if ($this->healthcheck()) {
if ($this->server->log_drain_notification_sent) {
$this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->update(['log_drain_notification_sent' => false]);
}
return;
}
if (! $this->server->log_drain_notification_sent) {
ray('Log drain container still unhealthy. Sending notification...');
// $this->server->team?->notify(new ContainerStopped('Coolify Log Drainer', $this->server, null));
$this->server->update(['log_drain_notification_sent' => true]);
}
} else {
if ($this->server->log_drain_notification_sent) {
$this->server->team?->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->update(['log_drain_notification_sent' => false]);
}
}
} catch (\Throwable $e) {
if (! isCloud()) {
send_internal_notification("CheckLogDrainContainerJob failed on ({$this->server->id}) with: ".$e->getMessage());
}
ray($e->getMessage());
return handleError($e);
}
}
}

View File

@@ -21,11 +21,10 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
{
try {
ray('Cleaning up helper containers on '.$this->server->name);
$containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false);
$containers = format_docker_command_output_to_json($containers);
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerId = data_get($container, 'ID');
$containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
$containerIds = collect(json_decode($containers))->pluck('ID');
if ($containerIds->count() > 0) {
foreach ($containerIds as $containerId) {
ray('Removing container '.$containerId);
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
class CleanupStaleMultiplexedConnections implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle()
{
$this->cleanupStaleConnections();
$this->cleanupNonExistentServerConnections();
}
private function cleanupStaleConnections()
{
$muxFiles = Storage::disk('ssh-mux')->files();
foreach ($muxFiles as $muxFile) {
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
$server = Server::where('uuid', $serverUuid)->first();
if (! $server) {
$this->removeMultiplexFile($muxFile);
continue;
}
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
$checkCommand = "ssh -O check -o ControlPath={$muxSocket} {$server->user}@{$server->ip} 2>/dev/null";
$checkProcess = Process::run($checkCommand);
if ($checkProcess->exitCode() !== 0) {
$this->removeMultiplexFile($muxFile);
} else {
$muxContent = Storage::disk('ssh-mux')->get($muxFile);
$establishedAt = Carbon::parse(substr($muxContent, 37));
$expirationTime = $establishedAt->addSeconds(config('constants.ssh.mux_persist_time'));
if (Carbon::now()->isAfter($expirationTime)) {
$this->removeMultiplexFile($muxFile);
}
}
}
}
private function cleanupNonExistentServerConnections()
{
$muxFiles = Storage::disk('ssh-mux')->files();
$existingServerUuids = Server::pluck('uuid')->toArray();
foreach ($muxFiles as $muxFile) {
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
if (! in_array($serverUuid, $existingServerUuids)) {
$this->removeMultiplexFile($muxFile);
}
}
}
private function extractServerUuidFromMuxFile($muxFile)
{
return substr($muxFile, 4);
}
private function removeMultiplexFile($muxFile)
{
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
$closeCommand = "ssh -O exit -o ControlPath={$muxSocket} localhost 2>/dev/null";
Process::run($closeCommand);
Storage::disk('ssh-mux')->delete($muxFile);
}
}

View File

@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -25,16 +24,6 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->uuid;
}
public function handle()
{
GetContainersStatus::run($this->server);

View File

@@ -2,7 +2,6 @@
namespace App\Jobs;
use App\Actions\Database\StopDatabase;
use App\Events\BackupCreated;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
@@ -22,7 +21,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
@@ -56,49 +54,42 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $backup_output = null;
public ?string $postgres_password = null;
public ?S3Storage $s3 = null;
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 middleware(): array
{
return [new WithoutOverlapping($this->backup->id)];
}
public function uniqueId(): int
{
return $this->backup->id;
}
public function handle(): void
{
try {
BackupCreated::dispatch($this->team->id);
// Check if team is exists
if (is_null($this->team)) {
$this->backup->update(['status' => 'failed']);
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);
$status = str(data_get($this->database, 'status'));
if (! $status->startsWith('running') && $this->database->id !== 0) {
ray('database not running');
@@ -134,6 +125,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else {
$databasesToBackup = $this->database->postgres_user;
}
$this->postgres_password = $envs->filter(function ($env) {
return str($env)->startsWith('POSTGRES_PASSWORD=');
})->first();
if ($this->postgres_password) {
$this->postgres_password = str($this->postgres_password)->after('POSTGRES_PASSWORD=')->value();
}
} elseif (str($databaseType)->contains('mysql')) {
$this->container_name = "{$this->database->name}-$serviceUuid";
$this->directory_name = $serviceName.'-'.$this->container_name;
@@ -238,7 +236,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
$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';
@@ -251,6 +248,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
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,
@@ -279,6 +279,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$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,
@@ -288,6 +291,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$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,
@@ -326,7 +332,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e;
} finally {
BackupCreated::dispatch($this->team->id);
if ($this->team) {
BackupCreated::dispatch($this->team->id);
}
}
}
@@ -336,7 +344,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$url = $this->database->internal_db_url;
if ($databaseWithCollections === 'all') {
$commands[] = 'mkdir -p '.$this->backup_dir;
if (str($this->database->image)->startsWith('mongo:4.0')) {
if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
@@ -351,13 +359,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
$commands[] = 'mkdir -p '.$this->backup_dir;
if ($collectionsToExclude->count() === 0) {
if (str($this->database->image)->startsWith('mongo:4.0')) {
if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location";
}
} else {
if (str($this->database->image)->startsWith('mongo:4.0')) {
if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
@@ -381,7 +389,18 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
try {
$commands[] = 'mkdir -p '.$this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
$backupCommand = 'docker exec';
if ($this->postgres_password) {
$backupCommand .= " -e PGPASSWORD=$this->postgres_password";
}
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);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
@@ -399,8 +418,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
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 === '') {
@@ -418,7 +440,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
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);
@@ -452,7 +478,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup->number_of_backups_locally === 0) {
$deletable = $this->backup->executions()->where('status', 'success');
} else {
$deletable = $this->backup->executions()->where('status', 'success')->orderByDesc('created_at')->skip($this->backup->number_of_backups_locally - 1);
$deletable = $this->backup->executions()->where('status', 'success')->skip($this->backup->number_of_backups_locally - 1);
}
foreach ($deletable->get() as $execution) {
delete_backup_locally($execution->filename, $this->server);
@@ -477,12 +503,32 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else {
$network = $this->database->destination->network;
}
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
$this->ensureHelperImageAvailable();
$fullImageName = $this->getFullImageName();
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.');
ray('Uploaded to S3. '.$this->backup_location.' to s3://'.$bucket.$this->backup_dir);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
throw $e;
@@ -491,4 +537,42 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
instant_remote_process([$command], $this->server);
}
}
private function ensureHelperImageAvailable(): void
{
$fullImageName = $this->getFullImageName();
$imageExists = $this->checkImageExists($fullImageName);
if (! $imageExists) {
$this->pullHelperImage($fullImageName);
}
}
private function checkImageExists(string $fullImageName): bool
{
$result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false);
return trim($result) === 'exists';
}
private function pullHelperImage(string $fullImageName): void
{
try {
instant_remote_process(["docker pull {$fullImageName}"], $this->server);
} catch (\Exception $e) {
$errorMessage = 'Failed to pull helper image: '.$e->getMessage();
$this->add_to_backup_output($errorMessage);
throw new \RuntimeException($errorMessage);
}
}
private function getFullImageName(): string
{
$settings = instanceSettings();
$helperImage = config('coolify.helper_image');
$latestVersion = $settings->helper_version;
return "{$helperImage}:{$latestVersion}";
}
}

View File

@@ -1,62 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Team;
use App\Notifications\Database\DailyBackup;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class DatabaseBackupStatusJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public function __construct() {}
public function handle()
{
// $teams = Team::all();
// foreach ($teams as $team) {
// $scheduled_backups = $team->scheduledDatabaseBackups()->get();
// if ($scheduled_backups->isEmpty()) {
// continue;
// }
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// }
// }
// $scheduled_backups = ScheduledDatabaseBackup::all();
// $databases = collect();
// $teams = collect();
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// $database = $scheduled_backup->database;
// $team = $database->team();
// $teams->put($team->id, $team);
// $databases->put("{$team->id}:{$database->name}", [
// 'failed_count' => $failed->count(),
// ]);
// }
// foreach ($databases as $name => $database) {
// [$team_id, $name] = explode(':', $name);
// $team = $teams->get($team_id);
// $team?->notify(new DailyBackup($databases));
// }
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase;
use App\Actions\Server\CleanupDocker;
use App\Actions\Service\DeleteService;
use App\Actions\Service\StopService;
use App\Models\Application;
@@ -30,8 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
public bool $deleteConfigurations = false,
public bool $deleteVolumes = false) {}
public bool $deleteConfigurations = true,
public bool $deleteVolumes = true,
public bool $dockerCleanup = true,
public bool $deleteConnectedNetworks = true
) {}
public function handle()
{
@@ -51,11 +55,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopDatabase::run($this->resource);
StopDatabase::run($this->resource, true);
break;
case 'service':
StopService::run($this->resource);
DeleteService::run($this->resource);
StopService::run($this->resource, true);
DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
break;
}
@@ -65,12 +69,31 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
if ($this->deleteConfigurations) {
$this->resource?->delete_configurations();
}
$isDatabase = $this->resource instanceof StandalonePostgresql
|| $this->resource instanceof StandaloneRedis
|| $this->resource instanceof StandaloneMongodb
|| $this->resource instanceof StandaloneMysql
|| $this->resource instanceof StandaloneMariadb
|| $this->resource instanceof StandaloneKeydb
|| $this->resource instanceof StandaloneDragonfly
|| $this->resource instanceof StandaloneClickhouse;
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
if (($this->dockerCleanup || $isDatabase) && $server) {
CleanupDocker::dispatch($server, true);
}
if ($this->deleteConnectedNetworks && ! $isDatabase) {
$this->resource?->delete_connected_networks($this->resource->uuid);
}
} catch (\Throwable $e) {
ray($e->getMessage());
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();
if ($this->dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
Artisan::queue('cleanup:stucked-resources');
}
}

View File

@@ -17,11 +17,13 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 300;
public $timeout = 600;
public int|string|null $usageBefore = null;
public $tries = 1;
public function __construct(public Server $server) {}
public ?string $usageBefore = null;
public function __construct(public Server $server, public bool $manualCleanup = false) {}
public function handle(): void
{
@@ -29,9 +31,10 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
if (! $this->server->isFunctional()) {
return;
}
if ($this->server->settings->is_force_cleanup_enabled) {
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
CleanupDocker::run(server: $this->server, force: true);
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;
}
@@ -39,12 +42,12 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
$this->usageBefore = $this->server->getDiskUsage();
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
CleanupDocker::run(server: $this->server, force: true);
CleanupDocker::run(server: $this->server);
return;
}
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
CleanupDocker::run(server: $this->server, force: false);
if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {
CleanupDocker::run(server: $this->server);
$usageAfter = $this->server->getDiskUsage();
if ($usageAfter < $this->usageBefore) {
$this->server->team?->notify(new DockerCleanup($this->server, 'Saved '.($this->usageBefore - $usageAfter).'% disk space.'));
@@ -56,7 +59,8 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
Log::info('No need to clean up '.$this->server->name);
}
} catch (\Throwable $e) {
ray($e->getMessage());
CleanupDocker::run(server: $this->server);
Log::error('DockerCleanupJob failed: '.$e->getMessage());
throw $e;
}
}

View File

@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
@@ -25,16 +24,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public GithubApp $github_app) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->github_app->uuid))];
}
public function uniqueId(): int
{
return $this->github_app->uuid;
}
public function handle()
{
try {

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Jobs;
use App\Actions\Server\UpdateCoolify;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class InstanceAutoUpdateJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 600;
public $tries = 1;
public function __construct() {}
public function handle(): void
{
UpdateCoolify::run();
}
}

View File

@@ -1,50 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
class PullCoolifyImageJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle(): void
{
try {
if (isDev() || isCloud()) {
return;
}
$settings = InstanceSettings::get();
$server = Server::findOrFail(0);
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
}
$latest_version = get_latest_version_of_coolify();
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$latest_version}"], $server, false);
$current_version = config('version');
if (! $settings->is_auto_update_enabled) {
return;
}
if ($latest_version === $current_version) {
return;
}
if (version_compare($latest_version, $current_version, '<')) {
return;
}
} catch (\Throwable $e) {
throw $e;
}
}
}

View File

@@ -8,8 +8,8 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -17,25 +17,25 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server) {}
public function __construct() {}
public function handle(): void
{
try {
$helperImage = config('coolify.helper_image');
ray("Pulling {$helperImage}");
instant_remote_process(["docker pull -q {$helperImage}"], $this->server, false);
ray('PullHelperImageJob done');
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
$settings = instanceSettings();
$latest_version = data_get($versions, 'coolify.helper.version');
$current_version = $settings->helper_version;
if (version_compare($latest_version, $current_version, '>')) {
// New version available
// $helperImage = config('coolify.helper_image');
// instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
$settings->update(['helper_version' => $latest_version]);
}
}
} catch (\Throwable $e) {
send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage());
ray($e->getMessage());

View File

@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
@@ -18,16 +17,6 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server) {}
public function handle(): void

View File

@@ -13,7 +13,6 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ScheduledTaskJob implements ShouldQueue
@@ -36,6 +35,8 @@ class ScheduledTaskJob implements ShouldQueue
public array $containers = [];
public string $server_timezone;
public function __construct($task)
{
$this->task = $task;
@@ -47,20 +48,27 @@ class ScheduledTaskJob implements ShouldQueue
throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
}
$this->team = Team::find($task->team_id);
$this->server_timezone = $this->getServerTimezone();
}
public function middleware(): array
private function getServerTimezone(): string
{
return [new WithoutOverlapping($this->task->id)];
}
if ($this->resource instanceof Application) {
$timezone = $this->resource->destination->server->settings->server_timezone;
public function uniqueId(): int
{
return $this->task->id;
return $timezone;
} elseif ($this->resource instanceof Service) {
$timezone = $this->resource->server->settings->server_timezone;
return $timezone;
}
return 'UTC';
}
public function handle(): void
{
try {
$this->task_log = ScheduledTaskExecution::create([
'scheduled_task_id' => $this->task->id,
@@ -121,6 +129,7 @@ class ScheduledTaskJob implements ShouldQueue
$this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
} finally {
}
}
}

View File

@@ -16,7 +16,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
@@ -24,7 +23,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $tries = 1;
public $timeout = 60;
public $containers;
@@ -43,16 +44,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
// public function middleware(): array
// {
// return [(new WithoutOverlapping($this->server->uuid))];
// }
// public function uniqueId(): int
// {
// return $this->server->uuid;
// }
public function handle()
{
try {
@@ -78,8 +69,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
return 'No containers found.';
}
GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
$this->checkLogDrainContainer();
$this->checkSentinel();
if ($this->server->isLogDrainEnabled()) {
$this->checkLogDrainContainer();
}
}
} catch (\Throwable $e) {
@@ -90,24 +82,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
}
private function checkSentinel()
{
if ($this->server->isSentinelEnabled()) {
$sentinelContainerFound = $this->containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-sentinel';
})->first();
if ($sentinelContainerFound) {
$status = data_get($sentinelContainerFound, 'State.Status');
if ($status !== 'running') {
PullSentinelImageJob::dispatch($this);
}
}
}
}
private function serverStatus()
{
['uptime' => $uptime] = $this->server->validateConnection();
['uptime' => $uptime] = $this->server->validateConnection(false);
if ($uptime) {
if ($this->server->unreachable_notification_sent === true) {
$this->server->update(['unreachable_notification_sent' => false]);

View File

@@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +25,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Team $team) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->team->uuid))];
}
public function uniqueId(): int
{
return $this->team->uuid;
}
public function handle()
{
try {

View File

@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +25,6 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->uuid;
}
public function handle()
{
if (! $this->server->isServerReady($this->tries)) {

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 60;
public $containers;
public $applications;
public $databases;
public $services;
public $previews;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public Server $server) {}
public function handle()
{
try {
if (! $this->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);
}
}
}

View File

@@ -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 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue
{
try {
CheckForUpdatesJob::dispatchSync();
$settings = InstanceSettings::get();
$settings = instanceSettings();
if (! $settings->new_version_available) {
Log::info('No new version available. Skipping update.');

View File

@@ -73,6 +73,8 @@ class Index extends Component
}
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
$this->remoteServerPort = $this->remoteServerPort;
$this->remoteServerUser = $this->remoteServerUser;
if (isDev()) {
$this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
@@ -139,7 +141,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
if (! $this->createdServer) {
return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
}
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') {
@@ -154,6 +156,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if ($this->servers->count() > 0) {
$this->selectedExistingServer = $this->servers->first()->id;
$this->updateServerDetails();
$this->currentState = 'select-existing-server';
return;
@@ -172,10 +175,19 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return;
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
$this->updateServerDetails();
$this->currentState = 'validate-server';
}
private function updateServerDetails()
{
if ($this->createdServer) {
$this->remoteServerPort = $this->createdServer->port;
$this->remoteServerUser = $this->createdServer->user;
}
}
public function getProxyType()
{
// Set Default Proxy Type
@@ -219,27 +231,35 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function savePrivateKey()
{
$this->validate([
'privateKeyName' => 'required',
'privateKey' => 'required',
'privateKeyName' => 'required|string|max:255',
'privateKeyDescription' => 'nullable|string|max:255',
'privateKey' => 'required|string',
]);
$this->createdPrivateKey = PrivateKey::create([
'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription,
'private_key' => $this->privateKey,
'team_id' => currentTeam()->id,
]);
$this->createdPrivateKey->save();
$this->currentState = 'create-server';
try {
$privateKey = PrivateKey::createAndStore([
'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription,
'private_key' => $this->privateKey,
'team_id' => currentTeam()->id,
]);
$this->createdPrivateKey = $privateKey;
$this->currentState = 'create-server';
} catch (\Exception $e) {
$this->addError('privateKey', 'Failed to save private key: '.$e->getMessage());
}
}
public function saveServer()
{
$this->validate([
'remoteServerName' => 'required',
'remoteServerHost' => 'required',
'remoteServerName' => 'required|string',
'remoteServerHost' => 'required|string',
'remoteServerPort' => 'required|integer',
'remoteServerUser' => 'required',
'remoteServerUser' => 'required|string',
]);
$this->privateKey = formatPrivateKey($this->privateKey);
$foundServer = Server::whereIp($this->remoteServerHost)->first();
if ($foundServer) {
@@ -269,7 +289,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function validateServer()
{
try {
config()->set('coolify.mux_enabled', false);
config()->set('constants.ssh.mux_enabled', false);
// EC2 does not have `uptime` command, lol
instant_remote_process(['ls /'], $this->createdServer, true);
@@ -277,9 +297,12 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdServer->settings()->update([
'is_reachable' => true,
]);
$this->serverReachable = true;
} catch (\Throwable $e) {
$this->serverReachable = false;
$this->createdServer->delete();
$this->createdServer->settings()->update([
'is_reachable' => false,
]);
return handleError(error: $e, livewire: $this);
}
@@ -296,6 +319,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]);
$this->getProxyType();
} catch (\Throwable $e) {
$this->createdServer->settings()->update([
'is_usable' => false,
]);
return handleError(error: $e, livewire: $this);
}
}
@@ -349,6 +376,21 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
);
}
public function saveAndValidateServer()
{
$this->validate([
'remoteServerPort' => 'required|integer|min:1|max:65535',
'remoteServerUser' => 'required|string',
]);
$this->createdServer->update([
'port' => $this->remoteServerPort,
'user' => $this->remoteServerUser,
'timezone' => 'UTC',
]);
$this->validateServer();
}
private function createNewPrivateKey()
{
$this->privateKeyName = generate_random_name();

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Livewire\CommandCenter;
use App\Models\Server;
use Livewire\Component;
class Index extends Component
{
public $servers = [];
public function mount()
{
$this->servers = Server::isReachable()->get();
}
public function render()
{
return view('livewire.command-center.index');
}
}

View File

@@ -30,8 +30,7 @@ class Dashboard extends Component
public function cleanup_queue()
{
$this->dispatch('success', 'Cleanup started.');
Artisan::queue('cleanup:application-deployment-queue', [
Artisan::queue('cleanup:deployment-queue', [
'--team-id' => currentTeam()->id,
]);
}
@@ -50,15 +49,6 @@ class Dashboard extends Component
])->sortBy('id')->groupBy('server_name')->toArray();
}
// public function getIptables()
// {
// $servers = Server::ownedByCurrentTeam()->get();
// foreach ($servers as $server) {
// checkRequiredCommands($server);
// $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server);
// ray($iptables);
// }
// }
public function render()
{
return view('livewire.dashboard');

View File

@@ -38,7 +38,7 @@ class Form extends Component
}
$this->destination->delete();
return redirect()->route('dashboard');
return redirect()->route('destination.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}

Some files were not shown because too many files have changed in this diff Show More