Compare commits

..

4 Commits

Author SHA1 Message Date
Andras Bacsai
3c04815da4 chore: Update user for s6-setuidgid command in init-script/up 2024-09-12 21:33:33 +02:00
Andras Bacsai
b053cfeee2 chore: Update user for s6-setuidgid command in init-script/up 2024-09-12 21:28:55 +02:00
Andras Bacsai
930157b5db chore: Remove unnecessary SSH command execution time logging 2024-09-12 21:17:17 +02:00
Andras Bacsai
786b6f11b2 test 2024-09-12 21:16:08 +02:00
76 changed files with 633 additions and 1975 deletions

View File

@@ -1,65 +1,46 @@
name: 🐞 Bug Report name: Bug report
description: "File a new bug report." description: "Create a new bug report."
title: "[Bug]: " title: "[Bug]: "
labels: ["🐛 Bug", "🔍 Triage"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: >-
> [!IMPORTANT] # 💎 Bounty program (with
> **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.) [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
# 💎 Bounty Program (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
- If you would like to prioritize the issue resolution, consider adding a bounty to this issue through our [Bounty Program](https://console.algora.io/org/coollabsio/bounties/new).
If you would like to prioritize the issue resolution, you can add bounty
to this issue.
Click [here](https://console.algora.io/org/coollabsio/bounties/new) to
get started.
- type: textarea - type: textarea
attributes: attributes:
label: Error Message and Logs label: Description
description: Provide a detailed description of the error or exception you encountered, along with any relevant log output. description: A clear and concise description of the problem
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Steps to Reproduce label: Minimal Reproduction (if possible, example repository)
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. description: Please provide a step by step guide to reproduce the issue.
value: |
1.
2.
3.
4.
validations: validations:
required: true required: true
- type: textarea
attributes:
label: Exception or Error
description: Please provide error logs if possible.
- type: input - type: input
attributes: attributes:
label: Example Repository URL label: Version
description: If applicable, provide a URL to a repository demonstrating the issue. description: Coolify's version (see top of your screen).
- 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: validations:
required: true required: true
- type: checkboxes
- type: dropdown
attributes: attributes:
label: Are you using Coolify Cloud? label: Cloud?
description: "Are you using the cloud version of Coolify?"
options: options:
- "No (self-hosted)" - label: 'Yes'
- "Yes (Coolify Cloud)" required: false
validations: - label: 'No'
required: true required: false
- 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

@@ -1,31 +0,0 @@
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,18 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: 🤔 Questions and Community Support - name: 🤔 Community Support (Chat)
url: https://coollabs.io/discord url: https://coollabs.io/discord
about: If you have any questions, reach out to us on Discord inside the "#support" channel. about: Reach out to us on Discord.
- name: 🙋‍♂️ Feature Requests
- name: 💡 Feature Request url: https://github.com/coollabsio/coolify/discussions/categories/new-features
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests about: All feature requests will be discussed here.
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

@@ -2,7 +2,7 @@ name: Coolify Helper Image (v4)
on: on:
push: push:
branches: [ "main" ] branches: [ "main", "next" ]
paths: paths:
- .github/workflows/coolify-helper.yml - .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile - docker/coolify-helper/Dockerfile

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
namespace App\Actions\Server; namespace App\Actions\Server;
use App\Jobs\PullHelperImageJob;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -56,13 +55,6 @@ class UpdateCoolify
return; 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); instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
remote_process([ remote_process([

View File

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

View File

@@ -514,7 +514,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
'hidden' => true, 'hidden' => true,
'ignore_errors' => true, 'ignore_errors' => true,
], [ ], [
"docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true", "docker network connect {$networkId} coolify-proxy || true",
'hidden' => true, 'hidden' => true,
'ignore_errors' => true, 'ignore_errors' => true,
]); ]);
@@ -919,10 +919,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} }
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { 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()) { if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); $envs->push("COOLIFY_BRANCH={$local_branch}");
} }
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\""); $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
} }
} }
@@ -978,10 +978,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} }
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') { 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()) { if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); $envs->push("COOLIFY_BRANCH={$local_branch}");
} }
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\""); $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
} }
} }
@@ -2049,10 +2049,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true, 'hidden' => true,
], ],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[ [
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true, 'hidden' => true,
@@ -2072,10 +2068,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true, 'hidden' => true,
], ],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[ [
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true, 'hidden' => true,
@@ -2118,10 +2110,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true, 'hidden' => true,
], ],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[ [
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true, 'hidden' => true,
@@ -2141,10 +2129,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true, 'hidden' => true,
], ],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[ [
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true, 'hidden' => true,
@@ -2173,10 +2157,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true, 'hidden' => true,
], ],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[ [
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true, 'hidden' => true,
@@ -2196,10 +2176,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true, 'hidden' => true,
], ],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[ [
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true, 'hidden' => true,

View File

@@ -4,7 +4,6 @@ namespace App\Jobs;
use App\Actions\Database\StopDatabase; use App\Actions\Database\StopDatabase;
use App\Events\BackupCreated; use App\Events\BackupCreated;
use App\Models\InstanceSettings;
use App\Models\S3Storage; use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution; use App\Models\ScheduledDatabaseBackupExecution;
@@ -479,37 +478,10 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
// private function upload_to_s3(): void
// {
// try {
// if (is_null($this->s3)) {
// return;
// }
// $key = $this->s3->key;
// $secret = $this->s3->secret;
// // $region = $this->s3->region;
// $bucket = $this->s3->bucket;
// $endpoint = $this->s3->endpoint;
// $this->s3->testConnection(shouldSave: true);
// $configName = new Cuid2;
// $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
// $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
// $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
// instant_remote_process($commands, $this->server);
// $this->add_to_backup_output('Uploaded to S3.');
// } catch (\Throwable $e) {
// $this->add_to_backup_output($e->getMessage());
// throw $e;
// } finally {
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
// instant_remote_process($removeConfigCommands, $this->server, false);
// }
// }
private function upload_to_s3(): void private function upload_to_s3(): void
{ {
try { try {
ray($this->backup_location);
if (is_null($this->s3)) { if (is_null($this->s3)) {
return; return;
} }
@@ -519,64 +491,20 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$bucket = $this->s3->bucket; $bucket = $this->s3->bucket;
$endpoint = $this->s3->endpoint; $endpoint = $this->s3->endpoint;
$this->s3->testConnection(shouldSave: true); $this->s3->testConnection(shouldSave: true);
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { $configName = new Cuid2;
$network = $this->database->service->destination->network;
} else {
$network = $this->database->destination->network;
}
$this->ensureHelperImageAvailable(); $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
$commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
$fullImageName = $this->getFullImageName(); $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server); instant_remote_process($commands, $this->server);
$this->add_to_backup_output('Uploaded to S3.'); $this->add_to_backup_output('Uploaded to S3.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage()); $this->add_to_backup_output($e->getMessage());
throw $e; throw $e;
} finally { } finally {
$command = "docker rm -f backup-of-{$this->backup->uuid}"; $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
instant_remote_process([$command], $this->server); $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
instant_remote_process($removeConfigCommands, $this->server, false);
} }
} }
private function ensureHelperImageAvailable(): void
{
$fullImageName = $this->getFullImageName();
$imageExists = $this->checkImageExists($fullImageName);
if (! $imageExists) {
$this->pullHelperImage($fullImageName);
}
}
private function checkImageExists(string $fullImageName): bool
{
$result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false);
return trim($result) === 'exists';
}
private function pullHelperImage(string $fullImageName): void
{
try {
instant_remote_process(["docker pull {$fullImageName}"], $this->server);
} catch (\Exception $e) {
$errorMessage = 'Failed to pull helper image: '.$e->getMessage();
$this->add_to_backup_output($errorMessage);
throw new \RuntimeException($errorMessage);
}
}
private function getFullImageName(): string
{
$settings = InstanceSettings::get();
$helperImage = config('coolify.helper_image');
$latestVersion = $settings->helper_version;
return "{$helperImage}:{$latestVersion}";
}
} }

View File

@@ -42,8 +42,8 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
$current_version = $settings->helper_version; $current_version = $settings->helper_version;
if (version_compare($latest_version, $current_version, '>')) { if (version_compare($latest_version, $current_version, '>')) {
// New version available // New version available
// $helperImage = config('coolify.helper_image'); $helperImage = config('coolify.helper_image');
// instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server); instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
$settings->update(['helper_version' => $latest_version]); $settings->update(['helper_version' => $latest_version]);
} }
} }

View File

@@ -0,0 +1,21 @@
<?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

@@ -20,8 +20,6 @@ class Navbar extends Component
public $isDeploymentProgress = false; public $isDeploymentProgress = false;
public $title = 'Configuration';
public function mount() public function mount()
{ {
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) { if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {

View File

@@ -2,16 +2,18 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Actions\Server\RunCommand;
use App\Models\Application; use App\Models\Application;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Attributes\On;
use Livewire\Component; use Livewire\Component;
class ExecuteContainerCommand extends Component class ExecuteContainerCommand extends Component
{ {
public $container; public string $command;
public string $container;
public Collection $containers; public Collection $containers;
@@ -21,6 +23,8 @@ class ExecuteContainerCommand extends Component
public string $type; public string $type;
public string $workDir = '';
public Server $server; public Server $server;
public Collection $servers; public Collection $servers;
@@ -29,13 +33,11 @@ class ExecuteContainerCommand extends Component
'server' => 'required', 'server' => 'required',
'container' => 'required', 'container' => 'required',
'command' => 'required', 'command' => 'required',
'workDir' => 'nullable',
]; ];
public function mount() public function mount()
{ {
if (! auth()->user()->isAdmin()) {
abort(403);
}
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->containers = collect(); $this->containers = collect();
$this->servers = collect(); $this->servers = collect();
@@ -60,13 +62,24 @@ class ExecuteContainerCommand extends Component
if ($this->resource->destination->server->isFunctional()) { if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server); $this->servers = $this->servers->push($this->resource->destination->server);
} }
$this->container = $this->resource->uuid;
$this->containers->push($this->container);
} elseif (data_get($this->parameters, 'service_uuid')) { } elseif (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service'; $this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$this->resource->applications()->get()->each(function ($application) {
$this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid'));
});
$this->resource->databases()->get()->each(function ($database) {
$this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid'));
});
if ($this->resource->server->isFunctional()) { if ($this->resource->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->server); $this->servers = $this->servers->push($this->resource->server);
} }
} }
if ($this->containers->count() > 0) {
$this->container = $this->containers->first();
}
} }
public function loadContainers() public function loadContainers()
@@ -89,65 +102,44 @@ class ExecuteContainerCommand extends Component
]; ];
$this->containers = $this->containers->push($payload); $this->containers = $this->containers->push($payload);
} }
} elseif (data_get($this->parameters, 'database_uuid')) {
if ($this->resource->isRunning()) {
$this->containers = $this->containers->push([
'server' => $server,
'container' => [
'Names' => $this->resource->uuid,
],
]);
}
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->resource->applications()->get()->each(function ($application) {
ray($application);
if ($application->isRunning()) {
$this->containers->push([
'server' => $this->resource->server,
'container' => [
'Names' => data_get($application, 'name').'-'.data_get($this->resource, 'uuid'),
],
]);
}
});
$this->resource->databases()->get()->each(function ($database) {
if ($database->isRunning()) {
$this->containers->push([
'server' => $this->resource->server,
'container' => [
'Names' => data_get($database, 'name').'-'.data_get($this->resource, 'uuid'),
],
]);
}
});
} }
} }
if ($this->containers->count() > 0) { if ($this->containers->count() > 0) {
$this->container = $this->containers->first(); if (data_get($this->parameters, 'application_uuid')) {
$this->container = data_get($this->containers->first(), 'container.Names');
} elseif (data_get($this->parameters, 'database_uuid')) {
$this->container = $this->containers->first();
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->container = $this->containers->first();
}
} }
} }
#[On('connectToContainer')] public function runCommand()
public function connectToContainer()
{ {
try { try {
$container_name = data_get($this->container, 'container.Names'); if (data_get($this->parameters, 'application_uuid')) {
if (is_null($container_name)) { $container = $this->containers->where('container.Names', $this->container)->first();
throw new \RuntimeException('Container not found.'); $container_name = data_get($container, 'container.Names');
if (is_null($container)) {
throw new \RuntimeException('Container not found.');
}
$server = data_get($container, 'server');
} else {
$container_name = $this->container;
$server = $this->servers->first();
} }
$server = data_get($this->container, 'server');
if ($server->isForceDisabled()) { if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.'); throw new \RuntimeException('Server is disabled.');
} }
$cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'";
$this->dispatch('send-terminal-command', if (! empty($this->workDir)) {
true, $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
$container_name, } else {
$server->uuid, $exec = "docker exec {$container_name} {$cmd}";
); }
$activity = RunCommand::run(server: $server, command: $exec);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -1,43 +0,0 @@
<?php
namespace App\Livewire\Project\Shared;
use App\Models\Server;
use Livewire\Attributes\On;
use Livewire\Component;
class Terminal extends Component
{
#[On('send-terminal-command')]
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
{
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
if ($isContainer) {
$status = getContainerStatus($server, $identifier);
if ($status !== 'running') {
return;
}
$command = generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} else {
$command = generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
}
// ssh command is sent back to frontend then to websocket
// this is done because the websocket connection is not available here
// a better solution would be to remove websocket on NodeJS and work with something like
// 1. Laravel Pusher/Echo connection (not possible without a sdk)
// 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies)
// 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used
// 4. Follow-up discussions here:
// - https://github.com/coollabsio/coolify/issues/2298
// - https://github.com/coollabsio/coolify/discussions/3362
$this->dispatch('send-back-command', $command);
}
public function render()
{
return view('livewire.project.shared.terminal');
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Livewire;
use App\Actions\Server\RunCommand as ServerRunCommand;
use App\Models\Server;
use Livewire\Component;
class RunCommand extends Component
{
public string $command;
public $server;
public $servers = [];
protected $rules = [
'server' => 'required',
'command' => 'required',
];
protected $validationAttributes = [
'server' => 'server',
'command' => 'command',
];
public function mount($servers)
{
$this->servers = $servers;
$this->server = $servers[0]->uuid;
}
public function runCommand()
{
$this->validate();
try {
$activity = ServerRunCommand::run(server: Server::where('uuid', $this->server)->first(), command: $this->command);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -1,76 +0,0 @@
<?php
namespace App\Livewire\Terminal;
use App\Models\Server;
use Livewire\Attributes\On;
use Livewire\Component;
class Index extends Component
{
public $selected_uuid = 'default';
public $servers = [];
public $containers = [];
public function mount()
{
if (! auth()->user()->isAdmin()) {
abort(403);
}
$this->servers = Server::isReachable()->get();
$this->containers = $this->getAllActiveContainers();
}
private function getAllActiveContainers()
{
return collect($this->servers)->flatMap(function ($server) {
if (! $server->isFunctional()) {
return [];
}
return $server->loadAllContainers()->map(function ($container) use ($server) {
$state = data_get_str($container, 'State')->lower();
if ($state->contains('running')) {
return [
'name' => data_get($container, 'Names'),
'connection_name' => data_get($container, 'Names'),
'uuid' => data_get($container, 'Names'),
'status' => data_get_str($container, 'State')->lower(),
'server' => $server,
'server_uuid' => $server->uuid,
];
}
return null;
})->filter();
});
}
public function updatedSelectedUuid()
{
$this->connectToContainer();
}
#[On('connectToContainer')]
public function connectToContainer()
{
if ($this->selected_uuid === 'default') {
$this->dispatch('error', 'Please select a server or a container.');
return;
}
$container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid);
$this->dispatch('send-terminal-command',
isset($container),
$container['connection_name'] ?? $this->selected_uuid,
$container['server_uuid'] ?? $this->selected_uuid
);
}
public function render()
{
return view('livewire.terminal.index');
}
}

View File

@@ -305,13 +305,6 @@ respond 404
'service' => 'coolify-realtime', 'service' => 'coolify-realtime',
'rule' => "Host(`{$host}`) && PathPrefix(`/app`)", 'rule' => "Host(`{$host}`) && PathPrefix(`/app`)",
], ],
'coolify-terminal-ws' => [
'entryPoints' => [
0 => 'http',
],
'service' => 'coolify-terminal',
'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)",
],
], ],
'services' => [ 'services' => [
'coolify' => [ 'coolify' => [
@@ -332,15 +325,6 @@ respond 404
], ],
], ],
], ],
'coolify-terminal' => [
'loadBalancer' => [
'servers' => [
0 => [
'url' => 'http://coolify-realtime:6002',
],
],
],
],
], ],
], ],
]; ];
@@ -370,16 +354,6 @@ respond 404
'certresolver' => 'letsencrypt', 'certresolver' => 'letsencrypt',
], ],
]; ];
$traefik_dynamic_conf['http']['routers']['coolify-terminal-wss'] = [
'entryPoints' => [
0 => 'https',
],
'service' => 'coolify-terminal',
'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)",
'tls' => [
'certresolver' => 'letsencrypt',
],
];
} }
$yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2);
$yaml = $yaml =
@@ -413,9 +387,6 @@ $schema://$host {
handle /app/* { handle /app/* {
reverse_proxy coolify-realtime:6001 reverse_proxy coolify-realtime:6001
} }
handle /terminal/ws/* {
reverse_proxy coolify-realtime:6002
}
reverse_proxy coolify:80 reverse_proxy coolify:80
}"; }";
$base64 = base64_encode($caddy_file); $base64 = base64_encode($caddy_file);
@@ -775,18 +746,6 @@ $schema://$host {
} }
} }
public function loadAllContainers(): Collection
{
if ($this->isFunctional()) {
$containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this);
$containers = format_docker_command_output_to_json($containers);
return collect($containers);
}
return collect([]);
}
public function loadUnmanagedContainers(): Collection public function loadUnmanagedContainers(): Collection
{ {
if ($this->isFunctional()) { if ($this->isFunctional()) {

View File

@@ -32,16 +32,6 @@ class ServiceApplication extends BaseModel
return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
} }
public function isRunning()
{
return str($this->status)->contains('running');
}
public function isExited()
{
return str($this->status)->contains('exited');
}
public function isLogDrainEnabled() public function isLogDrainEnabled()
{ {
return data_get($this, 'is_log_drain_enabled', false); return data_get($this, 'is_log_drain_enabled', false);

View File

@@ -25,16 +25,6 @@ class ServiceDatabase extends BaseModel
remote_process(["docker restart {$container_id}"], $this->service->server); remote_process(["docker restart {$container_id}"], $this->service->server);
} }
public function isRunning()
{
return str($this->status)->contains('running');
}
public function isExited()
{
return str($this->status)->contains('exited');
}
public function isLogDrainEnabled() public function isLogDrainEnabled()
{ {
return data_get($this, 'is_log_drain_enabled', false); return data_get($this, 'is_log_drain_enabled', false);

View File

@@ -75,11 +75,6 @@ class StandaloneClickhouse extends BaseModel
} }
} }
public function isRunning()
{
return (bool) str($this->status)->contains('running');
}
public function isExited() public function isExited()
{ {
return (bool) str($this->status)->startsWith('exited'); return (bool) str($this->status)->startsWith('exited');

View File

@@ -75,11 +75,6 @@ class StandaloneDragonfly extends BaseModel
} }
} }
public function isRunning()
{
return (bool) str($this->status)->contains('running');
}
public function isExited() public function isExited()
{ {
return (bool) str($this->status)->startsWith('exited'); return (bool) str($this->status)->startsWith('exited');

View File

@@ -75,11 +75,6 @@ class StandaloneKeydb extends BaseModel
} }
} }
public function isRunning()
{
return (bool) str($this->status)->contains('running');
}
public function isExited() public function isExited()
{ {
return (bool) str($this->status)->startsWith('exited'); return (bool) str($this->status)->startsWith('exited');

View File

@@ -75,11 +75,6 @@ class StandaloneMariadb extends BaseModel
} }
} }
public function isRunning()
{
return (bool) str($this->status)->contains('running');
}
public function isExited() public function isExited()
{ {
return (bool) str($this->status)->startsWith('exited'); return (bool) str($this->status)->startsWith('exited');

View File

@@ -79,11 +79,6 @@ class StandaloneMongodb extends BaseModel
} }
} }
public function isRunning()
{
return (bool) str($this->status)->contains('running');
}
public function isExited() public function isExited()
{ {
return (bool) str($this->status)->startsWith('exited'); return (bool) str($this->status)->startsWith('exited');

View File

@@ -76,11 +76,6 @@ class StandaloneMysql extends BaseModel
} }
} }
public function isRunning()
{
return (bool) str($this->status)->contains('running');
}
public function isExited() public function isExited()
{ {
return (bool) str($this->status)->startsWith('exited'); return (bool) str($this->status)->startsWith('exited');

View File

@@ -102,11 +102,6 @@ class StandalonePostgresql extends BaseModel
} }
} }
public function isRunning()
{
return (bool) str($this->status)->contains('running');
}
public function isExited() public function isExited()
{ {
return (bool) str($this->status)->startsWith('exited'); return (bool) str($this->status)->startsWith('exited');

View File

@@ -71,11 +71,6 @@ class StandaloneRedis extends BaseModel
} }
} }
public function isRunning()
{
return (bool) str($this->status)->contains('running');
}
public function isExited() public function isExited()
{ {
return (bool) str($this->status)->startsWith('exited'); return (bool) str($this->status)->startsWith('exited');

View File

@@ -40,20 +40,6 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul
return $containers; return $containers;
} }
function getCurrentServiceContainerStatus(Server $server, int $id): Collection
{
$containers = collect([]);
if (! $server->isSwarm()) {
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers);
$containers = $containers->filter();
return $containers;
}
return $containers;
}
function format_docker_command_output_to_json($rawOutput): Collection function format_docker_command_output_to_json($rawOutput): Collection
{ {
$outputLines = explode(PHP_EOL, $rawOutput); $outputLines = explode(PHP_EOL, $rawOutput);
@@ -229,12 +215,12 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
} }
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) { if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
$MINIO_BROWSER_REDIRECT_URL?->update([ $MINIO_BROWSER_REDIRECT_URL?->update([
'value' => generateFqdn($server, 'console-'.$uuid, true), 'value' => generateFqdn($server, 'console-'.$uuid),
]); ]);
} }
if (is_null($MINIO_SERVER_URL?->value)) { if (is_null($MINIO_SERVER_URL?->value)) {
$MINIO_SERVER_URL?->update([ $MINIO_SERVER_URL?->update([
'value' => generateFqdn($server, 'minio-'.$uuid, true), 'value' => generateFqdn($server, 'minio-'.$uuid),
]); ]);
} }
$payload = collect([ $payload = collect([

View File

@@ -478,7 +478,7 @@ function data_get_str($data, $key, $default = null): Stringable
return str($str); return str($str);
} }
function generateFqdn(Server $server, string $random, bool $forceHttps = false): string function generateFqdn(Server $server, string $random): string
{ {
$wildcard = data_get($server, 'settings.wildcard_domain'); $wildcard = data_get($server, 'settings.wildcard_domain');
if (is_null($wildcard) || $wildcard === '') { if (is_null($wildcard) || $wildcard === '') {
@@ -488,9 +488,6 @@ function generateFqdn(Server $server, string $random, bool $forceHttps = false):
$host = $url->getHost(); $host = $url->getHost();
$path = $url->getPath() === '/' ? '' : $url->getPath(); $path = $url->getPath() === '/' ? '' : $url->getPath();
$scheme = $url->getScheme(); $scheme = $url->getScheme();
if ($forceHttps) {
$scheme = 'https';
}
$finalFqdn = "$scheme://{$random}.$host$path"; $finalFqdn = "$scheme://{$random}.$host$path";
return $finalFqdn; return $finalFqdn;
@@ -789,7 +786,7 @@ function replaceLocalSource(Stringable $source, Stringable $replacedWith)
if ($source->startsWith('..')) { if ($source->startsWith('..')) {
$source = $source->replaceFirst('..', $replacedWith->value()); $source = $source->replaceFirst('..', $replacedWith->value());
} }
if ($source->endsWith('/') && $source->value() !== '/') { if ($source->endsWith('/')) {
$source = $source->replaceLast('/', ''); $source = $source->replaceLast('/', '');
} }
@@ -2103,16 +2100,16 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
// TODO: move this in a shared function // TODO: move this in a shared function
if (! $parsedServiceVariables->has('COOLIFY_APP_NAME')) { if (! $parsedServiceVariables->has('COOLIFY_APP_NAME')) {
$parsedServiceVariables->put('COOLIFY_APP_NAME', "\"{$resource->name}\""); $parsedServiceVariables->put('COOLIFY_APP_NAME', $resource->name);
} }
if (! $parsedServiceVariables->has('COOLIFY_SERVER_IP')) { if (! $parsedServiceVariables->has('COOLIFY_SERVER_IP')) {
$parsedServiceVariables->put('COOLIFY_SERVER_IP', "\"{$resource->destination->server->ip}\""); $parsedServiceVariables->put('COOLIFY_SERVER_IP', $resource->destination->server->ip);
} }
if (! $parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) { if (! $parsedServiceVariables->has('COOLIFY_ENVIRONMENT_NAME')) {
$parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\""); $parsedServiceVariables->put('COOLIFY_ENVIRONMENT_NAME', $resource->environment->name);
} }
if (! $parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) { if (! $parsedServiceVariables->has('COOLIFY_PROJECT_NAME')) {
$parsedServiceVariables->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\""); $parsedServiceVariables->put('COOLIFY_PROJECT_NAME', $resource->project()->name);
} }
$parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) { $parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) {
@@ -3232,6 +3229,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($isApplication && $isPullRequest) { if ($isApplication && $isPullRequest) {
$source = $source."-pr-$pullRequestId"; $source = $source."-pr-$pullRequestId";
} }
LocalFileVolume::updateOrCreate( LocalFileVolume::updateOrCreate(
[ [
'mount_path' => $target, 'mount_path' => $target,
@@ -3471,13 +3469,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$branch = "pull/{$pullRequestId}/head"; $branch = "pull/{$pullRequestId}/head";
} }
if ($originalResource->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { if ($originalResource->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$coolifyEnvironments->put('COOLIFY_BRANCH', "\"{$branch}\""); $coolifyEnvironments->put('COOLIFY_BRANCH', $branch);
} }
} }
// Add COOLIFY_CONTAINER_NAME to environment // Add COOLIFY_CONTAINER_NAME to environment
if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { if ($resource->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', "\"{$containerName}\""); $coolifyEnvironments->put('COOLIFY_CONTAINER_NAME', $containerName);
} }
if ($isApplication) { if ($isApplication) {
@@ -3550,7 +3548,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($isApplication) { if ($isApplication) {
$shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels;
$uuid = $resource->uuid; $uuid = $resource->uuid;
$network = data_get($resource, 'destination.network'); $network = $resource->destination->network;
if ($isPullRequest) { if ($isPullRequest) {
$uuid = "{$resource->uuid}-{$pullRequestId}"; $uuid = "{$resource->uuid}-{$pullRequestId}";
} }
@@ -3560,7 +3558,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} else { } else {
$shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels;
$uuid = $resource->uuid; $uuid = $resource->uuid;
$network = data_get($resource, 'destination.network'); $network = $resource->destination->network;
} }
if ($shouldGenerateLabelsExactly) { if ($shouldGenerateLabelsExactly) {
switch ($server->proxyType()) { switch ($server->proxyType()) {
@@ -3725,30 +3723,30 @@ function add_coolify_default_environment_variables(StandaloneRedis|StandalonePos
} }
if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_APP_NAME')->isEmpty()) { if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_APP_NAME')->isEmpty()) {
if ($isAssociativeArray) { if ($isAssociativeArray) {
$where_to_add->put('COOLIFY_APP_NAME', "\"{$resource->name}\""); $where_to_add->put('COOLIFY_APP_NAME', $resource->name);
} else { } else {
$where_to_add->push("COOLIFY_APP_NAME=\"{$resource->name}\""); $where_to_add->push("COOLIFY_APP_NAME={$resource->name}");
} }
} }
if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_SERVER_IP')->isEmpty()) { if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_SERVER_IP')->isEmpty()) {
if ($isAssociativeArray) { if ($isAssociativeArray) {
$where_to_add->put('COOLIFY_SERVER_IP', "\"{$ip}\""); $where_to_add->put('COOLIFY_SERVER_IP', $ip);
} else { } else {
$where_to_add->push("COOLIFY_SERVER_IP=\"{$ip}\""); $where_to_add->push("COOLIFY_SERVER_IP={$ip}");
} }
} }
if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_ENVIRONMENT_NAME')->isEmpty()) { if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_ENVIRONMENT_NAME')->isEmpty()) {
if ($isAssociativeArray) { if ($isAssociativeArray) {
$where_to_add->put('COOLIFY_ENVIRONMENT_NAME', "\"{$resource->environment->name}\""); $where_to_add->put('COOLIFY_ENVIRONMENT_NAME', $resource->environment->name);
} else { } else {
$where_to_add->push("COOLIFY_ENVIRONMENT_NAME=\"{$resource->environment->name}\""); $where_to_add->push("COOLIFY_ENVIRONMENT_NAME={$resource->environment->name}");
} }
} }
if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_PROJECT_NAME')->isEmpty()) { if ($where_to_check != null && $where_to_check->where('key', 'COOLIFY_PROJECT_NAME')->isEmpty()) {
if ($isAssociativeArray) { if ($isAssociativeArray) {
$where_to_add->put('COOLIFY_PROJECT_NAME', "\"{$resource->project()->name}\""); $where_to_add->put('COOLIFY_PROJECT_NAME', $resource->project()->name);
} else { } else {
$where_to_add->push("COOLIFY_PROJECT_NAME=\"{$resource->project()->name}\""); $where_to_add->push("COOLIFY_PROJECT_NAME={$resource->project()->name}");
} }
} }
} }

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.340', 'release' => '4.0.0-beta.336',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.340'; return '4.0.0-beta.336';

View File

@@ -3,11 +3,15 @@ services:
build: build:
context: . context: .
dockerfile: ./docker/dev/Dockerfile dockerfile: ./docker/dev/Dockerfile
args:
USER_ID: "${USERID:-9999}"
GROUP_ID: "${GROUPID:-9999}"
ports: ports:
- "${APP_PORT:-8000}:80" - "8000:8080"
environment: environment:
PUID: "${USERID:-1000}" PHP_OPCACHE_ENABLE: 1
PGID: "${GROUPID:-1000}" USER_ID: "${USERID:-9999}"
GROUP_ID: "${GROUPID:-9999}"
SSL_MODE: "off" SSL_MODE: "off"
AUTORUN_LARAVEL_STORAGE_LINK: "false" AUTORUN_LARAVEL_STORAGE_LINK: "false"
AUTORUN_LARAVEL_MIGRATION: "false" AUTORUN_LARAVEL_MIGRATION: "false"
@@ -44,17 +48,10 @@ services:
- /data/coolify/_volumes/redis/:/data - /data/coolify/_volumes/redis/:/data
# - coolify-redis-data-dev:/data # - coolify-redis-data-dev:/data
soketi: soketi:
build:
context: .
dockerfile: ./docker/coolify-realtime/Dockerfile
env_file: env_file:
- .env - .env
ports: ports:
- "${FORWARD_SOKETI_PORT:-6001}:6001" - "${FORWARD_SOKETI_PORT:-6001}:6001"
- "6002:6002"
volumes:
- ./storage:/var/www/html/storage
- ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js
environment: environment:
SOKETI_DEBUG: "false" SOKETI_DEBUG: "false"
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"

View File

@@ -87,7 +87,7 @@ services:
soketi: soketi:
condition: service_healthy condition: service_healthy
postgres: postgres:
volumes: volumes:
- coolify-db:/var/lib/postgresql/data - coolify-db:/var/lib/postgresql/data
environment: environment:
POSTGRES_USER: "${DB_USERNAME}" POSTGRES_USER: "${DB_USERNAME}"
@@ -110,24 +110,18 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.1'
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002"
volumes:
- /data/coolify/ssh:/var/www/html/storage/app/ssh
environment: environment:
APP_NAME: "${APP_NAME:-Coolify}"
SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" SOKETI_DEBUG: "${SOKETI_DEBUG:-false}"
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
healthcheck: healthcheck:
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ] test: wget -qO- http://127.0.0.1:6001/ready || exit 1
interval: 5s interval: 5s
retries: 10 retries: 10
timeout: 2s timeout: 2s
volumes: volumes:
coolify-db: coolify-db:
name: coolify-db name: coolify-db

View File

@@ -103,7 +103,7 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.0' image: 'quay.io/soketi/soketi:1.6-16-alpine'
pull_policy: always pull_policy: always
container_name: coolify-realtime container_name: coolify-realtime
restart: always restart: always
@@ -111,21 +111,16 @@ services:
- .env - .env
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002"
volumes:
- ./ssh:/var/www/html/storage/app/ssh
environment: environment:
APP_NAME: "${APP_NAME:-Coolify}"
SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" SOKETI_DEBUG: "${SOKETI_DEBUG:-false}"
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"] test: wget -qO- http://localhost:6001/ready || exit 1
interval: 5s interval: 5s
retries: 10 retries: 10
timeout: 2s timeout: 2s
volumes: volumes:
coolify-db: coolify-db:
name: coolify-db name: coolify-db

View File

@@ -24,9 +24,8 @@ services:
networks: networks:
- coolify - coolify
soketi: soketi:
image: 'quay.io/soketi/soketi:1.6-16-alpine'
container_name: coolify-realtime container_name: coolify-realtime
extra_hosts:
- 'host.docker.internal:host-gateway'
restart: always restart: always
networks: networks:
- coolify - coolify

View File

@@ -1,9 +0,0 @@
FROM quay.io/soketi/soketi:1.6-16-alpine
WORKDIR /terminal
RUN apk add --no-cache openssh-client make g++ python3
COPY docker/coolify-realtime/package.json ./
RUN npm i
RUN npm rebuild node-pty --update-binary
COPY docker/coolify-realtime/soketi-entrypoint.sh /soketi-entrypoint.sh
COPY docker/coolify-realtime/terminal-server.js /terminal/terminal-server.js
ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"]

View File

@@ -1,13 +0,0 @@
{
"private": true,
"type": "module",
"dependencies": {
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"cookie": "^0.6.0",
"axios": "1.7.5",
"dotenv": "^16.4.5",
"node-pty": "^1.0.0",
"ws": "^8.17.0"
}
}

View File

@@ -1,27 +0,0 @@
#!/bin/sh
# Function to timestamp logs
timestamp() {
date "+%Y-%m-%d %H:%M:%S"
}
# Start the terminal server in the background with logging
node /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 &
TERMINAL_PID=$!
# Start the Soketi process in the background with logging
node /app/bin/server.js start > >(while read line; do echo "$(timestamp) [SOKETI] $line"; done) 2>&1 &
SOKETI_PID=$!
# Function to forward signals to child processes
forward_signal() {
kill -$1 $TERMINAL_PID $SOKETI_PID
}
# Forward SIGTERM to child processes
trap 'forward_signal TERM' TERM
# Wait for any process to exit
wait -n
# Exit with status of process that exited first
exit $?

View File

@@ -1,229 +0,0 @@
import { WebSocketServer } from 'ws';
import http from 'http';
import pty from 'node-pty';
import axios from 'axios';
import cookie from 'cookie';
import 'dotenv/config'
const server = http.createServer((req, res) => {
if (req.url === '/ready') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK');
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
}
});
const verifyClient = async (info, callback) => {
const cookies = cookie.parse(info.req.headers.cookie || '');
// const origin = new URL(info.origin);
// const protocol = origin.protocol;
const xsrfToken = cookies['XSRF-TOKEN'];
// Generate session cookie name based on APP_NAME
const appName = process.env.APP_NAME || 'laravel';
const sessionCookieName = `${appName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_session`;
const laravelSession = cookies[sessionCookieName];
// Verify presence of required tokens
if (!laravelSession || !xsrfToken) {
return callback(false, 401, 'Unauthorized: Missing required tokens');
}
try {
// Authenticate with Laravel backend
const response = await axios.post(`http://coolify/terminal/auth`, null, {
headers: {
'Cookie': `${sessionCookieName}=${laravelSession}`,
'X-XSRF-TOKEN': xsrfToken
},
});
if (response.status === 200) {
// Authentication successful
callback(true);
} else {
callback(false, 401, 'Unauthorized: Invalid credentials');
}
} catch (error) {
console.error('Authentication error:', error.message);
callback(false, 500, 'Internal Server Error');
}
};
const wss = new WebSocketServer({ server, path: '/terminal/ws', verifyClient: verifyClient });
const userSessions = new Map();
wss.on('connection', (ws) => {
const userId = generateUserId();
const userSession = { ws, userId, ptyProcess: null, isActive: false };
userSessions.set(userId, userSession);
ws.on('message', (message) => handleMessage(userSession, message));
ws.on('error', (err) => handleError(err, userId));
ws.on('close', () => handleClose(userId));
});
const messageHandlers = {
message: (session, data) => session.ptyProcess.write(data),
resize: (session, { cols, rows }) => session.ptyProcess.resize(cols, rows),
pause: (session) => session.ptyProcess.pause(),
resume: (session) => session.ptyProcess.resume(),
checkActive: (session, data) => {
if (data === 'force' && session.isActive) {
killPtyProcess(session.userId);
} else {
session.ws.send(session.isActive);
}
},
command: (session, data) => handleCommand(session.ws, data, session.userId)
};
function handleMessage(userSession, message) {
const parsed = parseMessage(message);
if (!parsed) return;
Object.entries(parsed).forEach(([key, value]) => {
const handler = messageHandlers[key];
if (handler && (userSession.isActive || key === 'checkActive' || key === 'command')) {
handler(userSession, value);
}
});
}
function parseMessage(message) {
try {
return JSON.parse(message);
} catch (e) {
console.error('Failed to parse message:', e);
return null;
}
}
async function handleCommand(ws, command, userId) {
const userSession = userSessions.get(userId);
if (userSession && userSession.isActive) {
const result = await killPtyProcess(userId);
if (!result) {
// if terminal is still active, even after we tried to kill it, dont continue and show error
ws.send('unprocessable');
return;
}
}
const commandString = command[0].split('\n').join(' ');
const timeout = extractTimeout(commandString);
const sshArgs = extractSshArgs(commandString);
const hereDocContent = extractHereDocContent(commandString);
const options = {
name: 'xterm-color',
cols: 80,
rows: 30,
cwd: process.env.HOME,
};
// NOTE: - Initiates a process within the Terminal container
// Establishes an SSH connection to root@coolify with RequestTTY enabled
// Executes the 'docker exec' command to connect to a specific container
// If the user types 'exit', it terminates the container connection and reverts to the server.
const ptyProcess = pty.spawn('ssh', sshArgs.concat(['bash']), options);
userSession.ptyProcess = ptyProcess;
userSession.isActive = true;
ptyProcess.write(hereDocContent + '\n');
// clear the terminal if the user has clear command
ptyProcess.write('command -v clear >/dev/null 2>&1 && clear\n');
ws.send('pty-ready');
ptyProcess.onData((data) => ws.send(data));
ptyProcess.onExit(({ exitCode, signal }) => {
console.error(`Process exited with code ${exitCode} and signal ${signal}`);
userSession.isActive = false;
});
if (timeout) {
setTimeout(async () => {
await killPtyProcess(userId);
}, timeout * 1000);
}
}
async function handleError(err, userId) {
console.error('WebSocket error:', err);
await killPtyProcess(userId);
}
async function handleClose(userId) {
await killPtyProcess(userId);
userSessions.delete(userId);
}
async function killPtyProcess(userId) {
const session = userSessions.get(userId);
if (!session?.ptyProcess) return false;
return new Promise((resolve) => {
// Loop to ensure terminal is killed before continuing
let killAttempts = 0;
const maxAttempts = 5;
const attemptKill = () => {
killAttempts++;
// session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098
// patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947
session.ptyProcess.write('kill -TERM -$$ && exit\n');
setTimeout(() => {
if (!session.isActive || !session.ptyProcess) {
resolve(true);
return;
}
if (killAttempts < maxAttempts) {
attemptKill();
} else {
resolve(false);
}
}, 500);
};
attemptKill();
});
}
function generateUserId() {
return Math.random().toString(36).substring(2, 11);
}
function extractTimeout(commandString) {
const timeoutMatch = commandString.match(/timeout (\d+)/);
return timeoutMatch ? parseInt(timeoutMatch[1], 10) : null;
}
function extractSshArgs(commandString) {
const sshCommandMatch = commandString.match(/ssh (.+?) 'bash -se'/);
let sshArgs = sshCommandMatch ? sshCommandMatch[1].split(' ') : [];
sshArgs = sshArgs.map(arg => arg === 'RequestTTY=no' ? 'RequestTTY=yes' : arg);
if (!sshArgs.includes('RequestTTY=yes')) {
sshArgs.push('-o', 'RequestTTY=yes');
}
return sshArgs;
}
function extractHereDocContent(commandString) {
const delimiterMatch = commandString.match(/<< (\S+)/);
const delimiter = delimiterMatch ? delimiterMatch[1] : null;
const escapedDelimiter = delimiter.slice(1).trim().replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
const hereDocRegex = new RegExp(`<< \\\\${escapedDelimiter}([\\s\\S\\.]*?)${escapedDelimiter}`);
const hereDocMatch = commandString.match(hereDocRegex);
return hereDocMatch ? hereDocMatch[1] : '';
}
server.listen(6002, () => {
console.log('Server listening on port 6002');
});

View File

@@ -1,6 +1,9 @@
FROM serversideup/php:8.2-fpm-nginx-v2.2.1 FROM serversideup/php:8.3-fpm-nginx
USER root
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG USER_ID
ARG GROUP_ID
# https://github.com/cloudflare/cloudflared/releases # https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.4.1 ARG CLOUDFLARED_VERSION=2024.4.1
@@ -16,7 +19,7 @@ RUN apt-get update
RUN apt-get install postgresql-client-$POSTGRES_VERSION -y RUN apt-get install postgresql-client-$POSTGRES_VERSION -y
# Coolify requirements # Coolify requirements
RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof RUN apt-get install -y openssh-client git git-lfs jq lsof
RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY --chmod=755 docker/dev/etc/s6-overlay/ /etc/s6-overlay/ COPY --chmod=755 docker/dev/etc/s6-overlay/ /etc/s6-overlay/
@@ -40,7 +43,12 @@ RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc
RUN chmod +x /usr/bin/mc RUN chmod +x /usr/bin/mc
RUN { \ # RUN { \
echo 'upload_max_filesize=256M'; \ # echo 'upload_max_filesize=256M'; \
echo 'post_max_size=256M'; \ # echo 'post_max_size=256M'; \
} > /etc/php/current_version/cli/conf.d/upload-limits.ini # } > /etc/php/current_version/cli/conf.d/upload-limits.ini
RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID
RUN docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID --service nginx
USER www-data

View File

@@ -1,5 +1,5 @@
#!/command/execlineb -P #!/command/execlineb -P
foreground { foreground {
s6-sleep 5 s6-sleep 5
su - webuser -c "php /var/www/html/artisan start:horizon" php /var/www/html/artisan start:horizon
} }

View File

@@ -1,5 +1,5 @@
#!/command/execlineb -P #!/command/execlineb -P
foreground { foreground {
s6-sleep 5 s6-sleep 5
su - webuser -c "php /var/www/html/artisan start:scheduler" php /var/www/html/artisan start:scheduler
} }

View File

@@ -1,4 +1,5 @@
FROM serversideup/php:8.2-fpm-nginx-v2.2.1 as base FROM serversideup/php:8.3-fpm-nginx as base
USER root
WORKDIR /var/www/html WORKDIR /var/www/html
COPY composer.json composer.lock ./ COPY composer.json composer.lock ./
@@ -11,8 +12,10 @@ COPY --from=base --chown=9999:9999 /var/www/html .
RUN npm install RUN npm install
RUN npm run build RUN npm run build
FROM serversideup/php:8.2-fpm-nginx-v2.2.1 FROM serversideup/php:8.3-fpm-nginx
USER root
ARG USER_ID=9999
ARG GROUP_ID=9999
ARG TARGETPLATFORM ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases # https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.4.1 ARG CLOUDFLARED_VERSION=2024.4.1
@@ -32,7 +35,7 @@ RUN apt-get update
RUN apt-get install postgresql-client-$POSTGRES_VERSION -y RUN apt-get install postgresql-client-$POSTGRES_VERSION -y
# Coolify requirements # Coolify requirements
RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof vim RUN apt-get install -y openssh-client git git-lfs jq lsof vim
RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY docker/prod/nginx.conf /etc/nginx/conf.d/custom.conf COPY docker/prod/nginx.conf /etc/nginx/conf.d/custom.conf
@@ -64,10 +67,15 @@ RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi" ;fi"
RUN { \ # RUN { \
echo 'upload_max_filesize=256M'; \ # echo 'upload_max_filesize=256M'; \
echo 'post_max_size=256M'; \ # echo 'post_max_size=256M'; \
} > /etc/php/current_version/cli/conf.d/upload-limits.ini # } > /etc/php/current_version/cli/conf.d/upload-limits.ini
COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc COPY --from=minio/mc:RELEASE.2024-09-09T07-53-10Z /usr/bin/mc /usr/bin/mc
RUN chmod +x /usr/bin/mc RUN chmod +x /usr/bin/mc
RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID
RUN docker-php-serversideup-set-file-permissions --owner $USER_ID:$GROUP_ID --service nginx
USER www-data

View File

@@ -1,5 +1,5 @@
#!/command/execlineb -P #!/command/execlineb -P
foreground { foreground {
s6-sleep 5 s6-sleep 5
su - webuser -c "php /var/www/html/artisan start:horizon" php /var/www/html/artisan start:horizon
} }

View File

@@ -1,3 +1,2 @@
#!/command/execlineb -P #!/command/execlineb -P
s6-setuidgid webuser
php /var/www/html/artisan app:init --full-cleanup php /var/www/html/artisan app:init --full-cleanup

View File

@@ -1,5 +1,5 @@
#!/command/execlineb -P #!/command/execlineb -P
foreground { foreground {
s6-sleep 5 s6-sleep 5
su - webuser -c "php /var/www/html/artisan start:scheduler" php /var/www/html/artisan start:scheduler
} }

View File

@@ -6,7 +6,13 @@ APP_KEY=
APP_URL=http://localhost APP_URL=http://localhost
APP_PORT=8000 APP_PORT=8000
APP_DEBUG=true APP_DEBUG=true
SSH_MUX_ENABLED=false MUX_ENABLED=false
# Enable Laravel Telescope for debugging
TELESCOPE_ENABLED=false
# Selenium Driver URL for Dusk
DUSK_DRIVER_URL=http://selenium:4444
# PostgreSQL Database Configuration # PostgreSQL Database Configuration
DB_DATABASE=coolify DB_DATABASE=coolify
@@ -15,22 +21,9 @@ DB_PASSWORD=password
DB_HOST=host.docker.internal DB_HOST=host.docker.internal
DB_PORT=5432 DB_PORT=5432
# Ray Configuration #Set custom ray port
# Set to true to enable Ray
RAY_ENABLED=false
# Set custom ray port
RAY_PORT= RAY_PORT=
# Clockwork Configuration
CLOCKWORK_ENABLED=false
CLOCKWORK_QUEUE_COLLECT=true
# Enable Laravel Telescope for debugging
TELESCOPE_ENABLED=false
# Selenium Driver URL for Dusk
DUSK_DRIVER_URL=http://selenium:4444
# Special Keys for Andras # Special Keys for Andras
# For cache purging # For cache purging
BUNNY_API_KEY= BUNNY_API_KEY=

View File

@@ -48,7 +48,6 @@ services:
- PUSHER_APP_SECRET - PUSHER_APP_SECRET
- AUTOUPDATE - AUTOUPDATE
- SELF_HOSTED - SELF_HOSTED
- SSH_MUX_ENABLED
- SSH_MUX_PERSIST_TIME - SSH_MUX_PERSIST_TIME
- FEEDBACK_DISCORD_WEBHOOK - FEEDBACK_DISCORD_WEBHOOK
- WAITLIST - WAITLIST
@@ -110,24 +109,18 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.1'
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002"
volumes:
- /data/coolify/ssh:/var/www/html/storage/app/ssh
environment: environment:
APP_NAME: "${APP_NAME:-Coolify}"
SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" SOKETI_DEBUG: "${SOKETI_DEBUG:-false}"
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
healthcheck: healthcheck:
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ] test: wget -qO- http://127.0.0.1:6001/ready || exit 1
interval: 5s interval: 5s
retries: 10 retries: 10
timeout: 2s timeout: 2s
volumes: volumes:
coolify-db: coolify-db:
name: coolify-db name: coolify-db

View File

@@ -45,7 +45,7 @@ services:
- PUSHER_APP_SECRET - PUSHER_APP_SECRET
- AUTOUPDATE=true - AUTOUPDATE=true
- SELF_HOSTED=true - SELF_HOSTED=true
- SSH_MUX_ENABLED=false - MUX_ENABLED=false
- IS_WINDOWS_DOCKER_DESKTOP=true - IS_WINDOWS_DOCKER_DESKTOP=true
ports: ports:
- "${APP_PORT:-8000}:80" - "${APP_PORT:-8000}:80"
@@ -103,7 +103,7 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.0' image: 'quay.io/soketi/soketi:1.6-16-alpine'
pull_policy: always pull_policy: always
container_name: coolify-realtime container_name: coolify-realtime
restart: always restart: always
@@ -111,21 +111,16 @@ services:
- .env - .env
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002"
volumes:
- ./ssh:/var/www/html/storage/app/ssh
environment: environment:
APP_NAME: "${APP_NAME:-Coolify}"
SOKETI_DEBUG: "${SOKETI_DEBUG:-false}" SOKETI_DEBUG: "${SOKETI_DEBUG:-false}"
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"] test: wget -qO- http://localhost:6001/ready || exit 1
interval: 5s interval: 5s
retries: 10 retries: 10
timeout: 2s timeout: 2s
volumes: volumes:
coolify-db: coolify-db:
name: coolify-db name: coolify-db

View File

@@ -24,9 +24,8 @@ services:
networks: networks:
- coolify - coolify
soketi: soketi:
image: 'quay.io/soketi/soketi:1.6-16-alpine'
container_name: coolify-realtime container_name: coolify-realtime
extra_hosts:
- 'host.docker.internal:host-gateway'
restart: always restart: always
networks: networks:
- coolify - coolify

View File

@@ -5,30 +5,11 @@ set -e # Exit immediately if a command exits with a non-zero status
## $1 could be empty, so we need to disable this check ## $1 could be empty, so we need to disable this check
#set -u # Treat unset variables as an error and exit #set -u # Treat unset variables as an error and exit
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
CDN="https://cdn.coollabs.io/coolify-nightly"
DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.5" VERSION="1.4"
DOCKER_VERSION="26.0" DOCKER_VERSION="26.0"
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs} CDN="https://cdn.coollabs.io/coolify-nightly"
mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic
chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify
INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log"
exec > >(tee -a $INSTALLATION_LOG_WITH_DATE) 2>&1
getAJoke() {
JOKES=$(curl -s --max-time 2 https://v2.jokeapi.dev/joke/Programming?format=txt&type=single&amount=1 || true)
if [ "$JOKES" != "" ]; then
echo -e " - Until then, here's a joke for you:\n"
echo -e "$JOKES\n"
fi
}
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
ENV_FILE="/data/coolify/source/.env" ENV_FILE="/data/coolify/source/.env"
@@ -65,16 +46,12 @@ fi
LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',') LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',')
LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',') LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',')
LATEST_REALTIME_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',')
if [ -z "$LATEST_HELPER_VERSION" ]; then if [ -z "$LATEST_HELPER_VERSION" ]; then
LATEST_HELPER_VERSION=latest LATEST_HELPER_VERSION=latest
fi fi
if [ -z "$LATEST_REALTIME_VERSION" ]; then DATE=$(date +"%Y%m%d-%H%M%S")
LATEST_REALTIME_VERSION=latest
fi
if [ $EUID != 0 ]; then if [ $EUID != 0 ]; then
echo "Please run as root" echo "Please run as root"
@@ -96,29 +73,18 @@ if [ "$1" != "" ]; then
LATEST_VERSION="${LATEST_VERSION#v}" LATEST_VERSION="${LATEST_VERSION#v}"
fi fi
echo -e "\033[0;35m" echo -e "-------------"
cat << "EOF" echo -e "Welcome to Coolify v4 beta installer!"
_____ _ _ __ echo -e "This script will install everything for you."
/ ____| | (_)/ _|
| | ___ ___ | |_| |_ _ _
| | / _ \ / _ \| | | _| | | |
| |___| (_) | (_) | | | | | |_| |
\_____\___/ \___/|_|_|_| \__, |
__/ |
|___/
EOF
echo -e "\033[0m"
echo -e "Welcome to Coolify Installer!"
echo -e "This script will install everything for you. Sit back and relax."
echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n" echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n"
echo -e "---------------------------------------------" echo -e "-------------"
echo "| Operating System | $OS_TYPE $OS_VERSION"
echo "| Docker | $DOCKER_VERSION" echo "OS: $OS_TYPE $OS_VERSION"
echo "| Coolify | $LATEST_VERSION" echo "Coolify version: $LATEST_VERSION"
echo "| Helper | $LATEST_HELPER_VERSION" echo "Helper version: $LATEST_HELPER_VERSION"
echo "| Realtime | $LATEST_REALTIME_VERSION"
echo -e "---------------------------------------------\n" echo -e "-------------"
echo -e "1. Installing required packages (curl, wget, git, jq). " echo "Installing required packages..."
case "$OS_TYPE" in case "$OS_TYPE" in
arch) arch)
@@ -156,26 +122,24 @@ sles | opensuse-leap | opensuse-tumbleweed)
;; ;;
esac esac
echo -e "2. Check OpenSSH server configuration. "
# Detect OpenSSH server # Detect OpenSSH server
SSH_DETECTED=false SSH_DETECTED=false
if [ -x "$(command -v systemctl)" ]; then if [ -x "$(command -v systemctl)" ]; then
if systemctl status sshd >/dev/null 2>&1; then if systemctl status sshd >/dev/null 2>&1; then
echo " - OpenSSH server is installed." echo "OpenSSH server is installed."
SSH_DETECTED=true SSH_DETECTED=true
elif systemctl status ssh >/dev/null 2>&1; then fi
echo " - OpenSSH server is installed." if systemctl status ssh >/dev/null 2>&1; then
echo "OpenSSH server is installed."
SSH_DETECTED=true SSH_DETECTED=true
fi fi
elif [ -x "$(command -v service)" ]; then elif [ -x "$(command -v service)" ]; then
if service sshd status >/dev/null 2>&1; then if service sshd status >/dev/null 2>&1; then
echo " - OpenSSH server is installed." echo "OpenSSH server is installed."
SSH_DETECTED=true SSH_DETECTED=true
elif service ssh status >/dev/null 2>&1; then fi
echo " - OpenSSH server is installed." if service ssh status >/dev/null 2>&1; then
echo "OpenSSH server is installed."
SSH_DETECTED=true SSH_DETECTED=true
fi fi
fi fi
@@ -187,91 +151,104 @@ if [ "$SSH_DETECTED" = "false" ]; then
fi fi
# Detect SSH PermitRootLogin # Detect SSH PermitRootLogin
SSH_PERMIT_ROOT_LOGIN=$(sshd -T | grep -i "permitrootlogin" | awk '{print $2}') || true SSH_PERMIT_ROOT_LOGIN=false
if [ "$SSH_PERMIT_ROOT_LOGIN" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "without-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "prohibit-password" ]; then SSH_PERMIT_ROOT_LOGIN_CONFIG=$(grep "^PermitRootLogin" /etc/ssh/sshd_config | awk '{print $2}') || SSH_PERMIT_ROOT_LOGIN_CONFIG="N/A (commented out or not found at all)"
echo " - SSH PermitRootLogin is enabled." if [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "prohibit-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "without-password" ]; then
else echo "PermitRootLogin is enabled."
echo " - SSH PermitRootLogin is disabled." SSH_PERMIT_ROOT_LOGIN=true
echo " If you have problems with SSH, please read this: https://coolify.io/docs/knowledge-base/server/openssh" fi
if [ "$SSH_PERMIT_ROOT_LOGIN" != "true" ]; then
echo "###############################################################################"
echo "WARNING: PermitRootLogin is not enabled in /etc/ssh/sshd_config."
echo -e "It is set to $SSH_PERMIT_ROOT_LOGIN_CONFIG. Should be prohibit-password, yes or without-password.\n"
echo -e "Please make sure it is set, otherwise Coolify cannot connect to the host system. \n"
echo "###############################################################################"
fi fi
# Detect if docker is installed via snap # Detect if docker is installed via snap
if [ -x "$(command -v snap)" ]; then if [ -x "$(command -v snap)" ]; then
SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false") if snap list | grep -q docker; then
if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then echo "Docker is installed via snap."
echo " - Docker is installed via snap." echo "Please note that Coolify does not support Docker installed via snap."
echo " Please note that Coolify does not support Docker installed via snap." echo "Please remove Docker with snap (snap remove docker) and reexecute this script."
echo " Please remove Docker with snap (snap remove docker) and reexecute this script."
exit 1 exit 1
fi fi
fi fi
echo -e "3. Check Docker Installation. "
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker)" ]; then
echo " - Docker is not installed. Installing Docker. It may take a while."
getAJoke
case "$OS_TYPE" in case "$OS_TYPE" in
"almalinux") "almalinux")
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1 dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1 dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker)" ]; then
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." echo "Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1 exit 1
fi fi
systemctl start docker >/dev/null 2>&1 systemctl start docker
systemctl enable docker >/dev/null 2>&1 systemctl enable docker
;; ;;
"alpine") "alpine")
apk add docker docker-cli-compose >/dev/null 2>&1 apk add docker docker-cli-compose
rc-update add docker default >/dev/null 2>&1 rc-update add docker default
service docker start >/dev/null 2>&1 service docker start
if ! [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
echo " - Failed to install Docker with apk. Try to install it manually." echo "Docker installed successfully."
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information." else
exit 1 echo "Failed to install Docker with apk. Try to install it manually."
echo "Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
exit
fi fi
;; ;;
"arch") "arch")
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1 pacman -Sy docker docker-compose --noconfirm
systemctl enable docker.service >/dev/null 2>&1 systemctl enable docker.service
if ! [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
echo " - Failed to install Docker with pacman. Try to install it manually." echo "Docker installed successfully."
echo " Please visit https://wiki.archlinux.org/title/docker for more information." else
exit 1 echo "Failed to install Docker with pacman. Try to install it manually."
echo "Please visit https://wiki.archlinux.org/title/docker for more information."
exit
fi fi
;; ;;
"amzn") "amzn")
dnf install docker -y >/dev/null 2>&1 dnf install docker -y
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker} DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1 mkdir -p $DOCKER_CONFIG/cli-plugins
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1 curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1 chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
systemctl start docker >/dev/null 2>&1 systemctl start docker
systemctl enable docker >/dev/null 2>&1 systemctl enable docker
if ! [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
echo " - Failed to install Docker with dnf. Try to install it manually." echo "Docker installed successfully."
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information." else
exit 1 echo "Failed to install Docker with dnf. Try to install it manually."
echo "Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
exit
fi fi
;; ;;
*) *)
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh >/dev/null 2>&1 # Automated Docker installation
if ! [ -x "$(command -v docker)" ]; then curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} >/dev/null 2>&1 if [ -x "$(command -v docker)" ]; then
if ! [ -x "$(command -v docker)" ]; then echo "Docker installed successfully."
echo " - Docker installation failed." else
echo " Maybe your OS is not supported?" echo "Docker installation failed with Rancher script. Trying with official script."
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." curl https://get.docker.com | sh -s -- --version ${DOCKER_VERSION}
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Docker installation failed with official script."
echo "Maybe your OS is not supported?"
echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1 exit 1
fi fi
fi fi
esac esac
echo " - Docker installed successfully."
else
echo " - Docker is installed."
fi fi
echo -e "4. Check Docker Configuration. " echo -e "-------------"
echo -e "Check Docker Configuration..."
mkdir -p /etc/docker mkdir -p /etc/docker
# shellcheck disable=SC2015 # shellcheck disable=SC2015
test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" || cat >/etc/docker/daemon.json <<EOL test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" || cat >/etc/docker/daemon.json <<EOL
@@ -300,33 +277,34 @@ fi
mv "$TEMP_FILE" /etc/docker/daemon.json mv "$TEMP_FILE" /etc/docker/daemon.json
restart_docker_service() { restart_docker_service() {
# Check if systemctl is available # Check if systemctl is available
if command -v systemctl >/dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
echo " - Using systemctl to restart Docker." echo "Using systemctl to restart Docker..."
systemctl restart docker systemctl restart docker
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo " - Docker restarted successfully using systemctl." echo "Docker restarted successfully using systemctl."
else else
echo " - Failed to restart Docker using systemctl." echo "Failed to restart Docker using systemctl."
return 1 return 1
fi fi
# Check if service command is available # Check if service command is available
elif command -v service >/dev/null 2>&1; then elif command -v service >/dev/null 2>&1; then
echo " - Using service command to restart Docker." echo "Using service command to restart Docker..."
service docker restart service docker restart
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo " - Docker restarted successfully using service." echo "Docker restarted successfully using service."
else else
echo " - Failed to restart Docker using service." echo "Failed to restart Docker using service."
return 1 return 1
fi fi
# If neither systemctl nor service is available # If neither systemctl nor service is available
else else
echo " - Neither systemctl nor service command is available on this system." echo "Neither systemctl nor service command is available on this system."
return 1 return 1
fi fi
} }
@@ -334,30 +312,40 @@ restart_docker_service() {
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE")) DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE"))
if [ "$DIFF" != "" ]; then if [ "$DIFF" != "" ]; then
echo " - Docker configuration updated, restart docker daemon..." echo "Docker configuration updated, restart docker daemon..."
restart_docker_service restart_docker_service
else else
echo " - Docker configuration is up to date." echo "Docker configuration is up to date."
fi fi
else else
echo " - Docker configuration updated, restart docker daemon..." echo "Docker configuration updated, restart docker daemon..."
restart_docker_service restart_docker_service
fi fi
echo -e "5. Download required files from CDN. " echo -e "-------------"
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs}
mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic
chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify
echo "Downloading required files from CDN..."
curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh
echo -e "6. Make backup of .env to .env-$DATE"
# Copy .env.example if .env does not exist # Copy .env.example if .env does not exist
if [ -f $ENV_FILE ]; then if [ -f $ENV_FILE ]; then
echo "File exists: $ENV_FILE"
cat $ENV_FILE
echo "Copying .env to .env-$DATE"
cp $ENV_FILE $ENV_FILE-$DATE cp $ENV_FILE $ENV_FILE-$DATE
else else
echo " - File does not exist: $ENV_FILE" echo "File does not exist: $ENV_FILE"
echo " - Copying .env.production to .env-$DATE" echo "Copying .env.production to .env-$DATE"
cp /data/coolify/source/.env.production $ENV_FILE-$DATE cp /data/coolify/source/.env.production $ENV_FILE-$DATE
# Generate a secure APP_ID and APP_KEY # Generate a secure APP_ID and APP_KEY
sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE" sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
@@ -378,7 +366,6 @@ else
fi fi
# Merge .env and .env.production. New values will be added to .env # Merge .env and .env.production. New values will be added to .env
echo -e "7. Propagating .env with new values - if necessary."
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production > $ENV_FILE awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production > $ENV_FILE
if [ "$AUTOUPDATE" = "false" ]; then if [ "$AUTOUPDATE" = "false" ]; then
@@ -388,122 +375,33 @@ if [ "$AUTOUPDATE" = "false" ]; then
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
fi fi
fi fi
echo -e "8. Checking for SSH key for localhost access."
# Generate an ssh key (ed25519) at /data/coolify/ssh/keys/id.root@host.docker.internal
if [ ! -f /data/coolify/ssh/keys/id.root@host.docker.internal ]; then
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.root@host.docker.internal -q -N "" -C root@coolify
chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal
fi
addSshKey() {
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >>~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
}
if [ ! -f ~/.ssh/authorized_keys ]; then if [ ! -f ~/.ssh/authorized_keys ]; then
mkdir -p ~/.ssh mkdir -p ~/.ssh
chmod 700 ~/.ssh chmod 700 ~/.ssh
touch ~/.ssh/authorized_keys touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys addSshKey
fi fi
checkSshKeyInAuthorizedKeys() { if ! grep -qw "root@coolify" ~/.ssh/authorized_keys; then
grep -qw "root@coolify" ~/.ssh/authorized_keys addSshKey
return $? fi
}
checkSshKeyInCoolifyData() { bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}"
[ -s /data/coolify/ssh/keys/id.root@host.docker.internal ]
return $?
}
generateAuthorizedKeys() {
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
rm -f /data/coolify/ssh/keys/id.root@host.docker.internal.pub
}
generateSshKey() {
echo " - Generating SSH key."
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.root@host.docker.internal -q -N "" -C root@coolify
chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal
generateAuthorizedKeys
}
syncSshKeys() {
DB_RUNNING=$(docker inspect coolify-db --format '{{ .State.Status }}' 2>/dev/null)
# Check if SSH key exists in Coolify data but not in authorized_keys
if checkSshKeyInCoolifyData && ! checkSshKeyInAuthorizedKeys; then
# Add the existing Coolify SSH key to authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
# Check if SSH key exists in authorized_keys but not in Coolify data
elif checkSshKeyInAuthorizedKeys && ! checkSshKeyInCoolifyData; then
# Ensure Coolify DB is running before proceeding
if [ "$DB_RUNNING" = "running" ]; then
# Retrieve DB user and SSH key from Coolify database
DB_USER=$(docker inspect coolify-db --format '{{ .Config.Env }}' | grep -oP 'POSTGRES_USER=\K[^ ]+')
DB_SSH_KEY=$(docker exec coolify-db psql -U $DB_USER -d coolify -t -c "SELECT \"private_key\" FROM \"private_keys\" WHERE id = 0 AND team_id = 0 LIMIT 1;" -A -t)
if [ -z "$DB_SSH_KEY" ]; then
# If no key found in DB, generate a new one
echo " - SSH key not found in database. Generating new key."
generateSshKey
else
# If key found in DB, save it and update authorized_keys
echo " - SSH key found in database. Saving to file."
echo "$DB_SSH_KEY" > /data/coolify/ssh/keys/id.root@host.docker.internal
chmod 600 /data/coolify/ssh/keys/id.root@host.docker.internal
chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal
# Generate public key from private key and update authorized_keys
ssh-keygen -y -f /data/coolify/ssh/keys/id.root@host.docker.internal -C root@coolify > /data/coolify/ssh/keys/id.root@host.docker.internal.pub
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
rm -f /data/coolify/ssh/keys/id.root@host.docker.internal.pub
chmod 600 ~/.ssh/authorized_keys
fi
fi
# If SSH key doesn't exist in either location
elif ! checkSshKeyInAuthorizedKeys && ! checkSshKeyInCoolifyData; then
# Ensure Coolify DB is running before proceeding
if [ "$DB_RUNNING" = "running" ]; then
# Retrieve DB user and SSH key from Coolify database
DB_USER=$(docker inspect coolify-db --format '{{ .Config.Env }}' | grep -oP 'POSTGRES_USER=\K[^ ]+')
DB_SSH_KEY=$(docker exec coolify-db psql -U $DB_USER -d coolify -t -c "SELECT \"private_key\" FROM \"private_keys\" WHERE id = 0 AND team_id = 0 LIMIT 1;" -A -t)
if [ -z "$DB_SSH_KEY" ]; then
# If no key found in DB, generate a new one
echo " - SSH key not found in database. Generating new key."
generateSshKey
else
# If key found in DB, save it and update authorized_keys
echo " - SSH key found in database. Saving to file."
echo "$DB_SSH_KEY" > /data/coolify/ssh/keys/id.root@host.docker.internal
chmod 600 /data/coolify/ssh/keys/id.root@host.docker.internal
ssh-keygen -y -f /data/coolify/ssh/keys/id.root@host.docker.internal -C root@coolify > /data/coolify/ssh/keys/id.root@host.docker.internal.pub
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
fi
else
generateSshKey
fi
fi
}
syncSshKeys || true
chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify
echo -e "9. Installing Coolify ($LATEST_VERSION)"
echo -e " - It could take a while based on your server's performance, network speed, stars, etc."
echo -e " - Please wait."
getAJoke
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" >/dev/null 2>&1
echo " - Coolify installed successfully."
rm -f $ENV_FILE-$DATE rm -f $ENV_FILE-$DATE
echo "Waiting for 20 seconds for Coolify to be ready..."
echo " - Waiting for 20 seconds for Coolify (database migrations) to be ready."
getAJoke
sleep 20 sleep 20
echo -e "\033[0;35m echo "Please visit http://$(curl -4s https://ifconfig.io):8000 to get started."
____ _ _ _ _ _ echo -e "\nCongratulations! Your Coolify instance is ready to use.\n"
/ ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| |
| | / _ \| '_ \ / _\` | '__/ _\` | __| | | | |/ _\` | __| |/ _ \| '_ \/ __| |
| |__| (_) | | | | (_| | | | (_| | |_| |_| | | (_| | |_| | (_) | | | \__ \_|
\____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_)
|___/
\033[0m"
echo -e "\nYour instance is ready to use."
echo -e "Please visit http://$(curl -4s https://ifconfig.io):8000 to get started.\n"
echo -e "WARNING: We recommend you to backup your /data/coolify/source/.env file to a safe location, outside of this server."
cp /data/coolify/source/.env /data/coolify/source/.env.backup

View File

@@ -12,6 +12,7 @@ curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
# Merge .env and .env.production. New values will be added to .env # Merge .env and .env.production. New values will be added to .env
awk -F '=' '!seen[$1]++' /data/coolify/source/.env /data/coolify/source/.env.production > /data/coolify/source/.env.tmp && mv /data/coolify/source/.env.tmp /data/coolify/source/.env awk -F '=' '!seen[$1]++' /data/coolify/source/.env /data/coolify/source/.env.production > /data/coolify/source/.env.tmp && mv /data/coolify/source/.env.tmp /data/coolify/source/.env
# Check if PUSHER_APP_ID or PUSHER_APP_KEY or PUSHER_APP_SECRET is empty in /data/coolify/source/.env # Check if PUSHER_APP_ID or PUSHER_APP_KEY or PUSHER_APP_SECRET is empty in /data/coolify/source/.env
if grep -q "PUSHER_APP_ID=$" /data/coolify/source/.env; then if grep -q "PUSHER_APP_ID=$" /data/coolify/source/.env; then
sed -i "s|PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|g" /data/coolify/source/.env sed -i "s|PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|g" /data/coolify/source/.env

View File

@@ -1,16 +1,13 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.339" "version": "4.0.0-beta.330"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.340" "version": "4.0.0-beta.331"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.0"
},
"realtime": {
"version": "1.0.1"
} }
} }
} }

76
package-lock.json generated
View File

@@ -7,15 +7,9 @@
"dependencies": { "dependencies": {
"@tailwindcss/forms": "0.5.7", "@tailwindcss/forms": "0.5.7",
"@tailwindcss/typography": "0.5.13", "@tailwindcss/typography": "0.5.13",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"alpinejs": "3.14.0", "alpinejs": "3.14.0",
"cookie": "^0.6.0",
"dotenv": "^16.4.5",
"ioredis": "5.4.1", "ioredis": "5.4.1",
"node-pty": "^1.0.0", "tailwindcss-scrollbar": "0.1.0"
"tailwindcss-scrollbar": "0.1.0",
"ws": "^8.17.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "4.5.1", "@vitejs/plugin-vue": "4.5.1",
@@ -698,19 +692,6 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
}, },
"node_modules/@xterm/addon-fit": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
},
"node_modules/@xterm/xterm": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
},
"node_modules/alpinejs": { "node_modules/alpinejs": {
"version": "3.14.0", "version": "3.14.0",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.0.tgz", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.0.tgz",
@@ -959,15 +940,6 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
}, },
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -1028,18 +1000,6 @@
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
}, },
"node_modules/dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.692", "version": "1.4.692",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz",
@@ -1515,11 +1475,6 @@
"thenify-all": "^1.0.0" "thenify-all": "^1.0.0"
} }
}, },
"node_modules/nan": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz",
"integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw=="
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -1537,15 +1492,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/node-pty": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
"hasInstallScript": true,
"dependencies": {
"nan": "^2.17.0"
}
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@@ -2178,26 +2124,6 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}, },
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",

View File

@@ -20,14 +20,8 @@
"dependencies": { "dependencies": {
"@tailwindcss/forms": "0.5.7", "@tailwindcss/forms": "0.5.7",
"@tailwindcss/typography": "0.5.13", "@tailwindcss/typography": "0.5.13",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"alpinejs": "3.14.0", "alpinejs": "3.14.0",
"cookie": "^0.6.0",
"dotenv": "^16.4.5",
"ioredis": "5.4.1", "ioredis": "5.4.1",
"node-pty": "^1.0.0", "tailwindcss-scrollbar": "0.1.0"
"tailwindcss-scrollbar": "0.1.0",
"ws": "^8.17.0"
} }
} }

View File

@@ -4,18 +4,3 @@
// const app = createApp({}); // const app = createApp({});
// app.component("magic-bar", MagicBar); // app.component("magic-bar", MagicBar);
// app.mount("#vue"); // app.mount("#vue");
import { Terminal } from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css';
import { FitAddon } from '@xterm/addon-fit';
if (!window.term) {
window.term = new Terminal({
cols: 80,
rows: 30,
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
cursorBlink: true,
});
window.fitAddon = new FitAddon();
window.term.loadAddon(window.fitAddon);
}

View File

@@ -390,7 +390,7 @@ const magicActions = [{
}, },
{ {
id: 19, id: 19,
name: 'Goto: Terminal', name: 'Goto: Command Center',
icon: 'goto', icon: 'goto',
sequence: ['main', 'redirect'] sequence: ['main', 'redirect']
}, },
@@ -653,7 +653,7 @@ async function redirect() {
targetUrl.pathname = `/settings` targetUrl.pathname = `/settings`
break; break;
case 19: case 19:
targetUrl.pathname = `/terminal` targetUrl.pathname = `/command-center`
break; break;
case 20: case 20:
targetUrl.pathname = `/team/notifications` targetUrl.pathname = `/team/notifications`

View File

@@ -1,6 +1,6 @@
<div class="w-full"> <div class="w-full">
@if ($label) @if ($label)
<label class="flex gap-1 items-center mb-1 text-sm font-medium">{{ $label }} <label class="flex items-center gap-1 mb-1 text-sm font-medium">{{ $label }}
@if ($required) @if ($required)
<x-highlighted text="*" /> <x-highlighted text="*" />
@endif @endif
@@ -9,8 +9,7 @@
@endif @endif
</label> </label>
@endif @endif
<select {{ $attributes->merge(['class' => $defaultClass]) }} @required($required) <select {{ $attributes->merge(['class' => $defaultClass]) }} @required($required) wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" name={{ $id }} wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" name={{ $id }}
@if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} @else wire:model={{ $id }} @endif> @if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} @else wire:model={{ $id }} @endif>
{{ $slot }} {{ $slot }}

View File

@@ -1,7 +1,7 @@
<nav class="flex flex-col flex-1 bg-white border-r dark:border-coolgray-200 dark:bg-base" x-data="{ <nav class="flex flex-col flex-1 bg-white border-r dark:border-coolgray-200 dark:bg-base" x-data="{
switchWidth() { switchWidth() {
if (this.full === 'full') { if (this.full === 'full') {
localStorage.setItem('pageWidth', 'center'); localStorage.removeItem('pageWidth');
} else { } else {
localStorage.setItem('pageWidth', 'full'); localStorage.setItem('pageWidth', 'full');
} }
@@ -74,10 +74,8 @@
<button @click="setTheme('light')" class="px-1 dropdown-item-no-padding">Light</button> <button @click="setTheme('light')" class="px-1 dropdown-item-no-padding">Light</button>
<button @click="setTheme('system')" class="px-1 dropdown-item-no-padding">System</button> <button @click="setTheme('system')" class="px-1 dropdown-item-no-padding">System</button>
<div class="my-1 font-bold border-b dark:border-coolgray-500 dark:text-white text-md">Width</div> <div class="my-1 font-bold border-b dark:border-coolgray-500 dark:text-white text-md">Width</div>
<button @click="switchWidth()" class="px-1 dropdown-item-no-padding" <button @click="switchWidth()" class="px-1 dropdown-item-no-padding" x-show="full">Center</button>
x-show="full === 'full'">Center</button> <button @click="switchWidth()" class="px-1 dropdown-item-no-padding" x-show="!full">Full</button>
<button @click="switchWidth()" class="px-1 dropdown-item-no-padding"
x-show="full === 'center'">Full</button>
</div> </div>
</x-dropdown> </x-dropdown>
</div> </div>
@@ -228,9 +226,9 @@
</a> </a>
</li> </li>
<li> <li>
<a title="Terminal" <a title="Command Center"
class="{{ request()->is('terminal*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('command-center*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('terminal') }}"> href="{{ route('command-center') }}">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round"> stroke-linejoin="round">
@@ -238,7 +236,7 @@
<path d="M5 7l5 5l-5 5" /> <path d="M5 7l5 5l-5 5" />
<path d="M12 19l7 0" /> <path d="M12 19l7 0" />
</svg> </svg>
Terminal Command Center
</a> </a>
</li> </li>
<li> <li>

View File

@@ -9,10 +9,6 @@
open: false, open: false,
init() { init() {
this.pageWidth = localStorage.getItem('pageWidth'); this.pageWidth = localStorage.getItem('pageWidth');
if (!this.pageWidth) {
this.pageWidth = 'full';
localStorage.setItem('pageWidth', 'full');
}
} }
}" x-cloak class="mx-auto" :class="pageWidth === 'full' ? '' : 'max-w-7xl'"> }" x-cloak class="mx-auto" :class="pageWidth === 'full' ? '' : 'max-w-7xl'">
<div class="relative z-50 lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true"> <div class="relative z-50 lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true">

View File

@@ -0,0 +1,14 @@
<div>
<x-slot:title>
Command Center | Coolify
</x-slot>
<h1>Command Center</h1>
<div class="subtitle">Execute commands on your servers without leaving the browser.</div>
@if ($servers->count() > 0)
<livewire:run-command :servers="$servers" />
@else
<div>
<div>No servers found. Without a server, you won't be able to do much.</div>
</div>
@endif
</div>

View File

@@ -1,7 +1,7 @@
<nav wire:poll.10000ms="check_status"> <nav wire:poll.10000ms="check_status">
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :lastDeploymentInfo="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" /> <x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :lastDeploymentInfo="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
<div class="navbar-main"> <div class="navbar-main">
<nav class="flex flex-shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10"> <nav class="flex items-center flex-shrink-0 gap-6 scrollbar min-h-10 whitespace-nowrap">
<a href="{{ route('project.application.configuration', $parameters) }}"> <a href="{{ route('project.application.configuration', $parameters) }}">
Configuration Configuration
</a> </a>
@@ -13,12 +13,12 @@
</a> </a>
@if (!$application->destination->server->isSwarm()) @if (!$application->destination->server->isSwarm())
<a href="{{ route('project.application.command', $parameters) }}"> <a href="{{ route('project.application.command', $parameters) }}">
<button>Terminal</button> <button>Command</button>
</a> </a>
@endif @endif
<x-applications.links :application="$application" /> <x-applications.links :application="$application" />
</nav> </nav>
<div class="flex flex-wrap gap-2 items-center"> <div class="flex flex-wrap items-center gap-2">
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)) @if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div> <div>Please load a Compose file.</div>
@else @else

View File

@@ -8,20 +8,19 @@
</x-slide-over> </x-slide-over>
<div class="navbar-main"> <div class="navbar-main">
<nav <nav
class="flex overflow-x-scroll flex-shrink-0 gap-6 items-center whitespace-nowrap sm:overflow-x-hidden scrollbar min-h-10"> class="flex items-center flex-shrink-0 gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap">
<a class="{{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.configuration', $parameters) }}"> href="{{ route('project.database.configuration', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
<a class="{{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.command', $parameters) }}">
<button>Execute Command</button>
</a>
<a class="{{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.logs', $parameters) }}"> href="{{ route('project.database.logs', $parameters) }}">
<button>Logs</button> <button>Logs</button>
</a> </a>
<a class="{{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.command', $parameters) }}">
<button>Terminal</button>
</a>
@if ( @if (
$database->getMorphClass() === 'App\Models\StandalonePostgresql' || $database->getMorphClass() === 'App\Models\StandalonePostgresql' ||
$database->getMorphClass() === 'App\Models\StandaloneMongodb' || $database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
@@ -33,7 +32,7 @@
</a> </a>
@endif @endif
</nav> </nav>
<div class="flex flex-wrap gap-2 items-center"> <div class="flex flex-wrap items-center gap-2">
@if (!str($database->status)->startsWith('exited')) @if (!str($database->status)->startsWith('exited'))
<x-modal-confirmation @click="$wire.dispatch('restartEvent')"> <x-modal-confirmation @click="$wire.dispatch('restartEvent')">
<x-slot:button-title> <x-slot:button-title>

View File

@@ -3,8 +3,8 @@
{{ data_get_str($service, 'name')->limit(10) }} > Configuration | Coolify {{ data_get_str($service, 'name')->limit(10) }} > Configuration | Coolify
</x-slot> </x-slot>
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" /> <livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex flex-col gap-8 pt-6 h-full sm:flex-row"> <div class="flex flex-col h-full gap-8 pt-6 sm:flex-row">
<div class="flex flex-col gap-2 items-start min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a class="menu-item sm:min-w-fit" target="_blank" href="{{ $service->documentation() }}">Documentation <a class="menu-item sm:min-w-fit" target="_blank" href="{{ $service->documentation() }}">Documentation
<x-external-link /></a> <x-external-link /></a>
<a class="menu-item sm:min-w-fit" :class="activeTab === 'service-stack' && 'menu-item-active'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'service-stack' && 'menu-item-active'"
@@ -23,6 +23,10 @@
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'" @click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
href="#">Scheduled Tasks href="#">Scheduled Tasks
</a> </a>
<a class="menu-item sm:min-w-fit" :class="activeTab === 'execute-command' && 'menu-item-active'"
@click.prevent="activeTab = 'execute-command';
window.location.hash = 'execute-command'"
href="#">Execute Command</a>
<a class="menu-item sm:min-w-fit" :class="activeTab === 'logs' && 'menu-item-active'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'logs' && 'menu-item-active'"
@click.prevent="activeTab = 'logs'; @click.prevent="activeTab = 'logs';
window.location.hash = 'logs'" window.location.hash = 'logs'"
@@ -164,7 +168,7 @@
</div> </div>
</div> </div>
<div x-cloak x-show="activeTab === 'storages'"> <div x-cloak x-show="activeTab === 'storages'">
<div class="flex gap-2 items-center"> <div class="flex items-center gap-2">
<h2>Storages</h2> <h2>Storages</h2>
</div> </div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div> <div class="pb-4">Persistent storage to preserve data between deployments.</div>
@@ -187,6 +191,9 @@
<div x-cloak x-show="activeTab === 'logs'"> <div x-cloak x-show="activeTab === 'logs'">
<livewire:project.shared.logs :resource="$service" /> <livewire:project.shared.logs :resource="$service" />
</div> </div>
<div x-cloak x-show="activeTab === 'execute-command'">
<livewire:project.shared.execute-container-command :resource="$service" />
</div>
<div x-cloak x-show="activeTab === 'environment-variables'"> <div x-cloak x-show="activeTab === 'environment-variables'">
<livewire:project.shared.environment-variable.all :resource="$service" /> <livewire:project.shared.environment-variable.all :resource="$service" />
</div> </div>

View File

@@ -6,21 +6,17 @@
<livewire:activity-monitor header="Logs" showWaiting fullHeight /> <livewire:activity-monitor header="Logs" showWaiting fullHeight />
</x-slot:content> </x-slot:content>
</x-slide-over> </x-slide-over>
<h1>{{ $title }}</h1> <h1>Configuration</h1>
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" /> <x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
<div class="navbar-main" x-data> <div class="navbar-main" x-data>
<nav class="flex flex-shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10"> <nav class="flex items-center flex-shrink-0 gap-6 scrollbar min-h-10 whitespace-nowrap">
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}"
href="{{ route('project.service.configuration', $parameters) }}"> href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
<a class="{{ request()->routeIs('project.service.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.service.command', $parameters) }}">
<button>Terminal</button>
</a>
<x-services.links :service="$service" /> <x-services.links :service="$service" />
</nav> </nav>
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last"> <div class="flex flex-wrap items-center order-first gap-2 sm:order-last">
@if (str($service->status())->contains('running')) @if (str($service->status())->contains('running'))
<button @click="$wire.dispatch('restartEvent')" class="gap-2 button"> <button @click="$wire.dispatch('restartEvent')" class="gap-2 button">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -75,7 +71,7 @@
</x-modal-confirmation> </x-modal-confirmation>
@elseif (str($service->status())->contains('exited')) @elseif (str($service->status())->contains('exited'))
<button wire:click='stop(true)' class="gap-2 button"> <button wire:click='stop(true)' class="gap-2 button">
<svg class="w-5 h-5" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path fill="red" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" /> <path fill="red" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" />
<path fill="red" <path fill="red"
d="M17.003 20a4.895 4.895 0 0 0-2.404-4.173L22 3l-1.73-1l-7.577 13.126a5.699 5.699 0 0 0-5.243 1.503C3.706 20.24 3.996 28.682 4.01 29.04a1 1 0 0 0 1 .96h14.991a1 1 0 0 0 .6-1.8c-3.54-2.656-3.598-8.146-3.598-8.2Zm-5.073-3.003A3.11 3.11 0 0 1 15.004 20c0 .038.002.208.017.469l-5.9-2.624a3.8 3.8 0 0 1 2.809-.848ZM15.45 28A5.2 5.2 0 0 1 14 25h-2a6.5 6.5 0 0 0 .968 3h-2.223A16.617 16.617 0 0 1 10 24H8a17.342 17.342 0 0 0 .665 4H6c.031-1.836.29-5.892 1.803-8.553l7.533 3.35A13.025 13.025 0 0 0 17.596 28Z" /> d="M17.003 20a4.895 4.895 0 0 0-2.404-4.173L22 3l-1.73-1l-7.577 13.126a5.699 5.699 0 0 0-5.243 1.503C3.706 20.24 3.996 28.682 4.01 29.04a1 1 0 0 0 1 .96h14.991a1 1 0 0 0 .6-1.8c-3.54-2.656-3.598-8.146-3.598-8.2Zm-5.073-3.003A3.11 3.11 0 0 1 15.004 20c0 .038.002.208.017.469l-5.9-2.624a3.8 3.8 0 0 1 2.809-.848ZM15.45 28A5.2 5.2 0 0 1 14 25h-2a6.5 6.5 0 0 0 .968 3h-2.223A16.617 16.617 0 0 1 10 24H8a17.342 17.342 0 0 0 .665 4H6c.031-1.836.29-5.892 1.803-8.553l7.533 3.35A13.025 13.025 0 0 0 17.596 28Z" />

View File

@@ -4,39 +4,57 @@
</x-slot> </x-slot>
<livewire:project.shared.configuration-checker :resource="$resource" /> <livewire:project.shared.configuration-checker :resource="$resource" />
@if ($type === 'application') @if ($type === 'application')
<h1>Terminal</h1> <h1>Execute Command</h1>
<livewire:project.application.heading :application="$resource" /> <livewire:project.application.heading :application="$resource" />
<h2 class="pt-4">Command</h2>
<div class="pb-2">Run any one-shot command inside a container.</div>
@elseif ($type === 'database') @elseif ($type === 'database')
<h1>Terminal</h1> <h1>Execute Command</h1>
<livewire:project.database.heading :database="$resource" /> <livewire:project.database.heading :database="$resource" />
<h2 class="pt-4">Command</h2>
<div class="pb-2">Run any one-shot command inside a container.</div>
@elseif ($type === 'service') @elseif ($type === 'service')
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" title="Terminal" /> <h2>Execute Command</h2>
@endif @endif
<div x-init="$wire.loadContainers"> <div x-init="$wire.loadContainers">
<div class="pt-4" wire:loading wire:target='loadContainers'> <div class="pt-4" wire:loading wire:target='loadContainers'>
Loading resources... Loading containers...
</div> </div>
<div wire:loading.remove wire:target='loadContainers'> <div wire:loading.remove wire:target='loadContainers'>
@if (count($containers) > 0) @if (count($containers) > 0)
<form class="flex flex-col gap-2 justify-center pt-4 xl:items-end xl:flex-row" <form class="flex flex-col gap-2 pt-4" wire:submit='runCommand'>
wire:submit="$dispatchSelf('connectToContainer')"> <div class="flex gap-2">
<x-forms.input placeholder="ls -l" autofocus id="command" label="Command" required />
<x-forms.input id="workDir" label="Working directory" />
</div>
<x-forms.select label="Container" id="container" required> <x-forms.select label="Container" id="container" required>
<option disabled selected>Select container</option> <option disabled selected>Select container</option>
@foreach ($containers as $container) @if (data_get($this->parameters, 'application_uuid'))
<option value="{{ data_get($container, 'container.Names') }}"> @foreach ($containers as $container)
{{ data_get($container, 'container.Names') }} <option value="{{ data_get($container, 'container.Names') }}">
({{ data_get($container, 'server.name') }}) {{ data_get($container, 'container.Names') }} ({{ data_get($container, 'server.name') }})
</option>
@endforeach
@elseif(data_get($this->parameters, 'service_uuid'))
@foreach ($containers as $container)
<option value="{{ $container }}">
{{ $container }} ({{ data_get($servers, '0.name') }})
</option>
@endforeach
@else
<option value="{{ $container }}">
{{ $container }} ({{ data_get($servers, '0.name') }})
</option> </option>
@endforeach @endif
</x-forms.select> </x-forms.select>
<x-forms.button type="submit">Connect</x-forms.button> <x-forms.button type="submit">Run</x-forms.button>
</form> </form>
@else @else
<div class="pt-4">No containers are not running.</div> <div class="pt-4">No containers are not running.</div>
@endif @endif
</div> </div>
</div> </div>
<div class="mx-auto w-full"> <div class="w-full pt-10 mx-auto">
<livewire:project.shared.terminal /> <livewire:activity-monitor header="Command output" />
</div> </div>
</div> </div>

View File

@@ -1,225 +0,0 @@
<div x-data="data()">
{{-- <div x-show="!terminalActive" class="flex items-center justify-center w-full py-4 mx-auto h-[510px]">
<div class="p-1 w-full h-full rounded border dark:bg-coolgray-100 dark:border-coolgray-300">
<span class="font-mono text-sm text-gray-500" x-text="message"></span>
</div>
</div> --}}
<div x-ref="terminalWrapper"
:class="fullscreen ? 'fullscreen' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
<div id="terminal" wire:ignore></div>
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4 text-white" x-on:click="makeFullscreen"><svg
class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
</svg></button>
<button title="Fullscreen" x-show="!fullscreen && terminalActive" class="absolute right-4 top-6 text-white"
x-on:click="makeFullscreen"><svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />
<path fill="currentColor"
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
</g>
</svg></button>
</div>
@script
<script>
const MAX_PENDING_WRITES = 5;
let pendingWrites = 0;
let paused = false;
let socket;
let commandBuffer = '';
function keepAlive() {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
ping: true
}));
}
}
const keepAliveInterval = setInterval(keepAlive, 30000);
// Clear the interval when the component is destroyed
document.addEventListener('livewire:navigating', () => {
clearInterval(keepAliveInterval);
});
function initializeWebSocket() {
if (!socket || socket.readyState === WebSocket.CLOSED) {
// Only use port if Coolify is used with ip (so it has a port in the url)
let postPath = ':6002/terminal/ws';
const port = window.location.port;
if (!port) {
postPath = '/terminal/ws';
}
let url = window.location.hostname;
// make sure the port is not included
url = url.split(':')[0];
socket = new WebSocket((window.location.protocol === 'https:' ? 'wss://' : 'ws://') +
url +
postPath);
socket.onmessage = handleSocketMessage;
socket.onerror = (e) => {
console.error('WebSocket error:', e);
};
socket.onclose = () => {
console.log('WebSocket connection closed');
};
}
}
function handleSocketMessage(event) {
$data.message = '(connection closed)';
// Initialize Terminal
if (event.data === 'pty-ready') {
term.open(document.getElementById('terminal'));
$data.terminalActive = true;
term.reset();
term.focus();
document.querySelector('.xterm-viewport').classList.add('scrollbar', 'rounded')
$data.resizeTerminal()
} else if (event.data === 'unprocessable') {
term.reset();
$data.terminalActive = false;
$data.message = '(sorry, something went wrong, please try again)';
} else {
pendingWrites++;
term.write(event.data, flowControlCallback);
}
}
function flowControlCallback() {
pendingWrites--;
if (pendingWrites > MAX_PENDING_WRITES && !paused) {
paused = true;
socket.send(JSON.stringify({
pause: true
}));
return;
}
if (pendingWrites <= MAX_PENDING_WRITES && paused) {
paused = false;
socket.send(JSON.stringify({
resume: true
}));
return;
}
}
term.onData((data) => {
socket.send(JSON.stringify({
message: data
}));
// Type CTRL + D or exit in the terminal
if (data === '\x04' || (data === '\r' && stripAnsiCommands(commandBuffer).trim().includes('exit'))) {
checkIfProcessIsRunningAndKillIt();
setTimeout(() => {
$data.terminalActive = false;
term.reset();
}, 500);
commandBuffer = '';
} else if (data === '\r') {
commandBuffer = '';
} else {
commandBuffer += data;
}
});
function stripAnsiCommands(input) {
return input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
}
// Copy and paste
// Enables ctrl + c and ctrl + v
// defaults otherwise to ctrl + insert, shift + insert
term.attachCustomKeyEventHandler((arg) => {
if (arg.ctrlKey && arg.code === "KeyV" && arg.type === "keydown") {
navigator.clipboard.readText()
.then(text => {
socket.send(JSON.stringify({
message: text
}));
})
};
if (arg.ctrlKey && arg.code === "KeyC" && arg.type === "keydown") {
const selection = term.getSelection();
if (selection) {
navigator.clipboard.writeText(selection);
return false;
}
}
return true;
});
$wire.on('send-back-command', function(command) {
socket.send(JSON.stringify({
command: command
}));
});
window.addEventListener('beforeunload', function(e) {
checkIfProcessIsRunningAndKillIt();
});
function checkIfProcessIsRunningAndKillIt() {
socket.send(JSON.stringify({
checkActive: 'force'
}));
}
window.onresize = function() {
$data.resizeTerminal()
};
Alpine.data('data', () => ({
fullscreen: false,
terminalActive: false,
message: '(connection closed)',
init() {
this.$watch('terminalActive', (value) => {
this.$nextTick(() => {
if (value) {
$refs.terminalWrapper.style.display = 'block';
this.resizeTerminal();
} else {
$refs.terminalWrapper.style.display = 'none';
}
});
});
},
makeFullscreen() {
this.fullscreen = !this.fullscreen;
$nextTick(() => {
this.resizeTerminal()
})
},
resizeTerminal() {
if (!this.terminalActive) return;
fitAddon.fit();
const height = $refs.terminalWrapper.clientHeight;
const rows = height / term._core._renderService._charSizeService.height - 1;
var termWidth = term.cols;
var termHeight = parseInt(rows.toString(), 10);
term.resize(termWidth, termHeight);
socket.send(JSON.stringify({
resize: {
cols: termWidth,
rows: termHeight
}
}));
}
}));
initializeWebSocket();
</script>
@endscript
</div>

View File

@@ -0,0 +1,19 @@
<div>
<form class="flex flex-col justify-center gap-2 xl:items-end xl:flex-row" wire:submit='runCommand'>
<x-forms.input placeholder="ls -l" autofocus id="command" label="Command" required />
<x-forms.select label="Server" id="server" required>
@foreach ($servers as $server)
@if ($loop->first)
<option selected value="{{ $server->uuid }}">{{ $server->name }}</option>
@else
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
@endif
@endforeach
</x-forms.select>
<x-forms.button type="submit">Execute Command
</x-forms.button>
</form>
<div class="w-full pt-10 mx-auto">
<livewire:activity-monitor header="Command output" />
</div>
</div>

View File

@@ -194,7 +194,7 @@
@if ($server->settings->force_docker_cleanup) @if ($server->settings->force_docker_cleanup)
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency" <x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
label="Docker cleanup frequency" required label="Docker cleanup frequency" required
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." /> helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every 10 minutes." />
@else @else
<x-forms.input id="server.settings.docker_cleanup_threshold" <x-forms.input id="server.settings.docker_cleanup_threshold"
label="Docker cleanup threshold (%)" required label="Docker cleanup threshold (%)" required

View File

@@ -45,7 +45,7 @@
</div> </div>
</div> </div>
<div class="pt-4"> <div class="pt-4">
If you have any problems, please <a class="underline dark:text-white" href="{{ config('coolify.contact') }}" If you have any problem, please <a class="underline dark:text-white" href="{{ config('coolify.contact') }}"
target="_blank">contact us.</a> target="_blank">contact us.</a>
</div> </div>
@endif @endif

View File

@@ -1,34 +0,0 @@
<div>
<x-slot:title>
Terminal | Coolify
</x-slot>
<h1>Terminal</h1>
<div class="flex gap-2 items-end subtitle">
<div>Execute commands on your servers and containers without leaving the browser.</div>
<x-helper
helper="If you're having trouble connecting to your server, make sure that the port is open.<br><br><a class='underline' href='https://coolify.io/docs/knowledge-base/server/firewall/#terminal' target='_blank'>Documentation</a>"></x-helper>
</div>
<div>
<form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row"
wire:submit="$dispatchSelf('connectToContainer')">
<x-forms.select id="server" required wire:model.live="selected_uuid">
@foreach ($servers as $server)
@if ($loop->first)
<option disabled value="default">Select a server or container</option>
@endif
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
@foreach ($containers as $container)
@if ($container['server_uuid'] == $server->uuid)
<option value="{{ $container['uuid'] }}">
{{ $server->name }} -> {{ $container['name'] }}
</option>
@endif
@endforeach
@endforeach
</x-forms.select>
<x-forms.button type="submit">Connect</x-forms.button>
</form>
<livewire:project.shared.terminal />
</div>
</div>

View File

@@ -6,6 +6,7 @@ use App\Http\Controllers\OauthController;
use App\Http\Controllers\UploadController; use App\Http\Controllers\UploadController;
use App\Livewire\Admin\Index as AdminIndex; use App\Livewire\Admin\Index as AdminIndex;
use App\Livewire\Boarding\Index as BoardingIndex; use App\Livewire\Boarding\Index as BoardingIndex;
use App\Livewire\CommandCenter\Index as CommandCenterIndex;
use App\Livewire\Dashboard; use App\Livewire\Dashboard;
use App\Livewire\Dev\Compose as Compose; use App\Livewire\Dev\Compose as Compose;
use App\Livewire\ForcePasswordReset; use App\Livewire\ForcePasswordReset;
@@ -63,7 +64,6 @@ use App\Livewire\Tags\Show as TagsShow;
use App\Livewire\Team\AdminView as TeamAdminView; use App\Livewire\Team\AdminView as TeamAdminView;
use App\Livewire\Team\Index as TeamIndex; use App\Livewire\Team\Index as TeamIndex;
use App\Livewire\Team\Member\Index as TeamMemberIndex; use App\Livewire\Team\Member\Index as TeamMemberIndex;
use App\Livewire\Terminal\Index as TerminalIndex;
use App\Livewire\Waitlist\Index as WaitlistIndex; use App\Livewire\Waitlist\Index as WaitlistIndex;
use App\Models\GitlabApp; use App\Models\GitlabApp;
use App\Models\PrivateKey; use App\Models\PrivateKey;
@@ -153,14 +153,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/admin', TeamAdminView::class)->name('team.admin-view'); Route::get('/admin', TeamAdminView::class)->name('team.admin-view');
}); });
Route::get('/terminal', TerminalIndex::class)->name('terminal'); Route::get('/command-center', CommandCenterIndex::class)->name('command-center');
Route::post('/terminal/auth', function () {
if (auth()->check()) {
return response()->json(['authenticated' => true], 200);
}
return response()->json(['authenticated' => false], 401);
})->name('terminal.auth');
Route::prefix('invitations')->group(function () { Route::prefix('invitations')->group(function () {
Route::get('/{uuid}', [Controller::class, 'accept_invitation'])->name('team.invitation.accept'); Route::get('/{uuid}', [Controller::class, 'accept_invitation'])->name('team.invitation.accept');
@@ -183,20 +176,20 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/deployment', DeploymentIndex::class)->name('project.application.deployment.index'); Route::get('/deployment', DeploymentIndex::class)->name('project.application.deployment.index');
Route::get('/deployment/{deployment_uuid}', DeploymentShow::class)->name('project.application.deployment.show'); Route::get('/deployment/{deployment_uuid}', DeploymentShow::class)->name('project.application.deployment.show');
Route::get('/logs', Logs::class)->name('project.application.logs'); Route::get('/logs', Logs::class)->name('project.application.logs');
Route::get('/terminal', ExecuteContainerCommand::class)->name('project.application.command'); Route::get('/command', ExecuteContainerCommand::class)->name('project.application.command');
Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.application.scheduled-tasks'); Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.application.scheduled-tasks');
}); });
Route::prefix('project/{project_uuid}/{environment_name}/database/{database_uuid}')->group(function () { Route::prefix('project/{project_uuid}/{environment_name}/database/{database_uuid}')->group(function () {
Route::get('/', DatabaseConfiguration::class)->name('project.database.configuration'); Route::get('/', DatabaseConfiguration::class)->name('project.database.configuration');
Route::get('/logs', Logs::class)->name('project.database.logs'); Route::get('/logs', Logs::class)->name('project.database.logs');
Route::get('/terminal', ExecuteContainerCommand::class)->name('project.database.command'); Route::get('/command', ExecuteContainerCommand::class)->name('project.database.command');
Route::get('/backups', DatabaseBackupIndex::class)->name('project.database.backup.index'); Route::get('/backups', DatabaseBackupIndex::class)->name('project.database.backup.index');
Route::get('/backups/{backup_uuid}', DatabaseBackupExecution::class)->name('project.database.backup.execution'); Route::get('/backups/{backup_uuid}', DatabaseBackupExecution::class)->name('project.database.backup.execution');
}); });
Route::prefix('project/{project_uuid}/{environment_name}/service/{service_uuid}')->group(function () { Route::prefix('project/{project_uuid}/{environment_name}/service/{service_uuid}')->group(function () {
Route::get('/', ServiceConfiguration::class)->name('project.service.configuration'); Route::get('/', ServiceConfiguration::class)->name('project.service.configuration');
Route::get('/terminal', ExecuteContainerCommand::class)->name('project.service.command');
Route::get('/{stack_service_uuid}', ServiceIndex::class)->name('project.service.index'); Route::get('/{stack_service_uuid}', ServiceIndex::class)->name('project.service.index');
Route::get('/command', ExecuteContainerCommand::class)->name('project.service.command');
Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.service.scheduled-tasks'); Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.service.scheduled-tasks');
}); });

View File

@@ -5,30 +5,11 @@ set -e # Exit immediately if a command exits with a non-zero status
## $1 could be empty, so we need to disable this check ## $1 could be empty, so we need to disable this check
#set -u # Treat unset variables as an error and exit #set -u # Treat unset variables as an error and exit
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
CDN="https://cdn.coollabs.io/coolify"
DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.5" VERSION="1.4"
DOCKER_VERSION="26.0" DOCKER_VERSION="26.0"
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs} CDN="https://cdn.coollabs.io/coolify"
mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic
chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify
INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log"
exec > >(tee -a $INSTALLATION_LOG_WITH_DATE) 2>&1
getAJoke() {
JOKES=$(curl -s --max-time 2 https://v2.jokeapi.dev/joke/Programming?format=txt&type=single&amount=1 || true)
if [ "$JOKES" != "" ]; then
echo -e " - Until then, here's a joke for you:\n"
echo -e "$JOKES\n"
fi
}
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
ENV_FILE="/data/coolify/source/.env" ENV_FILE="/data/coolify/source/.env"
@@ -65,16 +46,12 @@ fi
LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',') LATEST_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',')
LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',') LATEST_HELPER_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',')
LATEST_REALTIME_VERSION=$(curl --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',')
if [ -z "$LATEST_HELPER_VERSION" ]; then if [ -z "$LATEST_HELPER_VERSION" ]; then
LATEST_HELPER_VERSION=latest LATEST_HELPER_VERSION=latest
fi fi
if [ -z "$LATEST_REALTIME_VERSION" ]; then DATE=$(date +"%Y%m%d-%H%M%S")
LATEST_REALTIME_VERSION=latest
fi
if [ $EUID != 0 ]; then if [ $EUID != 0 ]; then
echo "Please run as root" echo "Please run as root"
@@ -96,29 +73,18 @@ if [ "$1" != "" ]; then
LATEST_VERSION="${LATEST_VERSION#v}" LATEST_VERSION="${LATEST_VERSION#v}"
fi fi
echo -e "\033[0;35m" echo -e "-------------"
cat << "EOF" echo -e "Welcome to Coolify v4 beta installer!"
_____ _ _ __ echo -e "This script will install everything for you."
/ ____| | (_)/ _|
| | ___ ___ | |_| |_ _ _
| | / _ \ / _ \| | | _| | | |
| |___| (_) | (_) | | | | | |_| |
\_____\___/ \___/|_|_|_| \__, |
__/ |
|___/
EOF
echo -e "\033[0m"
echo -e "Welcome to Coolify Installer!"
echo -e "This script will install everything for you. Sit back and relax."
echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n" echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n"
echo -e "---------------------------------------------" echo -e "-------------"
echo "| Operating System | $OS_TYPE $OS_VERSION"
echo "| Docker | $DOCKER_VERSION" echo "OS: $OS_TYPE $OS_VERSION"
echo "| Coolify | $LATEST_VERSION" echo "Coolify version: $LATEST_VERSION"
echo "| Helper | $LATEST_HELPER_VERSION" echo "Helper version: $LATEST_HELPER_VERSION"
echo "| Realtime | $LATEST_REALTIME_VERSION"
echo -e "---------------------------------------------\n" echo -e "-------------"
echo -e "1. Installing required packages (curl, wget, git, jq). " echo "Installing required packages..."
case "$OS_TYPE" in case "$OS_TYPE" in
arch) arch)
@@ -156,26 +122,24 @@ sles | opensuse-leap | opensuse-tumbleweed)
;; ;;
esac esac
echo -e "2. Check OpenSSH server configuration. "
# Detect OpenSSH server # Detect OpenSSH server
SSH_DETECTED=false SSH_DETECTED=false
if [ -x "$(command -v systemctl)" ]; then if [ -x "$(command -v systemctl)" ]; then
if systemctl status sshd >/dev/null 2>&1; then if systemctl status sshd >/dev/null 2>&1; then
echo " - OpenSSH server is installed." echo "OpenSSH server is installed."
SSH_DETECTED=true SSH_DETECTED=true
elif systemctl status ssh >/dev/null 2>&1; then fi
echo " - OpenSSH server is installed." if systemctl status ssh >/dev/null 2>&1; then
echo "OpenSSH server is installed."
SSH_DETECTED=true SSH_DETECTED=true
fi fi
elif [ -x "$(command -v service)" ]; then elif [ -x "$(command -v service)" ]; then
if service sshd status >/dev/null 2>&1; then if service sshd status >/dev/null 2>&1; then
echo " - OpenSSH server is installed." echo "OpenSSH server is installed."
SSH_DETECTED=true SSH_DETECTED=true
elif service ssh status >/dev/null 2>&1; then fi
echo " - OpenSSH server is installed." if service ssh status >/dev/null 2>&1; then
echo "OpenSSH server is installed."
SSH_DETECTED=true SSH_DETECTED=true
fi fi
fi fi
@@ -187,91 +151,104 @@ if [ "$SSH_DETECTED" = "false" ]; then
fi fi
# Detect SSH PermitRootLogin # Detect SSH PermitRootLogin
SSH_PERMIT_ROOT_LOGIN=$(sshd -T | grep -i "permitrootlogin" | awk '{print $2}') || true SSH_PERMIT_ROOT_LOGIN=false
if [ "$SSH_PERMIT_ROOT_LOGIN" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "without-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN" = "prohibit-password" ]; then SSH_PERMIT_ROOT_LOGIN_CONFIG=$(grep "^PermitRootLogin" /etc/ssh/sshd_config | awk '{print $2}') || SSH_PERMIT_ROOT_LOGIN_CONFIG="N/A (commented out or not found at all)"
echo " - SSH PermitRootLogin is enabled." if [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "prohibit-password" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "yes" ] || [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "without-password" ]; then
else echo "PermitRootLogin is enabled."
echo " - SSH PermitRootLogin is disabled." SSH_PERMIT_ROOT_LOGIN=true
echo " If you have problems with SSH, please read this: https://coolify.io/docs/knowledge-base/server/openssh" fi
if [ "$SSH_PERMIT_ROOT_LOGIN" != "true" ]; then
echo "###############################################################################"
echo "WARNING: PermitRootLogin is not enabled in /etc/ssh/sshd_config."
echo -e "It is set to $SSH_PERMIT_ROOT_LOGIN_CONFIG. Should be prohibit-password, yes or without-password.\n"
echo -e "Please make sure it is set, otherwise Coolify cannot connect to the host system. \n"
echo "###############################################################################"
fi fi
# Detect if docker is installed via snap # Detect if docker is installed via snap
if [ -x "$(command -v snap)" ]; then if [ -x "$(command -v snap)" ]; then
SNAP_DOCKER_INSTALLED=$(snap list docker >/dev/null 2>&1 && echo "true" || echo "false") if snap list | grep -q docker; then
if [ "$SNAP_DOCKER_INSTALLED" = "true" ]; then echo "Docker is installed via snap."
echo " - Docker is installed via snap." echo "Please note that Coolify does not support Docker installed via snap."
echo " Please note that Coolify does not support Docker installed via snap." echo "Please remove Docker with snap (snap remove docker) and reexecute this script."
echo " Please remove Docker with snap (snap remove docker) and reexecute this script."
exit 1 exit 1
fi fi
fi fi
echo -e "3. Check Docker Installation. "
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker)" ]; then
echo " - Docker is not installed. Installing Docker. It may take a while."
getAJoke
case "$OS_TYPE" in case "$OS_TYPE" in
"almalinux") "almalinux")
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1 dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1 dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker)" ]; then
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." echo "Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1 exit 1
fi fi
systemctl start docker >/dev/null 2>&1 systemctl start docker
systemctl enable docker >/dev/null 2>&1 systemctl enable docker
;; ;;
"alpine") "alpine")
apk add docker docker-cli-compose >/dev/null 2>&1 apk add docker docker-cli-compose
rc-update add docker default >/dev/null 2>&1 rc-update add docker default
service docker start >/dev/null 2>&1 service docker start
if ! [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
echo " - Failed to install Docker with apk. Try to install it manually." echo "Docker installed successfully."
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information." else
exit 1 echo "Failed to install Docker with apk. Try to install it manually."
echo "Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
exit
fi fi
;; ;;
"arch") "arch")
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1 pacman -Sy docker docker-compose --noconfirm
systemctl enable docker.service >/dev/null 2>&1 systemctl enable docker.service
if ! [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
echo " - Failed to install Docker with pacman. Try to install it manually." echo "Docker installed successfully."
echo " Please visit https://wiki.archlinux.org/title/docker for more information." else
exit 1 echo "Failed to install Docker with pacman. Try to install it manually."
echo "Please visit https://wiki.archlinux.org/title/docker for more information."
exit
fi fi
;; ;;
"amzn") "amzn")
dnf install docker -y >/dev/null 2>&1 dnf install docker -y
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker} DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1 mkdir -p $DOCKER_CONFIG/cli-plugins
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1 curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1 chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
systemctl start docker >/dev/null 2>&1 systemctl start docker
systemctl enable docker >/dev/null 2>&1 systemctl enable docker
if ! [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
echo " - Failed to install Docker with dnf. Try to install it manually." echo "Docker installed successfully."
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information." else
exit 1 echo "Failed to install Docker with dnf. Try to install it manually."
echo "Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
exit
fi fi
;; ;;
*) *)
curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh >/dev/null 2>&1 # Automated Docker installation
if ! [ -x "$(command -v docker)" ]; then curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh
curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} >/dev/null 2>&1 if [ -x "$(command -v docker)" ]; then
if ! [ -x "$(command -v docker)" ]; then echo "Docker installed successfully."
echo " - Docker installation failed." else
echo " Maybe your OS is not supported?" echo "Docker installation failed with Rancher script. Trying with official script."
echo " - Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." curl https://get.docker.com | sh -s -- --version ${DOCKER_VERSION}
if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully."
else
echo "Docker installation failed with official script."
echo "Maybe your OS is not supported?"
echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1 exit 1
fi fi
fi fi
esac esac
echo " - Docker installed successfully."
else
echo " - Docker is installed."
fi fi
echo -e "4. Check Docker Configuration. " echo -e "-------------"
echo -e "Check Docker Configuration..."
mkdir -p /etc/docker mkdir -p /etc/docker
# shellcheck disable=SC2015 # shellcheck disable=SC2015
test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" || cat >/etc/docker/daemon.json <<EOL test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon.json.original-"$DATE" || cat >/etc/docker/daemon.json <<EOL
@@ -300,33 +277,34 @@ fi
mv "$TEMP_FILE" /etc/docker/daemon.json mv "$TEMP_FILE" /etc/docker/daemon.json
restart_docker_service() { restart_docker_service() {
# Check if systemctl is available # Check if systemctl is available
if command -v systemctl >/dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
echo " - Using systemctl to restart Docker." echo "Using systemctl to restart Docker..."
systemctl restart docker systemctl restart docker
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo " - Docker restarted successfully using systemctl." echo "Docker restarted successfully using systemctl."
else else
echo " - Failed to restart Docker using systemctl." echo "Failed to restart Docker using systemctl."
return 1 return 1
fi fi
# Check if service command is available # Check if service command is available
elif command -v service >/dev/null 2>&1; then elif command -v service >/dev/null 2>&1; then
echo " - Using service command to restart Docker." echo "Using service command to restart Docker..."
service docker restart service docker restart
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo " - Docker restarted successfully using service." echo "Docker restarted successfully using service."
else else
echo " - Failed to restart Docker using service." echo "Failed to restart Docker using service."
return 1 return 1
fi fi
# If neither systemctl nor service is available # If neither systemctl nor service is available
else else
echo " - Neither systemctl nor service command is available on this system." echo "Neither systemctl nor service command is available on this system."
return 1 return 1
fi fi
} }
@@ -334,30 +312,39 @@ restart_docker_service() {
if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then if [ -s /etc/docker/daemon.json.original-"$DATE" ]; then
DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE")) DIFF=$(diff <(jq --sort-keys . /etc/docker/daemon.json) <(jq --sort-keys . /etc/docker/daemon.json.original-"$DATE"))
if [ "$DIFF" != "" ]; then if [ "$DIFF" != "" ]; then
echo " - Docker configuration updated, restart docker daemon..." echo "Docker configuration updated, restart docker daemon..."
restart_docker_service restart_docker_service
else else
echo " - Docker configuration is up to date." echo "Docker configuration is up to date."
fi fi
else else
echo " - Docker configuration updated, restart docker daemon..." echo "Docker configuration updated, restart docker daemon..."
restart_docker_service restart_docker_service
fi fi
echo -e "5. Download required files from CDN. " echo -e "-------------"
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs}
mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic
chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify
echo "Downloading required files from CDN..."
curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh curl -fsSL $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh
echo -e "6. Make backup of .env to .env-$DATE"
# Copy .env.example if .env does not exist # Copy .env.example if .env does not exist
if [ -f $ENV_FILE ]; then if [ -f $ENV_FILE ]; then
echo "File exists: $ENV_FILE"
echo "Copying .env to .env-$DATE"
cp $ENV_FILE $ENV_FILE-$DATE cp $ENV_FILE $ENV_FILE-$DATE
else else
echo " - File does not exist: $ENV_FILE" echo "File does not exist: $ENV_FILE"
echo " - Copying .env.production to .env-$DATE" echo "Copying .env.production to .env-$DATE"
cp /data/coolify/source/.env.production $ENV_FILE-$DATE cp /data/coolify/source/.env.production $ENV_FILE-$DATE
# Generate a secure APP_ID and APP_KEY # Generate a secure APP_ID and APP_KEY
sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE" sed -i "s|^APP_ID=.*|APP_ID=$(openssl rand -hex 16)|" "$ENV_FILE-$DATE"
@@ -378,7 +365,7 @@ else
fi fi
# Merge .env and .env.production. New values will be added to .env # Merge .env and .env.production. New values will be added to .env
echo -e "7. Propagating .env with new values - if necessary." echo "Updating .env with new values (if necessary)..."
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production > $ENV_FILE awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production > $ENV_FILE
if [ "$AUTOUPDATE" = "false" ]; then if [ "$AUTOUPDATE" = "false" ]; then
@@ -388,122 +375,37 @@ if [ "$AUTOUPDATE" = "false" ]; then
sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env sed -i "s|AUTOUPDATE=.*|AUTOUPDATE=false|g" /data/coolify/source/.env
fi fi
fi fi
echo -e "8. Checking for SSH key for localhost access."
# Generate an ssh key (ed25519) at /data/coolify/ssh/keys/id.root@host.docker.internal
if [ ! -f /data/coolify/ssh/keys/id.root@host.docker.internal ]; then
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.root@host.docker.internal -q -N "" -C root@coolify
chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal
fi
addSshKey() {
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >>~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
}
if [ ! -f ~/.ssh/authorized_keys ]; then if [ ! -f ~/.ssh/authorized_keys ]; then
mkdir -p ~/.ssh mkdir -p ~/.ssh
chmod 700 ~/.ssh chmod 700 ~/.ssh
touch ~/.ssh/authorized_keys touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys addSshKey
fi fi
checkSshKeyInAuthorizedKeys() { if ! grep -qw "root@coolify" ~/.ssh/authorized_keys; then
grep -qw "root@coolify" ~/.ssh/authorized_keys addSshKey
return $? fi
}
checkSshKeyInCoolifyData() { bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}"
[ -s /data/coolify/ssh/keys/id.root@host.docker.internal ]
return $?
}
generateAuthorizedKeys() {
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
rm -f /data/coolify/ssh/keys/id.root@host.docker.internal.pub
}
generateSshKey() {
echo " - Generating SSH key."
ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.root@host.docker.internal -q -N "" -C root@coolify
chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal
generateAuthorizedKeys
}
syncSshKeys() {
DB_RUNNING=$(docker inspect coolify-db --format '{{ .State.Status }}' 2>/dev/null)
# Check if SSH key exists in Coolify data but not in authorized_keys
if checkSshKeyInCoolifyData && ! checkSshKeyInAuthorizedKeys; then
# Add the existing Coolify SSH key to authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
# Check if SSH key exists in authorized_keys but not in Coolify data
elif checkSshKeyInAuthorizedKeys && ! checkSshKeyInCoolifyData; then
# Ensure Coolify DB is running before proceeding
if [ "$DB_RUNNING" = "running" ]; then
# Retrieve DB user and SSH key from Coolify database
DB_USER=$(docker inspect coolify-db --format '{{ .Config.Env }}' | grep -oP 'POSTGRES_USER=\K[^ ]+')
DB_SSH_KEY=$(docker exec coolify-db psql -U $DB_USER -d coolify -t -c "SELECT \"private_key\" FROM \"private_keys\" WHERE id = 0 AND team_id = 0 LIMIT 1;" -A -t)
if [ -z "$DB_SSH_KEY" ]; then
# If no key found in DB, generate a new one
echo " - SSH key not found in database. Generating new key."
generateSshKey
else
# If key found in DB, save it and update authorized_keys
echo " - SSH key found in database. Saving to file."
echo "$DB_SSH_KEY" > /data/coolify/ssh/keys/id.root@host.docker.internal
chmod 600 /data/coolify/ssh/keys/id.root@host.docker.internal
chown 9999 /data/coolify/ssh/keys/id.root@host.docker.internal
# Generate public key from private key and update authorized_keys
ssh-keygen -y -f /data/coolify/ssh/keys/id.root@host.docker.internal -C root@coolify > /data/coolify/ssh/keys/id.root@host.docker.internal.pub
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
rm -f /data/coolify/ssh/keys/id.root@host.docker.internal.pub
chmod 600 ~/.ssh/authorized_keys
fi
fi
# If SSH key doesn't exist in either location
elif ! checkSshKeyInAuthorizedKeys && ! checkSshKeyInCoolifyData; then
# Ensure Coolify DB is running before proceeding
if [ "$DB_RUNNING" = "running" ]; then
# Retrieve DB user and SSH key from Coolify database
DB_USER=$(docker inspect coolify-db --format '{{ .Config.Env }}' | grep -oP 'POSTGRES_USER=\K[^ ]+')
DB_SSH_KEY=$(docker exec coolify-db psql -U $DB_USER -d coolify -t -c "SELECT \"private_key\" FROM \"private_keys\" WHERE id = 0 AND team_id = 0 LIMIT 1;" -A -t)
if [ -z "$DB_SSH_KEY" ]; then
# If no key found in DB, generate a new one
echo " - SSH key not found in database. Generating new key."
generateSshKey
else
# If key found in DB, save it and update authorized_keys
echo " - SSH key found in database. Saving to file."
echo "$DB_SSH_KEY" > /data/coolify/ssh/keys/id.root@host.docker.internal
chmod 600 /data/coolify/ssh/keys/id.root@host.docker.internal
ssh-keygen -y -f /data/coolify/ssh/keys/id.root@host.docker.internal -C root@coolify > /data/coolify/ssh/keys/id.root@host.docker.internal.pub
sed -i "/root@coolify/d" ~/.ssh/authorized_keys
cat /data/coolify/ssh/keys/id.root@host.docker.internal.pub >> ~/.ssh/authorized_keys
fi
else
generateSshKey
fi
fi
}
syncSshKeys || true
chown -R 9999:root /data/coolify
chmod -R 700 /data/coolify
echo -e "9. Installing Coolify ($LATEST_VERSION)"
echo -e " - It could take a while based on your server's performance, network speed, stars, etc."
echo -e " - Please wait."
getAJoke
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" >/dev/null 2>&1
echo " - Coolify installed successfully."
rm -f $ENV_FILE-$DATE rm -f $ENV_FILE-$DATE
echo "Waiting for 20 seconds for Coolify to be ready..."
echo " - Waiting for 20 seconds for Coolify (database migrations) to be ready."
getAJoke
sleep 20 sleep 20
echo -e "\033[0;35m echo "Please visit http://$(curl -4s https://ifconfig.io):8000 to get started."
____ _ _ _ _ _ echo -e "\nCongratulations! Your Coolify instance is ready to use.\n"
/ ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| |
| | / _ \| '_ \ / _\` | '__/ _\` | __| | | | |/ _\` | __| |/ _ \| '_ \/ __| | echo -e "Make sure you backup your /data/coolify/source/.env file to a safe location, outside of this server.\n"
| |__| (_) | | | | (_| | | | (_| | |_| |_| | | (_| | |_| | (_) | | | \__ \_|
\____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_)
|___/
\033[0m"
echo -e "\nYour instance is ready to use."
echo -e "Please visit http://$(curl -4s https://ifconfig.io):8000 to get started.\n"
echo -e "WARNING: We recommend you to backup your /data/coolify/source/.env file to a safe location, outside of this server."
cp /data/coolify/source/.env /data/coolify/source/.env.backup cp /data/coolify/source/.env /data/coolify/source/.env.backup
echo -e "Your .env file has been copied to /data/coolify/source/.env.backup\n"

View File

@@ -62,10 +62,10 @@ function sync:bunny {
# } # }
function db:reset { function db:reset {
bash spin exec -u webuser coolify php artisan migrate:fresh --seed bash spin exec coolify php artisan migrate:fresh --seed
} }
function db:reset-prod { function db:reset-prod {
bash spin exec -u webuser coolify php artisan migrate:fresh --force --seed --seeder=ProductionSeeder || bash spin exec coolify php artisan migrate:fresh --force --seed --seeder=ProductionSeeder ||
php artisan migrate:fresh --force --seed --seeder=ProductionSeeder php artisan migrate:fresh --force --seed --seeder=ProductionSeeder
} }
@@ -74,11 +74,11 @@ function mfs {
} }
function coolify { function coolify {
bash spin exec -u webuser coolify bash bash spin exec coolify bash
} }
function coolify:root { function coolify:root {
bash spin exec coolify bash bash spin exec -u root coolify bash
} }
function coolify:proxy { function coolify:proxy {
docker exec -ti coolify-proxy sh docker exec -ti coolify-proxy sh
@@ -93,7 +93,7 @@ function vite {
} }
function tinker { function tinker {
bash spin exec -u webuser coolify php artisan tinker bash spin exec coolify php artisan tinker
} }
# function build:helper { # function build:helper {

View File

@@ -1,16 +1,13 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.340" "version": "4.0.0-beta.336"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.341" "version": "4.0.0-beta.337"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.1"
},
"realtime": {
"version": "1.0.1"
} }
} }
} }